country_state_select 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +56 -0
- data/README.md +196 -111
- data/Rakefile +26 -0
- data/app/controllers/country_state_select/cscs_controller.rb +31 -8
- data/app/javascript/country_state_select/controller.js +66 -0
- data/app/javascript/country_state_select/country_state_select.js +245 -0
- data/config/importmap.rb +4 -0
- data/config/routes.rb +14 -4
- data/country_state_select.gemspec +14 -4
- data/docs/cities.md +35 -0
- data/docs/images/cascading-select-demo.png +0 -0
- data/docs/json-api.md +41 -0
- data/docs/migration-v4.md +32 -0
- data/gemfiles/rails_7.0.gemfile +10 -0
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/gemfiles/rails_7.2.gemfile +7 -0
- data/gemfiles/rails_8.0.gemfile +7 -0
- data/lib/country_state_select/configuration.rb +89 -0
- data/lib/country_state_select/country_metadata.rb +29 -0
- data/lib/country_state_select/data/dial_codes.yml +250 -0
- data/lib/country_state_select/data_sources/base.rb +27 -0
- data/lib/country_state_select/data_sources/city_state.rb +28 -0
- data/lib/country_state_select/engine.rb +31 -0
- data/lib/country_state_select/form_builder.rb +48 -0
- data/lib/country_state_select/localization.rb +41 -0
- data/lib/country_state_select/version.rb +1 -1
- data/lib/country_state_select.rb +83 -23
- data/lib/generators/country_state_select/install/install_generator.rb +22 -0
- data/lib/generators/country_state_select/install/templates/README +34 -0
- data/lib/generators/country_state_select/install/templates/initializer.rb +29 -0
- data/package.json +40 -0
- data/spec/country_state_select_spec.rb +2 -2
- data/spec/lib/country_state_select/configuration_spec.rb +63 -0
- data/spec/lib/country_state_select/country_metadata_spec.rb +42 -0
- data/spec/lib/country_state_select/filtering_spec.rb +68 -0
- data/spec/lib/country_state_select/localization_spec.rb +30 -0
- data/spec/requests/business_form_spec.rb +38 -0
- data/spec/requests/lookups_spec.rb +72 -0
- data/spec/spec_helper.rb +12 -4
- data/vendor/assets/javascript/country_state_select.js.erb +35 -21
- data/vendor/assets/javascript/country_state_select_vanilla.js +252 -0
- metadata +147 -17
- data/.circleci/config.yml +0 -15
- data/.travis.yml +0 -14
- data/lib/country_state_select/engine3.rb +0 -10
- data/lib/country_state_select/railtie.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '07096615806cb6927cc87f41dd9f52a468a4abbbc3995f14510e5d4a9c76586f'
|
|
4
|
+
data.tar.gz: 106a0cd51d710611182c4f07c2b28de1e2efde86bd0170f8ce0cedb784bfcee7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b478c6e74ed49cde30de69c0695d6ba82b32931bf778acd723d848b1e749f5389c359010f4c32a4753c8cea682839d28c1e741fe4f083eb8c03d2a6f065a23f
|
|
7
|
+
data.tar.gz: e3f8d5c2f1aebee96f819354a56d9d0d227c490134d4f8c1a012e434ad0500b7cad1227f604a0bd155b9d7a82f9258f239054daeb93bbbe437854440e780038d
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
ruby-versions:
|
|
10
|
+
name: Ruby ${{ matrix.ruby }} (latest Rails)
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
ruby: ['3.1', '3.2', '3.3', '3.4']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
|
21
|
+
bundler-cache: true
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: bundle exec rspec
|
|
24
|
+
|
|
25
|
+
rails-versions:
|
|
26
|
+
name: Rails ${{ matrix.rails }} (Ruby 3.3)
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
strategy:
|
|
29
|
+
fail-fast: false
|
|
30
|
+
matrix:
|
|
31
|
+
rails: ['7.0', '7.1', '7.2', '8.0']
|
|
32
|
+
env:
|
|
33
|
+
BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: ruby/setup-ruby@v1
|
|
37
|
+
with:
|
|
38
|
+
ruby-version: '3.3'
|
|
39
|
+
bundler-cache: true
|
|
40
|
+
- name: Run tests
|
|
41
|
+
run: bundle exec rspec
|
data/.gitignore
CHANGED
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
Modernization release. See [docs/migration-v4.md](docs/migration-v4.md) for the full upgrade guide — existing 3.x code (Ruby API and the legacy jQuery JS) keeps working unchanged.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Dependency-free JavaScript core (`app/javascript/country_state_select/country_state_select.js`), with builds for importmap-rails/npm (ESM) and Sprockets (`vendor/assets/javascript/country_state_select_vanilla.js`, compiled via `rake js:build`).
|
|
9
|
+
- A Stimulus controller (`country_state_select/controller`) with `states-url`/`cities-url`/`tom-select`/`announce`/`state-placeholder`/`city-placeholder` values.
|
|
10
|
+
- npm package (`package.json`) exposing the core and Stimulus controller as ES modules; `@hotwired/stimulus` is an optional peer dependency.
|
|
11
|
+
- Native `form_with`/`form_for` support: `f.country_select`, `f.state_select`, `f.city_select` — simple_form is no longer required. They render the model's current selection server-side (correct on edit forms without waiting on JS) and stamp Stimulus target data attributes automatically.
|
|
12
|
+
- `rails g country_state_select:install` generator (initializer + setup instructions).
|
|
13
|
+
- `CountryStateSelect.configure` with `priority_countries`, `only_countries`/`except_countries`, `flags`, `dial_codes`, `localize_names`, `cache_duration`, `draw_routes`, and a pluggable `data_source` (`CountryStateSelect::DataSources::Base`).
|
|
14
|
+
- `only:`/`except:`/`priority:` keyword options on `countries_collection`, `countries_except`, `collect_states`.
|
|
15
|
+
- Flag emoji and international dial code label decoration for countries.
|
|
16
|
+
- I18n-localized country names, with an optional soft integration with the `countries` gem.
|
|
17
|
+
- HTTP caching (`Cache-Control` + `ETag`, conditional `GET` → `304`) on the `/find_states`/`/find_cities` JSON endpoints; configurable via `cache_duration`.
|
|
18
|
+
- `config.draw_routes = false` + documented pattern for mounting the lookup routes at a custom path.
|
|
19
|
+
- An `aria-live` region announcing dropdown updates, and edit-form pre-population support in the new JS (skips a redundant AJAX round-trip when the server already rendered the correct selection; only fetches when a field is genuinely still a blank shell).
|
|
20
|
+
- `spec/dummy`: a runnable Rails 7/8 demo app (Propshaft + importmap + Stimulus) that doubles as the integration test target.
|
|
21
|
+
- CI matrix expanded to Rails 7.0/7.1/7.2/8.0 in addition to the Ruby version matrix (`gemfiles/`).
|
|
22
|
+
- `docs/json-api.md`, `docs/cities.md`, `docs/migration-v4.md`.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Requires Rails >= 7.0 and Ruby >= 3.1.** Rails 3–6 support (`engine3.rb`, `railtie.rb`) removed — stay on `country_state_select` 3.x for older Rails.
|
|
26
|
+
- `cities_collection` (the simple_form helper) now actually passes the country through to the lookup, instead of silently ignoring it — see the migration guide if you relied on the old (incorrect) behavior.
|
|
27
|
+
- `/find_states` and `/find_cities` now resolve through the configured data source and `only`/`except`/`priority` filters rather than calling the underlying data gem directly.
|
|
28
|
+
|
|
29
|
+
### Deprecated
|
|
30
|
+
- The jQuery + Chosen JavaScript (`vendor/assets/javascript/country_state_select.js.erb`) is kept for backward compatibility but is deprecated in favor of the new dependency-free build.
|
|
31
|
+
|
|
32
|
+
## 3.3.1
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **CSRF / verb:** the `find_states` and `find_cities` lookup routes now accept
|
|
36
|
+
`GET` (the correct verb for read-only data) in addition to `POST`, and the
|
|
37
|
+
controller skips forgery protection. Previously the front-end's AJAX `POST`
|
|
38
|
+
had no CSRF token and would raise `ActionController::InvalidAuthenticityToken`
|
|
39
|
+
on a default Rails app. The bundled JavaScript now issues `GET` requests.
|
|
40
|
+
- **Data mutation:** `countries_collection` no longer calls `except!` (bang) on
|
|
41
|
+
the hash that `city-state` memoizes, which permanently mutated the gem's
|
|
42
|
+
shared country data for the process. It now uses a non-mutating `reject`.
|
|
43
|
+
- **Consistent return shape:** `collect_cities` now always returns an `Array`
|
|
44
|
+
(empty when there is no data), matching `collect_states`, instead of `nil`.
|
|
45
|
+
- **Duplicate AJAX requests:** the state-change handler that triggers the city
|
|
46
|
+
lookup is now bound once via event delegation instead of being re-bound on
|
|
47
|
+
every country change (the old behaviour stacked listeners and fired multiple
|
|
48
|
+
requests per change — the long-standing `[142] FIXME`).
|
|
49
|
+
- **JavaScript hardening:** option markup now quotes the `value` attribute and
|
|
50
|
+
HTML-escapes labels; `html` and loop counters are properly scoped instead of
|
|
51
|
+
leaking to the global object.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- CI moved to GitHub Actions running the RSpec suite across Ruby 3.0–3.3. The
|
|
55
|
+
unused Travis and CircleCI configs (the latter never ran the tests) were
|
|
56
|
+
removed.
|
data/README.md
CHANGED
|
@@ -1,155 +1,240 @@
|
|
|
1
|
-
## Country State Select
|
|
2
|
-
[](https://github.com/arvindvyas/Country-State-Select/actions/workflows/ci.yml)
|
|
3
3
|
[](https://codeclimate.com/github/arvindvyas/Country-State-Select)
|
|
4
4
|
|
|
5
|
+
Country State Select gives you cascading Country → State/Province → City dropdowns for Rails forms: pick a country, the state field repopulates; pick a state, the city field repopulates.
|
|
5
6
|
|
|
7
|
+

|
|
6
8
|
|
|
7
|
-
Country
|
|
9
|
+
Country/state/state data comes from the [city-state](https://github.com/loureirorg/city-state) gem (pluggable — see [Custom data sources](#custom-data-sources)).
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
**v4.0 is a modernization release.** Highlights:
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
- Dependency-free JavaScript (no jQuery) with a bundled [Stimulus](https://stimulus.hotwired.dev/) controller
|
|
14
|
+
- Works with importmap-rails, Sprockets, Propshaft, or npm/esbuild/webpack
|
|
15
|
+
- Native `form_with`/`form_for` support (`f.country_select`, `f.state_select`, `f.city_select`) — simple_form is no longer required
|
|
16
|
+
- `priority_countries`, `only`/`except` filtering, flags, dial codes, I18n-localized names
|
|
17
|
+
- A pluggable data source adapter
|
|
18
|
+
- HTTP-cached JSON lookup endpoints
|
|
19
|
+
- An install generator (`rails g country_state_select:install`)
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
Upgrading from 3.x? See [docs/migration-v4.md](docs/migration-v4.md) — the legacy jQuery/Chosen JavaScript still works, but Rails < 7.0 is no longer supported (stay on the 3.x gem for older Rails).
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
https://country-state-select.herokuapp.com
|
|
23
|
+
## Installation
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'country_state_select'
|
|
27
|
+
```
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
```
|
|
30
|
+
bundle install
|
|
31
|
+
rails g country_state_select:install # optional: initializer + setup instructions
|
|
32
|
+
```
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
Requires Rails >= 7.0 and Ruby >= 3.1.
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Then run
|
|
27
|
-
|
|
28
|
-
bundle install
|
|
36
|
+
## JavaScript setup
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
In the latest version we have removed all the dependencies of js and you are feel free to use any js you can use predefine method to populate data
|
|
38
|
+
Pick the one that matches your app. All of them talk to the same `/find_states` and `/find_cities` JSON endpoints (see [docs/json-api.md](docs/json-api.md)).
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
## Setup Gem
|
|
35
|
-
if not autoloaded than please add in your application.js
|
|
36
|
-
//= require country_state_select
|
|
40
|
+
### Importmap + Stimulus (Rails 7/8 default)
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
The application makes use of the chosen-rails library for a better user experience. This can be disabled if desired.
|
|
40
|
-
|
|
41
|
-
If you're using chosen-rails, please add the following to app/assets/javascripts/application.js:
|
|
42
|
-
|
|
43
|
-
//= require chosen-jquery
|
|
42
|
+
The engine registers its pins automatically — no `pin` lines to write yourself. In `app/javascript/application.js`:
|
|
44
43
|
|
|
44
|
+
```js
|
|
45
|
+
import "country_state_select"
|
|
46
|
+
import { Application } from "@hotwired/stimulus"
|
|
47
|
+
import CountryStateSelectController from "country_state_select/controller"
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
const application = Application.start()
|
|
50
|
+
application.register("country-state-select", CountryStateSelectController)
|
|
51
|
+
```
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
Or, with `stimulus-rails`' `app/javascript/controllers/index.js` auto-loading convention, just re-export it as its own controller file:
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
```js
|
|
56
|
+
// app/javascript/controllers/country_state_select_controller.js
|
|
57
|
+
export { default } from "country_state_select/controller"
|
|
58
|
+
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
Then in a view, wrap your fields in a controller container:
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
```erb
|
|
63
|
+
<div data-controller="country-state-select">
|
|
64
|
+
<%= f.country_select :country %>
|
|
65
|
+
<%= f.state_select :state, :country %>
|
|
66
|
+
<%= f.city_select :city, :state, :country %>
|
|
67
|
+
</div>
|
|
68
|
+
```
|
|
60
69
|
|
|
61
|
-
|
|
70
|
+
Stimulus controller values (all optional, set as `data-country-state-select-<value>-value="..."`):
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
| Value | Default | Purpose |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `states-url` | `/find_states` | Override if you disabled `draw_routes` and mounted the engine elsewhere |
|
|
75
|
+
| `cities-url` | `/find_cities` | Same, for cities |
|
|
76
|
+
| `tom-select` | `false` | Enhance the selects with [Tom Select](https://tom-select.js.org/) once populated (requires `TomSelect` to be loaded globally) |
|
|
77
|
+
| `announce` | `true` | Announce dropdown updates via an `aria-live` region |
|
|
78
|
+
| `state-placeholder` / `city-placeholder` | `""` | Blank option text shown before a real selection |
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
### Sprockets (no jQuery)
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
```js
|
|
83
|
+
//= require country_state_select_vanilla
|
|
84
|
+
```
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
```js
|
|
87
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
88
|
+
CountryStateSelect({ country_id: "country_field_id", state_id: "state_field_id" })
|
|
89
|
+
})
|
|
90
|
+
```
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## States / Provinces Field
|
|
92
|
+
### npm / esbuild / webpack
|
|
82
93
|
|
|
83
|
-
|
|
94
|
+
```
|
|
95
|
+
npm install country_state_select @hotwired/stimulus
|
|
96
|
+
```
|
|
84
97
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
```js
|
|
99
|
+
import CountryStateSelectController from "country_state_select/controller"
|
|
100
|
+
// or, for the framework-agnostic core directly:
|
|
101
|
+
import CountryStateSelect from "country_state_select"
|
|
102
|
+
```
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
`@hotwired/stimulus` is an optional peer dependency — only needed if you import the `/controller` entry point.
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
### Legacy jQuery + Chosen (deprecated)
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
Still bundled for apps upgrading from 3.x. New apps should use one of the options above.
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
```js
|
|
111
|
+
//= require country_state_select
|
|
112
|
+
//= require chosen-jquery // optional, if you want the Chosen UI
|
|
113
|
+
```
|
|
98
114
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return CountryStateSelect({
|
|
115
|
+
```js
|
|
116
|
+
$(document).on('turbo:load', function () {
|
|
117
|
+
CountryStateSelect({
|
|
103
118
|
country_id: "country_field_id",
|
|
104
|
-
state_id: "state_field_id"
|
|
105
|
-
|
|
106
|
-
}
|
|
119
|
+
state_id: "state_field_id",
|
|
120
|
+
chosen_ui: true,
|
|
121
|
+
chosen_options: { disable_search_threshold: 10 }
|
|
122
|
+
})
|
|
123
|
+
})
|
|
107
124
|
```
|
|
108
|
-
If you are using CoffeeScript, use the following:
|
|
109
|
-
```coffescript
|
|
110
|
-
$(document).on 'ready page:load', ->
|
|
111
|
-
CountryStateSelect({ country_id: "country_field_id", state_id: "state_field_id" })
|
|
112
|
-
```
|
|
113
125
|
|
|
114
|
-
|
|
126
|
+
## Using it in a form
|
|
127
|
+
|
|
128
|
+
### `form_with` (no simple_form required)
|
|
115
129
|
|
|
116
|
-
|
|
130
|
+
```erb
|
|
131
|
+
<%= form_with model: @business do |f| %>
|
|
132
|
+
<div data-controller="country-state-select">
|
|
133
|
+
<%= f.country_select :country, include_blank: true %>
|
|
134
|
+
<%= f.state_select :state, :country %>
|
|
135
|
+
<%= f.city_select :city, :state, :country %>
|
|
136
|
+
</div>
|
|
137
|
+
<% end %>
|
|
138
|
+
```
|
|
117
139
|
|
|
118
|
-
|
|
140
|
+
- `state_select`/`city_select` read the model's *current* country/state to render the right options server-side — on an edit form, the saved state and city show up correctly without waiting on JavaScript.
|
|
141
|
+
- When the parent selection has no states/cities (e.g. a country with no states), the field renders as a plain text input, same as before.
|
|
142
|
+
- `only:`, `except:`, and `priority:` options work on all three: `f.country_select :country, priority: %w[US CA]`.
|
|
119
143
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# chosen UI improvements (optional)
|
|
144
|
+
### simple_form (unchanged from 3.x)
|
|
123
145
|
|
|
124
|
-
|
|
146
|
+
```erb
|
|
147
|
+
<%= simple_form_for @form_object do |f| %>
|
|
148
|
+
<%= f.input :country_field, collection: CountryStateSelect.countries_collection %>
|
|
125
149
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
<% options = { form: f, field_names: { country: :country_field, state: :state_field } } %>
|
|
151
|
+
<%= f.input :state_field, CountryStateSelect.state_options(options) %>
|
|
152
|
+
<% end %>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
City support: see [docs/cities.md](docs/cities.md).
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# config/initializers/country_state_select.rb
|
|
161
|
+
CountryStateSelect.configure do |config|
|
|
162
|
+
config.priority_countries = %w[US CA GB] # shown first in the dropdown
|
|
163
|
+
config.only_countries = %w[US CA GB IN] # restrict the list
|
|
164
|
+
config.except_countries = %w[KP] # or exclude specific ones
|
|
165
|
+
|
|
166
|
+
config.flags = true # "🇮🇳 India"
|
|
167
|
+
config.dial_codes = true # "India (+91)"
|
|
168
|
+
config.localize_names = true # translate names via I18n / the `countries` gem
|
|
169
|
+
|
|
170
|
+
config.cache_duration = 1.day # HTTP cache on /find_states, /find_cities; nil disables it
|
|
171
|
+
config.draw_routes = true # set false to mount the engine at a custom path instead
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Every option here can also be passed per-call: `CountryStateSelect.countries_collection(priority: %w[US])`, `f.country_select :country, only: %w[US CA]`.
|
|
176
|
+
|
|
177
|
+
### Custom data sources
|
|
178
|
+
|
|
179
|
+
Swap out `city-state` (e.g. for a database table, or the `countries` gem) by implementing the adapter interface:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
class MyDataSource < CountryStateSelect::DataSources::Base
|
|
183
|
+
def countries
|
|
184
|
+
{ US: 'United States', IN: 'India' } # code => English name
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def states(country_code)
|
|
188
|
+
[[:CA, 'California'], [:NY, 'New York']] # [code, name] pairs
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def cities(state_code, country_code)
|
|
192
|
+
['Los Angeles', 'San Francisco'] # plain names
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
CountryStateSelect.configure { |c| c.data_source = MyDataSource.new }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Custom route path
|
|
200
|
+
|
|
201
|
+
By default the engine draws `/find_states` and `/find_cities` at the top level. To mount it elsewhere:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# config/initializers/country_state_select.rb
|
|
205
|
+
CountryStateSelect.configure { |c| c.draw_routes = false }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# config/routes.rb
|
|
210
|
+
scope path: '/csc' do
|
|
211
|
+
match 'find_states' => 'country_state_select/cscs#find_states', via: :get
|
|
212
|
+
match 'find_cities' => 'country_state_select/cscs#find_cities', via: :get
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Then pass the matching URLs to the JS: `states_url: '/csc/find_states'` (or the Stimulus `states-url-value`).
|
|
217
|
+
|
|
218
|
+
## JSON API
|
|
219
|
+
|
|
220
|
+
The lookup endpoints are documented in [docs/json-api.md](docs/json-api.md) — useful if you're driving the selects from a SPA or mobile client instead of the bundled JS.
|
|
221
|
+
|
|
222
|
+
## Development
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
bundle install
|
|
226
|
+
bundle exec rspec
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The dummy Rails app used for integration tests lives in `spec/dummy` and doubles as a runnable demo:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
cd spec/dummy
|
|
233
|
+
bin/rails server
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
CI runs the suite across Ruby 3.1–3.4 (latest Rails) and Rails 7.0/7.1/7.2/8.0 (see `gemfiles/`).
|
|
152
237
|
|
|
153
238
|
## Contributing
|
|
154
|
-
|
|
155
|
-
|
|
239
|
+
|
|
240
|
+
Fork, fix, then send a pull request.
|
data/Rakefile
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks'
|
|
4
|
+
|
|
5
|
+
namespace :js do
|
|
6
|
+
desc 'Compile app/javascript/country_state_select/country_state_select.js into the ' \
|
|
7
|
+
'Sprockets-loadable vendor/assets/javascript/country_state_select_vanilla.js build'
|
|
8
|
+
task :build do
|
|
9
|
+
source_path = File.expand_path('app/javascript/country_state_select/country_state_select.js', __dir__)
|
|
10
|
+
dest_path = File.expand_path('vendor/assets/javascript/country_state_select_vanilla.js', __dir__)
|
|
11
|
+
|
|
12
|
+
source = File.read(source_path)
|
|
13
|
+
body = source.sub('export default function CountryStateSelect(options) {', 'function CountryStateSelect(options) {')
|
|
14
|
+
raise "Could not find the expected export in #{source_path} — did the function signature change?" if body == source
|
|
15
|
+
|
|
16
|
+
File.write(dest_path, <<~JS)
|
|
17
|
+
// GENERATED FILE — do not edit directly.
|
|
18
|
+
// Source: app/javascript/country_state_select/country_state_select.js
|
|
19
|
+
// Regenerate with `rake js:build` after editing the source.
|
|
20
|
+
(function (global) {
|
|
21
|
+
#{body.gsub(/^/, ' ').rstrip}
|
|
22
|
+
|
|
23
|
+
global.CountryStateSelect = CountryStateSelect;
|
|
24
|
+
})(this);
|
|
25
|
+
JS
|
|
26
|
+
|
|
27
|
+
puts "Wrote #{dest_path}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -3,20 +3,43 @@
|
|
|
3
3
|
# @author : Arvind Vyas
|
|
4
4
|
module CountryStateSelect
|
|
5
5
|
class CscsController < ApplicationController
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# These are read-only data lookups served over GET, so CSRF tokens are not
|
|
7
|
+
# required. Skipping forgery protection also keeps the endpoints usable when
|
|
8
|
+
# a host app still wires them up as POST without sending a token.
|
|
9
|
+
skip_forgery_protection if respond_to?(:skip_forgery_protection)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
def find_states
|
|
12
|
+
# `[[code, name], ...]` shape — the front-end uses element 0 as the option
|
|
13
|
+
# value (ISO code) and element 1 as the label. Goes through the
|
|
14
|
+
# configured data source (not `CS` directly) so a custom adapter and
|
|
15
|
+
# only/except/priority filtering apply here too.
|
|
16
|
+
render_cached(CountryStateSelect.raw_states(params[:country_id]), params[:country_id])
|
|
12
17
|
end
|
|
13
18
|
|
|
14
|
-
#
|
|
19
|
+
# Send it a state_id and country_id; returns the cities of that state.
|
|
15
20
|
def find_cities
|
|
16
|
-
|
|
21
|
+
render_cached(
|
|
22
|
+
CountryStateSelect.collect_cities(params[:state_id], params[:country_id]),
|
|
23
|
+
[params[:state_id], params[:country_id]]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# The underlying data is static per params, so this is safe to cache
|
|
30
|
+
# both at the HTTP layer (Cache-Control) and via conditional GET
|
|
31
|
+
# (ETag/304), avoiding a render entirely on repeat lookups. Disabled by
|
|
32
|
+
# setting `CountryStateSelect.configuration.cache_duration = nil`.
|
|
33
|
+
def render_cached(data, cache_key)
|
|
34
|
+
duration = CountryStateSelect.configuration.cache_duration
|
|
35
|
+
|
|
36
|
+
if duration
|
|
37
|
+
expires_in duration, public: true
|
|
38
|
+
return unless stale?(etag: cache_key)
|
|
39
|
+
end
|
|
17
40
|
|
|
18
41
|
respond_to do |format|
|
|
19
|
-
format.json { render json:
|
|
42
|
+
format.json { render json: data }
|
|
20
43
|
end
|
|
21
44
|
end
|
|
22
45
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Stimulus wrapper around country_state_select.js — the idiomatic way to
|
|
2
|
+
// wire this up on Rails 7/8 with Hotwire/Turbo and no manual JS per page.
|
|
3
|
+
//
|
|
4
|
+
// <div data-controller="country-state-select"
|
|
5
|
+
// data-country-state-select-tom-select-value="true">
|
|
6
|
+
// <select data-country-state-select-target="country" ...>
|
|
7
|
+
// <select data-country-state-select-target="state" ...>
|
|
8
|
+
// <select data-country-state-select-target="city" ...> (optional)
|
|
9
|
+
// </div>
|
|
10
|
+
//
|
|
11
|
+
// Register it once in your controllers/index.js:
|
|
12
|
+
//
|
|
13
|
+
// import { Application } from '@hotwired/stimulus'
|
|
14
|
+
// import CountryStateSelectController from 'country_state_select/controller'
|
|
15
|
+
// const application = Application.start()
|
|
16
|
+
// application.register('country-state-select', CountryStateSelectController)
|
|
17
|
+
//
|
|
18
|
+
// Requires `@hotwired/stimulus` as a peer dependency in the host app —
|
|
19
|
+
// it is not bundled or re-exported by this package.
|
|
20
|
+
//
|
|
21
|
+
// Imports the bare specifier "country_state_select" (the importmap pin /
|
|
22
|
+
// npm package name) rather than a relative path — a relative import would
|
|
23
|
+
// resolve against this file's own fingerprinted asset URL under Sprockets/
|
|
24
|
+
// Propshaft (e.g. controller-a1b2c3.js), bypassing the import map entirely
|
|
25
|
+
// and 404ing, since browsers only consult import maps for bare specifiers.
|
|
26
|
+
import { Controller } from '@hotwired/stimulus';
|
|
27
|
+
import CountryStateSelect from 'country_state_select';
|
|
28
|
+
|
|
29
|
+
export default class CountryStateSelectController extends Controller {
|
|
30
|
+
static targets = ['country', 'state', 'city'];
|
|
31
|
+
static values = {
|
|
32
|
+
statesUrl: { type: String, default: '/find_states' },
|
|
33
|
+
citiesUrl: { type: String, default: '/find_cities' },
|
|
34
|
+
tomSelect: { type: Boolean, default: false },
|
|
35
|
+
announce: { type: Boolean, default: true },
|
|
36
|
+
statePlaceholder: { type: String, default: '' },
|
|
37
|
+
cityPlaceholder: { type: String, default: '' }
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
connect() {
|
|
41
|
+
this.instance = CountryStateSelect({
|
|
42
|
+
country_id: this.countryTarget.id,
|
|
43
|
+
state_id: this.stateTarget.id,
|
|
44
|
+
city_id: this.hasCityTarget ? this.cityTarget.id : undefined,
|
|
45
|
+
states_url: this.statesUrlValue,
|
|
46
|
+
cities_url: this.citiesUrlValue,
|
|
47
|
+
state_placeholder: this.statePlaceholderValue,
|
|
48
|
+
city_placeholder: this.cityPlaceholderValue,
|
|
49
|
+
announce: this.announceValue,
|
|
50
|
+
enhance: this.tomSelectValue ? this.tomSelectEnhancer.bind(this) : undefined
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disconnect() {
|
|
55
|
+
if (this.instance) this.instance.destroy();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tomSelectEnhancer(selectEl, opts) {
|
|
59
|
+
if (typeof TomSelect === 'undefined') {
|
|
60
|
+
console.warn('country-state-select: tomSelectValue is true but TomSelect is not loaded'); // eslint-disable-line no-console
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return new TomSelect(selectEl, opts);
|
|
65
|
+
}
|
|
66
|
+
}
|