pico_phone-rails 0.5.0 → 0.6.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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6cddadddef5d95e3c070bd31fb0399be628771ee521156dc1432c196cf938f7
|
|
4
|
+
data.tar.gz: da7202512a00127735002e1b8f0eb143a5e2466d8e712895f07cbe5eb11831cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d508fcda421a174e0d201aa23abf472534000a8f2f878e813bc0f9bba504545c93271bbfeb4283ecbcb960ad74d20f483406938d7a965f16ff1f0c24379cbc3c
|
|
7
|
+
data.tar.gz: 18ee00d8bc5a2cf4a6b02a22ed3f69249fc83cdeb3117826beb32afd4627d26d317893ecb5bec775f800273eecbf9170641f57661a3e28fb3717ca5d63bbf49b
|
data/README.md
CHANGED
|
@@ -239,9 +239,37 @@ parent as the field -- `data-controller` sits on the `<input>` itself,
|
|
|
239
239
|
which can't have descendants, so the target is found via the field's
|
|
240
240
|
parent rather than Stimulus's usual descendant-scoped target lookup)
|
|
241
241
|
while the user types. Never rewrites the field's value while it has
|
|
242
|
-
focus -- only on blur
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
focus -- only on blur.
|
|
243
|
+
|
|
244
|
+
On blur, a number that matches `region:` reformats to national format, the
|
|
245
|
+
same as the plain (non-`live`) helper. A number that's valid for a
|
|
246
|
+
*different* country -- one that carries its own explicit signal, a leading
|
|
247
|
+
`+` or a recognized IDD exit code (e.g. `"011 44 20 7946 0958"` dialed out
|
|
248
|
+
of a US-configured field) -- reformats to **international** format instead,
|
|
249
|
+
so it's clearly shown as a foreign number rather than misleadingly bare
|
|
250
|
+
national-style digits. A bare national-style number with no country signal
|
|
251
|
+
of its own (e.g. `"020 7946 0958"` typed into a `region: "US"` field) is
|
|
252
|
+
left exactly as typed -- there's no reliable way to tell which of several
|
|
253
|
+
countries it might belong to from the digits alone, and guessing wrong
|
|
254
|
+
would mean showing the user a different, real phone number than the one
|
|
255
|
+
they meant.
|
|
256
|
+
|
|
257
|
+
Whether a cross-country match also clears the error is controlled by
|
|
258
|
+
`strict:`:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
<%= f.pico_phone_field :phone, region: "US", live: true, strict: false %>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- `strict: true` (default) -- only a same-region match clears the error.
|
|
265
|
+
A number valid for a different country still reformats to international,
|
|
266
|
+
but the error stays, matching a validator that enforces `region:`.
|
|
267
|
+
- `strict: false` -- any globally-valid number clears the error, matching
|
|
268
|
+
a validator that accepts any country (no `region:` option, or
|
|
269
|
+
`possible:` with no region constraint). **Keep `strict:` in sync with
|
|
270
|
+
whether the model's own `PhoneValidator` sets `region:`** -- pairing
|
|
271
|
+
`strict: false` here with a validator that still enforces `region:`
|
|
272
|
+
shows no error live, but the record still fails validation on submit.
|
|
245
273
|
|
|
246
274
|
`ValidationsController` uses your app's normal CSRF protection -- the
|
|
247
275
|
controller reads the token from the page's `<meta name="csrf-token">`
|
|
@@ -24,12 +24,16 @@ module PicoPhone
|
|
|
24
24
|
|
|
25
25
|
# @param region [String, nil]
|
|
26
26
|
# @param validate_path [String] the mounted engine's validate endpoint
|
|
27
|
+
# @param strict [Boolean] whether a number valid for a different country than +region+
|
|
28
|
+
# still counts as invalid (matching a validator that enforces +region:+) or clears
|
|
29
|
+
# the error (matching a validator that accepts any valid number)
|
|
27
30
|
# @return [Hash] Stimulus data attributes for +live:+ validation
|
|
28
|
-
def self.live_validation_data(region, validate_path)
|
|
31
|
+
def self.live_validation_data(region, validate_path, strict: true)
|
|
29
32
|
{
|
|
30
33
|
controller: "phone",
|
|
31
34
|
action: "input->phone#validate blur->phone#reformat",
|
|
32
35
|
phone_region_value: region.to_s,
|
|
36
|
+
phone_strict_value: strict,
|
|
33
37
|
phone_url_value: validate_path
|
|
34
38
|
}
|
|
35
39
|
end
|
|
@@ -52,11 +56,12 @@ module PicoPhone
|
|
|
52
56
|
# @param value [String, PicoPhone::PhoneNumber, nil]
|
|
53
57
|
# @param region [String, nil] ISO 3166-1 alpha-2 region for interpreting +value+ when it's a raw string
|
|
54
58
|
# @param live [Boolean] wire up debounced validation/reformatting; requires the engine to be mounted
|
|
59
|
+
# @param strict [Boolean] see {PicoPhone::Rails.live_validation_data}; only relevant when +live:+ is true
|
|
55
60
|
# @return [String] an HTML-safe +<input type="tel">+ tag
|
|
56
|
-
def pico_phone_field_tag(name, value = nil, region: nil, live: false, **options)
|
|
61
|
+
def pico_phone_field_tag(name, value = nil, region: nil, live: false, strict: true, **options)
|
|
57
62
|
display_value = PicoPhone::Rails.phone_field_display_value(value, region)
|
|
58
63
|
if live
|
|
59
|
-
data = PicoPhone::Rails.live_validation_data(region, pico_phone_rails.validate_path)
|
|
64
|
+
data = PicoPhone::Rails.live_validation_data(region, pico_phone_rails.validate_path, strict: strict)
|
|
60
65
|
options[:data] = data.merge(options[:data] || {})
|
|
61
66
|
end
|
|
62
67
|
text_field_tag(name, display_value, options.merge(type: "tel"))
|
|
@@ -77,12 +82,14 @@ module PicoPhone
|
|
|
77
82
|
# called as an instance method on the form's object, a Proc is called with the object, same resolution
|
|
78
83
|
# rules as {Extraction.extract_phone_numbers_from} and {PhoneSearchIndex.maintain_phone_search_index}
|
|
79
84
|
# @param live [Boolean] wire up debounced validation/reformatting; requires the engine to be mounted
|
|
85
|
+
# @param strict [Boolean] see {PicoPhone::Rails.live_validation_data}; only relevant when +live:+ is true
|
|
80
86
|
# @return [String] an HTML-safe +<input type="tel">+ tag
|
|
81
|
-
def pico_phone_field(method, region: nil, live: false, **options)
|
|
87
|
+
def pico_phone_field(method, region: nil, live: false, strict: true, **options)
|
|
82
88
|
resolved_region = PicoPhone::Rails.resolve_region(region, object)
|
|
83
89
|
display_value = PicoPhone::Rails.phone_field_display_value(object.public_send(method), resolved_region)
|
|
84
90
|
if live
|
|
85
|
-
|
|
91
|
+
validate_path = @template.pico_phone_rails.validate_path
|
|
92
|
+
data = PicoPhone::Rails.live_validation_data(resolved_region, validate_path, strict: strict)
|
|
86
93
|
options[:data] = data.merge(options[:data] || {})
|
|
87
94
|
end
|
|
88
95
|
text_field(method, options.merge(value: display_value, type: "tel"))
|
|
@@ -5,7 +5,7 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
5
5
|
// unreachable via Stimulus's normal descendant-scoped static targets.
|
|
6
6
|
// Found by hand instead, scoped to the input's parent.
|
|
7
7
|
export default class extends Controller {
|
|
8
|
-
static values = { url: String, region: String, debounce: { type: Number, default: 300 } }
|
|
8
|
+
static values = { url: String, region: String, strict: { type: Boolean, default: true }, debounce: { type: Number, default: 300 } }
|
|
9
9
|
|
|
10
10
|
connect() {
|
|
11
11
|
this.errorElement = this.element.parentElement?.querySelector('[data-phone-target="error"]') ?? null
|
|
@@ -18,7 +18,8 @@ export default class extends Controller {
|
|
|
18
18
|
|
|
19
19
|
async reformat() {
|
|
20
20
|
const data = await this.check()
|
|
21
|
-
|
|
21
|
+
const formatted = data.national ?? data.international
|
|
22
|
+
if (formatted) this.element.value = formatted
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async check() {
|
|
@@ -29,7 +30,7 @@ export default class extends Controller {
|
|
|
29
30
|
const response = await fetch(this.urlValue, {
|
|
30
31
|
method: "POST",
|
|
31
32
|
headers,
|
|
32
|
-
body: JSON.stringify({ phone: this.element.value, region: this.regionValue }),
|
|
33
|
+
body: JSON.stringify({ phone: this.element.value, region: this.regionValue, strict: this.strictValue }),
|
|
33
34
|
})
|
|
34
35
|
const data = await response.json()
|
|
35
36
|
if (this.errorElement) this.errorElement.textContent = data.valid || data.blank ? "" : data.message
|
|
@@ -10,22 +10,60 @@ module PicoPhone
|
|
|
10
10
|
phone = params[:phone].to_s
|
|
11
11
|
return render json: { valid: false, blank: true } if phone.strip.empty?
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if phone_number.valid?
|
|
16
|
-
render json: {
|
|
17
|
-
valid: true,
|
|
18
|
-
blank: false,
|
|
19
|
-
e164: safe_phone_call(phone_number, :e164),
|
|
20
|
-
national: safe_phone_call(phone_number, :national)
|
|
21
|
-
}
|
|
22
|
-
else
|
|
23
|
-
render json: { valid: false, blank: false, message: I18n.t("errors.messages.invalid_phone") }
|
|
24
|
-
end
|
|
13
|
+
render json: response_for(phone, params[:region].presence)
|
|
25
14
|
end
|
|
26
15
|
|
|
27
16
|
private
|
|
28
17
|
|
|
18
|
+
def response_for(phone, region)
|
|
19
|
+
regional = PicoPhone.parse(phone, region)
|
|
20
|
+
return regional_payload(regional) if matches_region?(regional, region)
|
|
21
|
+
|
|
22
|
+
# `regional`'s #valid?/#country are scoped to `region` even when the input carries its
|
|
23
|
+
# own country code (a leading "+" or a recognized IDD exit code, e.g. US's "011") --
|
|
24
|
+
# #valid_countries isn't, since it works off the actually-extracted country code, and
|
|
25
|
+
# #e164/#international are unaffected by the scoping either. No second parse needed.
|
|
26
|
+
# A bare national-style number with no country code of its own (e.g. "020 7946 0958"
|
|
27
|
+
# typed into a region: "US" field) stays invalid here -- #valid_countries comes back
|
|
28
|
+
# empty because there's no signal pointing at any specific country to check against.
|
|
29
|
+
return invalid_payload if regional.valid_countries.empty?
|
|
30
|
+
|
|
31
|
+
cross_country_payload(regional)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def matches_region?(phone_number, region)
|
|
35
|
+
region ? phone_number.valid_for_country?(region) : phone_number.valid?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def regional_payload(phone_number)
|
|
39
|
+
{
|
|
40
|
+
valid: true,
|
|
41
|
+
blank: false,
|
|
42
|
+
e164: safe_phone_call(phone_number, :e164),
|
|
43
|
+
national: safe_phone_call(phone_number, :national)
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cross_country_payload(phone_number)
|
|
48
|
+
strict = params.fetch(:strict, true)
|
|
49
|
+
payload = {
|
|
50
|
+
valid: !strict,
|
|
51
|
+
blank: false,
|
|
52
|
+
e164: safe_phone_call(phone_number, :e164),
|
|
53
|
+
international: safe_phone_call(phone_number, :international)
|
|
54
|
+
}
|
|
55
|
+
payload[:message] = invalid_message if strict
|
|
56
|
+
payload
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def invalid_payload
|
|
60
|
+
{ valid: false, blank: false, message: invalid_message }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def invalid_message
|
|
64
|
+
I18n.t("errors.messages.invalid_phone")
|
|
65
|
+
end
|
|
66
|
+
|
|
29
67
|
# @return [String, nil] nil if unparseable, matching PhoneSearchIndex's guard
|
|
30
68
|
def safe_phone_call(phone_number, method_name)
|
|
31
69
|
phone_number.public_send(method_name)
|