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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +41 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +1 -0
  5. data/CHANGELOG.md +56 -0
  6. data/README.md +196 -111
  7. data/Rakefile +26 -0
  8. data/app/controllers/country_state_select/cscs_controller.rb +31 -8
  9. data/app/javascript/country_state_select/controller.js +66 -0
  10. data/app/javascript/country_state_select/country_state_select.js +245 -0
  11. data/config/importmap.rb +4 -0
  12. data/config/routes.rb +14 -4
  13. data/country_state_select.gemspec +14 -4
  14. data/docs/cities.md +35 -0
  15. data/docs/images/cascading-select-demo.png +0 -0
  16. data/docs/json-api.md +41 -0
  17. data/docs/migration-v4.md +32 -0
  18. data/gemfiles/rails_7.0.gemfile +10 -0
  19. data/gemfiles/rails_7.1.gemfile +7 -0
  20. data/gemfiles/rails_7.2.gemfile +7 -0
  21. data/gemfiles/rails_8.0.gemfile +7 -0
  22. data/lib/country_state_select/configuration.rb +89 -0
  23. data/lib/country_state_select/country_metadata.rb +29 -0
  24. data/lib/country_state_select/data/dial_codes.yml +250 -0
  25. data/lib/country_state_select/data_sources/base.rb +27 -0
  26. data/lib/country_state_select/data_sources/city_state.rb +28 -0
  27. data/lib/country_state_select/engine.rb +31 -0
  28. data/lib/country_state_select/form_builder.rb +48 -0
  29. data/lib/country_state_select/localization.rb +41 -0
  30. data/lib/country_state_select/version.rb +1 -1
  31. data/lib/country_state_select.rb +83 -23
  32. data/lib/generators/country_state_select/install/install_generator.rb +22 -0
  33. data/lib/generators/country_state_select/install/templates/README +34 -0
  34. data/lib/generators/country_state_select/install/templates/initializer.rb +29 -0
  35. data/package.json +40 -0
  36. data/spec/country_state_select_spec.rb +2 -2
  37. data/spec/lib/country_state_select/configuration_spec.rb +63 -0
  38. data/spec/lib/country_state_select/country_metadata_spec.rb +42 -0
  39. data/spec/lib/country_state_select/filtering_spec.rb +68 -0
  40. data/spec/lib/country_state_select/localization_spec.rb +30 -0
  41. data/spec/requests/business_form_spec.rb +38 -0
  42. data/spec/requests/lookups_spec.rb +72 -0
  43. data/spec/spec_helper.rb +12 -4
  44. data/vendor/assets/javascript/country_state_select.js.erb +35 -21
  45. data/vendor/assets/javascript/country_state_select_vanilla.js +252 -0
  46. metadata +147 -17
  47. data/.circleci/config.yml +0 -15
  48. data/.travis.yml +0 -14
  49. data/lib/country_state_select/engine3.rb +0 -10
  50. data/lib/country_state_select/railtie.rb +0 -10
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # ISO 3166-1 alpha-2 => ITU international dial code (without the leading +).
4
+ # Territories that share a country's dial code (e.g. Caribbean NANP members)
5
+ # repeat that code, matching real-world usage.
6
+ AC: '247'
7
+ AD: '376'
8
+ AE: '971'
9
+ AF: '93'
10
+ AG: '1268'
11
+ AI: '1264'
12
+ AL: '355'
13
+ AM: '374'
14
+ AO: '244'
15
+ AQ: '672'
16
+ AR: '54'
17
+ AS: '1684'
18
+ AT: '43'
19
+ AU: '61'
20
+ AW: '297'
21
+ AX: '358'
22
+ AZ: '994'
23
+ BA: '387'
24
+ BB: '1246'
25
+ BD: '880'
26
+ BE: '32'
27
+ BF: '226'
28
+ BG: '359'
29
+ BH: '973'
30
+ BI: '257'
31
+ BJ: '229'
32
+ BL: '590'
33
+ BM: '1441'
34
+ BN: '673'
35
+ BO: '591'
36
+ BQ: '599'
37
+ BR: '55'
38
+ BS: '1242'
39
+ BT: '975'
40
+ BW: '267'
41
+ BY: '375'
42
+ BZ: '501'
43
+ CA: '1'
44
+ CC: '61'
45
+ CD: '243'
46
+ CF: '236'
47
+ CG: '242'
48
+ CH: '41'
49
+ CI: '225'
50
+ CK: '682'
51
+ CL: '56'
52
+ CM: '237'
53
+ CN: '86'
54
+ CO: '57'
55
+ CR: '506'
56
+ CU: '53'
57
+ CV: '238'
58
+ CW: '599'
59
+ CX: '61'
60
+ CY: '357'
61
+ CZ: '420'
62
+ DE: '49'
63
+ DJ: '253'
64
+ DK: '45'
65
+ DM: '1767'
66
+ DO: '1'
67
+ DZ: '213'
68
+ EC: '593'
69
+ EE: '372'
70
+ EG: '20'
71
+ EH: '212'
72
+ ER: '291'
73
+ ES: '34'
74
+ ET: '251'
75
+ FI: '358'
76
+ FJ: '679'
77
+ FK: '500'
78
+ FM: '691'
79
+ FO: '298'
80
+ FR: '33'
81
+ GA: '241'
82
+ GB: '44'
83
+ GD: '1473'
84
+ GE: '995'
85
+ GF: '594'
86
+ GG: '44'
87
+ GH: '233'
88
+ GI: '350'
89
+ GL: '299'
90
+ GM: '220'
91
+ GN: '224'
92
+ GP: '590'
93
+ GQ: '240'
94
+ GR: '30'
95
+ GT: '502'
96
+ GU: '1671'
97
+ GW: '245'
98
+ GY: '592'
99
+ HK: '852'
100
+ HN: '504'
101
+ HR: '385'
102
+ HT: '509'
103
+ HU: '36'
104
+ ID: '62'
105
+ IE: '353'
106
+ IL: '972'
107
+ IM: '44'
108
+ IN: '91'
109
+ IO: '246'
110
+ IQ: '964'
111
+ IR: '98'
112
+ IS: '354'
113
+ IT: '39'
114
+ JE: '44'
115
+ JM: '1876'
116
+ JO: '962'
117
+ JP: '81'
118
+ KE: '254'
119
+ KG: '996'
120
+ KH: '855'
121
+ KI: '686'
122
+ KM: '269'
123
+ KN: '1869'
124
+ KP: '850'
125
+ KR: '82'
126
+ KW: '965'
127
+ KY: '1345'
128
+ KZ: '7'
129
+ LA: '856'
130
+ LB: '961'
131
+ LC: '1758'
132
+ LI: '423'
133
+ LK: '94'
134
+ LR: '231'
135
+ LS: '266'
136
+ LT: '370'
137
+ LU: '352'
138
+ LV: '371'
139
+ LY: '218'
140
+ MA: '212'
141
+ MC: '377'
142
+ MD: '373'
143
+ ME: '382'
144
+ MF: '590'
145
+ MG: '261'
146
+ MH: '692'
147
+ MK: '389'
148
+ ML: '223'
149
+ MM: '95'
150
+ MN: '976'
151
+ MO: '853'
152
+ MP: '1670'
153
+ MQ: '596'
154
+ MR: '222'
155
+ MS: '1664'
156
+ MT: '356'
157
+ MU: '230'
158
+ MV: '960'
159
+ MW: '265'
160
+ MX: '52'
161
+ MY: '60'
162
+ MZ: '258'
163
+ NA: '264'
164
+ NC: '687'
165
+ NE: '227'
166
+ NF: '672'
167
+ NG: '234'
168
+ NI: '505'
169
+ NL: '31'
170
+ 'NO': '47'
171
+ NP: '977'
172
+ NR: '674'
173
+ NU: '683'
174
+ NZ: '64'
175
+ OM: '968'
176
+ PA: '507'
177
+ PE: '51'
178
+ PF: '689'
179
+ PG: '675'
180
+ PH: '63'
181
+ PK: '92'
182
+ PL: '48'
183
+ PM: '508'
184
+ PR: '1'
185
+ PS: '970'
186
+ PT: '351'
187
+ PW: '680'
188
+ PY: '595'
189
+ QA: '974'
190
+ RE: '262'
191
+ RO: '40'
192
+ RS: '381'
193
+ RU: '7'
194
+ RW: '250'
195
+ SA: '966'
196
+ SB: '677'
197
+ SC: '248'
198
+ SD: '249'
199
+ SE: '46'
200
+ SG: '65'
201
+ SH: '290'
202
+ SI: '386'
203
+ SJ: '47'
204
+ SK: '421'
205
+ SL: '232'
206
+ SM: '378'
207
+ SN: '221'
208
+ SO: '252'
209
+ SR: '597'
210
+ SS: '211'
211
+ ST: '239'
212
+ SV: '503'
213
+ SX: '1721'
214
+ SY: '963'
215
+ SZ: '268'
216
+ TC: '1649'
217
+ TD: '235'
218
+ TG: '228'
219
+ TH: '66'
220
+ TJ: '992'
221
+ TK: '690'
222
+ TL: '670'
223
+ TM: '993'
224
+ TN: '216'
225
+ TO: '676'
226
+ TR: '90'
227
+ TT: '1868'
228
+ TV: '688'
229
+ TW: '886'
230
+ TZ: '255'
231
+ UA: '380'
232
+ UG: '256'
233
+ US: '1'
234
+ UY: '598'
235
+ UZ: '998'
236
+ VA: '39'
237
+ VC: '1784'
238
+ VE: '58'
239
+ VG: '1284'
240
+ VI: '1340'
241
+ VN: '84'
242
+ VU: '678'
243
+ WF: '681'
244
+ WS: '685'
245
+ XK: '383'
246
+ YE: '967'
247
+ YT: '262'
248
+ ZA: '27'
249
+ ZM: '260'
250
+ ZW: '263'
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CountryStateSelect
4
+ module DataSources
5
+ # Adapter interface. Implement this to back CountryStateSelect with a
6
+ # different data provider (a database table, the `countries` gem, a
7
+ # remote API, ...) instead of the bundled `city-state` gem — this is
8
+ # what closes out issue #25 (limited/missing city data) without the
9
+ # gem itself owning the data problem.
10
+ class Base
11
+ # @return [Hash{Symbol=>String}] ISO code => English country name
12
+ def countries
13
+ raise NotImplementedError
14
+ end
15
+
16
+ # @return [Array<Array(Symbol,String)>] raw [code, name] pairs
17
+ def states(_country_code)
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # @return [Array<String>] city names
22
+ def cities(_state_code, _country_code)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'city-state'
4
+
5
+ module CountryStateSelect
6
+ module DataSources
7
+ # Default adapter, backed by the `city-state` gem (unchanged behavior
8
+ # from 3.x). `CS.countries` returns a Hash memoized for the whole
9
+ # process, so every read here is non-mutating.
10
+ class CityState < Base
11
+ def countries
12
+ CS.countries.reject { |k, _| k == :COUNTRY_ISO_CODE }
13
+ end
14
+
15
+ def states(country_code)
16
+ return [] if country_code.nil? || country_code.to_s.empty?
17
+
18
+ (CS.states(country_code) || {}).to_a
19
+ end
20
+
21
+ def cities(state_code, country_code)
22
+ return [] if state_code.nil? || state_code.to_s.empty?
23
+
24
+ CS.cities(state_code.to_sym, country_code.to_s.empty? ? nil : country_code.to_sym) || []
25
+ end
26
+ end
27
+ end
28
+ end
@@ -5,6 +5,37 @@
5
5
  module CountryStateSelect
