Adding dynamic record with Cascading select in rails
This is one of the most common scenarios the developers face every now and then. Adding dynamic child record with cascading dropdown/select. Yes right, two complex mysteries in one episode. This can be achieved in many various ways in rails but I tried with a specific gem and jQuery.
Recently I came across implementing cascading select in rails with adding dynamic child record in parent page. I am assuming we all know how to implement cascading select in rails. if not, then please look no further. Because I am going to write another article on that though tons of similar articles can be found online.
Case scenario
A training will be hosted in different cities of a country. We have to build such a system where admin can select country and then select the city under that country and save the cities with a training record.
My stack
- Rails: 5.2.4.5
- Ruby: 2.6.6
- jQuery: 3.4.1
- Bootstrap: 4.0.0
The final outcome will be like this. Adding dynamic record with cascading city.
So I used gem cocoon to make the nested forms easier for the child records in one place. First include the gem in the Gemfile.
gem "cocoon"
Bundle install and then add it to the application.js
//= require cocoon
Now it's time to add functionalities to rails.
Models:
class Training < ApplicationRecord
has_many :cities
accepts_nested_attributes_for :cities, allow_destroy: true
end
class City < ApplicationRecord
belongs_to :training
end
Controllers:
class CountriesController < ApplicationController
def cities
country = Country.find(params[:id])
@cities = country.cities
respond_to do |format|
format.json {
render json: {cities: @cities}
}
end
end
end
class TrainingController < ApplicationController
def create
@training = Training.new(training_params)
if @training.save
redirect_to @training
else
render :new
end
end
private
def training_params
params.require(:training).permit(:name, :description, cities_attributes: [:id, :city_id, :_destroy])
end
end
View:
_form.html.erb (Yeah, I know why not slim :) )
<%= form_for training do |form| %>
<div class="card">
<div class="card-body">
<div class="form-group">
<label class="font-weight-bold">
Name
</label>
<%= form.text_field :name, class: 'form-control'%>
</div>
<div class="form-group">
<label class="font-weight-bold">
Description
</label>
<%= form.text_area :description, class: 'form-control', maxlength: 255 %>
</div>
<div>
<%= form.fields_for :cities do |city| %>
<div class="row align-items-center pl-2">
<%= render 'city_fields', f: city, city: city.object %>
</div>
<% end %>
<div class='links mt-3'>
<%= link_to_add_association 'Add city', form, :cities %>
</div>
</div>
</div>
<div class="card-footer">
<div class="card-footer-item text-right">
<%= link_to 'Cancel', trainings_path, class: 'btn btn-subtle-dark mr-3'%>
<%= form.submit 'Save', class: 'btn btn-primary'%>
</div>
</div>
</div>
<%- end %>
Partial view to render the set of child inputs (_city_fields.html.erb)
<div class="row pt-3 nested-fields align-items-center">
<div class="col-auto">
<%= link_to_remove_association f, class: 'btn btn-sm btn-icon btn-secondary' do %>
<i class="far fa-trash-alt"></i> <span class="sr-only">Remove</span>
<%- end %>
</div>
<div class="col-auto div-value-source">
<%= f.select :country_id, options_from_collection_for_select(Country.all, :id, :name, city.try(:country_id)), {prompt: 'Select any'}, class: 'form-control custom-select country' %>
</div>
<div class="col-auto div-match-type">
<%= f.select :city_id, options_for_select([]), {}, class: 'form-control custom-select city' %>
<%= hidden_field_tag :hdn_city_id, city.id, class: 'hdn_city_id' %>
</div>
</div>
Note: nested-fields
class has to be in the inputs partial view div, otherwise remove link of cocoon won't work.
Now the final and sweet part. Code from jQuery
$(document).on('change', '.country', function(e){
load_cities($(this));
})
function load_cities(country) {
$.ajax('/countries/' + country.val() + '/cities', {
type: 'GET',
dataType: 'json',
error: function(jqXHR, textStatus, errorThrown) {
console.log("AJAX Error: " + textStatus);
},
success: function (data) {
var selected = '';
var city_select = country.parent().parent().find('.city')
hdn_city_id = country.parent().parent().find('.hdn_city_id').val()
city_select.empty();
$.each(data.cities, function(i, city) {
selected = hdn_city_id == city.id ? 'selected' : '';
city_select.append(`<option ${selected} value=${city.id}>${city.display_name}</option>`);
});
}
});
}
I hope this will make your life easy. Thanks for reading the article.