diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eabe42..3d0490c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`checkbox_group` — batched checkbox group for array-valued fields** (the + tag/facet-picker shape): `f.checkbox_group(:tag_ids, Tag.all, value: :id, + label: :name, variant: :pill, size: :sm)`, or via field inference + (`f.field :tag_ids, as: :checkbox_group, collection: Tag.all, value: :id`). + Shares one array-valued field name with a leading empty-array hidden field, + derives the checked set from the model's current value, and renders under both + themes. `variant:` (`:stack`/`:inline`/`:pill`) is layout-only, no JS. + +### Fixed + +- **`Form(validate: true)` never fired client-side validation on submit**: the + coordinator controller was attached but no `data-action` wired its `onSubmit` + handler, so submitting an invalid form was not blocked. `apply_validation_coordinator` + now emits `submit->forms--validations--form#onSubmit` (joined with any + caller-supplied `data-action`). +- **`fields_for` iterated a Hash-backed association (JSONB), emitting bogus + indices**: a Hash responds to `#each_with_index`, so a JSONB column rendered + with `nested_attributes: false` produced `scope[assoc][0][field]`, `[1]`, … + instead of a single `scope[assoc][field]`. It is now treated as a single + nested scope; only genuine collections (Enumerable, not Hash) iterate. + - **`Forms::Base` declarative form classes**: subclass, declare fields in `#fields` where `self` IS the form (bare `field :email`, no `f.` prefix), render with `render UserForm.new(model: @user)`. Class-level `form_options` diff --git a/README.md b/README.md index 8f84d4c..f7e10cc 100644 --- a/README.md +++ b/README.md @@ -351,13 +351,29 @@ f.fields_for(:settings, nested_attributes: false) do |s| end f.collection_check_boxes(:role_ids, Role.all, :id, :name) do |b| - render b.check_box + render b.check_box # per-item control, full custom layout render b.label end +# The batched "tag/facet picker" shape: one array-valued field name, checked +# state derived from the model (record.tag_ids), sensible defaults, no block. +f.checkbox_group(:tag_ids, Tag.all, value: :id, label: :name) +f.checkbox_group(:tag_ids, Tag.all, value: :id, + label: ->(t) { t.name.presence || t.slug }, # Symbol method or Proc + variant: :pill, # :stack (default) | :inline | :pill + size: :sm) # daisyUI checkbox size +# ...or through field inference: +f.field :tag_ids, as: :checkbox_group, collection: Tag.all, value: :id, label: :name + f.collection_select(:country_id, Country.all, :id, :name, prompt: "Select…") ``` +`checkbox_group` submits an array param (`user[tag_ids][]`) with a leading +empty-array hidden field, so deselecting everything still submits. The checked +set comes from the model's current value matched by each item's resolved +`value:` — re-rendering an edit form pre-checks the right boxes. The `:pill` +variant styles the active chip with Tailwind's `has-[:checked]:` (no JS). + `Form(model: @item, scope: false)` emits **bare** field names (`name="quantity"`) — the shape phlex-reactive row editors and `