6
6
  module Rails
7
7
  class Engine < ::Rails::Engine
8
+ initializer 'country_state_select.form_builder' do
9
+ ActiveSupport.on_load(:action_view) do
10
+ ActionView::Helpers::FormBuilder.include CountryStateSelect::FormBuilder
11
+ end
12
+ end
13
+
14
+ # Sprockets/Propshaft: makes both the new dependency-free build
15
+ # (vendor/assets/javascript/country_state_select_vanilla.js) and the
16
+ # deprecated jQuery build resolvable via `//= require` or
17
+ # `stylesheet_link_tag`, and makes app/javascript resolvable as an
18
+ # asset path for importmap-rails pins (see config/importmap.rb).
19
+ initializer 'country_state_select.assets' do |app|
20
+ if app.config.respond_to?(:assets)
21
+ app.config.assets.paths << root.join('app/javascript').to_s
22
+ app.config.assets.paths << root.join('vendor/assets/javascript').to_s
23
+ end
24
+ end
25
+
26
+ # Registers this engine's pins (country_state_select,
27
+ # country_state_select/controller) with importmap-rails, if the host
28
+ # app has it installed, so apps get them for free instead of having
29
+ # to hand-write `pin "country_state_select", to: ...` themselves.
30
+ # Safe to define even when importmap-rails isn't present — Rails
31
+ # initializer ordering just no-ops a `before:` reference to a name
32
+ # that doesn't exist.
33
+ initializer 'country_state_select.importmap', before: 'importmap' do |app|
34
+ next unless app.config.respond_to?(:importmap)
35
+
36
+ app.config.importmap.paths << root.join('config/importmap.rb')
37
+ app.config.importmap.cache_sweepers << root.join('app/javascript')
38
+ end
8
39
  end
