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
|
@@ -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
|
data/lib/country_state_select.rb
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'yaml'
|
|
3
4
|
require 'country_state_select/version'
|
|
4
|
-
require '
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
52
|
-
|
|
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 /^[
|
|
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
|
|
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
|