pico_phone-rails 0.3.0 → 0.5.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 +67 -0
- data/config/routes.rb +5 -0
- data/lib/pico_phone/rails/engine.rb +26 -0
- data/lib/pico_phone/rails/form_helper.rb +92 -0
- data/lib/pico_phone/rails/javascript/pico_phone/rails/phone_controller.js +38 -0
- data/lib/pico_phone/rails/javascript.rb +3 -0
- data/lib/pico_phone/rails/railtie.rb +8 -0
- data/lib/pico_phone/rails/validations_controller.rb +37 -0
- data/lib/pico_phone/rails/version.rb +1 -1
- data/lib/pico_phone/rails.rb +1 -0
- metadata +14 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e4c80e86756e81b40665de3fe0c6436ee462b8d5ea011992e3a8507eaedb323
|
|
4
|
+
data.tar.gz: 8ce58cb67fc8b73d92ba3b945b303a5c7360265a6ac217e5c0e1e1806c2157b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca51d0d37018f62fceba7ac87b2f0ebd42f4e537b55d9814e33488b48ea30aa822fd18eef92713d4a9b464e8849298e91ca41b430d1f99d471aba28b371b494b
|
|
7
|
+
data.tar.gz: dba580ca4a43fe7b83e776cf4e8430a8ca19cf9fc97f0f059c423fdd4e6ae6117f9c1dd702b4ccf878a13f3a848ac271041b62e3350d9143b230cc16523f1d33
|
data/README.md
CHANGED
|
@@ -181,6 +181,73 @@ reasoning as `containing_phone_number` above.
|
|
|
181
181
|
every model, so identical names would collide and one would silently shadow
|
|
182
182
|
the other.)
|
|
183
183
|
|
|
184
|
+
### Form field helper
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
<%= pico_phone_field_tag :phone, @contact.phone, region: "US" %>
|
|
188
|
+
<%= f.pico_phone_field :phone, region: "US" %>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Renders an `<input type="tel">` showing the number in national format when
|
|
192
|
+
it parses validly, or exactly what the user typed otherwise -- so re-editing
|
|
193
|
+
an invalid or partial number never shows a blank or garbled reformat. Works
|
|
194
|
+
whether the attribute is a plain string or already a `PicoPhone::PhoneNumber`
|
|
195
|
+
(via the attribute type above), and accepts `region:` as a String, Symbol
|
|
196
|
+
(instance method on the form's object), or Proc, same resolution rules as
|
|
197
|
+
extraction and the search index:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
f.pico_phone_field :phone, region: ->(contact) { contact.organization.region }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Named `pico_phone_field`/`pico_phone_field_tag` rather than Rails' own
|
|
204
|
+
`phone_field`/`f.phone_field` (an existing core alias for `telephone_field`,
|
|
205
|
+
a plain `<input type="tel">` with no formatting) -- redefining a core Rails
|
|
206
|
+
helper would silently change behavior for every `phone_field` call in an
|
|
207
|
+
app, not just ones backed by a PicoPhone-managed attribute.
|
|
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, reformatted to national format if what's there
|
|
243
|
+
parses validly, the same reformat-on-blur behavior as the plain
|
|
244
|
+
(non-`live`) helper.
|
|
245
|
+
|
|
246
|
+
`ValidationsController` uses your app's normal CSRF protection -- the
|
|
247
|
+
controller reads the token from the page's `<meta name="csrf-token">`
|
|
248
|
+
(rendered by Rails' own `csrf_meta_tags`, already in any standard layout)
|
|
249
|
+
and sends it with every request, no extra setup needed.
|
|
250
|
+
|
|
184
251
|
### ActiveJob serializer
|
|
185
252
|
|
|
186
253
|
```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
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PicoPhone
|
|
4
|
+
module Rails
|
|
5
|
+
# Computes what a phone field should display: the number's national
|
|
6
|
+
# format if it parses validly, the raw string otherwise -- so a user
|
|
7
|
+
# re-editing an invalid or partial number sees exactly what they typed,
|
|
8
|
+
# not a blank or garbled reformat. Shared by {FormHelper} and
|
|
9
|
+
# {FormBuilderExtension}.
|
|
10
|
+
#
|
|
11
|
+
# @param value [String, PicoPhone::PhoneNumber, nil]
|
|
12
|
+
# @param region [String, nil] ISO 3166-1 alpha-2 region for interpreting +value+ when it's a raw string
|
|
13
|
+
# @return [String, nil]
|
|
14
|
+
def self.phone_field_display_value(value, region)
|
|
15
|
+
return nil if value.nil?
|
|
16
|
+
return (value.valid? ? value.national : value.to_s) if value.is_a?(PicoPhone::PhoneNumber)
|
|
17
|
+
|
|
18
|
+
string = value.to_s
|
|
19
|
+
return string if string.empty?
|
|
20
|
+
|
|
21
|
+
phone_number = PicoPhone.parse(string, region)
|
|
22
|
+
phone_number.valid? ? phone_number.national : string
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param region [String, nil]
|
|
26
|
+
# @param validate_path [String] the mounted engine's validate endpoint
|
|
27
|
+
# @return [Hash] Stimulus data attributes for +live:+ validation
|
|
28
|
+
def self.live_validation_data(region, validate_path)
|
|
29
|
+
{
|
|
30
|
+
controller: "phone",
|
|
31
|
+
action: "input->phone#validate blur->phone#reformat",
|
|
32
|
+
phone_region_value: region.to_s,
|
|
33
|
+
phone_url_value: validate_path
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Included into ActionView::Base by the railtie once ActionView loads.
|
|
38
|
+
# Adds +pico_phone_field_tag+, a +text_field_tag+-like helper that
|
|
39
|
+
# displays national format for a valid number while leaving the
|
|
40
|
+
# underlying +<input>+ free to submit whatever string the user types.
|
|
41
|
+
#
|
|
42
|
+
# Named distinctly from Rails' own +phone_field_tag+/+f.phone_field+
|
|
43
|
+
# (a built-in alias for +telephone_field+, plain +<input type="tel">+
|
|
44
|
+
# with no formatting) rather than overriding it -- redefining a core
|
|
45
|
+
# Rails helper would silently change behavior for every +phone_field+
|
|
46
|
+
# call in an app, not just ones backed by a PicoPhone-managed attribute.
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# pico_phone_field_tag :phone, @contact.phone, region: "US"
|
|
50
|
+
module FormHelper
|
|
51
|
+
# @param name [String, Symbol]
|
|
52
|
+
# @param value [String, PicoPhone::PhoneNumber, nil]
|
|
53
|
+
# @param region [String, nil] ISO 3166-1 alpha-2 region for interpreting +value+ when it's a raw string
|
|
54
|
+
# @param live [Boolean] wire up debounced validation/reformatting; requires the engine to be mounted
|
|
55
|
+
# @return [String] an HTML-safe +<input type="tel">+ tag
|
|
56
|
+
def pico_phone_field_tag(name, value = nil, region: nil, live: false, **options)
|
|
57
|
+
display_value = PicoPhone::Rails.phone_field_display_value(value, region)
|
|
58
|
+
if live
|
|
59
|
+
data = PicoPhone::Rails.live_validation_data(region, pico_phone_rails.validate_path)
|
|
60
|
+
options[:data] = data.merge(options[:data] || {})
|
|
61
|
+
end
|
|
62
|
+
text_field_tag(name, display_value, options.merge(type: "tel"))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Included into ActionView::Helpers::FormBuilder by the railtie once
|
|
67
|
+
# ActionView loads. Adds +f.pico_phone_field+, mirroring
|
|
68
|
+
# {FormHelper#pico_phone_field_tag} but bound to the form's object.
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# f.pico_phone_field :phone, region: "US"
|
|
72
|
+
# f.pico_phone_field :phone, region: ->(contact) { contact.organization.region }
|
|
73
|
+
module FormBuilderExtension
|
|
74
|
+
# @param method [Symbol] the attribute to render
|
|
75
|
+
# @param region [String, Symbol, Proc, nil] ISO 3166-1 alpha-2 region for interpreting the attribute's
|
|
76
|
+
# raw value when it isn't already a {PicoPhone::PhoneNumber} -- a String is used as-is, a Symbol is
|
|
77
|
+
# called as an instance method on the form's object, a Proc is called with the object, same resolution
|
|
78
|
+
# rules as {Extraction.extract_phone_numbers_from} and {PhoneSearchIndex.maintain_phone_search_index}
|
|
79
|
+
# @param live [Boolean] wire up debounced validation/reformatting; requires the engine to be mounted
|
|
80
|
+
# @return [String] an HTML-safe +<input type="tel">+ tag
|
|
81
|
+
def pico_phone_field(method, region: nil, live: false, **options)
|
|
82
|
+
resolved_region = PicoPhone::Rails.resolve_region(region, object)
|
|
83
|
+
display_value = PicoPhone::Rails.phone_field_display_value(object.public_send(method), resolved_region)
|
|
84
|
+
if live
|
|
85
|
+
data = PicoPhone::Rails.live_validation_data(resolved_region, @template.pico_phone_rails.validate_path)
|
|
86
|
+
options[:data] = data.merge(options[:data] || {})
|
|
87
|
+
end
|
|
88
|
+
text_field(method, options.merge(value: display_value, type: "tel"))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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, 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
|
+
if (data.valid) this.element.value = data.national
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async check() {
|
|
25
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" }
|
|
26
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
27
|
+
if (csrfToken) headers["X-CSRF-Token"] = csrfToken
|
|
28
|
+
|
|
29
|
+
const response = await fetch(this.urlValue, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify({ phone: this.element.value, region: this.regionValue }),
|
|
33
|
+
})
|
|
34
|
+
const data = await response.json()
|
|
35
|
+
if (this.errorElement) this.errorElement.textContent = data.valid || data.blank ? "" : data.message
|
|
36
|
+
return data
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -37,6 +37,14 @@ module PicoPhone
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
initializer "pico_phone_rails.form_helper" do
|
|
41
|
+
ActiveSupport.on_load(:action_view) do
|
|
42
|
+
require "pico_phone/rails/form_helper"
|
|
43
|
+
include PicoPhone::Rails::FormHelper
|
|
44
|
+
::ActionView::Helpers::FormBuilder.include PicoPhone::Rails::FormBuilderExtension
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
40
48
|
initializer "pico_phone_rails.i18n" do |app|
|
|
41
49
|
app.config.i18n.load_path += Dir[File.expand_path("locale/*.yml", __dir__)]
|
|
42
50
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
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
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] nil if unparseable, matching PhoneSearchIndex's guard
|
|
30
|
+
def safe_phone_call(phone_number, method_name)
|
|
31
|
+
phone_number.public_send(method_name)
|
|
32
|
+
rescue TypeError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
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.5.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,8 +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
|
|
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
|
|
78
86
|
- lib/pico_phone/rails/locale/en.yml
|
|
79
87
|
- lib/pico_phone/rails/normalizer.rb
|
|
80
88
|
- lib/pico_phone/rails/phone_search_index.rb
|
|
@@ -82,6 +90,7 @@ files:
|
|
|
82
90
|
- lib/pico_phone/rails/region_resolution.rb
|
|
83
91
|
- lib/pico_phone/rails/serializers/phone_number_serializer.rb
|
|
84
92
|
- lib/pico_phone/rails/type.rb
|
|
93
|
+
- lib/pico_phone/rails/validations_controller.rb
|
|
85
94
|
- lib/pico_phone/rails/version.rb
|
|
86
95
|
homepage: https://github.com/gjack/pico_phone-rails
|
|
87
96
|
licenses:
|
|
@@ -110,5 +119,6 @@ requirements: []
|
|
|
110
119
|
rubygems_version: 3.6.9
|
|
111
120
|
specification_version: 4
|
|
112
121
|
summary: 'Rails integration for pico_phone: attribute type, validator, normalizer,
|
|
113
|
-
free-text extraction, phone search index,
|
|
122
|
+
free-text extraction, phone search index, form field helpers, live validation, and
|
|
123
|
+
ActiveJob serializer'
|
|
114
124
|
test_files: []
|