9
40
  end
10
41
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CountryStateSelect
4
+ # Native `form_with`/`form_for` support — `f.state_select` renders a plain
5
+ # HTML <select> (or a text <input> when the current country has no states,
6
+ # matching simple_form's `state_options` behavior) without requiring
7
+ # simple_form. Both helpers stamp `data-controller`/`data-*-target`
8
+ # attributes so the bundled Stimulus controller (app/javascript/
9
+ # country_state_select/controller.js) wires up automatically — wrap the
10
+ # fields in a `data-controller="country-state-select"` container and no
11
+ # further JS is needed.
12
+ module FormBuilder
13
+ FILTER_KEYS = %i[only except priority].freeze
14
+
15
+ def country_select(method, options = {}, html_options = {})
16
+ collection = CountryStateSelect.countries_collection(options.slice(*FILTER_KEYS))
17
+ merged_html = html_options.reverse_merge(data: { 'country-state-select-target': 'country' })
18
+
19
+ select(method, collection, options.except(*FILTER_KEYS), merged_html)
20
+ end
21
+
22
+ def state_select(method, country_method, options = {}, html_options = {})
23
+ country_value = object.public_send(country_method)
24
+ states = CountryStateSelect.collect_states(country_value, **options.slice(*FILTER_KEYS))
25
+ merged_html = html_options.reverse_merge(data: { 'country-state-select-target': 'state' })
26
+
27
+ if states.empty?
28
+ # FormBuilder#text_field (generated for the `field_helpers` list)
29
+ # only takes (method, options) — html attributes and options share
30
+ # one hash, unlike `select`'s separate options/html_options.
31
+ text_field(method, options.except(*FILTER_KEYS).merge(merged_html))
32
+ else
33
+ select(method, states, options.except(*FILTER_KEYS), merged_html)
34
+ end
35
+ end
36
+
37
+ def city_select(method, state_method, country_method, options = {}, html_options = {})
38
+ cities = CountryStateSelect.collect_cities(object.public_send(state_method), object.public_send(country_method))
39
+ merged_html = html_options.reverse_merge(data: { 'country-state-select-target': 'city' })
40
+
41
+ if cities.empty?
42
+ text_field(method, options.merge(merged_html))
43
+ else
44
+ select(method, cities.zip(cities), options, merged_html)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CountryStateSelect
4
+ # Best-effort translation of country names for the current I18n.locale.
5
+ #
6
+ # Two lookup strategies, tried in order, so neither is a hard dependency:
7
+ #
8
+ # 1. The `countries` gem (ISO3166::Country), if the host app already
9
+ # depends on it — it ships translations for all ISO locales.
10
+ # 2. A `country_state_select.countries.<ISO_CODE>` key in the host
11
+ # app's own locale files, for apps that want to supply their own.
12
+ #
13
+ # Falls back to the English name from the data source when neither
14
+ # strategy has a translation, so `localize_names` is always safe to turn
15
+ # on even with sparse translation coverage.
16
+ module Localization
17
+ module_function
18
+
19
+ def country_name(code, english_name)
20
+ return english_name unless CountryStateSelect.configuration.localize_names
21
+
22
+ via_countries_gem(code) || via_i18n(code) || english_name
23
+ end
24
+
25
+ def via_countries_gem(code)
26
+ return nil unless defined?(::ISO3166::Country)
27
+
28
+ ::ISO3166::Country.new(code.to_s)&.translations&.[](I18n.locale.to_s)
29
+ rescue StandardError
30
+ nil
31
+ end
32
+
33
+ def via_i18n(code)
34
+ key = "country_state_select.countries.#{code}"
35
+ translated = I18n.t(key, default: nil)
36
+ translated if translated.present?
37
+ rescue StandardError
38
+ nil
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CountryStateSelect
4
- VERSION = '3.2.0'
4
+ VERSION = '4.0.0'
5
5
  end
