Select

v0.0.10

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

The Mishka Chelekom Mob Select shows the current choice in a trigger and opens a list of options to pick from, on iOS and Android. It is the native port of the headless Select and runs on Mob.

Overview of the Mob Select component for iOS and Android

Select renders a column. An optional label sits above a bordered trigger. The trigger shows the label of each chosen option, or the placeholder in muted text when nothing is chosen. The ▾ at the end of the row turns into ▴ while the list is open.

The list opens in flow, directly below the trigger, and does not float over the page. A list of options is as long as its data, and a floating list would cover the field it came from. The list uses the surface of the Mob Menu component, so a select and a menu look alike, and a chosen option shows a ✓. For a long list, put the same options in a bottom Mob Drawer.

The screen owns the state. You keep value and open in assigns and pass them in. Taps arrive in handle_info/2, and MishkaSelect.toggle/3 returns the next {value, close?} pair. Its third argument sets the mode. In single mode, a pick replaces the value and closes the list. In multiple mode, a pick adds the option, or removes it when it is already chosen, and the list stays open.

Write options as <MishkaSelectOption> children, or build them from data with option/3. Both make the same node, so you can mix them in one select. Consecutive options that share a group form one run under a heading. The order you write the options in is the grouping.

Generate the select component

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

bash
mix mishka.ui.gen.mob select

The generator registers the <Select> tag so you can use it in ~MOB markup. It also generates the Mob Menu component, which draws the list. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaSelect>. Without a prefix the tag is <Select>.

Single choice select

Picking an option replaces the value and closes the list. The screen passes in the current value and open state, and MishkaSelect.toggle/3 returns the next value and whether the list closes.

Single choice select

Single choice card with a select captioned COUNTRY showing the muted placeholder Choose a country with the list closed, in the MishkaMob showcase app on Android
A select labelled COUNTRY open with a list of United Kingdom, Iran, Germany and Japan underneath.

Left: the example as it loads. Right: after a tap. A trigger shows the current choice and opens a list.

elixir
<MishkaSelect value={@country} open={@open?} on_toggle={:open} on_select={:pick}>
  <MishkaSelectOption id={:uk} label="United Kingdom" />
  <MishkaSelectOption id={:ir} label="Iran" />
</MishkaSelect>

{value, close?} = MishkaSelect.toggle(@country, id, false)

Multiple select

In multiple mode, a pick adds the option to value and keeps the list open, so you can choose several in a row. A chosen option shows a ✓. The Rust option has disabled={true}, so you cannot pick it.

Multiple select

Multiple card with a select captioned LANGUAGES showing the muted placeholder Choose any with the list closed, in the MishkaMob showcase app on Android
A LANGUAGES select open with Gleam ticked, Elixir and Erlang unticked and Rust greyed out.

Left: the example as it loads. Right: after a tap. Picking accumulates and the list stays open.

elixir
<MishkaSelect value={@langs} multiple={true} open={@open?} on_select={:pick}>
  <MishkaSelectOption id={:elixir} label="Elixir" />
  <MishkaSelectOption id={:erlang} label="Erlang" />
  <MishkaSelectOption id={:rust} label="Rust" disabled={true} />
</MishkaSelect>

{value, close?} = MishkaSelect.toggle(@langs, id, true)

Grouped select options

Options that sit next to each other and share a group get one heading above them. The component sorts nothing, so the order you write the options in is the grouping. This select is also multiple, so the list stays open while you pick.

Grouped select options

Grouped card with a select captioned TOPPINGS showing Pepperoni as its chosen value and the list closed, in the MishkaMob showcase app on Android
A TOPPINGS select open with a CLASSIC heading over Cheese and Pepperoni, and a VEGGIE heading over Mushroom and Onion.

Left: the example as it loads. Right: after a tap. A heading above each run of options.

elixir
<MishkaSelect value={@toppings} multiple={true} open={@open?} on_select={:pick}>
  <MishkaSelectOption id={:cheese} label="Cheese" group="CLASSIC" />
  <MishkaSelectOption id={:pepperoni} label="Pepperoni" group="CLASSIC" />
  <MishkaSelectOption id={:mushroom} label="Mushroom" group="VEGGIE" />
  <MishkaSelectOption id={:onion} label="Onion" group="VEGGIE" />
</MishkaSelect>

# CONSECUTIVE options sharing a group belong to it, so your order is the
# grouping — nothing is sorted underneath you.

Disabled select and options from data

A disabled select shows muted text and does not open. Its options come from a list. option/3 builds the same node as <MishkaSelectOption>, so you can map data to options.

Disabled select and options from data

Disabled, and options from data card with a select captioned LOCKED showing United Kingdom in muted text with the list closed, in the MishkaMob showcase app on Android
heex
<MishkaSelect value={:uk} disabled={true}>
  {Enum.map(@countries, fn {id, label} -> MishkaSelect.option(id, label) end)}
</MishkaSelect>

Props

PropTypeDefaultDescription
valueoption id, list or nilnilThe current choice. A list in multiple mode.
openbooleanfalseWhether the list is shown. The screen owns it.
multiplebooleanfalseAllows several choices. Your handler passes the same mode to toggle/3.
placeholderstring"Select…"Trigger text when nothing is chosen.
labelstringnilCaption above the trigger.
disabledbooleanfalseMutes the trigger and unwires it.
on_toggleevent tag (atom)NoneSends {:tap, tag} from the trigger.
on_selectevent tag (atom)NoneSends {:tap, {tag, option_id}} from an option.
idstringnilTest tags for the trigger (<id>-trigger-open or -closed) and for every option (<id>-option-<option_id>-selected or -idle).

Option attributes

Each <MishkaSelectOption> takes these attributes. Every attribute except id is optional.

AttributeDefaultDescription
idNoneWhat on_select reports, and the stem of the option's test tag.
labelNoneThe row's text.
disabledfalseMutes the row and unwires it.
groupnilA heading above the run this option starts.

Helper functions

The generated MishkaSelect module exposes these functions:

FunctionDescription
option(id, label, opts)Builds one option node. Options are :disabled and :group.
toggle(value, id, multiple?)Returns the next {value, close?} after picking id. Single mode replaces the value and closes. Multiple mode adds or removes the id and stays open.
display(value, labels, placeholder)Returns the trigger text: the chosen labels joined with a comma, or the placeholder when nothing is chosen.
group_runs(options)Chunks options into {group, options} runs of consecutive options that share a group.

Events

Your screen receives two messages in handle_info/2:

  • on_toggle sends {:tap, tag} when you tap the trigger. A disabled select drops it.
  • on_select sends {:tap, {tag, option_id}} when you tap an option.

With on_toggle={:open} and on_select={:pick}, the screen handles them like this:

elixir
def handle_info({:tap, :open}, socket) do
  {:noreply, Mob.Socket.assign(socket, :open?, not socket.assigns.open?)}
end

def handle_info({:tap, {:pick, id}}, socket) do
  {value, close?} = MishkaSelect.toggle(socket.assigns.country, id, false)

  {:noreply,
   socket
   |> Mob.Socket.assign(:country, value)
   |> Mob.Socket.assign(:open?, not close?)}
end

Differences from the web component

  • The list renders in flow under the trigger. It does not float beside it, so side does not apply.
  • name, form, required and readonly are not ported, because they are form plumbing.
  • highlight_on_hover is not ported, because a touch screen has no hover.
  • The *_class attributes are not ported. Mob draws native views, so there is no CSS.