Corex.TagsInput (Corex v0.2.0)

View Source

Tags input for Phoenix LiveView forms. Behavior follows Zag.js Tags Input.

Anatomy

Minimal

<.tags_input class="tags-input" value={["alpha", "beta"]}>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>

With label

<.tags_input class="tags-input" value={["alpha", "beta"]}>
  <:label>Tags</:label>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>

Translation

<.tags_input
  class="tags-input"
  value={["lorem", "duis"]}
  translation={%Corex.TagsInput.Translation{
    placeholder: "Add lorem or duis",
    delete_tag_trigger_label: "Remove %{tag}",
    tag_edited: "Edit %{tag}. Press enter to save or escape to cancel."
  }}
>
  <:label>Keywords</:label>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>

API

Requires a stable id on <.tags_input>.

FunctionActionReturns
set_value/2Set tag list (client)%Phoenix.LiveView.JS{}
set_value/3Set tag list (server)socket
add_value/2Add one tag (client)%Phoenix.LiveView.JS{}
add_value/3Add one tag (server)socket
remove_value/2Remove one tag (client)%Phoenix.LiveView.JS{}
remove_value/3Remove one tag (server)socket
clear_value/1Clear all tags (client)%Phoenix.LiveView.JS{}
clear_value/2Clear all tags (server)socket

Events

Pick an event name and pass it to on_* on <.tags_input>.

Server events

EventWhenPayload
on_value_change="tags_value_changed"Tag list changes%{"id" => id, "value" => tags}
on_value_invalid="tags_value_invalid"Invalid tag or max overflow%{"id" => id, "reason" => reason}

on_value_change

<.tags_input
  class="tags-input"
  value={["lorem", "duis", "donec"]}
  on_value_change="tags_value_changed"
>
  <:label>Tags</:label>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>
def handle_event("tags_value_changed", %{"id" => _id, "value" => value}, socket) do
  {:noreply, assign(socket, :tags, value)}
end

Client events

EventWhenevent.detail
on_value_change_client="tags-client-changed"Tag list changesid, value
on_value_invalid_client="tags-client-invalid"Invalid tag or maxid, reason

Patterns

Pass value for the initial tag list on mount. The machine owns updates after that unless you use set_value/2 or form field.

Form

For cross-cutting invalid styling and error presentation, see the Forms guide. With field={@form[:…]}, pass auto_invalid for alert borders from visible errors, or invalid={true} to force the alert state.

With field={f[:tags]}, each tag is submitted as its own request param (post[tags][]), so Phoenix receives a list:

%{"post" => %{"tags" => ["alpha", "beta"]}}

Use an Ecto field typed {:array, :string} (or cast a list in your changeset). A :string column with comma-splitting in the changeset is not supported by this component.

When there are no tags, the component still submits one name[] hidden input (empty string) so the form param is present. Phoenix decodes that as [""]; Ecto cast/3 on {:array, :string} turns it into [].

validate_required/3 alone does not treat an empty list as blank. Pair {:array, :string} with validate_length(:tags, min: 1) (or equivalent) so cleared tags surface errors.

The delimiter attribute only controls how new tags are split when typing or pasting in the control (default ,). It does not affect form submission.

In LiveView, rebuild the form from the changeset on phx-change so validation stays in sync. Use field={@form[:tags]} only.

embedded_schema do
  field :tags, {:array, :string}
end

def changeset(profile, attrs) do
  profile
  |> cast(attrs, [:tags])
  |> validate_required([:tags])
  |> validate_length(:tags, min: 1)
end
<.form for={@form} phx-change="validate" phx-submit="save">
  <.tags_input field={@form[:tags]} class="tags-input">
    <:label>Keywords</:label>
    <:close><.heroicon name="hero-x-mark" /></:close>
    <:error :let={msg}>
      <.heroicon name="hero-exclamation-circle" />
      {msg}
    </:error>
  </.tags_input>
  <.action type="submit" class="button ui-accent">Save</.action>
