Tags input

v0.0.10

v0.0.10StableMobStatelessApache-2.0
Installation mix mishka.ui.gen.mob tags_input
Source View Source

The Mishka Chelekom Mob Tags Input is a bordered control that holds removable tag tokens above a text field for the next tag, on iOS and Android. It is the native port of the headless Tags Input and runs on Mob.

Overview of the Mob Tags Input component for iOS and Android

Each token is a Pill with its own ✕. The text field below the tokens is Mob's native TextField, and its text is called the draft. The draft field draws no box or underline of its own, so the border of the control is the only outline.

The screen owns the state: the tags list and the draft text. Typing sends the draft to the screen through on_draft. The return key sends on_add, and a tap on a token's ✕ sends on_remove.

A phone has no keydown to hook, but a text field reports a submit when the return key is pressed, and the component sets the key to done. The submit event is a bare notification with no text, because the bridge passes only a handle with it. So the screen commits the draft it already holds from on_draft and clears it.

Two helpers do the list work. MishkaTagsInput.add/3 trims the draft, ignores blank input and rejects a duplicate, so adding elixir twice leaves one tag. Pass allow_duplicates: true to allow repeats. MishkaTagsInput.remove/2 removes a tag by value.

How tokens wrap

Neither renderer has a flow layout, and nothing on the Elixir side of the bridge can measure text. A single row of tokens would run off the edge, so MishkaTagsInput.wrap/2 packs the tags into rows with a character budget. Each tag costs its length plus 5, and the budget is wrap_chars, which defaults to 34. A tag longer than the whole budget gets a row of its own.

The budget is an estimate, so the default leans toward under-packing. A sparse row looks sparse, while an over-packed row squeezes its last token. Raise wrap_chars for a wider control and lower it for a narrow one.

Generate the tags input component

Run this from your Mob app's root to add the component to lib/<app>/components/:

bash
mix mishka.ui.gen.mob tags_input

The generator registers the tag, so you can use it in ~MOB markup. Tags Input is built on the Pill component, so the generator creates pill as well, with the same prefixes. It also copies the shared Event helper module, unless you pass --no-kit. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the module is MishkaTagsInput and the tag is <MishkaTagsInput>. Without a prefix, the tag is <TagsInput>.

Add and remove tags

Pass the tags list and the draft text, and give each event its own tag: on_draft, on_add and on_remove. The user types a draft, presses return to commit it and taps a token's ✕ to remove that tag. The submit event carries no text, so the handler commits the draft the screen already holds with MishkaTagsInput.add/3 and then clears it.

Add and remove tags

Add and remove card: a tags input with removable tags design, phoenix and elixir above the placeholder Add a tag, and a gray line counting 3 tags below
A tags input holding the pills phoenix and elixir, with the line 2 tag(s): phoenix, elixir below.

Left: the example as it loads. Right: after a tap. Each token's close mark removes it.

elixir
<MishkaTagsInput
  tags={@tags}
  draft={@draft}
  on_draft={:draft}
  on_add={:add}
  on_remove={:remove}
/>

# submit carries no text — commit the draft you already hold
def handle_info({:submit, :add}, socket) do
  {:noreply, socket
     |> assign(:tags, MishkaTagsInput.add(socket.assigns.tags, socket.assigns.draft))
     |> assign(:draft, "")}
end

Tags input without a box

The control's box comes from props, so you can remove it. Set border_width={0} and padding={0}, with background={:background} to match the screen, and only the tokens and the draft field remain.

Tags input without a box

It ships no look card: a borderless tags input with the tags design, phoenix and elixir above the placeholder No box at all, then a gray note about refused duplicates
heex
<MishkaTagsInput tags={@tags} border_width={0} padding={0} background={:background} />

Empty and disabled tags input

With no tags, the control is just its draft field and its placeholder. disabled={true} mutes the tokens and the draft field and unwires everything.

Empty and disabled tags input

Empty and disabled card: an empty tags input with the placeholder Nothing yet, above a muted disabled one holding the tags locked and readonly and the placeholder Disabled
heex
<MishkaTagsInput tags={[]} placeholder="Nothing yet" />
<MishkaTagsInput tags={["locked"]} disabled={true} />

Props

Props of the <MishkaTagsInput> tag.

PropTypeDefaultDescription
tagslist of strings[]The current tokens.
draftstring""The value of the text field.
placeholderstringnilPlaceholder of the draft field. It shows Add a tag… when unset.
disabledbooleanfalseMutes the control and unwires everything.
on_draft / on_add / on_removeevent tagsNoneEvents for typing, for the return key and for a token's ✕. See Events below.
background / corner_radius / paddingcolor / radius / spacing:surface / :radius_md / :space_smThe control's own box. border_width: 0 removes the outline.
border_color / border_widthcolor / number:border / 1The outline. The draft field inside draws no box of its own.
spacenumber6Gap between tokens.
wrap_charsnumber34Characters per row of tokens. Nothing on the Elixir side can measure text.
idstringnilTags the draft as <id>-draft and each token as <id>-tag-<tag>.
add/3, remove/2 and wrap/2helpersNoneTrim and reject blanks and duplicates, remove by value, and pack token rows.

Events

Your screen receives these messages in handle_info/2.

  • on_draft sends {:change, tag, text} as the draft is typed.
  • on_add sends {:submit, tag} when the user presses return. It carries no text, so commit the draft you already hold, as the first example shows.
  • on_remove sends {:tap, {tag, tag_string}} when the user taps a token's ✕, where tag_string is the tag to remove.
elixir
def handle_info({:change, :draft, text}, socket) do
  {:noreply, assign(socket, :draft, text)}
end

def handle_info({:tap, {:remove, tag}}, socket) do
  {:noreply, assign(socket, :tags, MishkaTagsInput.remove(socket.assigns.tags, tag))}
end

Differences from the web component

  • name and input_name are not ported, because they are form plumbing.
  • The *_class attributes are not ported. The control's box is a set of props.