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: 8e4c80e86756e81b40665de3fe0c6436ee462b8d5ea011992e3a8507eaedb323
4
- data.tar.gz: 8ce58cb67fc8b73d92ba3b945b303a5c7360265a6ac217e5c0e1e1806c2157b7
3
+ metadata.gz: a6cddadddef5d95e3c070bd31fb0399be628771ee521156dc1432c196cf938f7
4
+ data.tar.gz: da7202512a00127735002e1b8f0eb143a5e2466d8e712895f07cbe5eb11831cf
5
5
  SHA512:
6
- metadata.gz: ca51d0d37018f62fceba7ac87b2f0ebd42f4e537b55d9814e33488b48ea30aa822fd18eef92713d4a9b464e8849298e91ca41b430d1f99d471aba28b371b494b
7
- data.tar.gz: dba580ca4a43fe7b83e776cf4e8430a8ca19cf9fc97f0f059c423fdd4e6ae6117f9c1dd702b4ccf878a13f3a848ac271041b62e3350d9143b230cc16523f1d33
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, reformatted to national format if what's there
243
- parses validly, the same reformat-on-blur behavior as the plain
244
- (non-`live`) helper.
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
- data = PicoPhone::Rails.live_validation_data(resolved_region, @template.pico_phone_rails.validate_path)
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
- if (data.valid) this.element.value = data.national
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
- phone_number = PicoPhone.parse(phone, params[:region].presence)
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PicoPhone
4
4
  module Rails
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pico_phone-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gabi Jack