pico_phone-rails 0.4.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 +4 -4
- data/README.md +70 -0
- data/config/routes.rb +5 -0
- data/lib/pico_phone/rails/engine.rb +26 -0
- data/lib/pico_phone/rails/form_helper.rb +31 -2
- data/lib/pico_phone/rails/javascript/pico_phone/rails/phone_controller.js +39 -0
- data/lib/pico_phone/rails/javascript.rb +3 -0
- data/lib/pico_phone/rails/validations_controller.rb +75 -0
- data/lib/pico_phone/rails/version.rb +1 -1
- data/lib/pico_phone/rails.rb +1 -0
- metadata +13 -4
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
|
@@ -206,6 +206,76 @@ a plain `<input type="tel">` with no formatting) -- redefining a core Rails
|
|
|
206
206
|
helper would silently change behavior for every `phone_field` call in an
|
|
207
207
|
app, not just ones backed by a PicoPhone-managed attribute.
|
|
208
208
|
|
|
209
|
+
### Live validation
|
|
210
|
+
|
|
211
|
+
Mount the engine:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# config/routes.rb
|
|
215
|
+
mount PicoPhone::Rails::Engine, at: "/pico_phone"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The controller is auto-pinned into `importmap-rails` when present, but
|
|
219
|
+
still needs to be registered with your Stimulus application -- pinning
|
|
220
|
+
only makes it importable, the same as any other pinned module:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
// app/javascript/controllers/index.js
|
|
224
|
+
import { application } from "controllers/application"
|
|
225
|
+
import PhoneController from "pico_phone/rails/phone_controller"
|
|
226
|
+
application.register("phone", PhoneController)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Then opt in per field:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
<%= f.pico_phone_field :phone, region: "US", live: true %>
|
|
233
|
+
<span data-phone-target="error"></span>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Debounces input and POSTs to the mounted engine, showing the error message
|
|
237
|
+
in a sibling `data-phone-target="error"` element (anywhere under the same
|
|
238
|
+
parent as the field -- `data-controller` sits on the `<input>` itself,
|
|
239
|
+
which can't have descendants, so the target is found via the field's
|
|
240
|
+
parent rather than Stimulus's usual descendant-scoped target lookup)
|
|
241
|
+
while the user types. Never rewrites the field's value while it has
|
|
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.
|
|
273
|
+
|
|
274
|
+
`ValidationsController` uses your app's normal CSRF protection -- the
|
|
275
|
+
controller reads the token from the page's `<meta name="csrf-token">`
|
|
276
|
+
(rendered by Rails' own `csrf_meta_tags`, already in any standard layout)
|
|
277
|
+
and sends it with every request, no extra setup needed.
|
|
278
|
+
|
|
209
279
|
### ActiveJob serializer
|
|
210
280
|
|
|
211
281
|
```ruby
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
require "pico_phone/rails/validations_controller"
|
|
5
|
+
|
|
6
|
+
module PicoPhone
|
|
7
|
+
module Rails
|
|
8
|
+
class Engine < ::Rails::Engine
|
|
9
|
+
isolate_namespace PicoPhone::Rails
|
|
10
|
+
|
|
11
|
+
initializer "pico_phone_rails.assets" do |app|
|
|
12
|
+
app.config.assets.paths << Pathname.new(__dir__).join("javascript") if app.config.respond_to?(:assets)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# importmap-rails draws config.importmap.paths in its own "importmap"
|
|
16
|
+
# initializer -- appending after that is a no-op, so this must run before.
|
|
17
|
+
# A no-op itself if importmap-rails isn't installed.
|
|
18
|
+
initializer "pico_phone_rails.importmap", before: "importmap" do |app|
|
|
19
|
+
next unless app.config.respond_to?(:importmap)
|
|
20
|
+
|
|
21
|
+
app.config.importmap.paths << Pathname.new(__dir__).join("javascript.rb")
|
|
22
|
+
app.config.importmap.cache_sweepers << Pathname.new(__dir__).join("javascript")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -22,6 +22,22 @@ module PicoPhone
|
|
|
22
22
|
phone_number.valid? ? phone_number.national : string
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# @param region [String, nil]
|
|
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)
|
|
30
|
+
# @return [Hash] Stimulus data attributes for +live:+ validation
|
|
31
|
+
def self.live_validation_data(region, validate_path, strict: true)
|
|
32
|
+
{
|
|
33
|
+
controller: "phone",
|
|
34
|
+
action: "input->phone#validate blur->phone#reformat",
|
|
35
|
+
phone_region_value: region.to_s,
|
|
36
|
+
phone_strict_value: strict,
|
|
37
|
+
phone_url_value: validate_path
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
25
41
|
# Included into ActionView::Base by the railtie once ActionView loads.
|
|
26
42
|
# Adds +pico_phone_field_tag+, a +text_field_tag+-like helper that
|
|
27
43
|
# displays national format for a valid number while leaving the
|
|
@@ -39,9 +55,15 @@ module PicoPhone
|
|
|
39
55
|
# @param name [String, Symbol]
|
|
40
56
|
# @param value [String, PicoPhone::PhoneNumber, nil]
|
|
41
57
|
# @param region [String, nil] ISO 3166-1 alpha-2 region for interpreting +value+ when it's a raw string
|
|
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
|
|
42
60
|
# @return [String] an HTML-safe +<input type="tel">+ tag
|
|
43
|
-
def pico_phone_field_tag(name, value = nil, region: nil, **options)
|
|
61
|
+
def pico_phone_field_tag(name, value = nil, region: nil, live: false, strict: true, **options)
|
|
44
62
|
display_value = PicoPhone::Rails.phone_field_display_value(value, region)
|
|
63
|
+
if live
|
|
64
|
+
data = PicoPhone::Rails.live_validation_data(region, pico_phone_rails.validate_path, strict: strict)
|
|
65
|
+
options[:data] = data.merge(options[:data] || {})
|
|
66
|
+
end
|
|
45
67
|
text_field_tag(name, display_value, options.merge(type: "tel"))
|
|
46
68
|
end
|
|
47
69
|
end
|
|
@@ -59,10 +81,17 @@ module PicoPhone
|
|
|
59
81
|
# raw value when it isn't already a {PicoPhone::PhoneNumber} -- a String is used as-is, a Symbol is
|
|
60
82
|
# called as an instance method on the form's object, a Proc is called with the object, same resolution
|
|
61
83
|
# rules as {Extraction.extract_phone_numbers_from} and {PhoneSearchIndex.maintain_phone_search_index}
|
|
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
|
|
62
86
|
# @return [String] an HTML-safe +<input type="tel">+ tag
|
|
63
|
-
def pico_phone_field(method, region: nil, **options)
|
|
87
|
+
def pico_phone_field(method, region: nil, live: false, strict: true, **options)
|
|
64
88
|
resolved_region = PicoPhone::Rails.resolve_region(region, object)
|
|
65
89
|
display_value = PicoPhone::Rails.phone_field_display_value(object.public_send(method), resolved_region)
|
|
90
|
+
if live
|
|
91
|
+
validate_path = @template.pico_phone_rails.validate_path
|
|
92
|
+
data = PicoPhone::Rails.live_validation_data(resolved_region, validate_path, strict: strict)
|
|
93
|
+
options[:data] = data.merge(options[:data] || {})
|
|
94
|
+
end
|
|
66
95
|
text_field(method, options.merge(value: display_value, type: "tel"))
|
|
67
96
|
end
|
|
68
97
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// data-controller sits on the <input> itself, which can't have children --
|
|
4
|
+
// so the error element (a sibling, placed wherever the caller wants) is
|
|
5
|
+
// unreachable via Stimulus's normal descendant-scoped static targets.
|
|
6
|
+
// Found by hand instead, scoped to the input's parent.
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static values = { url: String, region: String, strict: { type: Boolean, default: true }, debounce: { type: Number, default: 300 } }
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.errorElement = this.element.parentElement?.querySelector('[data-phone-target="error"]') ?? null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
validate() {
|
|
15
|
+
clearTimeout(this.timeout)
|
|
16
|
+
this.timeout = setTimeout(() => this.check(), this.debounceValue)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async reformat() {
|
|
20
|
+
const data = await this.check()
|
|
21
|
+
const formatted = data.national ?? data.international
|
|
22
|
+
if (formatted) this.element.value = formatted
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async check() {
|
|
26
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" }
|
|
27
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
28
|
+
if (csrfToken) headers["X-CSRF-Token"] = csrfToken
|
|
29
|
+
|
|
30
|
+
const response = await fetch(this.urlValue, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers,
|
|
33
|
+
body: JSON.stringify({ phone: this.element.value, region: this.regionValue, strict: this.strictValue }),
|
|
34
|
+
})
|
|
35
|
+
const data = await response.json()
|
|
36
|
+
if (this.errorElement) this.errorElement.textContent = data.valid || data.blank ? "" : data.message
|
|
37
|
+
return data
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller"
|
|
4
|
+
|
|
5
|
+
module PicoPhone
|
|
6
|
+
module Rails
|
|
7
|
+
# `region:` always arrives as an already-resolved String -- no record lookup.
|
|
8
|
+
class ValidationsController < ActionController::Base
|
|
9
|
+
def validate
|
|
10
|
+
phone = params[:phone].to_s
|
|
11
|
+
return render json: { valid: false, blank: true } if phone.strip.empty?
|
|
12
|
+
|
|
13
|
+
render json: response_for(phone, params[:region].presence)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
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
|
+
|
|
67
|
+
# @return [String, nil] nil if unparseable, matching PhoneSearchIndex's guard
|
|
68
|
+
def safe_phone_call(phone_number, method_name)
|
|
69
|
+
phone_number.public_send(method_name)
|
|
70
|
+
rescue TypeError
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/pico_phone/rails.rb
CHANGED
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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabi Jack
|
|
@@ -56,8 +56,11 @@ description: 'pico_phone-rails wires the pico_phone gem into Rails: a :phone_num
|
|
|
56
56
|
class macro that rewrites phone attributes to E.164 before validation, extract_phone_numbers_from
|
|
57
57
|
for pulling phone numbers out of free text (with an optional persisted backend for
|
|
58
58
|
cross-record search), maintain_phone_search_index for keeping search columns in
|
|
59
|
-
sync on an existing phone-number table,
|
|
60
|
-
|
|
59
|
+
sync on an existing phone-number table, pico_phone_field_tag/f.pico_phone_field
|
|
60
|
+
form helpers that display national format for a valid number without discarding
|
|
61
|
+
what the user typed, live: true on those same helpers for debounced Stimulus-driven
|
|
62
|
+
validation and reformatting via a mountable engine, and an ActiveJob serializer
|
|
63
|
+
so a PhoneNumber survives being passed as a job argument.'
|
|
61
64
|
email:
|
|
62
65
|
- gabi@gabijack.com
|
|
63
66
|
executables: []
|
|
@@ -66,6 +69,7 @@ extra_rdoc_files: []
|
|
|
66
69
|
files:
|
|
67
70
|
- LICENSE.txt
|
|
68
71
|
- README.md
|
|
72
|
+
- config/routes.rb
|
|
69
73
|
- lib/generators/pico_phone/rails/extracted_phone_numbers/extracted_phone_numbers_generator.rb
|
|
70
74
|
- lib/generators/pico_phone/rails/extracted_phone_numbers/templates/create_pico_phone_rails_extracted_phone_numbers.rb.tt
|
|
71
75
|
- lib/generators/pico_phone/rails/phone_number/phone_number_generator.rb
|
|
@@ -73,9 +77,12 @@ files:
|
|
|
73
77
|
- lib/generators/pico_phone/rails/phone_number/templates/model.rb.tt
|
|
74
78
|
- lib/phone_validator.rb
|
|
75
79
|
- lib/pico_phone/rails.rb
|
|
80
|
+
- lib/pico_phone/rails/engine.rb
|
|
76
81
|
- lib/pico_phone/rails/extracted_phone_number.rb
|
|
77
82
|
- lib/pico_phone/rails/extraction.rb
|
|
78
83
|
- lib/pico_phone/rails/form_helper.rb
|
|
84
|
+
- lib/pico_phone/rails/javascript.rb
|
|
85
|
+
- lib/pico_phone/rails/javascript/pico_phone/rails/phone_controller.js
|
|
79
86
|
- lib/pico_phone/rails/locale/en.yml
|
|
80
87
|
- lib/pico_phone/rails/normalizer.rb
|
|
81
88
|
- lib/pico_phone/rails/phone_search_index.rb
|
|
@@ -83,6 +90,7 @@ files:
|
|
|
83
90
|
- lib/pico_phone/rails/region_resolution.rb
|
|
84
91
|
- lib/pico_phone/rails/serializers/phone_number_serializer.rb
|
|
85
92
|
- lib/pico_phone/rails/type.rb
|
|
93
|
+
- lib/pico_phone/rails/validations_controller.rb
|
|
86
94
|
- lib/pico_phone/rails/version.rb
|
|
87
95
|
homepage: https://github.com/gjack/pico_phone-rails
|
|
88
96
|
licenses:
|
|
@@ -111,5 +119,6 @@ requirements: []
|
|
|
111
119
|
rubygems_version: 3.6.9
|
|
112
120
|
specification_version: 4
|
|
113
121
|
summary: 'Rails integration for pico_phone: attribute type, validator, normalizer,
|
|
114
|
-
free-text extraction, phone search index,
|
|
122
|
+
free-text extraction, phone search index, form field helpers, live validation, and
|
|
123
|
+
ActiveJob serializer'
|
|
115
124
|
test_files: []
|