Corex.Select (Corex v0.1.0-alpha.25)

View Source

Phoenix implementation of Zag.js Select.

Examples

The placeholder can be set via the placeholder_text attribute or via the optional :placeholder slot. When both are provided, the slot content is shown (the slot overrides the attribute).

Minimal

This example assumes the import of .icon from Core Components, you are free to replace it

<.select
  id="my-select"
  class="select"
  placeholder_text="Select a country"
  collection={[
    %{label: "France", id: "fra", disabled: true},
    %{label: "Belgium", id: "bel"},
    %{label: "Germany", id: "deu"},
    %{label: "Netherlands", id: "nld"},
    %{label: "Switzerland", id: "che"},
    %{label: "Austria", id: "aut"}
  ]}
>
  <:trigger>
    <.icon name="hero-chevron-down" />
  </:trigger>
</.select>

Grouped

This example assumes the import of .icon from Core Components, you are free to replace it

<.select
  class="select"
  placeholder_text="Select a country"
  collection={[
    %{label: "France", id: "fra", group: "Europe"},
    %{label: "Belgium", id: "bel", group: "Europe"},
    %{label: "Germany", id: "deu", group: "Europe"},
    %{label: "Netherlands", id: "nld", group: "Europe"},
    %{label: "Switzerland", id: "che", group: "Europe"},
    %{label: "Austria", id: "aut", group: "Europe"},
    %{label: "Japan", id: "jpn", group: "Asia"},
    %{label: "China", id: "chn", group: "Asia"},
    %{label: "South Korea", id: "kor", group: "Asia"},
    %{label: "Thailand", id: "tha", group: "Asia"},
    %{label: "USA", id: "usa", group: "North America"},
    %{label: "Canada", id: "can", group: "North America"},
    %{label: "Mexico", id: "mex", group: "North America"}
  ]}
>
  <:trigger>
    <.icon name="hero-chevron-down" />
  </:trigger>
</.select>

With custom placeholder slot

Use the :placeholder slot to fully customize the placeholder (e.g. icon + text):

<.select
  id="my-select"
  class="select"
  collection={[...]}
>
  <:placeholder>
    <.icon name="hero-globe-alt" class="opacity-60" />
    <span>Choose a country</span>
  </:placeholder>
  <:trigger>
    <.icon name="hero-chevron-down" />
  </:trigger>
</.select>

### Custom

This example requires the installation of Flagpack to display the use of custom item rendering. This example assumes the import of .icon from Core Components, you are free to replace it

<.select
  class="select"
  placeholder_text="Select a country"
  collection={[
    %{label: "France", id: "fra"},
    %{label: "Belgium", id: "bel"},
    %{label: "Germany", id: "deu"},
    %{label: "Netherlands", id: "nld"},
    %{label: "Switzerland", id: "che"},
    %{label: "Austria", id: "aut"}
  ]}
>
  <:label>
    Country of residence
  </:label>
  <:item :let={item}>
    <Flagpack.flag name={String.to_atom(item.id)} />
    {item.label}
  </:item>
  <:trigger>
    <.icon name="hero-chevron-down" />
  </:trigger>
  <:item_indicator>
    <.icon name="hero-check" />
  </:item_indicator>
</.select>

Custom Grouped

This example requires the installation of Flagpack to display the use of custom item rendering. This example assumes the import of .icon from Core Components, you are free to replace it

<.select
  class="select"
  placeholder_text="Select a country"
  collection={[
    %{label: "France", id: "fra", group: "Europe"},
    %{label: "Belgium", id: "bel", group: "Europe"},
    %{label: "Germany", id: "deu", group: "Europe"},
    %{label: "Japan", id: "jpn", group: "Asia"},
    %{label: "China", id: "chn", group: "Asia"},
    %{label: "South Korea", id: "kor", group: "Asia"}
  ]}
>
  <:item :let={item}>
    <Flagpack.flag name={String.to_atom(item.id)} />
    {item.label}
  </:item>
  <:trigger>
    <.icon name="hero-chevron-down" />
  </:trigger>
  <:item_indicator>
    <.icon name="hero-check" />
  </:item_indicator>
