Autocomplete

v0.0.10

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

The Mishka Chelekom Mob Autocomplete is a text field that suggests completions as you type, on iOS and Android. It is the native port of the headless Autocomplete and runs on Mob.

Overview of the Mob Autocomplete component for iOS and Android

Autocomplete and Combobox use the same machinery. They differ in what the value is. A combobox picks from a fixed list, so its value is one of the options. An autocomplete accepts free text and only suggests. The query is the value, and choosing a suggestion fills it in.

Autocomplete renders through Combobox, so filtering and the list surface are the same. You pass suggestions as a list of strings. Each suggestion is its own id, so on_select hands back the suggestion's text and you do not have to look anything up.

The default filter is :starts_with, so suggestions are prefix matches. Set it to :contains to match anywhere in a suggestion. When the query names a suggestion exactly, the component drops that suggestion, because there is nothing left to suggest about it. It drops only that one: with Iran typed, the list still offers Iranian Rial. If dropping it leaves nothing, the panel is not drawn, because a No suggestions row would be wrong when the real reason is that you already typed the answer.

The screen owns query and open, and the events arrive in handle_info/2. Tapping the field sends on_press on every tap, typing sends on_query, and an optional ▾ (trigger) sends on_toggle. The panel renders in flow, so to close it on an outside tap, put an on_tap on the container around the field. Do not close it on on_blur, because choosing a suggestion blurs the field too.

For a field that collects several values as pills, see the Pills Input page. It shows an autocomplete-style picker built from a pills input and a suggestion list.

Generate the autocomplete component

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

bash
mix mishka.ui.gen.mob autocomplete

The generator registers the component's tag, so you can use it in ~MOB markup. It also generates the Combobox component, which does the filtering and draws the list. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaAutocomplete>. Without a prefix the tag is <Autocomplete>.

Autocomplete suggestions as you type

Matching uses the start of each suggestion by default, and a suggestion vanishes once the text matches it exactly. The text in the field is the value. Typing opens the list, and choosing a suggestion fills the field and closes the list. Use on_press, not on_focus, to reopen it. A field that already has the caret reports no new focus when you tap it, and closing the list does not take the caret away, so on_focus would never reopen the list.

Autocomplete suggestions as you type

Card titled Suggest as you type, with an empty Autocomplete field showing the placeholder Type a city, a clear X and a down arrow, and the list closed
An autocomplete field with an open list of six cities from Tehran to Lisbon.

Left: the example as it loads. Right: after a tap. The caret opens the list of suggestions.

elixir
<MishkaAutocomplete
  query={@query}
  suggestions={@cities}
  open={@open?}
  on_query={:query}
  on_select={:choose}
  on_press={:open}
/>

# Typing opens it and is the value; the chosen suggestion IS the text.
def handle_info({:change, :query, text}, socket) do
  {:noreply, socket |> assign(:query, text) |> assign(:open?, true)}
end

def handle_info({:tap, {:choose, text}}, socket) do
  {:noreply, socket |> assign(:query, text) |> assign(:open?, false)}
end

# on_press, NOT on_focus: focus is an edge, so a field that already has
# the caret reports nothing when you tap it again — and closing the list
# does not take the caret away, so the obvious way to reopen it did
# nothing at all.
def handle_info({:tap, :open}, socket), do: {:noreply, assign(socket, :open?, true)}
def handle_info({:tap, :close}, socket), do: {:noreply, assign(socket, :open?, false)}

Autocomplete with contains matching

filter={:contains} matches anywhere in a suggestion instead of at the start. With the showcase cities, typing bon finds Lisbon, which a prefix match would miss.

Autocomplete with contains matching

Card titled Contains matching, with an Autocomplete field showing the placeholder Matches anywhere above an open list of six cities: Tehran, Toronto, Tokyo, Berlin, Bergen and Lisbon
heex
<MishkaAutocomplete query={@query} suggestions={@cities} filter={:contains} />

Helper functions

FunctionDescription
suggest(suggestions, query, opts)Returns the suggestions to show for a query. It returns an empty list once the query already names one exactly. Pass mode: :contains to match anywhere.
exact?(suggestions, query)Returns true when the query already names one of the suggestions exactly.
exact_match?(suggestion, query)Returns true when one suggestion is exactly what the query says.

Props

PropTypeDefaultDescription
querystring""The field's text. It is the value.
openbooleanfalseWhether the suggestions are shown. The screen owns it.
suggestionslist of strings[]What to offer.
filter:starts_with or :contains:starts_withMatch mode. Prefix by default.
placeholderstring"Type to search…"Field placeholder.
clearbooleanfalseShows a ✕ button that clears the text.
empty_textstring"No suggestions"Shown when nothing matches.
disabledbooleanfalseMutes the field and unwires it.
on_queryevent tag (atom)NoneSends {:change, tag, text} as you type.
on_selectevent tag (atom)NoneSends {:tap, {tag, text}} with the suggestion's text, not an id.
on_clearevent tag (atom)NoneSends {:tap, tag} from the ✕.
on_pressevent tag (atom)NoneSends {:tap, tag} on every tap of the field.
on_focusevent tag (atom)NoneSends {:focus, tag} when the field gains focus.
on_blurevent tag (atom)NoneSends {:blur, tag} when the field loses focus.
triggerbooleanfalseShows a ▾ button that opens and closes the panel.
on_toggleevent tag (atom)NoneSends {:tap, tag} from the ▾.
trigger_iconstring or {closed, open}"▾" and "▴"The glyph of the trigger button, for callers who want their own.
clear_iconstring"✕"The glyph of the clear button.
idstringnilTest tags for the field, the buttons and every suggestion.

Autocomplete drops only suggestions and hands every other prop to Combobox, so the Combobox props apply here as well.

Events

Your screen receives these messages in handle_info/2:

  • on_query sends {:change, tag, text} as you type.
  • on_select sends {:tap, {tag, text}} with the suggestion's text.
  • on_clear, on_toggle and on_press send {:tap, tag}.
  • on_focus sends {:focus, tag} and on_blur sends {:blur, tag}.

The first example handles typing, choosing, opening and closing. With on_clear={:clear} and on_toggle={:toggle}, the clear button and the ▾ need two more clauses.

elixir
def handle_info({:tap, :clear}, socket), do: {:noreply, Mob.Socket.assign(socket, :query, "")}

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

Differences from the web component

  • name, required and readonly are not ported, because they are form plumbing.
  • auto_highlight is not ported, because there is no focus ring to highlight.
  • inline is not ported. Typing the completion into the field ahead of the caret fights every mobile input method.
  • The *_class attributes are not ported. Mob draws native views, so there is no CSS.
  • id is ported as a test handle.