Pills input

v0.0.10

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

The Mishka Chelekom Mob Pills Input is a bordered control that holds pills you supply beside a text field, on iOS and Android. It is the native port of the headless Pills Input and runs on Mob.

Overview of the Mob Pills Input component for iOS and Android

Pills Input does not own its pills. You pass the pill nodes as children, and they can be Mob Pill components, avatars, color swatches or anything else. The control draws them in rows above a text field. The field has no border of its own, because the control around it is the box.

Tags Input owns its tokens instead: you hand it strings and it renders them as pills with ✕ buttons. Use Tags Input for a list of strings, and Pills Input when the tokens are richer, such as a recipient picker that shows avatars. The web component draws the same line with its pills slot.

The screen owns the state: the draft text and the list of pills. on_draft reports typing. on_add reports the return key and carries no text, so you commit the draft the screen already holds.

Pills Input owns no suggestion list, but a picker usually wants one. on_press tells the screen to open the list, on_draft filters it, and a tap on a row adds a pill, or removes it when that pill is already there. Removing on a second tap keeps a picker from collecting the same recipient twice. The example below shows the whole pattern.

Mob has no flow layout, and a row runs off the edge instead of wrapping. The control packs the pills into rows itself, against a budget of wrap_chars characters per row. It costs each pill by the text inside it plus 5 for padding, the ✕ and the gap. Mob reports no geometry back to the screen, so this is an estimate. A pill with no text, such as an avatar or swatch, costs a flat 10. When the pills are uniform, set per_row to chunk by count instead.

Generate the pills input component

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

bash
mix mishka.ui.gen.mob pills_input

The generator registers the <PillsInput> tag so you can use it in ~MOB markup. It does not generate other components. The pills are yours: the example below builds them with the Pill component, which you add with mix mishka.ui.gen.mob pill. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaPillsInput>. Without a prefix the tag is <PillsInput>.

Pills input as a picker

Tap the field to open the list, type to filter it, and tap a name to add it as a pill. Tapping a name you already have removes it. One clause does both, which also means the same person cannot be added twice. Pressing return still commits what you typed.

Pills input as a picker

Pills input card with a bordered field holding an Ada Lovelace pill above the placeholder Add a recipient, with no suggestions open, in the MishkaMob showcase app on Android
A recipient pills input with an Ada Lovelace pill and an open list of five names with Ada ticked.

Left: the example as it loads. Right: after a tap. Tap the field for the list, tap a name to add it.

elixir
<MishkaPillsInput draft={@draft} on_draft={:typed} on_add={:commit} on_press={:open}>
  {pills(@chosen)}
</MishkaPillsInput>
{suggestions_menu(@chosen, @draft)}

# ONE clause does add and remove: a tap on a name you already have is a
# request to drop it, which is also what stops duplicates existing.
def handle_info({:tap, {:pick, name}}, socket) do
  chosen = socket.assigns.chosen

  next =
    if name in chosen, do: List.delete(chosen, name), else: chosen ++ [name]

  {:noreply, socket |> assign(:chosen, next) |> assign(:draft, "")}
end

Props

The children of <MishkaPillsInput> are the pills.

PropTypeDefaultDescription
draftstring""The text field's value.
placeholderstringnilPlaceholder of the draft field.
disabledbooleanfalseMutes the control and unwires it.
on_draftevent tag (atom)NoneSends {:change, tag, text} as the draft is typed.
on_addevent tag (atom)NoneSends {:submit, tag} on return. It carries no text, so commit your own draft.
on_pressevent tag (atom)NoneSends {:tap, tag} on every tap of the draft.
on_focusevent tag (atom)NoneSends {:focus, tag} when the draft gains focus.
on_blurevent tag (atom)NoneSends {:blur, tag} when the draft loses focus.
spacenumber6Gap between pills.
wrap_charsnumber34Characters per pill row.
per_rownumbernilA fixed count of pills per row, for uniform pills.
idstringnilTest tag on the draft field (<id>-draft).

Helper functions

FunctionDescription
pack(pills, budget)Packs pills into rows against a character budget, costing each pill by the text it contains.

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 you press return. It has no text.
  • on_press sends {:tap, tag} on every tap of the draft.
  • on_focus sends {:focus, tag} and on_blur sends {:blur, tag}.

With on_draft={:typed} and on_add={:commit}, the screen stores the typed draft and commits it on return. Typing also opens the picker list, so someone who types before the list appears does not have to tap again.

elixir
def handle_info({:change, :typed, text}, socket) do
  {:noreply, socket |> Mob.Socket.assign(:draft, text) |> Mob.Socket.assign(:open?, true)}
end

# A submit carries no payload, so the draft the screen already holds is what commits.
def handle_info({:submit, :commit}, socket) do
  draft = String.trim(socket.assigns.draft)

  if draft == "" or draft in socket.assigns.chosen do
    {:noreply, Mob.Socket.assign(socket, :draft, "")}
  else
    {:noreply,
     socket
     |> Mob.Socket.assign(:chosen, socket.assigns.chosen ++ [draft])
     |> Mob.Socket.assign(:draft, "")}
  end
end

Differences from the web component

  • input_name is not ported, because it is form plumbing. The id prop only sets a test tag on the draft field.
  • The *_class attributes are not ported. Mob draws native views, so there is no CSS.