phlex-forms 0.2.5 → 0.2.7

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: '0038bad89aee72fb6b4e162bfc83dc9afbaadbc2280f3ea23be2985e81774d41'
4
- data.tar.gz: 9b35b71658c4ffbb14667a79e6e85e4a0437d6953b64921272ae629a443a0ac2
3
+ metadata.gz: d963c5c0eb08c6027d236da0489af8e2b7fd5ef258092c91cc280c8fe4711c5e
4
+ data.tar.gz: d1a686ce5b6b249c3860584c8b7a9447e4198efe7d0b6c9aed86441a3d7ee915
5
5
  SHA512:
6
- metadata.gz: b8ada79a4a87a79096b16f82d000ea0e5620096ebe4b8fff9560ae5e556ff982ff6e13ccb52c77e0d7ea72e0d094055595870468de9c40d9f498350bab399eb4
7
- data.tar.gz: bce76ac9a71f33cab8c15f6f4b529b636da88d802c4b86ba07ee90948daaa1a2e1c0637f898a11be75e37ab4b8dd7abb8f42ce654a9481907b5cbcc5a299263a
6
+ metadata.gz: 4563f81fac25d23e76303ee2c96f2734add870c5b5707b8d45683a62479fccc30d0cd36464492627bb7bbabba209f41df8d657514d37a7fd748715d1af3150ab
7
+ data.tar.gz: 39961525f504962d9345cb6e9ab6215c624717d710db5e801b6fad872a8b60dc792233f66f653711c17aa694aa961263d720a8c75084e54d6832c55315c57f65
data/CHANGELOG.md CHANGED
@@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
  the Control's own visible `label:` / `hint:` get stable ids and the field wires
24
24
  `aria-labelledby` / `aria-describedby` at them (no duplicate markup). Absent a
25
25
  name, output is unchanged.
26
+ - **`checkbox_group` `item_label:`** — the per-item text accessor, so a
27
+ `f.field(:tags, as: :checkbox_group, label: "Tags", item_label: ->(t){…})`
28
+ renders a visible group heading (`label:`, via the Control) AND custom item
29
+ labels at once — previously `label:` on the `f.field` path was eaten by the
30
+ heading and items fell back to `to_s`. `item_label:` wins over `label:` for the
31
+ item text; on the bare verb `label:` stays the item accessor and `item_label:`
32
+ is an alias. Absent it, behavior is unchanged.
26
33
 
27
34
  ### Changed
28
35
 
data/README.md CHANGED
@@ -368,9 +368,13 @@ f.checkbox_group(:tag_ids, Tag.all, value: :id,
368
368
  variant: :pill, # :stack (default) | :inline | :pill
369
369
  size: :sm, # daisyUI checkbox size
370
370
  aria: { label: "Tags" }) # names the group for screen readers
371
- # ...or through field inference (the field's label/hint name the group):
371
+ # ...or through field inference. Here `label:`/`hint:` are the field's VISIBLE
372
+ # heading + description (rendered by the Control, which also names the group);
373
+ # `item_label:` gives the per-item text, so you get a heading AND custom item
374
+ # labels at once:
372
375
  f.field :tag_ids, as: :checkbox_group, collection: Tag.all, value: :id,
373
- label: "Tags", hint: "Pick any"
376
+ label: "Tags", hint: "Pick any",
377
+ item_label: ->(t) { t.name.presence || t.slug }, variant: :pill
374
378
 
375
379
  f.collection_select(:country_id, Country.all, :id, :name, prompt: "Select…")