</.select>

Phoenix Form Integration

When using with Phoenix forms, you must add an id to the form using the Corex.Form.get_form_id/1 function.

Controller

defmodule MyAppWeb.PageController do
  use MyAppWeb, :controller

  def home(conn, params) do
    form = Phoenix.Component.to_form(Map.get(params, "user", %{}), as: :user)
    render(conn, :home, form: form)
  end
end
<.form :let={f} as={:user} for={@form} id={get_form_id(@form)} method="get">
  <.select
    field={f[:country]}
    class="select"
    placeholder_text="Select a country"
    collection={[
      %{label: "France", id: "fra", disabled: true},
      %{label: "Belgium", id: "bel"},
      %{label: "Germany", id: "deu"},
      %{label: "Netherlands", id: "nld"},
      %{label: "Switzerland", id: "che"},
      %{label: "Austria", id: "aut"}
    ]}
  >
    <:label>Your country of residence</:label>
    <:trigger>
      <.icon name="hero-chevron-down" />
    </:trigger>
    <:error :let={msg}>
      <.icon name="hero-exclamation-circle" class="icon" />
      {msg}
    </:error>
  </.select>
  <button type="submit">Submit</button>
</.form>

Live View

When using Phoenix form in a Live view you must also add controlled mode. This allows the Live view to be the source of truth and the component to be in sync accordingly.

defmodule MyAppWeb.SelectLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    form = to_form(%{"country" => nil}, as: :user)
    {:ok, assign(socket, :form, form)}
  end

  def render(assigns) do
    ~H"""
    <.form as={:user} for={@form} id={get_form_id(@form)}>
      <.select
        field={@form[:country]}
        class="select"
        controlled
        placeholder_text="Select a country"
        collection={[
          %{label: "France", id: "fra", disabled: true},
          %{label: "Belgium", id: "bel"},
          %{label: "Germany", id: "deu"},
          %{label: "Netherlands", id: "nld"},
          %{label: "Switzerland", id: "che"},
          %{label: "Austria", id: "aut"}
        ]}
      >
        <:label>Your country of residence</:label>
        <:trigger>
          <.icon name="hero-chevron-down" />
        </:trigger>
        <:error :let={msg}>
          <.icon name="hero-exclamation-circle" class="icon" />
          {msg}
        </:error>
      </.select>
      <button type="submit">Submit</button>
    </.form>
    """
  end
end

With Ecto changeset

When using Ecto changeset for validation and inside a Live view you must enable the controlled mode.

This allows the Live View to be the source of truth and the component to be in sync accordingly.

First create your schema and changeset:

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :country, :string
    timestamps(type: :utc_datetime)
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :country])
    |> validate_required([:name, :country])
  end
end
defmodule MyAppWeb.UserLive do
  use MyAppWeb, :live_view
  alias MyApp.Accounts.User

  def mount(_params, _session, socket) do
    {:ok, assign(socket, :form, to_form(User.changeset(%User{}, %{})))}
  end

  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset = User.changeset(%User{}, user_params)
    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
  end

  def render(assigns) do
    ~H"""
    <.form for={@form} id={get_form_id(@form)} phx-change="validate">
      <.select
        field={@form[:country]}
        class="select"
        controlled
        placeholder_text="Select a country"
        collection={[
          %{label: "France", id: "fra"},
          %{label: "Belgium", id: "bel"},
          %{label: "Germany", id: "deu"}
        ]}
      >
        <:label>Your country of residence</:label>
        <:trigger>
          <.icon name="hero-chevron-down" />
        </:trigger>
        <:error :let={msg}>
          <.icon name="hero-exclamation-circle" class="icon" />
          {msg}
        </:error>
      </.select>
    </.form>
    """
  end
end

API Control

# Client-side
<button phx-click={Corex.Select.set_value("my-select", "fra")}>
  Check
</button>