</.form>

Style

Target parts with data-scope and data-part, or use Corex Design: import tokens and tags-input.css, then set class="tags-input" on <.tags_input>.

[data-scope="tags-input"][data-part="root"] {}
[data-scope="tags-input"][data-part="control"] {}
[data-scope="tags-input"][data-part="input"] {}
@import "../corex/corex.css";

Stack modifiers on the host (class on <.tags_input>). Combine axes, for example tags-input ui-accent ui-size-lg.

Axes: Semantic (ui-accent, ui-brand, ui-alert, ui-info, ui-success), Size (ui-size-smui-size-xl), Radius (ui-rounded-*). No variant axis. See the modifier guide.

Semantic modifiers set palette variables for focus and accent ink on the typing field and tag chips. The typing surface stays the shared ui-input treatment. Tags input has no variant axis.

Semantic

Palette for focus and accent ink on the typing field and tag chips.

ModifierClasses
Defaulttags-input
Accenttags-input ui-accent
Brandtags-input ui-brand
Alerttags-input ui-alert
Infotags-input ui-info
Successtags-input ui-success

Size

ModifierClasses
SMtags-input ui-size-sm
MDtags-input ui-size-md
LGtags-input ui-size-lg
XLtags-input ui-size-xl

Summary

API

Append one tag from phx-click. Dispatches corex:tags-input:add-value with detail.value.

Append one tag from handle_event (tags_input_add_value).

Clear tags from phx-click. Dispatches corex:tags-input:clear-value.

Clear tags from handle_event (tags_input_clear_value).

Remove one tag from phx-click. Dispatches corex:tags-input:remove-value with detail.value.

Remove one tag from handle_event (tags_input_remove_value).

Replace all tags from phx-click. Dispatches corex:tags-input:set-value with detail.value as a string list.

Replace all tags from handle_event (tags_input_set_value).

Components

tags_input(assigns)

Attributes

  • id (:string) - Stable id for the component. Defaults to nil.
  • field (Phoenix.HTML.FormField) - Form field; sets id, name, form, value from the field value, and errors when used_input?/1. Defaults to nil.
  • name (:string) - Name attribute for form submission. Defaults to nil.
  • form (:string) - Form id to associate the control with. Defaults to nil.
  • invalid (:boolean) - Whether the control has validation errors. Defaults to nil.
  • auto_invalid (:boolean) - When true with field, set invalid from visible changeset errors (default false). Defaults to false.
  • disabled (:boolean) - Whether the control is disabled. Defaults to false.
  • read_only (:boolean) - Whether the control is read-only. Defaults to false.
  • required (:boolean) - Whether the control is required. Defaults to false.
  • value (:list) - Initial list of tag strings; JSON-encoded for the hook. Defaults to [].
  • errors (:list) - Error messages when not using field=. Defaults to [].
  • dir (:string) - When nil, derived from document. Defaults to nil. Must be one of nil, "ltr", or "rtl".
  • max (:integer) - Maximum number of tags. Defaults to nil.
  • delimiter (:string) - Delimiter for splitting typed or pasted text into new tags in the UI (default comma). Form submission always uses name[] list params when field or name is set. Defaults to nil.
  • blur_behavior (:string) - Blur behavior for pending input text. Defaults to nil. Must be one of nil, "add", or "clear".
  • add_on_paste (:boolean) - Defaults to false.
  • allow_duplicates (:boolean) - Defaults to false.
  • allow_overflow (:boolean) - Defaults to false.
  • editable (:boolean) - Defaults to nil.
  • auto_focus (:boolean) - Defaults to false.
  • placeholder (:string) - Defaults to nil.
  • translation (Corex.TagsInput.Translation) - Translatable strings for placeholder and Zag delete/edit labels. Defaults to %Corex.TagsInput.Translation{placeholder: nil, delete_tag_trigger_label: nil, tag_edited: nil}.
  • on_value_change (:string) - Defaults to nil.
  • on_value_change_client (:string) - Defaults to nil.
  • on_input_value_change (:string) - Defaults to nil.
  • on_input_value_change_client (:string) - Defaults to nil.
  • on_highlight_change (:string) - Defaults to nil.
  • on_highlight_change_client (:string) - Defaults to nil.
  • on_value_invalid (:string) - LiveView event name for Zag onValueInvalid; push_event payload %{"id" => id, "reason" => "rangeOverflow" | "invalidTag"}. See moduledoc section on on_value_invalid. Defaults to nil.

  • on_value_invalid_client (:string) - DOM event name on the hook root for Zag onValueInvalid; detail %{id, reason} with reason rangeOverflow or invalidTag. See moduledoc section on on_value_invalid. Defaults to nil.
  • Global attributes are accepted.

