Chained select fields for custom data structures
In this post I want to demonstrate:
- defining data structures in a model
- adding conditional
validations
- displaying
select
fields in a form
1. TWO chained fields #
Mission: Select car brand and model
rails g scaffold cars brand model description:text
Define collections and validations:
# app/models/car.rb
class Car < ApplicationRecord
# 2 levels of data. sometimes level 2 can be blank.
CARS = { audi: %w[a1 a2 a3 a4 a5 a6 s8],
kia: %w[ceed sportage],
tesla: ['model x', 'model 3'],
nissan: [],
bmw: %w[i3 s8] }.freeze
validates :description, presence: true
validates :brand, presence: true
# sportage can not be in bmw
validates :model, inclusion: { in: ->(record) { record.models }, allow_blank: true }
# nissan should have no model
validates :model, presence: { if: ->(record) { record.models.present? } }
def brands
CARS.keys
end
def models
return [] unless brand.present?
CARS[brand.to_sym] || []
end
end
Select fields in form:
# app/views/addresses/_form.html.erb
<%= form_with(model: car) do |form| %>
<%= form.select :brand, car.brands, {include_blank: true}, {} %>
<%= form.select :model, car.models, {include_blank: true}, {} %>
<%= form.text_area :description %>
<%= form.submit %>
<% end %>
2. TWO chained fields - hash of hashes #
In this case we’ll have our data in the form of a hash of hashes (slightly different data structure):
rails g scaffold order country city
# order.rb
class Order < ApplicationRecord
CS2 = {
'usa' => { los_angeles: 'los angeles', new_york: 'new york', chicago: 'chicago' },
'poland' => { gdansk: 'gdansk', wroclaw: 'wroclaw', warsaw: 'warsaw' },
'ukraine' => { kyiv: 'kyiv', lviv: 'lviv', kharkiv: 'kharkiv', ivano_frankivsk: "ivano-frankivsk" }
}
validates :country, presence: true
validates :city, presence: true
validates :city, inclusion: { in: ->(record) { record.city_opts.values.map(&:to_s) }, allow_blank: true }
def country_opts
CS2.keys
end
def city_opts
return [] unless country.present?
CS2[country].invert || []
end
end
We will update the form as in the previous example:
# orders/_form.html.erb
<%= form_with(model: order) do |form| %>
<%= form.select :country, order.country_opts, { include_blank: true }, {} %>
<%= form.select :city, order.city_opts, {include_blank: true}, {} %>
<%= form.submit %>
<% end %>
Result:
3. THREE chained fields #
Mission: Select address country, region, city
rails g scaffold address country region city description:text
Define collections and validations:
# app/models/address.rb
# 3 levels of data. sometimes level 2 or level 3 can be blank
CS3 = { us:
{ california: ['sacramento', 'los angeles'],
maryland: %w[annapolis baltimore] },
de: { bayern: {}, turingen: {} },
pl: {},
ua:
{ north: %w[chernihiv kyiv],
west: %w[lviv bukovel] } }.freeze
validates :description, presence: true
validates :country, presence: true
# california can not be in de
validates :region, inclusion: { in: ->(record) { record.regions.map(&:to_s) }, allow_blank: true }
# pl should have no region
validates :region, presence: { if: ->(record) { record.regions.present? } }
# sacramento can not be in bayern
validates :city, inclusion: { in: ->(record) { record.cities }, allow_blank: true }
# de should have no city
validates :city, presence: { if: ->(record) { record.cities.present? } }
def countries
CS3.keys
end
def regions
return [] unless country.present?
CS3[country.to_sym].keys || []
end
def cities
return [] unless country.present? && region.present?
CS3[country.to_sym][region.to_sym] || []
end
Select fields in form:
# app/views/addresses/_form.html.erb
<%= form_with(model: address) do |form| %>
<%= form.select :country, address.countries, {include_blank: true}, {} %>
<%= form.select :region, address.regions, {include_blank: true}, {} %>
<%= form.select :city, address.cities.map { | k, v | [k.capitalize, k] }, {include_blank: true}, {} %>
<%= form.text_area :description %>
<%= form.submit %>
<% end %>
Now when your backend is solid, feel free to add some interactivity to your form with Hotwire. This will be explored in the next post.
Did you like this article? Did it save you some time?