<button phx-click={Corex.Select.toggle_value("my-select")}>
  Toggle
</button>

# Server-side
def handle_event("set_value", _, socket) do
  {:noreply, Corex.Select.set_value(socket, "my-select", "fra")}
end

Styling

Use data attributes to target elements:

  • [data-scope="select"][data-part="root"] - Label wrapper
  • [data-scope="select"][data-part="control"] - Select control
  • [data-scope="select"][data-part="label"] - Label text
  • [data-scope="select"][data-part="input"] - Hidden input
  • [data-scope="select"][data-part="error"] - Error message

State-specific styling:

  • [data-state="open"] - When select is open
  • [data-state="closed"] - When select is closed
  • [data-disabled] - When select is disabled
  • [data-readonly] - When select is read-only
  • [data-invalid] - When select has validation errors

If you wish to use the default Corex styling, you can use the class select on the component. This requires to install mix corex.design first and import the component css file.

@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/select.css";

You can then use modifiers

<.select class="select select--accent select--lg">

Learn more about modifiers and Corex Design

Summary

Functions

select(assigns)

Attributes

  • id (:string)

  • collection (:list) - Defaults to [].

  • controlled (:boolean) - Whether the select is controlled. Defaults to false.

  • placeholder_text (:string) - The placeholder text of the select when no value is selected. Defaults to nil.

  • value (:list) - The value of the select. Defaults to [].

  • disabled (:boolean) - Whether the select is disabled. Defaults to false.

  • close_on_select (:boolean) - Whether to close the select on select. Defaults to true.

  • dir (:string) - The direction of the select. When nil, derived from document (html lang + config :rtl_locales). Defaults to nil.

  • loop_focus (:boolean) - Whether to loop focus the select. Defaults to false.

  • multiple (:boolean) - Whether to allow multiple selection. Defaults to false.

  • invalid (:boolean) - Whether the select is invalid. Defaults to false.

  • name (:string) - The name of the select.

  • form (:string) - The id of the form of the select.

  • read_only (:boolean) - Whether the select is read only. Defaults to false.

  • required (:boolean) - Whether the select is required. Defaults to false.

  • prompt (:string) - the prompt for select inputs. Defaults to nil.

  • on_value_change (:string) - Server event name to push on value change. Payload includes value (list), path (current path without locale), id, items. Use Enum.at(value, 0) for the first selected value. Defaults to nil.

  • on_value_change_client (:any) - Client-side only: either a string (CustomEvent name to dispatch) or a Phoenix.LiveView.JS command. For JS commands, placeholders are replaced at run time: __VALUE__ (selected value(s) as JSON array), __VALUE_0__ (first value). For redirect-on-select use redirect instead (no placeholders).

    Defaults to nil.

  • redirect (:boolean) - When true, the first selected value is used as the destination URL. When not connected the hook sets window.location; when connected use on_value_change and redirect(socket, to: Enum.at(value, 0)) in your handler. Same approach as menu's redirect. Per item: set redirect: false on an item to disable redirect for that item; set new_tab: true to open that item's URL in a new tab. Defaults to false.

  • positioning (Corex.Positioning) - Positioning options for the dropdown. Defaults to %Corex.Positioning{hide_when_detached: true, strategy: "fixed", placement: "bottom", gutter: 8, shift: 0, overflow_padding: 0, arrow_padding: 4, flip: true, slide: true, overlap: false, same_width: true, fit_viewport: false}.

  • field (Phoenix.HTML.FormField) - A form field struct retrieved from the form, for example: @form[:country]. Automatically sets id, name, value, and errors from the form field.

  • errors (:list) - List of error messages to display. Defaults to [].

  • Global attributes are accepted.

Slots

  • label - The label content. Accepts attributes:
    • class (:string)
  • trigger (required) - The trigger button content.
  • placeholder - Custom placeholder content. When provided, overrides the placeholder_text attribute.
  • item_indicator - Optional indicator for selected items.
  • error - Accepts attributes:
    • class (:string)
  • item - Custom content for each item. Receives the item as :let binding.