pico_phone-rails 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad67a4a67bfcdfd2e94072a2989dd25c23896ed5d97ef6b506a08c31548fe9f1
4
- data.tar.gz: 1414db39189fdaed84124aaf1e10e8abb5ed02414668130a9f37ceb4b682ccad
3
+ metadata.gz: 8e4c80e86756e81b40665de3fe0c6436ee462b8d5ea011992e3a8507eaedb323
4
+ data.tar.gz: 8ce58cb67fc8b73d92ba3b945b303a5c7360265a6ac217e5c0e1e1806c2157b7
5
5
  SHA512:
6
- metadata.gz: 6aaeb277b412aa8acea4729c56a9e1c371a3e967cf4c4162d9aa430e1409842e075cf810ff2db665b605dff47ebd18e85d3367cbff4c7652fc6ed92563371700
7
- data.tar.gz: e31a7f628e3b16386fc79ccc3f0d9ceb67d41db69901ccbe7ff455475da9afc3e5a05fc316b5f85946071eb4e333bb7f9e0bb10a92955d3bb99bd5f721f89b7a
6
+ metadata.gz: ca51d0d37018f62fceba7ac87b2f0ebd42f4e537b55d9814e33488b48ea30aa822fd18eef92713d4a9b464e8849298e91ca41b430d1f99d471aba28b371b494b
7
+ data.tar.gz: dba580ca4a43fe7b83e776cf4e8430a8ca19cf9fc97f0f059c423fdd4e6ae6117f9c1dd702b4ccf878a13f3a848ac271041b62e3350d9143b230cc16523f1d33
data/README.md CHANGED
@@ -206,6 +206,48 @@ 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, 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
+
209
251
  ### ActiveJob serializer
210
252
 
211
253
  ```ruby
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ PicoPhone::Rails::Engine.routes.draw do
4
+ post "validate", to: "validations#validate"
5
+ end
@@ -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,18 @@ 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
+ # @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
+
25
37
  # Included into ActionView::Base by the railtie once ActionView loads.
26
38
  # Adds +pico_phone_field_tag+, a +text_field_tag+-like helper that
27
39
  # displays national format for a valid number while leaving the
@@ -39,9 +51,14 @@ module PicoPhone
39
51
  # @param name [String, Symbol]
40
52
  # @param value [String, PicoPhone::PhoneNumber, nil]
41
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
42
55
  # @return [String] an HTML-safe +<input type="tel">+ tag
43
- def pico_phone_field_tag(name, value = nil, region: nil, **options)
56
+ def pico_phone_field_tag(name, value = nil, region: nil, live: false, **options)
44
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
45
62
  text_field_tag(name, display_value, options.merge(type: "tel"))
46
63
  end
47
64
  end
@@ -59,10 +76,15 @@ module PicoPhone
59
76
  # raw value when it isn't already a {PicoPhone::PhoneNumber} -- a String is used as-is, a Symbol is
60
77
  # called as an instance method on the form's object, a Proc is called with the object, same resolution
61
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
62
80
  # @return [String] an HTML-safe +<input type="tel">+ tag
63
- def pico_phone_field(method, region: nil, **options)
81
+ def pico_phone_field(method, region: nil, live: false, **options)
64
82
  resolved_region = PicoPhone::Rails.resolve_region(region, object)
65
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
66
88
  text_field(method, options.merge(value: display_value, type: "tel"))
67
89
  end
68
90
  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
+ }
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ pin "pico_phone/rails/phone_controller", to: "pico_phone/rails/phone_controller.js"
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PicoPhone
4
4
  module Rails
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -16,3 +16,4 @@ require "pico_phone/rails/normalizer"
16
16
  require "pico_phone/rails/extraction"
17
17
  require "pico_phone/rails/phone_search_index"
18
18
  require "pico_phone/rails/railtie" if defined?(Rails::Railtie)
19
+ require "pico_phone/rails/engine" if defined?(Rails::Railtie)
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.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, and an ActiveJob serializer so a PhoneNumber
60
- survives being passed as a job argument.'
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, and ActiveJob serializer'
122
+ free-text extraction, phone search index, form field helpers, live validation, and
123
+ ActiveJob serializer'
115
124
  test_files: []