如何处理嵌套表单的条件验证?

How to deal with conditional validations of the nested forms?

我似乎无法理解以下内容:

我对嵌套表单使用 cocoon,其中每个表单都通过 Flatpickr 请求 start_date 和 end_date。我想做两件事中的一件(以最简单的为准)。

表格

<div class="form-container col col-sm-6 col-lg-12">
  <%= simple_form_for [@room_type, @age_table, @extra_guest] do |f|%>
  <div class="options-form">

  <div class="options-form-item">
  <div class="row">
    <div class="col col-sm-10">
      <%= f.association :age_table, prompt: "Select the relevant age table of the Park (e.g. kid/adult etc.)", :collection => @age_table_list,value_method: :id, label_method: false %>
    </div>
  </div>
  </div>

  <div class="options-form-item">
  <h4 class="p-3">Price for guest per period:</h4 class="m-3">
    <%= f.simple_fields_for :extra_guest_prices do |price| %>
    <div class="reservation-details">
      <%= render 'extra_guest_price_fields', f: price %>
    </div>
    <% end %>
    <div>
      <%= link_to_add_association f, :extra_guest_prices do %>
      <div class="option-add-option-price">
        <div class="prices-border">
          <i class="fas fa-plus"></i> Add another period
        </div>
      </div>
      <% end %>
    </div>
    <div class="row">
      <div class="col col-sm-6"> <%= f.button :submit, "Save new option", class: "create-reservation-btn"%>
      </div>
    </div>
    <% end %>
  </div>
</div>
</div>
</div>


<script>
  $(document).ready(function(){
    // $(document).find("#first_date")).flatpickr();

    const startDateInput = $(document).find(".first_date")
    const endDateInput = $(document).find(".second_date")

    if (startDateInput && endDateInput) {
    const unvailableDates = JSON.parse(document.querySelector('.widget-content').dataset.unavailable)

    flatpickr(startDateInput, {
      // minDate: 'today',
      // dateFormat: 'd-m-Y',
    disable: unvailableDates,
      format: "d-m-Y",
      altFormat: "d-m-Y",
      altInput: true,
    onChange: function(selectedDates, selectedDate) {
      if (selectedDate === '') {
        endDateInput.disabled = true;
      }
      let minDate = selectedDates[0];
      minDate.setDate(minDate.getDate() + 1);
      endDateCalendar.set('minDate', minDate);
      endDateInput.disabled = false;
    }
  });
    const endDateCalendar =
    flatpickr(endDateInput, {
      // dateFormat: 'd-m-Y',
        disable: unvailableDates,
      format: "d-m-Y",
      altFormat: "d-m-Y",
      altInput: true,
      },
      );

  };
});
</script>

嵌套形式

  <div class="nested-fields border-bottom">
    <div class="row">
      <div class="col col-sm-6"><%= f.input :price, placeholder: "e.g. 12.99" %></div>
    </div>

    <div class="row">
      <div class="col col-sm-6"><%= f.input :start_date,
          as: :string,
          label:"Start date",
          placeholder: "From",
          wrapper_html: { class: "inline_field_wrapper" },
          input_html:{ class: "first_date"} %></div>
      <div class="col col-sm-6"><%= f.input :end_date,
          as: :string,
          label:"End date",
          placeholder: "to",
          wrapper_html: { class: "inline_field_wrapper" },
          input_html:{ class: "second_date"} %></div>
    </div>
    <div hidden class= "widget-content" data-unavailable="<%= @extra_guest.unavailable_dates.to_json %>"></div>

    <div class="col col-sm-6 option-price-delete">
      <%= link_to_remove_association f do %>
      <i class="fas fa-trash"> Delete price</i>
      <% end %>
    </div>
</div>


<script>
  $(document).on('cocoon:after-insert', function(e, added_guest_price_form){
    // $(added_guest_price_form.find("#first_date")).flatpickr();

  const startDateInput = $(added_guest_price_form.find(".first_date"))
  const endDateInput = $(added_guest_price_form.find(".second_date"))

  if (startDateInput && endDateInput) {
    const unavailableDates = JSON.parse(document.querySelector('.widget-content').dataset.unavailable)
    // console.log(unvailableDates)

    flatpickr(startDateInput, {
      format: "d-m-Y",
      altFormat: "d-m-Y",
      altInput: true,
    // minDate: 'today',
    // dateFormat: 'd-m-Y',
    disable: unavailableDates,
    onChange: function(selectedDates, selectedDate) {
      if (selectedDate === '') {
        endDateInput.disabled = true;
      }
      let minDate = selectedDates[0];
      minDate.setDate(minDate.getDate() + 1);
      endDateCalendar.set('minDate', minDate);
      endDateInput.disabled = false;
    }
  });
    const endDateCalendar =
      flatpickr(endDateInput, {
        format: "d-m-Y",
        altFormat: "d-m-Y",
        altInput: true,
        // dateFormat: 'd-m-Y',
        disable: unavailableDates,
        },
      );

};
  });
</script>

验证者

class ExtraGuestPriceAvailabilityValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    extra_guest_prices = ExtraGuestPrice.where("extra_guest_id =?", record.extra_guest_id)
    extra_guest_price = ExtraGuestPrice.where("id=?", record.id)

    if extra_guest_price.empty?
      date_ranges = extra_guest_prices.map { |b| b.start_date..b.end_date }
      date_ranges.each do |range|
        if range.include? value
          record.errors.add(attribute, "is overlapping with another period")
        end
      end
    else
      date_ranges = extra_guest_prices.where.not('id=?', record.id).map { |b| b.start_date..b.end_date }
      date_ranges.each do |range|
        if range.include? value
          record.errors.add(attribute, "is overlapping with another period")
        end
      end
    end
  end
end

您如何设置验证器?我认为您正在单独验证每个 ExtraGuestPrice,您应该立即验证整个集合。

类似

class ExtraGuest
  validate :no_dates_overlap

  def no_dates_overlap
    ranges = [] # cache proccessed date ranges

    # iterate over all extra_guest_prices
    extra_guest_prices.each do |egp|
      if ranges.any? { |rng| rng.include?(egp.start_date) || rng.include?(egp.end_date) }
        # if a previous range includes this dates, add the error and break the loop
        errors.add(:extra_guest_prices, "Dates overlap")
        break
      else
        # else, cache it and check the next
        ranges << (egp.start_date..egp.end_date)
      end
    end
  end

我没有对此进行测试,但我希望你能理解。