Checkbox group

v0.0.10

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

The Mob Checkbox Group is a labeled set of checkboxes with an optional select all parent, for iOS and Android apps built with Mob. It is part of Mishka Chelekom and is the native port of the headless Checkbox Group for Phoenix LiveView.

Overview of the Mob Checkbox Group component for iOS and Android

MishkaCheckboxGroup draws every row, and the parent, with the Checkbox component. The parent is an ordinary checkbox whose indeterminate state the group derives from its rows.

A checkbox group is mostly selection bookkeeping. The group exposes it as three helpers, so each handler does not derive it again:

  • toggle/2 adds or removes one id from the selection.
  • select_all/2 returns what a tap on the parent should produce: all ids, unless every id is already selected, in which case none. A partly selected group fills up and does not clear.
  • parent_state/2 returns the {checked?, indeterminate?} that the parent should render.
elixir
MishkaCheckboxGroup.toggle([:a], :b)
# [:a, :b]

MishkaCheckboxGroup.select_all([:a], [:a, :b])
# [:a, :b]

MishkaCheckboxGroup.parent_state([:a], [:a, :b])
# {false, true}

Write rows as <MishkaCheckboxGroupItem> tags, one per row, the native version of the Phoenix component's <:checkbox> slot. Each tag takes id, label and disabled. An item has no on_* prop of its own. The group's single on_change serves every row, because each row reports its own id, and the message arrives in handle_info/2. When the rows come from data, item/3 builds the identical node.

Set select_all to draw the parent above the rows with a divider under it. select_all_label changes its text, which defaults to Select all.

Give the group an id to name each row for device tests. With id="langs", the :otp row is tagged langs-otp-checked or langs-otp-empty, and the parent is tagged langs-all-mixed when only some rows are selected. The marks are drawn, not typed, so these tags are the only thing a device test can read.

Generate the checkbox group component

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

bash
mix mishka.ui.gen.mob checkbox_group

The generator registers the component's tag, so you can use it in ~MOB markup. It also copies the shared Event module and generates the Checkbox component, because the group draws its rows with it. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaCheckboxGroup>. Without a prefix the tag is <CheckboxGroup>. To generate the prefixed version, run:

bash
mix mishka.ui.gen.mob checkbox_group --module-prefix mishka_

Checkbox group with select all

Set select_all and the group draws a parent checkbox above the rows. The parent shows the mixed state when only some rows are selected. Rows are <MishkaCheckboxGroupItem> tags, and the group derives the parent state from them.

The row handler receives {:tap, {tag, item_id}} and the parent handler receives {:tap, tag}. select_all/2 takes the current selection and the list of every row id, which the example keeps in @all_ids.

Checkbox group with select all

Card titled Select all, with a LIBRARIES checkbox group whose Select all libraries parent shows a dash above BEAM checked and OTP, Phoenix and Ecto unchecked, then Selected: beam
A LIBRARIES group with Select all libraries and the rows BEAM, OTP, Phoenix and Ecto all checked.

Left: the example as it loads. Right: after a tap. The select all parent ticks every row at once.

elixir
<MishkaCheckboxGroup
  value={@value}
  select_all={true}
  on_change={:item}
  on_select_all={:all}
  id="langs"
>
  <MishkaCheckboxGroupItem id={:beam} label="BEAM" />
  <MishkaCheckboxGroupItem id={:otp} label="OTP" />
  <MishkaCheckboxGroupItem id={:phoenix} label="Phoenix" />
  <MishkaCheckboxGroupItem id={:ecto} label="Ecto" />
</MishkaCheckboxGroup>

# No item tag carries its own on_*: one on_change serves every row,
# because each reports its own id. An item tap carries that id; the
# parent's does not need to, because there is only one parent. Both
# reducers live in the component so a screen never re-derives them.
def handle_info({:tap, {:item, id}}, socket) do
  {:noreply, Mob.Socket.assign(socket, :value,
    MishkaCheckboxGroup.toggle(socket.assigns.value, id))}
