Corex.TagsInput (Corex v0.1.0)

View Source

Phoenix implementation of 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

Controlled

<.tags_input
  class="tags-input"
  controlled
  value={@tags}
  on_value_change="tags_changed"
>
  <:label>Keywords</:label>
  <:close><.heroicon name="hero-x-mark" /></:close>
</.tags_input>
def handle_event("tags_changed", %{"value" => value}, socket) do
  {:noreply, assign(socket, :tags, value)}
end

Form

For cross-cutting invalid styling and error presentation, see the Forms guide. Pass invalid={Corex.FormField.invalid?(@form[:tags])} when you want alert borders after validation.

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. Do not use controlled on form examples; 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 button--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/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/tags-input.css";

Stack modifiers on the host (class on <.tags_input>).

Color

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

Size

ModifierClasses
SMtags-input tags-input--sm
MDtags-input tags-input--md
LGtags-input tags-input--lg
XLtags-input tags-input--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)
  • value (:list) - Initial or controlled list of tag strings; JSON-encoded for the hook. Defaults to [].
  • controlled (:boolean) - Defaults to false.
  • disabled (:boolean) - Defaults to false.
  • read_only (:boolean) - Defaults to false.
  • invalid (:boolean) - Defaults to false.
  • required (:boolean) - Defaults to false.
  • name (:string) - Defaults to nil.
  • form (:string) - Defaults to nil.
  • errors (:list) - Error messages when not using field=. Defaults to [].
  • field (Phoenix.HTML.FormField) - Form field; sets id, name, form, value from the field value, and errors when used_input?/1. Defaults to nil.
  • 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)

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)

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)

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)

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