Slots

  • label - Accepts attributes:
    • class (:string)
  • close (required) - Content for each delete trigger.
  • error - Accepts attributes:
    • class (:string)

API

add_value(tags_input_id, value)

@spec add_value(String.t(), String.t()) :: Phoenix.LiveView.JS.t()

Append one tag from phx-click. Dispatches corex:tags-input:add-value with detail.value.

<.action phx-click={Corex.TagsInput.add_value("my-tags", "new")}>Add tag</.action>
<.tags_input id="my-tags" class="tags-input">
  <:control><span /></:control>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>

add_value(socket, tags_input_id, value)

Append one tag from handle_event (tags_input_add_value).

def handle_event("add_tag", %{"tag" => t}, socket) do
  {:noreply, Corex.TagsInput.add_value(socket, "my-tags", t)}
end

clear_value(tags_input_id)

@spec clear_value(String.t()) :: Phoenix.LiveView.JS.t()

Clear tags from phx-click. Dispatches corex:tags-input:clear-value.

<.action phx-click={Corex.TagsInput.clear_value("my-tags")}>Clear</.action>
<.tags_input id="my-tags" class="tags-input">
  <:control><span /></:control>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>

clear_value(socket, tags_input_id)

Clear tags from handle_event (tags_input_clear_value).

def handle_event("clear_tags", _, socket) do
  {:noreply, Corex.TagsInput.clear_value(socket, "my-tags")}
end

remove_value(tags_input_id, value)

@spec remove_value(String.t(), String.t()) :: Phoenix.LiveView.JS.t()

Remove one tag from phx-click. Dispatches corex:tags-input:remove-value with detail.value.

<.action phx-click={Corex.TagsInput.remove_value("my-tags", "a")}>Remove a</.action>
<.tags_input id="my-tags" class="tags-input">
  <:control><span /></:control>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>

remove_value(socket, tags_input_id, value)

Remove one tag from handle_event (tags_input_remove_value).

def handle_event("remove_tag", %{"tag" => t}, socket) do
  {:noreply, Corex.TagsInput.remove_value(socket, "my-tags", t)}
end

set_value(tags_input_id, value)

@spec set_value(
  String.t(),
  keyword()
) :: Phoenix.LiveView.JS.t()

Replace all tags from phx-click. Dispatches corex:tags-input:set-value with detail.value as a string list.

<.action phx-click={Corex.TagsInput.set_value("my-tags", ["a", "b"])}>Reset tags</.action>
<.tags_input id="my-tags" value={[]} class="tags-input">
  <:control><span /></:control>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>
document.getElementById("my-tags")?.dispatchEvent(
  new CustomEvent("corex:tags-input:set-value", {
    bubbles: false,
    detail: { value: ["a", "b"] },
  })
);

set_value(socket, tags_input_id, value)

Replace all tags from handle_event (tags_input_set_value).

def handle_event("set_tags", _, socket) do
  {:noreply, Corex.TagsInput.set_value(socket, "my-tags", ["x"])}
end