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