Erabi — Pickers for Phoenix

Erabi (選び, "choosing") is pickers for Phoenix LiveView: the counterpart to Koyomi for everything that is not a date. It gives your app:

  • select, multi_select, combobox, tags, person_picker, color_picker, emoji_picker and icon_picker, each a field of a form that posts what the browser's own <select> would, and that is that <select> until its hook mounts (and with JavaScript off);
  • options as you have them: ["a", "b"], [{"To do", :todo}], a keyword list, maps with colors, hints, icons and groups, your user structs;
  • colored options from a palette of nine, each pair of chip and text checked for contrast in light and dark (WCAG AAA), and your own CSS colors;
  • people and agents: avatars (an image, or initials on the person's color), agents marked as such, and "me" first;
  • a color picker: the palette's swatches, and with custom any color, in an OKLCH area that holds only colors a screen shows, a hex box, the eyedropper, recent colors and the contrast with a background;
  • an emoji picker, as Notion's and Slack's: every emoji by category, a search in eight languages by name, tag and :shortcode: (the same on the server, for an agent's tool), skin tones, recent emoji, a preview, and a tab of your own icons; and an icon picker of your icons alone;
  • tags that people type, with "Create “…”" under your rules (a pattern, lengths, how many);
  • the same rules in the browser, as people choose, and on the server in Erabi.Changeset, held to the same answers by one fixture;
  • search as people type: on the server (search={&Cities.search/1}, with on_mount Erabi.Live and no event clauses of yours), or in the browser for the options it has, ranked by the same matcher as Jumon's palette, with the matched letters marked;
  • lists of thousands of options, drawn a screenful at a time;
  • on phones, every popup a sheet from the bottom of the screen, above the keyboard, with Done and a swipe down to close;
  • the keyboard and screen readers of the WAI-ARIA combobox and listbox patterns: arrows, Home and End, typeahead, Backspace on chips, Escape, announcements of what was added and removed;
  • editors of a grid's cells, for Masu (Erabi.cell_editor/2);
  • with Issho, who else is choosing: the people and agents who have a picker open, the option each is on, and an agent that goes through the options before its change lands (Erabi.Issho);
  • Erabi.Test, to choose and search in Phoenix.LiveViewTest with no browser.

The pickers are a prebuilt JavaScript bundle in the Hex package, 14 KB gzipped; the color picker (6 KB) and the emoji and icon pickers (7 KB, and their emoji as the picker first opens) are entries of their own, for the apps that have them. Your app does not run Node.js for it: you add the hook and one style sheet.

What the pickers do

Pickersselect/1 (typeahead; a box to filter from 10 options), multi_select/1 (chips; the list stays open), combobox/1 (type to find; create keeps free text), tags/1 (type, Enter or a comma; "Create “…”"), person_picker/1 (avatars, agents, me, multiple), color_picker/1 (swatches; custom for any color as #rrggbb), emoji_picker/1 (search, categories, tones, recent; your icons in a tab), icon_picker/1
Optionsanything a <select> takes; maps or Erabi.Option with :label, :hint, :color, :icon, :avatar, :kind, :group, :keywords, :priority, :disabled (or its reason); user structs by :id and :name
Formsposts name (or name[], after an empty one, so that none clears it), in the order chosen; phx-change and phx-submit as they are; errors with aria-invalid and aria-describedby; a <label for> names the picker
Rulesrequired, min, max, create, min_length, max_length, pattern: in the browser and in Erabi.Changeset.validate_choice/4, with Ecto's messages and translate_error
Searcha function of the query in the LiveView (Erabi.Live): pages of 50 as the list scrolls, async in a task, stale answers dropped, recent answers cached, a retry when one does not come
Matchingcase, accents and width do not count; letters in order ("nwiss" finds "New issue"); word starts first; one typo in a word of four letters; keywords and hints too
Lookschips in nine colors, avatars, icons drawn by your :icon slot, groups with headings, hints; light and dark; the Sumi theme
Accessibilitythe WAI-ARIA combobox (select-only and editable) and listbox patterns, aria-activedescendant, typeahead, announcements, forced colors, reduced motion
WordsEnglish, Portuguese, Spanish, French, German, Italian, Dutch and Japanese; yours with messages or Gettext
Choosing togetherwith Issho: the others who have a picker open on its field, their options marked, a peek of the list where they are; Erabi.Issho.choose/5 for an agent to choose as people watch
GridsErabi.cell_editor/2 and erabiEditors(): a picker as the editor of a Masu column
TestingErabi.Test.choose/3, choices/2, chosen/2, search/3

Install

Erabi needs Elixir 1.18 or later (it uses the built-in JSON module), Phoenix 1.7.14 or 1.8, Phoenix LiveView 1.1 or later, and Ecto 3.13 or later.

Add erabi to the deps in mix.exs:

def deps do
  [
    {:erabi, "~> 0.1"}
  ]
end

Then run:

mix deps.get
mix erabi.install

The installer adds the hook to assets/js/app.js and the style sheet to assets/css/app.css, and prints what it changed. Give --dry-run to see the changes first. It does not add what a file already has, so you can run it again.

The manual steps

To make the same changes by hand, import the hook in assets/js/app.js and add it to the hooks of the LiveSocket:

import { Erabi } from "erabi"

const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks, Erabi},
})

