phlex-forms 0.2.8 → 0.3.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: 9e69e13250ea52121abd2e7e16f91e17b63f7e470131d333513037ad57cbb4cb
4
- data.tar.gz: a5d9179052c02fa4462434d08df73164e5cca3506df42fdd6d5f78852a8cf0ab
3
+ metadata.gz: f09c916a2ff918b4abceddb58267e59d2a7584090aabb8daeacfd2112bf36d9f
4
+ data.tar.gz: b19313a3517a7289a7dc46575e47965ce62849c712cdf27cc6e3fd0cbed13db8
5
5
  SHA512:
6
- metadata.gz: 53ea74ca2f23ad56b79180c453dafb9dd766604dfb380f579b30bb76ca9fd5be957a4b0e77bb9035069074563b25d63b80e1d372057aefda0d7be8cf1eb313ea
7
- data.tar.gz: dc905da47d50a3a43f83adb65faa3f386dcfbcd70e1d7d25d7e646894f994331eb1c3c309eb666271cf03ef1e182c94a1ea19a2a42515af13d7fb1529ecd77b0
6
+ metadata.gz: 557674f0906795a1f9a09e9d815c6b398b1cd8e8dfd60481512815c85cf276dc3372543272771a1b5dd5cb1a20f45c475bf2254c89d3ab303b1f997b9a1df067
7
+ data.tar.gz: 3994c3583c2c091b0f035df86f3ad9f0428ad7b955cd03bdb8c649756e0840cb474cbba290c916445919fff6afcb73f84f6dca948294a4b7970d2ba43e8b6b30
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - **`FileInput` multiple uploads submit an array** (Rails `file_field` parity):
13
+ with `multiple:` set, the input name now gets `[]` appended (unless already
14
+ present), for the standalone component, the `Plain` variant, and the form
15
+ builder path. Without it the browser collapsed a multi-file selection into
16
+ ONE scalar param, which a host app's `params.expect(attr: [])` silently
17
+ discarded — uploads no-opped with a success response.
18
+
10
19
  ### Added
11
20
 