@@ -1,23 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
3
4
  require 'country_state_select/version'
4
- require 'city-state'
5
+ require 'country_state_select/configuration'
6
+ require 'country_state_select/data_sources/base'
7
+ require 'country_state_select/data_sources/city_state'
8
+ require 'country_state_select/localization'
9
+ require 'country_state_select/country_metadata'
10
+ require 'country_state_select/form_builder'
5
11
  require 'rails'
6
12
 
7
13
  module CountryStateSelect
8
- # Collect the Countries
9
- def self.countries_collection
10
- CS.countries.except!(:COUNTRY_ISO_CODE).collect { |p| [p[1], p[0]] }.compact
14
+ # Collect the Countries as [label, code] pairs, honoring configuration
15
+ # (priority_countries, only/except, flags, dial_codes, localize_names)
16
+ # and any per-call overrides passed in `options`.
17
+ #
18
+ # CountryStateSelect.countries_collection
19
+ # CountryStateSelect.countries_collection(priority: %w[US IN], only: %w[US IN GB])
20
+ def self.countries_collection(options = {})
21
+ config = configuration
22
+ raw = config.data_source_instance.countries
23
+
24
+ only = options.fetch(:only, config.only_countries)
25
+ except = options.fetch(:except, config.except_countries)
26
+ priority = options.fetch(:priority, config.priority_countries)
27
+
28
+ raw = raw.select { |code, _| only.map(&:to_s).include?(code.to_s) } if only
29
+ raw = raw.reject { |code, _| except.map(&:to_s).include?(code.to_s) } if except
30
+
31
+ labeled = raw.collect do |code, name|
32
+ localized = Localization.country_name(code, name)
33
+ [CountryMetadata.decorate(code, localized), code]
34
+ end
35
+
36
+ reorder_by_priority(labeled, priority)
11
37
  end