end

def handle_info({:tap, :all}, socket) do
  {:noreply, Mob.Socket.assign(socket, :value,
    MishkaCheckboxGroup.select_all(socket.assigns.value, @all_ids))}
end

def handle_info(_msg, socket), do: {:noreply, socket}

Checkbox group rows from data

When the rows come from a list, item/3 builds the same node that the tag does, so you can use either. Call it with an id and a label, as in the example. This group leaves select_all off, so it has no parent and no divider.

Checkbox group rows from data

Card titled Rows from data, with a NOTIFY ME BY checkbox group of three rows, Email checked and SMS and Push unchecked, and no select all parent
elixir
@channels [{:email, "Email"}, {:sms, "SMS"}, {:push, "Push"}]

# A comprehension is easier to write than generated markup, so reach for
# item/2 here and for the tag when the rows are literal.
<MishkaCheckboxGroup value={@value} on_change={:item}>
  {Enum.map(@channels, fn {id, label} -> item(id, label) end)}
</MishkaCheckboxGroup>

def handle_info({:tap, {:item, id}}, socket) do
  {:noreply, Mob.Socket.assign(socket, :value,
    MishkaCheckboxGroup.toggle(socket.assigns.value, id))}
end

Disabled checkbox group

Put disabled on an item tag to disable one row, or on the group to disable every row and the parent.

Disabled checkbox group

Card titled Disabled, with ONE ITEM OFF showing BEAM checked and a muted Ecto (unavailable), above WHOLE GROUP OFF with a muted Select all (off) parent and four muted rows
heex
<MishkaCheckboxGroup value={@value} on_change={:item}>
  <MishkaCheckboxGroupItem id={:beam} label="BEAM" />
  <MishkaCheckboxGroupItem id={:ecto} label="Ecto" disabled={true} />
</MishkaCheckboxGroup>

<MishkaCheckboxGroup disabled={true} select_all={true}>…</MishkaCheckboxGroup>

Props

PropTypeDefaultDescription
valuelist of ids[]The selected ids.
labelstringnilGroup heading.
select_allbooleanfalseDraws the parent checkbox.
select_all_labelstring"Select all"Text of the parent checkbox.
disabledbooleanfalseDisables every row, the parent included.
on_changeevent tagNoneSent as {:tap, {tag, item_id}}.
on_select_allevent tagNoneSent as {:tap, tag} for the parent.
spacenumber12Gap between rows.
color and sizesee Checkbox:primary and 22Passed to every row, the parent included. See Checkbox.
idstringnilPrefix for each row's test tag, such as langs-otp-checked.
<MishkaCheckboxGroupItem>slotNoneOne row, with id (what on_change reports), label and disabled.
item/3builderNoneBuilds the same node as the item tag, for rows that come from data.
toggle/2, select_all/2 and parent_state/2helpersNoneThe selection helpers, so each screen does not derive them again.

Events

Your screen receives {:tap, {tag, item_id}} from on_change for a row and {:tap, tag} from on_select_all for the parent, both in handle_info/2. The parent message needs no id, because a group has one parent. These handlers use the group's toggle/2 and select_all/2 helpers:

elixir
def handle_info({:tap, {:item, id}}, socket) do
  {:noreply, Mob.Socket.assign(socket, :value,
    MishkaCheckboxGroup.toggle(socket.assigns.value, id))}
end

def handle_info({:tap, :all}, socket) do
  {:noreply, Mob.Socket.assign(socket, :value,
    MishkaCheckboxGroup.select_all(socket.assigns.value, @all_ids))}
end

def handle_info(_msg, socket), do: {:noreply, socket}

Differences from the web component

  • name is not ported. It is HTML form plumbing.
  • The *_class attributes are not ported.
  • The indicator_icon slot is not ported, because the Checkbox draws the tick and the dash.