376
380
  ```
@@ -381,6 +385,13 @@ set comes from the model's current value matched by each item's resolved
381
385
  `value:` — re-rendering an edit form pre-checks the right boxes. The `:pill`
382
386
  variant styles the active chip with Tailwind's `has-[:checked]:` (no JS).
383
387
 
388
+ **Two labels, no collision.** Through `f.field`, `label:` is the field's visible
389
+ group heading (the Control renders it); the per-item text comes from `item_label:`
390
+ (a Symbol method or Proc). On the bare `f.checkbox_group` verb there is no Control
391
+ heading, so `label:` *is* the per-item accessor (and `item_label:` is accepted as
392
+ an alias). Either way `value:` is the submitted value; `item_label:` wins over
393
+ `label:` for the item text when both are present.
394
+
384
395
  A `role="group"` needs an **accessible name** for assistive tech. The verb has
385
396
  no bespoke naming option — HTML/ARIA attributes pass straight through to the
386
397
  group, so name it with plain `aria:` (`aria: { label: "Tags" }` for a literal
@@ -8,10 +8,13 @@ module Forms
8
8
  # resolved value: of its item against the model's current set.
9
9
  #
10
10
  # f.checkbox_group(:tag_ids, Tag.all, value: :id,
11
- # label: ->(t) { t.name.presence || t.slug }, variant: :pill, size: :sm)
11
+ # item_label: ->(t) { t.name.presence || t.slug }, variant: :pill, size: :sm)
12
12
  #
13
- # value: method or proc -> the submitted value of each item (default :id)
14
- # label: method or proc -> the visible text of each item (default :to_s)
13
+ # The item value/text accessors (value:/label:/item_label:) live on the BUILDER
14
+ # (Forms::Field#checkbox_group), which pre-resolves each item to
15
+ # { value:, label:, checked:, id: } before this leaf renders. This leaf is
16
+ # presentation-only — it receives the resolved options: array, never the raw
17
+ # accessors.
15
18
  # variant: :stack (default) | :inline | :pill — layout only, zero JS
16
19
  # size: daisyUI checkbox size modifier (:xs :sm :md :lg :xl)
17
20
  #
data/lib/forms/field.rb CHANGED
@@ -112,8 +112,20 @@ module Forms
112
112
  #
113
113
  # field.checkbox_group(Tag.all, value: :id, label: ->(t) { t.name })
114
114
  #
115
- # value:/label: are a method name (Symbol) or a proc taking the item.
116
- def checkbox_group(collection, value: :id, label: :to_s, **)
115
+ # value: is a method name (Symbol) or a proc taking the item -> its submitted
116
+ # value. The per-item visible text comes from item_label: (Symbol/Proc/String)
117
+ # if given, else label:; when NEITHER is given each item is labelled by the
118
+ # first of name/title/label/to_s it responds to (the same LABEL_METHODS chain
119
+ # Inference uses for association choices) — so a plain
120
+ # `f.field(:tags, as: :checkbox_group, label: "Tags")` shows readable item
121
+ # text without an explicit accessor.
122
+ #
123
+ # item_label: exists so the `f.field` path can pass a visible group heading as
124
+ # `label:` (consumed by the Control) AND still customize the per-item text
125
+ # here — the two no longer collide. item_label: is consumed here; it never
126
+ # leaks to the group div.
127
+ def checkbox_group(collection, value: :id, label: nil, item_label: nil, **)
128
+ text = item_label || label
117
129
  # The model's current value is already the raw values (e.g. record.tag_ids
118
130
  # => [1, 3]), so compare against them directly — don't re-resolve value:.
119
131
  selected = Array(field_value)
@@ -121,7 +133,7 @@ module Forms
121
133
  item_value = resolve_item(item, value)
122
134
  {
123
135
  value: item_value,
124
- label: resolve_item(item, label),
136
+ label: text ? resolve_item(item, text) : infer_item_label(item),
125
137
  checked: selected.include?(item_value),
126
138
  id: "#{field_id}_#{item_value}"
127
139
  }
@@ -256,9 +268,25 @@ module Forms
256
268
  validator.options.key?(:if) || validator.options.key?(:unless) || validator.options.key?(:on)
257
269
  end
258
270
 
259
- # value:/label: for checkbox_group: a Proc taking the item, or a method name.
271
+ # value:/label:/item_label: for checkbox_group. A Proc is called with the
272
+ # item; a String is literal text (the same for every item — no method
273
+ # dispatch, so a stray string can't NoMethodError); anything else (a Symbol)
274
+ # is sent to the item as a method name.
260
275
  def resolve_item(item, accessor)
261
- accessor.respond_to?(:call) ? accessor.call(item) : item.public_send(accessor)
276
+ case accessor
277
+ when Proc then accessor.call(item)
278
+ when String then accessor
279
+ else item.public_send(accessor)
280
+ end
281
+ end
282
+
283
+ # Default per-item label when no label:/item_label: was given: the first of
284
+ # name/title/label/to_s the item responds to (mirrors PhlexForms::Inference's
285
+ # LABEL_METHODS for association choices, so option text is picked the same way
286
+ # across the gem).
287
+ def infer_item_label(item)
288
+ method = PhlexForms::Inference::LABEL_METHODS.find { |m| item.respond_to?(m) }
289
+ item.public_send(method || :to_s)
262
290
  end
263
291
 
264
292
  def field_attributes
data/lib/forms/live.rb CHANGED
@@ -168,10 +168,16 @@ module Forms
168
168
  # widget (rendered in the block) is driven by this root, which then DOM-owns
169
169
  # its hidden field. Name/id derived through field_name/field_id — the same
170
170
  # path the rootless render uses, so the [name=…]/#…_query selectors match.
171
+ # This form IS a reactive component, so it has reactive_tags/reactive_filter
172
+ # itself — the same 0.12.2 escape-hatch sugar Forms::TagField uses, emitting
173
+ # both filter selectors the client needs (issue #6 Caveats 1 & 2).
171
174
  tag = self.class.live_tags_declaration
172
175
  return attrs unless tag
173
176
 
174
- mix(attrs, Forms::TagField.root_tag_attributes(name: field_name(tag[:name]), id: field_id(tag[:name])))
177
+ query_id = Forms::TagField.query_id(field_id(tag[:name]))
178
+ mix(attrs,
179
+ reactive_tags(name: field_name(tag[:name])),
180
+ reactive_filter(input: "##{query_id}"))
175
181
  end
176
182
 
177
183
  # Untouched fields get no error set, so nothing flashes before the user
@@ -18,11 +18,13 @@ module Forms
18
18
  # The reactive_tags_* client helpers require phlex-reactive >= 0.11.4.
19
19
  #
20
20
  # It uses the reactive_tags_add/option/remove helpers for the chip/query/option
21
- # behavior, but emits the ROOT's `data-reactive-tags-field` raw rather than via
22
- # reactive_tags(:tags): that helper compiles a SYMBOL through the class-level
23
- # reactive_scope, but a form builder's wire name is per-instance ("user[tags]").
24
- # The data attribute IS the public contract; any CSS selector works (issue #6
25
- # Caveats 1 & 2). Likewise the query input targets by #id so it never submits.
21
+ # behavior, and the ROOT's wire attrs come from the 0.12.2 escape-hatch sugar:
22
+ # reactive_tags(name: @name) takes the per-instance wire name verbatim ("user[tags]")
23
+ # the class-level reactive_scope compile can't express it validated at render;
24
+ # reactive_filter(input: "#…_query") targets the query input by id so it never
25
+ # submits, and (unlike the old raw -input-only attr) also emits
26
+ # data-reactive-filter-option so the 0.12.x client type-ahead actually runs
27
+ # (issue #6 Caveats 1 & 2).
26
28
  class TagField < Phlex::HTML
27
29
  include Phlex::Reactive::ClientBindings
28
30
 
@@ -48,21 +50,26 @@ module Forms
48
50
  end
49
51
  end
50
52
 
51
- # The root's tag wire attrs. Raw, not reactive_tags(:tags)/reactive_filter(:q)
52
- # (Caveats 1 & 2): target the hidden field by [name=…] and the query input by
53
- # #id (an id selector means the query input never submits a stray param).
54
- # Public so Forms::Live can hoist these onto the <form> root when the widget
55
- # is lifted rootless.
56
- def self.root_tag_attributes(name:, id:)
57
- { data: {
58
- reactive_tags_field: %([name="#{name}"]),
59
- reactive_filter_input: "##{id}_query"
60
- } }
61
- end
53
+ # The query input's id the reactive_filter(input:) target and the id the
54
+ # search input itself carries. Public so Forms::Live can derive the same id
55
+ # when it hoists the tag wire attrs onto the <form> root (rootless widget).
56
+ def self.query_id(id) = "#{id}_query"
62
57
 
63
58
  private
64
59
 
65
- def root_tag_attributes = self.class.root_tag_attributes(name: @name, id: @id)
60
+ # The root's tag wire attrs, via the 0.12.2 escape-hatch sugar (Caveats 1 & 2):
61
+ # reactive_tags(name:) takes the instance-dynamic wire name verbatim and
62
+ # validates it at render; reactive_filter(input:) targets the query input by
63
+ # #id (so it never submits a stray param) and emits both filter selectors the
64
+ # 0.12.x client needs to run the type-ahead. Both are private helpers from
65
+ # ClientBindings — Forms::Live has its own copies (it's a reactive component)
66
+ # and calls them directly when hoisting these onto the <form> root.
67
+ def root_tag_attributes
68
+ mix(
69
+ reactive_tags(name: @name),
70
+ reactive_filter(input: "##{self.class.query_id(@id)}")
71
+ )
72
+ end
66
73
 
67
74
  # The widget body WITHOUT its root wrapper — shared with the rootless variant
68
75
  # so chip/template/suggestion markup never drifts between the two.
@@ -93,7 +100,7 @@ module Forms
93
100
 
94
101
  def query_attributes
95
102
  {
96
- id: "#{@id}_query", type: "search", autocomplete: "off",
103
+ id: self.class.query_id(@id), type: "search", autocomplete: "off",
97
104
  placeholder: @placeholder, class: input_classes,
98
105
  "aria-invalid": @error || nil
99
106
  }.compact
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexForms
4
- VERSION = "0.2.5"
4
+ VERSION = "0.2.7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson