Rails 和 jQuery 中的动态下拉(选择框)菜单不可逆
A dynamic dropdown (selectbox) menu in Rails with jQuery is not reversible
在由 Rails 6.1 控制的网站表单上,我想实现一个“动态”或级联下拉菜单,以便第二个下拉菜单中的选项根据 selected 项目而变化第一个下拉菜单。
具体来说,我有一个与 Country
和 Town
模型关联的 Person
模型。关系是 Person
belongs_to
Town
,其中 belongs_to
Country
在 has_many
中。定义了方法 Country#name
和 Town#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 下拉菜单和每个 OPTGROUP
的 LABEL
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 的方式具有优点,如果无论如何并非没有缺点,
- Rails端编码最少,
- 用户即使禁用 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();
}
})
})
这里有几点需要注意:
- 主要区别在于,这将
hide
和 show
,具体取决于用户的选择,并且永远不会删除或修改 HTML 中的任何其他部分。
- 为了使操作可逆,这明确
show
部分,这些部分可能已被隐藏。
- 当所选 国家 不包含 城镇 时,城镇 的整个选择框将被隐藏,或者未选择 国家。
- 当
change()
发生时,即当用户选择一个新的国家时,用户之前的选择(如果存在)将被清除。如果没有这个,用户可以在技术上提交具有相互矛盾组合的表单数据,例如 Country=Australia 和 Town=Nairobi。
这是 jsfiddle of the sample code for experimenting. The full code is also available at Github Gist。
在由 Rails 6.1 控制的网站表单上,我想实现一个“动态”或级联下拉菜单,以便第二个下拉菜单中的选项根据 selected 项目而变化第一个下拉菜单。
具体来说,我有一个与 Country
和 Town
模型关联的 Person
模型。关系是 Person
belongs_to
Town
,其中 belongs_to
Country
在 has_many
中。定义了方法 Country#name
和 Town#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 下拉菜单和每个 OPTGROUP
的 LABEL
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 的方式具有优点,如果无论如何并非没有缺点,
- Rails端编码最少,
- 用户即使禁用 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();
}
})
})
这里有几点需要注意:
- 主要区别在于,这将
hide
和show
,具体取决于用户的选择,并且永远不会删除或修改 HTML 中的任何其他部分。 - 为了使操作可逆,这明确
show
部分,这些部分可能已被隐藏。 - 当所选 国家 不包含 城镇 时,城镇 的整个选择框将被隐藏,或者未选择 国家。
- 当
change()
发生时,即当用户选择一个新的国家时,用户之前的选择(如果存在)将被清除。如果没有这个,用户可以在技术上提交具有相互矛盾组合的表单数据,例如 Country=Australia 和 Town=Nairobi。
这是 jsfiddle of the sample code for experimenting. The full code is also available at Github Gist。