Episodes

Resources

StimulusJS - https://stimulusjs.org/

Within the city_selector_controller.js, some code was changed so that the behavior works as expected. Mainly, the select boxes were not clearing the previous values.
Download Source Code

Summary

# Terminal
rails new template --webpack
yarn add stimulus
rails g model state name abbr
rails g model county name state:belongs_to
rails g model city name county:belongs_to
rails g scaffold accounts name city:belongs_to

# app/javascript/packs/application.js
console.log('Hello World from Webpacker')

import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("./controllers", true, /\.js$/)
application.load(definitionsFromContext(context))

# app/views/layouts/application.html.erb
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application' %>

# models/state.rb
class State < ApplicationRecord
  has_many :counties

  scope :name_asc, -> { order(name: :asc) }
  scope :select_collection, -> { name_asc.map { |s| [s.name, s.id, { url: Rails.application.routes.url_helpers.state_counties_path(s) }] } }
end

# models/county.rb
class County < ApplicationRecord
  belongs_to :state
  has_many :cities

  scope :name_asc, -> { order(name: :asc) }
  scope :select_collection, -> { name_asc.map { |s| [s.name, s.id, { url: Rails.application.routes.url_helpers.state_county_cities_path(state_id: s.state_id, county_id: s.id) }] } }
end

# models/city.rb
class City < ApplicationRecord
  belongs_to :county, optional: true
  has_many :accounts

  scope :name_asc, -> { order(name: :asc) }
  scope :select_collection, -> { name_asc.map { |s| [s.name, s.id] } }
end

# accounts/_form.html.erb
  <div data-controller='city-selector'>
    <%= form.label :state_id %>
    <%= form.select :state_id,
                    State.select_collection,
                    { include_blank: true },
                    {
                      class: 'form-control',
                      data: {
                        target: 'city-selector.state_input',
                        action: 'city-selector#state_changed'
                      }
                    }
    %>

    <%= form.label :county_id %>
    <%= form.select :county_id, [], {}, { 
                    class: 'form-control',
                    data: {
                      target: 'city-selector.county_input',
                      action: 'city-selector#county_changed'
                    }                                          
                  } %>

    <%= form.label :city_id %>
    <%= form.select :city_id, [], {}, { class: 'form-control', 
                    data: {
                      target: 'city-selector.city_input',
                      city_id: account&.city_id,
                      county_id: account&.city&.county_id,
                      state_id: account&.city&.county&.state_id
                    } } %>
  </div>

# config/routes.rb
Rails.application.routes.draw do
  resources :states, only: [] do
    # /states/:state_id/counties
    resources :counties, only: :index do
      resources :cities, only: :index
    end
  end

  resources :accounts
  root to: 'accounts#index'
end

# counties_controller.rb
class CountiesController < ApplicationController
  def index
    state = State.find(params[:state_id])
    counties = state.counties
    render json: counties.select_collection
  end
end

# cities_controller.rb
class CitiesController < ApplicationController
  def index
    state = State.find(params[:state_id])
    county = state.counties.find(params[:county_id])
    cities = county.cities
    render json: cities.select_collection
  end
end

# app/javascript/packs/controllers/city_selector_controller.js
import { Controller } from 'stimulus'
export default class extends Controller {

  initialize() {
    console.log('hello from city-selector controller')
    this.check_forms()    
  }

  check_forms() {
    if (this.city.getAttribute('data-county-id')){
      this.state.value = this.city.getAttribute('data-state-id')
      this.state_changed()
    } 
  }

  state_changed() {
    var that = this
    this.request_data(this.state_url, function (response) {
      that.county.innerText = null;
      that.city.innerText = null;
      that.county.appendChild(document.createElement('option'))
      for (let i = 0; i < response.length; i++) {
        var opt = document.createElement('option')
        opt.textContent = response[i][0]
        opt.value = response[i][1]
        opt.setAttribute('url', response[i][2]['url'])
        that.county.appendChild(opt)
      }

      if (that.city.getAttribute('data-county-id')) {
        that.county.value = that.city.getAttribute('data-county-id')
        that.city.removeAttribute('data-county-id')
        that.city.innerText = null;
        that.county_changed()
      }
    })
  }

  county_changed() {
    var that = this
    this.request_data(this.county_url, function (response) {
      that.city.innerText = null;
      that.city.appendChild(document.createElement('option'))
      for (let i = 0; i < response.length; i++) {
        var opt = document.createElement('option')
        opt.textContent = response[i][0]
        opt.value = response[i][1]
        that.city.appendChild(opt)
      }

      if (that.city.getAttribute('data-city-id')) {
        that.city.value = that.city.getAttribute('data-city-id')
        that.city.removeAttribute('data-city-id')
      }
    })
  }

  get state() {
    return this.targets.find('state_input')
  }

  get county() {
    return this.targets.find('county_input')
  }

  get city() {
    return this.targets.find('city_input')
  }

  get state_url() {
    return this.state.selectedOptions[0].getAttribute('url')
  }

  get county_url() {
    return this.county.selectedOptions[0].getAttribute('url')
  }

  request_data(url, callback) {
    Rails.ajax({
      type: 'GET',
      url: url,
      success: callback,
      error: function (response) { }
    })
  }
}