Guide

# Tag fields

field :tags, as: :tags renders a polished tag/chip input — daisyUI chrome over phlex-reactive's client-only tag primitives, submitting one comma-joined param.

## A tag input, written once

`as: :tags` renders a tag/chip input: type-ahead over your suggestions, Enter or click to add a chip, × to remove — all client-side (form state, no server round trips). phlex-forms owns the polished chrome (label, error/hint, daisyUI styling, model binding); the behavior rides on [phlex-reactive](https://phlex-reactive.zoolutions.llc)'s tag primitives.

**Requires phlex-reactive ≥ 0.11.4** — the `:tags` control is only registered when it's loaded.

```ruby
f.field :tags, as: :tags, suggestions: %w[Ruby Rails Hotwire Postgres]
```

## Try it

The widget below is the real `Forms::TagField`, rendered live on this page. Type to filter the suggestions, Enter or click to add a tag, × to remove one. The hidden field (inspect it) carries the comma-joined value.

Ruby

×

Rails

×

×

- Ruby
- Rails
- Hotwire
- Postgres
- Stimulus
- Turbo
- Phlex
- Kamal

## Suggestions: Array or haystack Hash

Pass an **Array** of tags, or a **Hash** of `tag => haystack` where the haystack is the synonym text the type-ahead filter matches against — so typing "db" surfaces "Postgres".

```ruby
# plain list — the filter matches the tag text
f.field :tags, as: :tags, suggestions: %w[Ruby Rails Hotwire]

# haystack Hash — the filter matches the synonyms, the chip shows the tag
f.field :tags, as: :tags, suggestions: {
  "Postgres" => "postgres database db sql",
  "Redis"    => "redis cache kv key value"
}
```

## The model splits the comma-joined value

The widget submits **one comma-joined param** (`post[tags] = "Ruby,Rails"`) — the primitive's wire contract. The visible type-ahead input carries **no `name`**, so it never posts a stray param; only a hidden field does. Have the model split the string back into an array:

```ruby
class Post < ApplicationRecord
  attribute :tags, default: []          # a text[] / JSONB column, say

  # accept the widget's "Ruby,Rails" and a normal Array alike
  def tags=(value)
    super(value.is_a?(String) ? value.split(",").map(&:strip).reject(&:empty?) : value)
  end
end
```

> **Note:** A future release may ship the split writer (or an `ActiveModel::Type`) so `field :tags, as: :tags` works against `attribute :tags, array: true` with zero model code.

## Theme parity

Under the plain theme the widget keeps the **entire** client wire contract (the tag behavior still works) but ships **zero** daisyUI classes; the invalid state rides `aria-invalid` on the query input. Custom styling overrides the leaf's class seams (`root_classes`, `chip_classes`, `menu_classes`, …).

## Validating tags in a live form

A standalone tag widget is its own [reactive](https://phlex-forms.zoolutions.llc/docs/live-validation) root, so inside a `live` form the outer validation root would skip its hidden field. Declare `live_tags` to lift the widget's wire attributes onto the `<form>` root and render it rootless — then the form owns the hidden field and `:validate` runs your real validators over the tags:

```ruby
class PostForm < Forms::Base
  live model: Post
  live_tags :tags, suggestions: %w[Ruby Rails Hotwire]

  def fields
    field :title
    field :tags, as: :tags     # renders rootless; the form validates it
    submit :primary
  end
end
```

phlex-reactive's tag controller reads **one** tag field per root, so a live form lifts **at most one** — a second `live_tags` raises. A second tag input must stay a standalone (non-live) `field :other, as: :tags`.

## Constraints

- **phlex-reactive ≥ 0.11.4** is required — without it the `:tags` control role isn't registered (the gem still boots and every other field works).
- Tags **can't contain a comma** — it's the value separator.
- One lifted (live-validated) tag field per form; see above.