Toggle group

v0.0.10

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

Mob Toggle Group is a row of toggle buttons that share one selection, in single or multiple mode, for iOS and Android. It comes from Mishka Chelekom and runs on Mob, which renders native Jetpack Compose and SwiftUI views from Elixir. It is the native port of the headless Toggle Group.

Overview of the Mob Toggle Group component for iOS and Android

Toggle Group builds on Toggle, so a lone toggle and a grouped one are the same button. The group adds the selection rule. The screen owns the value, and the group sends {:tap, {tag, item_id}} when an item is pressed.

Single and multiple mode share one reducer

press/3 takes the current value, the pressed id and whether the group is multiple, and returns the next value. It is the whole behavior, so you do not reimplement it per screen.

  • In multiple mode the value is a set. Pressing an item adds it, and pressing it again removes it.
  • In single mode the value is one id. Pressing the pressed item clears it back to nil. A radio group cannot be cleared, and a segmented control always keeps a selection.

value is a list in multiple mode and a bare id in single mode, matching the web component's value attribute. pressed?/2 tells you whether an id is pressed in either mode.

Items are slot children

Write each item as a <MishkaToggleGroupItem> tag, the way the Phoenix component writes it. An item takes id, label and an optional disabled. id is what on_change reports and the stem of the item's test tag. item/3 builds the identical node, so tags and function calls mix freely in one group. Use the function when the items come from data.

heex
<MishkaToggleGroup value={@align} on_change={:align} id="align">
  <MishkaToggleGroupItem id={:left} label="Left" />
  <MishkaToggleGroupItem id={:center} label="Center" />
  <MishkaToggleGroupItem id={:right} label="Right" disabled={true} />
</MishkaToggleGroup>

Styling and orientation

The group has no look of its own. Every styling prop that Toggle takes, such as color, text_color, background, label_color, padding, corner_radius, border_color, border_width and text_size, passes straight through to each item. You style the group by styling its buttons. space={0} joins the items into one bar.

Orientation decides whether an item hugs its label. Horizontally each item hugs, because an item that filled the row would push its siblings off the screen. A vertical group fills each item instead, so the buttons share one width and do not form a ragged staircase.

Generate the toggle group component

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

bash
mix mishka.ui.gen.mob toggle_group

The generator registers the tag, so you can use it in ~MOB markup. Toggle Group calls Toggle, so the generator creates the toggle module too, without asking. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaToggleGroup>. Without a prefix, the tag is <ToggleGroup>.

Single selection toggle group

In single mode, pressing the pressed button clears the group. A radio group cannot do that. press/3 is the reducer, so call it with false in handle_info/2. Alias the generated module first, because press/3 lives there.

Single selection toggle group

Single card: a joined bar of three toggle buttons, Left, Center and Right, with Center pressed in black, and the gray text Value: :center below it
A bordered toggle group with Left pressed beside Center and Right, and the line Value: :left below.

Left: the example as it loads. Right: after a tap. A single group keeps one pressed button.

elixir
<MishkaToggleGroup value={@align} on_change={:align} space={0} border_width={0}>
  <MishkaToggleGroupItem id={:left} label="Left" />
  <MishkaToggleGroupItem id={:center} label="Center" />
  <MishkaToggleGroupItem id={:right} label="Right" />
</MishkaToggleGroup>

# press/3 is the reducer: in single mode, pressing the pressed one clears.
def handle_info({:tap, {:align, id}}, socket) do
  next = MishkaToggleGroup.press(socket.assigns.align, id, false)
  {:noreply, Mob.Socket.assign(socket, :align, next)}
end

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

Multiple selection toggle group

multiple: true makes the value a set, so several items can be pressed at once. Pass true as the last argument of press/3.

Multiple selection toggle group

Multiple card: a joined bar of toggle buttons Bold, Italic and Underline with Bold and Italic pressed in black, and a gray value readout below listing bold and italic
A toggle group with Bold, Italic and Underline all pressed and a value readout listing the three.

Left: the example as it loads. Right: after a tap. A multiple group holds a set of pressed buttons.

elixir
<MishkaToggleGroup value={@style} multiple={true} on_change={:style}>
  <MishkaToggleGroupItem id={:bold} label="Bold" />
  <MishkaToggleGroupItem id={:italic} label="Italic" />
  <MishkaToggleGroupItem id={:under} label="Underline" />