12
38
 
13
- # Pass array of unwanted countries to get back all except those in the array
39
+ # Pass array of unwanted countries to get back all except those in the array.
40
+ # Kept for backward compatibility with 3.x; prefer `countries_collection(except: ...)`.
14
41
  def self.countries_except(*except)
15
- countries_collection.collect { |c| c unless except.include?(c[1]) }.compact
42
+ except = except.flatten.compact
43
+ countries_collection(except: except.presence)
16
44
  end
17
45
 
18
46
  # Return either the State (String) or States (Array)
19
47
  def self.states_collection(f, options)
20
- states = collect_states(f.object.send(options[:country]))
48
+ states = collect_states(f.object.send(options[:country]), **filter_opts(options))
21
49
  return f.object.send(options[:state]) if states.empty?
22
50
 
23
51
  states
@@ -25,31 +53,45 @@ module CountryStateSelect
25
53
 
26
54
  # Return either the City (String) or Cities (Array)
27
55
  def self.cities_collection(f, options)
28
- cities = collect_cities(f.object.send(options[:state]))
56
+ collect_cities(f.object.send(options[:state]), f.object.send(options[:country]))
57
+ end
58
+
59
+ # Return the raw [code, name] pairs for a given Country's states/provinces
60
+ # — the shape the JSON lookup endpoint hands to the front-end (element 0
61
+ # is the option value, element 1 is the label).
62
+ def self.raw_states(country, only: nil, except: nil, priority: nil)
63
+ return [] if country.nil? || country.to_s.empty?
64
+
65
+ states = configuration.data_source_instance.states(country) # [[code, name], ...]
66
+ states = states.select { |code, _| only.map(&:to_s).include?(code.to_s) } if only
67
+ states = states.reject { |code, _| except.map(&:to_s).include?(code.to_s) } if except
68
+
69
+ reorder_by_priority(states, priority, index: 0)
29
70
  end
