Rails 和 jQuery 中的动态下拉(选择框)菜单不可逆

A dynamic dropdown (selectbox) menu in Rails with jQuery is not reversible

在由 Rails 6.1 控制的网站表单上,我想实现一个“动态”或级联下拉菜单,以便第二个下拉菜单中的选项根据 selected 项目而变化第一个下拉菜单。

具体来说,我有一个与 CountryTown 模型关联的 Person 模型。关系是 Person belongs_to Town,其中 belongs_to Countryhas_many 中。定义了方法 Country#nameTown#name。在 new 网站上的表单中 create a Person,用户 select a country from a首先是下拉菜单(select 框),然后是第二个下拉菜单中的 town

我基本上遵循了程序 #88 Dynamic Select Menus (revised),但在标准 jQuery 中重写了它,而不是 Railcasts 中的 Coffee。

简而言之,我使用 Rail 的 form 为 towns 创建了一个 selectbox。grouped_collection_select辅助方法;生成的HTML中的部分包含许多OPTGROUP,每个都对应一个国家,其中多个child 城镇belongs_to。关联的 jQuery 脚本过滤第二个(即 Town)下拉菜单,比较 selected 项目在 Country 下拉菜单和每个 OPTGROUPLABEL Town 下拉菜单(select 框)。

有点效果,但有一个严重的缺陷。基本上,它在第一次点击时起作用。然而,一旦用户改变主意并 re-selects 一个不同的 国家 towns 的所有选项都会消失。换句话说,用户的第一选择是不可逆的。那是一个糟糕的界面。

如何解决才能使用户的选择始终可逆?

下面是表单视图(hrb.erb)和Javascript jQuery代码中的相关部分。这里,person 是模型 Person 的新实例。它使用 Rails 6.1.4、Ruby 3.0.1 和 jQuery 3.5.1 进行测试。

erb.html 为表格:

<%= form_with(model: person, local: true) do |form| %>
  <div class="field">
    <%= form.label 'town_id.country_id', 'Country'%>
    <%= form.collection_select town_id.country_id', Country.all,
        :id, :name, include_blank: true %>
  </div>

  <div class="field">
    <%= form.label 'place.town_id' %>
    <%= form.grouped_collection_select 'place.town_id', Country.all,
        :towns, :name, :id, :name, include_blank: true %>
  </div>
<% end %>

Javascript jQuery:

 var contsel = "#"+$.escapeSelector('person_place.town_id.country_id');
 $(contsel).change(function(){
   var prefsel = "#"+$.escapeSelector('person_place.town_id');
   var contsel = "#"+$.escapeSelector('person_place.town_id.country_id');
   var country = $.escapeSelector($(contsel+' :selected').text());
   var towns = $(prefsel).html();
   var options = $(towns).filter("optgroup[label='"+country+"']").html();
   if (options) {
     $(prefsel).html(options);
   } else {
     $(prefsel).empty();
   }
 })

有很多方法可以使用 Web 应用程序框架实现动态下拉菜单。至于Rails的方法, this question 的答案似乎总结得特别好。

我认为 OP 基于 OPTGROUP 标签使用 jQuery 的方式具有优点,如果无论如何并非没有缺点,

  1. Rails端编码最少,
  2. 用户即使禁用 Javascript 也可以做出(并提交)有意义的选择(好吧,谁会呢?)。

OP的Javascript代码使得生成的HTML不可逆,因为它使用filter()方法破坏性地修改了HTML。在用户选择提示的 change() 的第一个操作中,HTML 被不可逆地修改。例如,一旦选择了 Country=Australia,就 Javascript 代码而言,除澳大利亚以外的所有 towns 的内容都会被删除。因此,由于任何后续操作都将基于修改后的 HTML,因此 Javascript 代码对任何其他 国家 .

中的城镇一无所知。

下面的 Javascript jQuery 代码应该可以解决这个问题,使用户的选择可逆。它基于OP的示例代码,除了额外的两行之外,前半部分相同。

$(function(){
 var contsel = "#" + $.escapeSelector('person_place.town_id.country_id');
 var townsel = "#" + $.escapeSelector('person_place.town_id');
 $(townsel).parent().hide();
 $(contsel).change(function() {
   var townsel = "#" + $.escapeSelector('person_place.town_id');
   var contsel = "#" + $.escapeSelector('person_place.town_id.country_id');
   var country = $.escapeSelector($(contsel + ' :selected').text());
   var towns = $(townsel).html();
   var options = $(towns).filter("optgroup[label='" + country + "']").html();
   if (options) {
     $(townsel).parent().show();
     $(townsel).show();
     $(townsel + " optgroup").hide();
     $(townsel + " optgroup[label='" + country + "']").show();
     $(townsel).find('option:selected').prop("selected", false);
   } else {
     $(townsel).hide();
     $(townsel).parent().hide();
   }
 })
})

这里有几点需要注意:

  1. 主要区别在于,这将 hideshow,具体取决于用户的选择,并且永远不会删除或修改 HTML 中的任何其他部分。
  2. 为了使操作可逆,这明确 show 部分,这些部分可能已被隐藏。
  3. 当所选 国家 不包含 城镇 时,城镇 的整个选择框将被隐藏,或者未选择 国家
  4. change()发生时,即当用户选择一个新的国家时,用户之前的选择(如果存在)将被清除。如果没有这个,用户可以在技术上提交具有相互矛盾组合的表单数据,例如 Country=Australia 和 Town=Nairobi。

这是 jsfiddle of the sample code for experimenting. The full code is also available at Github Gist