</MishkaToggleGroup>

def handle_info({:tap, {:style, id}}, socket) do
  next = MishkaToggleGroup.press(socket.assigns.style, id, true)
  {:noreply, Mob.Socket.assign(socket, :style, next)}
end

Vertical toggle group

orientation={:vertical} stacks the items in a column, and stacked items share one width.

Vertical toggle group

Vertical card: a stacked group of three toggle buttons, List, Grid and Cards, with Grid pressed in black, in the MishkaMob showcase app on Android
heex
<MishkaToggleGroup orientation={:vertical} value={@view} on_change={:view}>
  <MishkaToggleGroupItem id={:list} label="List" />
  <MishkaToggleGroupItem id={:grid} label="Grid" />
  <MishkaToggleGroupItem id={:cards} label="Cards" />
</MishkaToggleGroup>

Style a toggle group with props

The group has no look of its own. These two groups differ only in props: spaced pills in violet, and joined square buttons with a border. The code leaves out the items. Put the <MishkaToggleGroupItem> tags inside each group, as in the previous examples.

Style a toggle group with props

Styled by props, not by a stylesheet card: a spaced violet pill group with Center pressed above a joined square group with Center pressed in near black
heex
# Spaced pills
<MishkaToggleGroup space={8} corner_radius={20} padding={12} color={0xFF7C3AED} />

# Joined and square
<MishkaToggleGroup space={0} corner_radius={2} border_width={1} />

Disabled toggle group

Disable one item, or the whole group. The group's disabled cascades to every item. A disabled item still shows whether it is pressed.

Disabled toggle group

Disabled card: two toggle bars, ONE ITEM OFF with Left and a muted Right (off), and WHOLE GROUP OFF with a muted A (off) and B (off) filled gray
heex
# One item off:
<MishkaToggleGroupItem id={:right} label="Right" disabled={true} />

# The whole group off — it cascades to every item:
<MishkaToggleGroup disabled={true} value={@align}>
  <MishkaToggleGroupItem id={:left} label="Left" />
  <MishkaToggleGroupItem id={:right} label="Right" />
</MishkaToggleGroup>

Helper functions

The generated MishkaToggleGroup module exports these functions.

FunctionDescription
press/3Returns the next value after pressing an id, for both modes.
pressed?/2Whether an id is pressed for a given value, in either mode.
item/3Builds the same node as <MishkaToggleGroupItem>, for items that come from data.

Props

PropTypeDefaultDescription
valueid, list of ids or nilnilThe pressed item or items. A list in multiple mode.
multiplebooleanfalseAllows several items to be pressed at once.
disabledbooleanfalseDisables every item.
on_changeevent tagNoneSent as {:tap, {tag, item_id}}.
orientation:horizontal or :vertical:horizontalLayout axis.
spacenumber8Gap between items. 0 joins them into a bar.
color, text_color, background, label_colorsee ToggleunsetForwarded to every item. The pressed pair, then the idle pair.
padding, corner_radiussee ToggleunsetAlso forwarded to every item.
border_color, border_width, text_sizesee ToggleunsetAlso forwarded. border_width: 0 gives a segmented bar's inner buttons.
fill_widthbooleantrueThe container's width, not the items'. false lets it hug, for a segmented bar.
idstringnilPrefix for each item's test tag, as <id>-<item>-pressed.

Item props

PropTypeDefaultDescription
idany termNoneWhat on_change reports, and the stem of the item's test tag.
labelstringNoneThe button text.
disabledbooleanfalseDisables this item.

Events

on_change sends {:tap, {tag, item_id}} to your screen, which receives it in handle_info/2. Pass the pressed id to press/3 to get the next value:

elixir
def handle_info({:tap, {:align, id}}, socket) do
  next = MishkaToggleGroup.press(socket.assigns.align, id, false)
  {:noreply, Mob.Socket.assign(socket, :align, next)}
end

Differences from the web component

  • name and form are not ported, because they are form plumbing.
  • loop is not ported. It moves focus with the arrow keys, and a phone has no focus ring to move.
  • The *_class attributes are not ported. Every visual is a prop instead.