30
71
 
31
- # Return the collected States for a given Country
32
- def self.collect_states(country)
33
- CS.states(country).collect { |p| [p[1], p[0]] }.compact
72
+ # Return the collected States for a given Country, as [name, code] pairs
73
+ # — the shape simple_form/options_for_select expects (label, then value).
74
+ def self.collect_states(country, only: nil, except: nil, priority: nil)
75
+ raw_states(country, only: only, except: except, priority: priority).collect { |code, name| [name, code] }
34
76
  end
35
77
 
36
- # Return the cities of given state and country
78
+ # Return the cities of given state and country.
37
79
  def self.collect_cities(state_id = '', country_id = '')
38
- return [] if state_id.nil? || country_id.nil?
80
+ return [] if state_id.nil? || country_id.nil? || state_id.to_s.empty?
39
81
 
40
- CS.cities(state_id.to_sym, country_id.to_sym)
82
+ configuration.data_source_instance.cities(state_id, country_id)
41
83
  end
42
84
 
43
85
  # Return a hash for use in the simple_form
44
86
  def self.state_options(options)
45
87
  states = states_collection(options[:form], options[:field_names])
46
- options = merge_hash(options, states)
88
+ merge_hash(options, states)
47
89
  end
48
90
 
49
91
  # Return a hash for use in the simple_form
50
92
  def self.city_options(options)
51
- cities = cities_collection(options[:form], options[:field_names])
52
- options = merge_hash(options, cities)
93
+ cities = cities_collection(options[:form], options[:field_names])
94
+ merge_hash(options, cities)
53
95
  end
54
96
 
55
97
  # Create hash to use in the simple_form
@@ -58,15 +100,33 @@ module CountryStateSelect
58
100
  options = options.merge(as: :string) if collections.instance_of?(String)
59
101
  options
60
102
  end
103
+
104
+ # Moves entries whose code is in `priority` (in that order) to the front
105
+ # of `collection`, followed by everything else in its original order.
106
+ def self.reorder_by_priority(collection, priority, index: 1)
107
+ return collection if priority.nil? || priority.empty?
108
+
109
+ priority_codes = priority.map(&:to_s)
110
+ by_code = collection.group_by { |entry| entry[index].to_s }
111
+
112
+ head = priority_codes.filter_map { |code| by_code[code]&.first }
113
+ tail = collection - head
114
+
115
+ head + tail
116
+ end
117
+ private_class_method :reorder_by_priority
118
+
119
+ def self.filter_opts(options)
120
+ return {} unless options.is_a?(Hash)
121
+
122
+ options.slice(:only, :except, :priority)
123
+ end
124
+ private_class_method :filter_opts
61
125
  end