12
21
  - **`checkbox_group` — batched checkbox group for array-valued fields** (the
@@ -45,6 +54,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
45
54
 
46
55
  ### Fixed
47
56
 
57
+ - **A caller width class stacked with the `w-full` default instead of replacing
58
+ it**: `DelegatedField#width_class` joined `"w-full"` and the caller's
59
+ `class:` verbatim, so `Forms::Select(class: "w-36")` emitted
60
+ `class="... w-full w-36"` and stylesheet source order — not author intent —
61
+ picked the winner (in practice `w-full`, which blew filter selects up to full
62
+ width; zazu#2934). `ClassMerge` gained a width family (anchored `w-*`, so
63
+ `min-w-*`/`max-w-*` still compose) and `width_class` merges through it —
64
+ last width wins, matching the daisyui size/color family semantics.
65
+
48
66
  - **`f.Radio` / `Field#radio` rendered the model's current value on every radio
49
67
  instead of each radio's own value**: `field_attributes` carried `value:
50
68
  field_value` and was splatted after the explicit positional value, clobbering
data/lib/forms/field.rb CHANGED
@@ -30,6 +30,12 @@ module Forms
30
30
  # PhlexForms::Theme), so the same field renders daisy or plain.
31
31
 
32
32
  def input(*modifiers, type: :text, **)
33
+ # type: :hidden reroutes to #hidden so EVERY path to a hidden field lands on
34
+ # the bare leaf — `f.Input(:token, :hidden)` and `f.Input(:token, type:
35
+ # :hidden)` would otherwise still emit the styled `input w-full`. Positional
36
+ # modifiers are dropped with it: a hidden field has no visual variants.
37
+ return hidden(**) if type.to_s == "hidden"
38
+
33
39
  theme[:input].new(*modifiers, type:, **field_attributes, **)
34
40
  end
35
41
 
@@ -43,8 +49,13 @@ module Forms
43
49
  end
44
50
  alias rich_text_area rich_textarea
45
51
 
52
+ # Bare <input type="hidden">, never the styled :input leaf — daisyui's
53
+ # `.input` beats WebKit's non-!important `input[type=hidden] { display: none }`
54
+ # and turns the field into a focusable phantom tab stop in Safari. Built from
55
+ # the explicit accessors (not field_attributes) so the meaningless
56
+ # `error: invalid?` is never introduced.
46
57
  def hidden(**)
47
- theme[:input].new(type: :hidden, **field_attributes, **)
58
+ theme[:hidden].new(name: field_name, id: field_id, value: field_value, **)
48
59
  end
49
60
 
50
61
  # daisyui v5 "icon/text inside the field" wrapper. The block renders the
@@ -8,7 +8,10 @@ module Forms
8
8
  def initialize(*modifiers, name: nil, id: nil, multiple: false, accept: nil,
9
9
  error: false, disabled: false, required: false, full_width: true, **attributes)
10
10
  @modifiers = normalize_modifiers(modifiers)
11
- @name = name
11
+ # Rails file_field parity: a multiple input needs an array param name, or
12
+ # the browser collapses the selection into one scalar file — which a host
13
+ # app's `params.expect(attr: [])` then silently discards.
14
+ @name = multiple && name && !name.to_s.end_with?("[]") ? "#{name}[]" : name
12
15
  @id = id
13
16
  @multiple = multiple
14
17
  @accept = accept
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Forms
4
+ # A model-bound hidden field: a bare <input type="hidden"> carrying name/id/
5
+ # value plus whatever the caller passes through. Deliberately NOT a
6
+ # DelegatedField/DaisyUI::Input like Forms::Input — a hidden field must never
7
+ # carry visual styling. daisyui's `.input { display: inline-flex }` overrides
8
+ # WebKit's *non*-!important UA rule `input[type=hidden] { display: none }`, so a
9
+ # styled hidden field renders as an empty box and joins the keyboard tab order
10
+ # in Safari (Chromium's UA rule is !important, hiding the bug there).
11
+ #
12
+ # Forms::Hidden.new(name: "user[token]", id: "user_token", value: "abc123")
13
+ #
14
+ # Same component under both themes (:hidden role) — there are no styling
15
+ # classes for a Plain twin to strip. It takes no positional variants and no
16
+ # error:: a hidden field has no visual or error state.
17
+ class Hidden < Phlex::HTML
18
+ def initialize(name: nil, id: nil, value: nil, **attributes)
19
+ @name = name
20
+ @id = id
21
+ @value = value
22
+ @attributes = attributes
23
+ super()
24
+ end
25
+
26
+ def view_template
27
+ input(type: "hidden", name: @name, id: @id, value: @value.to_s, **@attributes)
28
+ end
29
+ end
30
+ end
@@ -9,14 +9,21 @@ module PhlexForms
9
9
  # the same family, the LAST occurrence wins — matching Tailwind/daisyui
10
10
  # intuition (the later class overrides the earlier).
11
11
  #
12
- # We deliberately do NOT depend on tailwind_merge: on form fields, callers pass
13
- # daisyui modifiers (which this handles) rather than conflicting core Tailwind
14
- # utilities, so tailwind_merge would add a runtime dependency for a conflict
15
- # that does not occur here. Non-conflicting utilities (`w-full`, `py-0`, ...)
16
- # simply pass through in order.
12
+ # Width utilities (`w-*`) are the one core-Tailwind family this gem injects a
13
+ # default for (DelegatedField's `w-full`), so they conflict the same way: a
14
+ # caller's `w-36` must REPLACE the default, or both land on the element and
15
+ # stylesheet source order — not author intent decides which applies.
16
+ #
17
+ # We deliberately do NOT depend on tailwind_merge: daisyui modifier families
18
+ # and the width family are the only conflicts that occur on form fields, so
19
+ # tailwind_merge would add a runtime dependency for conflicts this handles.
20
+ # Other non-conflicting utilities (`py-0`, `min-w-32`, ...) pass through in
21
+ # order.
17
22
  module ClassMerge
18
23
  SIZE = /-(xs|sm|md|lg|xl)\z/
19
24
  COLOR = /-(primary|secondary|accent|neutral|info|success|warning|error)\z/
25
+ # Anchored so min-w-*/max-w-* (different CSS properties) stay out of the family.
26
+ WIDTH = /\Aw-/
20
27
 
21
28
  module_function
22
29
 
@@ -43,9 +50,12 @@ module PhlexForms
43
50
  end
44
51
  end
45
52
 
46
- # A stable bucket key like "input:size" / "select:color", or nil if the token
47
- # is not a recognized daisyui size/color modifier.
53
+ # A stable bucket key like "width" / "input:size" / "select:color", or nil if
54
+ # the token is not a recognized conflicting family. WIDTH is checked first so
55
+ # a `w-*` token can never be mis-bucketed as a daisyui modifier.
48
56
  def family_key(token)
57
+ return "width" if WIDTH.match?(token)
58
+
49
59
  if (m = token.match(SIZE))
50
60
  "#{token[0...m.begin(0)]}:size"
51
61
  elsif (m = token.match(COLOR))
@@ -42,10 +42,13 @@ module PhlexForms
42
42
  attrs.compact
43
43
  end
44
44
 
45
+ # Merged via ClassMerge so a caller width (`w-36`) REPLACES the default
46
+ # instead of stacking with it (`w-full w-36` leaves the winner to stylesheet
47
+ # source order — that's how admin filter selects went full-width, zazu#2934).
45
48
  def width_class
46
49
  return @attributes[:class] unless @full_width
47
50
 
48
- ["w-full", @attributes[:class]].compact.join(" ")
51
+ ClassMerge.merge("w-full", @attributes[:class])
49
52
  end
50
53
 
51
54
  # Unstyled variant of binding_attributes for the Plain theme: caller classes
@@ -4,7 +4,9 @@ module PhlexForms
4
4
  # A theme maps component roles (:input, :select, :control, ...) to component
5
5
  # classes. The binding contract — the leaf initializer signatures
6
6
  # (*modifiers, name:, id:, value:, error:, required:, ...) — is the stable
7
- # interface, so the same form class renders under any theme.
7
+ # interface, so the same form class renders under any theme. The one exception
8
+ # is :hidden, which takes name:/id:/value: only — a hidden field has neither a
9
+ # visual variant nor an error state.
8
10
  #
9
11
  # Built-ins:
10
12
  # Theme.daisy — the DaisyUI-delegating Forms::* leaves (default when the
@@ -56,7 +58,7 @@ module PhlexForms
56
58
  textarea: Forms::Textarea, rich_textarea: Forms::RichTextarea,
57
59
  checkbox: Forms::Checkbox, toggle: Forms::Toggle, radio: Forms::Radio,
58
60
  checkbox_group: Forms::CheckboxGroup,
59
- file: Forms::FileInput, wrapped_input: Forms::WrappedInput,
61
+ file: Forms::FileInput, wrapped_input: Forms::WrappedInput, hidden: Forms::Hidden,
60
62
  control: Forms::FormControl, label: Forms::Label,
61
63
  field_error: Forms::FieldError, field_hint: Forms::FieldHint,
62
64
  submit: Forms::Submit, row: Forms::Row, group: Forms::Group,
@@ -74,7 +76,7 @@ module PhlexForms
74
76
  textarea: Forms::Plain::Textarea, rich_textarea: Forms::Plain::Textarea,
75
77
  checkbox: Forms::Plain::Checkbox, toggle: Forms::Plain::Checkbox, radio: Forms::Plain::Radio,
76
78
  checkbox_group: Forms::Plain::CheckboxGroup,
77
- file: Forms::Plain::FileInput, wrapped_input: Forms::Plain::WrappedInput,
79
+ file: Forms::Plain::FileInput, wrapped_input: Forms::Plain::WrappedInput, hidden: Forms::Hidden,
78
80
  control: Forms::Plain::Control, label: Forms::Plain::Label,
79
81
  field_error: Forms::Plain::FieldError, field_hint: Forms::Plain::FieldHint,
80
82
  submit: Forms::Plain::Submit, row: Forms::Plain::Row, group: Forms::Plain::Group,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PhlexForms
4
- VERSION = "0.2.8"
4
+ VERSION = "0.3.0"
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.8
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -128,6 +128,7 @@ files:
128
128
  - lib/forms/form.rb
129
129
  - lib/forms/form_control.rb
130
130
  - lib/forms/group.rb
131
+ - lib/forms/hidden.rb
131
132
  - lib/forms/input.rb
132
133
  - lib/forms/label.rb
133
134
  - lib/forms/live.rb