If you use color_picker, emoji_picker or icon_picker (or such a column in Masu), import their entries too. They are bundles of their own (6 and 7 KB gzipped), so that apps without them do not load them; without the import, such a picker stays the browser's <select>, and the console says what is missing:

import "erabi/color"
import "erabi/emoji"

The emoji picker fetches its emoji (a file of the page's language) from your endpoint; the installer adds the line that serves them, above the endpoint's own Plug.Static:

plug Plug.Static, at: "/erabi/emoji", from: {:erabi, "priv/emoji"}, gzip: true

Import the style sheet in assets/css/app.css (and the Sumi theme, if you want it: Sumi is the design system of Hattori AI, the makers of Erabi, Koyomi, Masu, Kotoba and Jumon). Put these lines below the other @import lines of app.css:

@import "../../deps/erabi/priv/static/erabi.css";
/* @import "../../deps/erabi/priv/static/erabi-sumi.css"; */

NODE_PATH=deps

import { Erabi } from "erabi" resolves through the NODE_PATH of your esbuild profile, which must have deps/. The Phoenix generators already set it. If your profile has no NODE_PATH, add it in config/config.exs:

env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}

Use

Import the components in the html_helpers of your web module:

import Erabi.Components

Then, in a form:

<.select field={@form[:status]} options={@statuses} />
<.multi_select field={@form[:labels]} options={@labels} max={3} />
<.tags field={@form[:tags]} options={@tags} create />
<.person_picker field={@form[:owner_id]} people={@members} me={@current_user} />
<.color_picker field={@form[:color]} custom />
<.emoji_picker field={@form[:icon]} />

with options as the app has them:

@statuses [
  %{value: :todo, label: "To do", color: :gray},
  %{value: :doing, label: "In progress", color: :yellow},
  %{value: :done, label: "Done", color: :green}
]

and the same rules in the changeset:

def changeset(task, params) do
  task
  |> cast(params, [:status, :labels])
  |> Erabi.Changeset.validate_choice(:status, [required: true], options: @statuses)
  |> Erabi.Changeset.validate_choice(:labels, [max: 3], options: Labels.all())
end

A search on the server, as people type:

on_mount Erabi.Live
<.combobox field={@form[:city_id]} search={&Places.cities/1} selected={@city} />

Guides

Development

mix deps.get
mix erabi.build   # the bundle and the style sheets, into priv/static
mix dev           # every picker at http://localhost:4099

Tests

mix test                   # Elixir
mix test --include build   # the build and the package
mix test.js                # the browser's matcher and rules
mix test.e2e               # the pickers in Chromium, Firefox and WebKit

The rules and the matcher that both the server and the browser apply are tested on both sides with one fixture each: test/fixtures/rules.json, and test/fixtures/match.json, which is Jumon's.

Release

mix release builds the bundle, checks priv/static and the version of package.json, publishes to Hex and tags the version.

Contributing

See CONTRIBUTING.md: when a change needs an issue first, and what a pull request needs.

License

Erabi is released under the MIT License. See LICENSE. Its icons are Lucide's (ISC). See NOTICE.