如何处理嵌套表单的条件验证?
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
我没有对此进行测试,但我希望你能理解。
我似乎无法理解以下内容:
我对嵌套表单使用 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
我没有对此进行测试,但我希望你能理解。