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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +13 -2
- data/lib/forms/checkbox_group.rb +6 -3
- data/lib/forms/field.rb +33 -5
- data/lib/forms/live.rb +7 -1
- data/lib/forms/tag_field.rb +25 -18
- data/lib/phlex_forms/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d963c5c0eb08c6027d236da0489af8e2b7fd5ef258092c91cc280c8fe4711c5e
|
|
4
|
+
data.tar.gz: d1a686ce5b6b249c3860584c8b7a9447e4198efe7d0b6c9aed86441a3d7ee915
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
data/lib/forms/checkbox_group.rb
CHANGED
|
@@ -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
|
-
#
|
|
11
|
+
# item_label: ->(t) { t.name.presence || t.slug }, variant: :pill, size: :sm)
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
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
|
|
116
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/forms/tag_field.rb
CHANGED
|
@@ -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,
|
|
22
|
-
# reactive_tags(:
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
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
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
data/lib/phlex_forms/version.rb
CHANGED