62
126
 
63
127
  case ::Rails.version.to_s
64
- when /^[4-7]./
128
+ when /^[7-9]\./, /^\d{2}\./
65
129
  require 'country_state_select/engine'
66
- when /^3\.[12]/
67
- require 'country_state_select/engine3'
68
- when /^3\.0/
69
- require 'country_state_select/railtie'
70
130
  else
71
- raise 'Unsupported rails version'
131
+ raise 'Unsupported Rails version — CountryStateSelect 4.x requires Rails >= 7.0. Use CountryStateSelect 3.x for older Rails.'
72
132
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module CountryStateSelect
6
+ module Generators
7
+ # rails g country_state_select:install
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ desc 'Creates a CountryStateSelect initializer and prints JS setup instructions.'
12
+
13
+ def copy_initializer
14
+ template 'initializer.rb', 'config/initializers/country_state_select.rb'
15
+ end
16
+
17
+ def show_readme
18
+ readme 'README' if behavior == :invoke
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ ===============================================================================
2
+
3
+ CountryStateSelect is installed. An initializer was created at
4
+ config/initializers/country_state_select.rb — uncomment what you need.
5
+
6
+ Next, pick ONE JavaScript integration:
7
+
8
+ Importmap (Rails 7/8 default) — pins are registered automatically, in
9
+ app/javascript/application.js:
10
+
11
+ import "country_state_select"
12
+ import { Application } from "@hotwired/stimulus"
13
+ import CountryStateSelectController from "country_state_select/controller"
14
+ Application.start().register("country-state-select", CountryStateSelectController)
15
+
16
+ Sprockets (no jQuery required):
17
+
18
+ //= require country_state_select_vanilla
19
+
20
+ npm / esbuild / webpack:
21
+
22
+ npm install country_state_select @hotwired/stimulus
23
+
24
+ Then in a view, wrap your fields in a Stimulus controller container:
25
+
26
+ <div data-controller="country-state-select">
27
+ <%= f.country_select :country %>
28
+ <%= f.state_select :state, :country %>
29
+ </div>
30
+
31
+ See https://github.com/arvindvyas/Country-State-Select#readme for the full
32
+ guide, including simple_form usage and the legacy jQuery/Chosen path.
33
+
34
+ ===============================================================================
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ CountryStateSelect.configure do |config|
4
+ # ISO codes shown first in the country dropdown.
5
+ # config.priority_countries = %w[US CA GB]
6
+
7
+ # Restrict the country list instead of showing all ~250 countries.
8
+ # config.only_countries = %w[US CA GB IN AU]
9
+ # config.except_countries = %w[KP]
10
+
11
+ # Decorate country labels.
12
+ # config.flags = true # "🇮🇳 India"
13
+ # config.dial_codes = true # "India (+91)"
14
+
15
+ # Translate country names via I18n / the `countries` gem, when available.
16
+ # config.localize_names = true
17
+
18
+ # How long the JSON lookup endpoints are HTTP-cached, in seconds.
19
+ # Set to nil to disable caching.
20
+ # config.cache_duration = 1.day
21
+
22
+ # Set to false if you want to mount the engine at a custom path instead
23
+ # of the default top-level /find_states, /find_cities routes.
24
+ # config.draw_routes = false
25
+
26
+ # Swap in your own data source (a DB table, the `countries` gem, ...) by
27
+ # implementing CountryStateSelect::DataSources::Base and assigning it here.
28
+ # config.data_source = MyApp::CountryDataSource.new
29
+ end