defmodule Corex.Combobox do @moduledoc ~S''' Phoenix implementation of [Zag.js Combobox](https://zagjs.com/components/react/combobox). ### Minimal This example assumes the import of `.icon` from `Core Components`, you are free to replace it ```heex <.combobox class="combobox" placeholder="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" /> ``` ### Grouped ```heex <.combobox class="combobox" placeholder="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" /> ``` ### Extended This example requires the installation of [Flagpack](https://hex.pm/packages/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 ```heex <.combobox class="combobox" placeholder="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"} ]} > <:item :let={item}> {item.label} <:trigger> <.icon name="hero-chevron-down" /> <:clear_trigger> <.icon name="hero-backspace" /> <:item_indicator> <.icon name="hero-check" /> ``` ### Extended Grouped This example requires the installation of [Flagpack](https://hex.pm/packages/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 ```heex <.combobox class="combobox" placeholder="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}> {item.label} <:trigger> <.icon name="hero-chevron-down" /> <:clear_trigger> <.icon name="hero-backspace" /> <:item_indicator> <.icon name="hero-check" /> ``` ## Styling Use data attributes to target elements: - `[data-scope="combobox"][data-part="root"]` - Container - `[data-scope="combobox"][data-part="control"]` - Control wrapper - `[data-scope="combobox"][data-part="input"]` - Input field - `[data-scope="combobox"][data-part="trigger"]` - Trigger button - `[data-scope="combobox"][data-part="clear-trigger"]` - Clear button - `[data-scope="combobox"][data-part="content"]` - Dropdown content - `[data-scope="combobox"][data-part="item-group"]` - Group container - `[data-scope="combobox"][data-part="item-group-label"]` - Group label - `[data-scope="combobox"][data-part="item"]` - Item wrapper - `[data-scope="combobox"][data-part="item-text"]` - Item text - `[data-scope="combobox"][data-part="item-indicator"]` - Optional indicator ''' @doc type: :component use Phoenix.Component alias Corex.Combobox.Connect alias Corex.Combobox.Anatomy.{Props, Root, Label, Control, Input, Positioner, Content} @doc """ Renders a combobox component. """ attr(:id, :string, required: false, doc: "The id of the combobox, useful for API to identify the combobox" ) attr(:collection, :list, default: [], doc: "The collection of items to display in the combobox" ) attr(:controlled, :boolean, default: false, doc: "Whether the combobox is controlled") attr(:on_open_change, :string, default: nil, doc: "The server event name to trigger on open change" ) attr(:on_open_change_client, :string, default: nil, doc: "The client event name to trigger on open change" ) attr(:bubble, :boolean, default: false, doc: "Whether the client events are bubbled") attr(:disabled, :boolean, default: false, doc: "Whether the combobox is disabled") attr(:open, :boolean, default: false, doc: "Whether the combobox is open") attr(:value, :list, default: [], doc: "The value of the combobox") attr(:placeholder, :string, default: nil, doc: "The placeholder of the combobox") attr(:always_submit_on_enter, :boolean, default: false, doc: "Whether to always submit on enter" ) attr(:auto_focus, :boolean, default: false, doc: "Whether to auto focus the combobox") attr(:close_on_select, :boolean, default: true, doc: "Whether to close the combobox on select") attr(:dir, :string, default: "ltr", doc: "The direction of the combobox") attr(:input_behavior, :string, default: "autohighlight", doc: "The input behavior of the combobox" ) attr(:loop_focus, :boolean, default: false, doc: "Whether to loop focus the combobox") attr(:multiple, :boolean, default: false, doc: "Whether to allow multiple selection") attr(:invalid, :boolean, default: false, doc: "Whether the combobox is invalid") attr(:name, :string, doc: "The name of the combobox") attr(:form, :string, doc: "The id of the form of the combobox") attr(:read_only, :boolean, default: false, doc: "Whether the combobox is read only") attr(:required, :boolean, default: false, doc: "Whether the combobox is required") attr(:on_input_value_change, :string, default: nil, doc: "The server event name to trigger on input value change" ) attr(:on_value_change, :string, default: nil, doc: "The server event name to trigger on value change" ) attr(:positioning, :map, default: %Corex.Positioning{}, doc: "The positioning of the combobox") attr(:rest, :global) slot(:label, required: false, doc: "The label content") slot(:trigger, required: true, doc: "The trigger button content") slot(:clear_trigger, required: false, doc: "The clear button content") slot(:item_indicator, required: false, doc: "Optional indicator for selected items") slot :error, required: false do attr(:class, :string, required: false) end slot(:item, required: false, doc: "Custom content for each item. Receives the item as :let binding" ) attr(:field, Phoenix.HTML.FormField, doc: "A form field struct retrieved from the form, for example: @form[:country]. Automatically sets id, name, value, and errors from the form field" ) attr(:errors, :list, default: [], doc: "List of error messages to display" ) def combobox(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do errors = field.errors || [] assigns |> assign(field: nil) |> assign(:errors, Enum.map(errors, &Corex.Gettext.translate_error(&1))) |> assign_new(:id, fn -> field.id end) |> assign_new(:form, fn -> field.form.id end) |> assign_new(:name, fn -> field.name end) |> assign( :value, if(field.value, do: [field.value], else: []) ) |> combobox() end def combobox(assigns) do assigns = assigns |> assign_new(:id, fn -> "combobox-#{System.unique_integer([:positive])}" end) |> assign_new(:name, fn -> "name-#{System.unique_integer([:positive])}" end) |> assign_new(:form, fn -> nil end) grouped_items = Enum.group_by(assigns.collection, &Map.get(&1, :group)) has_groups = grouped_items |> Map.keys() |> Enum.any?(& &1) assigns = assign(assigns, :grouped_items, grouped_items) assigns = assign(assigns, :has_groups, has_groups) ~H"""
{render_slot(@label)}
{render_slot(@error, msg)}
{group}
  • {render_slot(@item, item)} {item.label} {render_slot(@item_indicator)}
  • {render_slot(@item, item)} {item.label} {render_slot(@item_indicator)}
  • """ end end