Field

v0.0.10

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

The Mishka Chelekom Mob Field is a wrapper that puts a label, a hint and validation errors around any control on iOS and Android. It is the native port of the headless Field and runs on Mob.

Overview of the Mob Field component for iOS and Android

Field draws three parts in a column: the label, your control and a footer. The label is the label text, followed by a * in the error color when required is on. The control is the tag's child. It can be a TextField, a switch or any other node. The footer shows the description hint, or the errors when there are any.

Errors replace the hint. When errors holds at least one message, Field hides the description and lists each message after a ✕, so color is not the only signal that something is wrong. Blank messages (nil and empty strings) are ignored. The error color defaults to :error, a theme token, so it follows light, dark and custom themes.

The screen owns the state. Field keeps none, and it has no event props of its own. Your screen holds the control's value and the errors list, and the control sends its events to the screen. A text field with on_change={:email} sends {:change, :email, text} to handle_info/2.

On the web, Field wires ids and aria-describedby so a screen reader announces the label, the hint and the error together. A phone has no ids to wire, so the port keeps what the user sees: the label above, the hint and errors below, and the visual state that marks a field as invalid.

Generate the field component

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

bash
mix mishka.ui.gen.mob field

The generator registers the tag, so you can use it in ~MOB markup. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaField>. Without a prefix, the tag is <Field>.

Field with a label, hint and required mark

Set label to show text above the control, description to show a hint below it and required={true} to add a * after the label. The control is the tag's child. Here it is a text field that sends its text to the screen as {:change, :email, text}. In the snippet, text_input(...) stands for the function that builds your TextField node.

Field with a label, hint and required mark

Field example card titled Label, hint and required, with a red asterisk after the label Email, a text box with the placeholder you@example.com and the hint We'll never share it.
heex
<MishkaField label="Email" description="We'll never share it." required={true}>
  {[text_input(value: @email, placeholder: "[email protected]", on_change: :email)]}
</MishkaField>

Errors replace the hint

Pass errors as a list of strings. While the list holds a message, the field hides the hint and shows each message after a ✕ in the error color. In the showcase, type a value without an @ and the hint gives way to the error. The control tints its own border to match. MishkaField.invalid?/1 takes a props map and returns true when errors holds a non-blank message, so you can choose the border color from the same list. In the snippet, input() and the … description stand for your own control and text.

Errors replace the hint

Field example card titled Errors replace the hint, with an Email label, a text box with the placeholder you@example.com and the hint We'll never share it. below, no error
elixir
<MishkaField label="Email" description="…" errors={@errors}>{[input()]}</MishkaField>

# The control tints its own border; invalid?/1 is exposed so it can match.
border_color={if MishkaField.invalid?(%{errors: @errors}), do: :error, else: :border}

A whole form with validation

Errors on a field the user has not reached yet are noise, so this form shows a field's errors only after the field has changed. The Create account button shows every error at once. The form has a name, an email address, an optional age, a role chosen with a segmented control, a short bio and a disabled plan field. The role strip always shows one segment chosen, so Owner looks picked before you tap it. The form still counts the role as unanswered until you do. The snippet shows the state handling: a touched set, a submitted? flag and a shown/3 helper that decides whether a field's errors are visible. The showcase app receives {:change, tag, value} events in a handle_change/3 callback. In your own screen, handle the same event in handle_info/2.

A whole form with validation

Field example card titled A whole form, with required Full name and Email address boxes, an Age box holding 18 and a Role control with Owner selected
A form where Full name, Email address and Role show red error messages under their fields after Create account was tapped.

Left: the example as it loads. Right: after a tap. Submitting touches every field, so all the errors appear at once.

elixir
# A field is "touched" the first time it changes — the web calls this
# used_input?. Submitting touches everything, so nothing stays hidden.
def handle_change({:form, key}, value, socket) do
  form = Map.put(socket.assigns.form, key, value)
  touched = MapSet.put(socket.assigns.touched, key)

  {:noreply,
   socket |> assign(:form, form) |> assign(:touched, touched)}
end

def handle_info({:tap, :submit}, socket) do
  {:noreply, Mob.Socket.assign(socket, :submitted?, true)}
end

# shown/3 decides whether a field's errors are visible yet.
defp shown(errors, key, socket) do
  if socket.assigns.submitted? or key in socket.assigns.touched,
    do: errors,
    else: []
end

Disabled field

Set disabled={true} to mute the field's label.

Disabled field

Field example card titled Disabled, with a muted Account id label, a gray text box holding mishka-4821 and the hint Assigned when you signed up. below it
heex
<MishkaField label="Account id" disabled={true}>
  {[text_input(value: "mishka-4821", enabled: false)]}
</MishkaField>

Props

Props of the <MishkaField> tag. The children are the control.

PropTypeDefaultDescription
labelstringnilText above the control.
descriptionstringnilHint below the control. Hidden while there are errors.
errorslist of strings[]Validation messages. Each one starts with a ✕, so color is not the only signal.
requiredbooleanfalseAdds a * after the label.
disabledbooleanfalseMutes the label. It does not reach the control.
spacenumber6Gap between the parts.
label_color / description_colorcolor token or ARGB integer:on_surface / :mutedColor of the label and of the hint.
error_colorcolor token or ARGB integer:errorColor of the errors, the ✕ and the required *. A theme token, so it follows the theme.
text_sizetext token:smSize of the label, the hint and the errors.
invalid?/1helperNoneTakes a props map and returns true when it holds at least one non-blank error, so a control can match its border.

Differences from the web component

  • id, for and name are not ported. On the web they link the label and the hint to the control for assistive technology.
  • The *_class attributes are not ported. Color, size and spacing are props.