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>.
| Function | Action | Returns |
|---|---|---|
set_value/2 | Set tag list (client) | %Phoenix.LiveView.JS{} |
set_value/3 | Set tag list (server) | socket |
add_value/2 | Add one tag (client) | %Phoenix.LiveView.JS{} |
add_value/3 | Add one tag (server) | socket |
remove_value/2 | Remove one tag (client) | %Phoenix.LiveView.JS{} |
remove_value/3 | Remove one tag (server) | socket |
clear_value/1 | Clear all tags (client) | %Phoenix.LiveView.JS{} |
clear_value/2 | Clear all tags (server) | socket |
Events
Pick an event name and pass it to on_* on <.tags_input>.
Server events
| Event | When | Payload |
|---|---|---|
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)}
endClient events
| Event | When | event.detail |
|---|---|---|
on_value_change_client="tags-client-changed" | Tag list changes | id, value |
on_value_invalid_client="tags-client-invalid" | Invalid tag or max | id, 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)}
endForm
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
| Modifier | Classes |
|---|---|
| Default | tags-input |
| Accent | tags-input tags-input--accent |
| Brand | tags-input tags-input--brand |
| Alert | tags-input tags-input--alert |
| Info | tags-input tags-input--info |
| Success | tags-input tags-input--success |
Size
| Modifier | Classes |
|---|---|
| SM | tags-input tags-input--sm |
| MD | tags-input tags-input--md |
| LG | tags-input tags-input--lg |
| XL | tags-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
Attributes
id(:string)value(:list) - Initial or controlled list of tag strings; JSON-encoded for the hook. Defaults to[].controlled(:boolean) - Defaults tofalse.disabled(:boolean) - Defaults tofalse.read_only(:boolean) - Defaults tofalse.invalid(:boolean) - Defaults tofalse.required(:boolean) - Defaults tofalse.name(:string) - Defaults tonil.form(:string) - Defaults tonil.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 tonil.dir(:string) - When nil, derived from document. Defaults tonil. Must be one ofnil,"ltr", or"rtl".max(:integer) - Maximum number of tags. Defaults tonil.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 tonil.blur_behavior(:string) - Blur behavior for pending input text. Defaults tonil. Must be one ofnil,"add", or"clear".add_on_paste(:boolean) - Defaults tofalse.allow_duplicates(:boolean) - Defaults tofalse.allow_overflow(:boolean) - Defaults tofalse.editable(:boolean) - Defaults tonil.auto_focus(:boolean) - Defaults tofalse.placeholder(:string) - Defaults tonil.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 tonil.on_value_change_client(:string) - Defaults tonil.on_input_value_change(:string) - Defaults tonil.on_input_value_change_client(:string) - Defaults tonil.on_highlight_change(:string) - Defaults tonil.on_highlight_change_client(:string) - Defaults tonil.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 tonil.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 tonil.- Global attributes are accepted.
Slots
label- Accepts attributes:class(:string)
close(required) - Content for each delete trigger.error- Accepts attributes:class(:string)
API
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>
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 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 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 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 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
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"] },
})
);
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