defmodule SigmaKit.Components.Forms.Autocomplete do @moduledoc false use Phoenix.Component @doc """ A combo box is a select box that allows you to search for options. It uses the [Tom Select library](https://tom-select.js.org/) under the hood. ## Usage: ```heex <.combo_box label="Single" placeholder="Select an option.." options={@widget_category_options} field={@form[:widget_category_names]} help_text="You can select only one option" /> <.combo_box label="Multiple" multiple options={@widget_category_options} field={@form[:widget_category_names]} help_text="You can select multiple options" /> <.combo_box multiple create label="Create new options" options={@widget_category_options} field={@form[:widget_category_names]} help_text="You can create new options" /> ``` ## Remote data source If you want to use your live view as a remote data source, you can set the `remote_options_event_name` option, which is similar to a `phx-change` event. When a user starts typing this will trigger an event with the name you pass. You handle the event in your live veiw return a list of options. The event will be passed the search term as first argument. ```heex <.combo_box label="Remote single" placeholder="Select an option.." remote_options_event_name="combo_box_search" field={@form[:widget_category_names]} help_text="You can select only one option" /> ``` ```elixir # @impl true def handle_event("combo_box_search", payload, socket) do # `payload` will be a string ("some search term") # Do your search and turn the results into a list of maps with `text` and `value` keys results = Widget.search_widget_categories(payload) |> Enum.map(&%{text: &1.name, value: &1.name}) # Make sure you return a map with a `results` key. The value of the `results` key must be a list of maps with `text` and `value` keys {:reply, %{results: results}, socket} end ``` More docs on https://petal.build/components/combo-box. """ attr(:field, :any, doc: "the field to generate the input for. eg. `@form[:name]`. Needs to be a %Phoenix.HTML.FormField{}." ) attr(:class, :string, default: nil, doc: "the class to add to the input") attr(:wrapper_class, :string, default: nil, doc: "the wrapper div classes") attr(:options, :list, doc: ~s|A list of options. eg. ["Admin", "User"] (label and value will be the same) or if you want the value to be different from the label: ["Admin": "admin", "User": "user"]. We use https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#options_for_select/2 underneath.|, default: [] ) attr(:multiple, :boolean, default: false, doc: "can multiple choices be selected?") attr(:create, :boolean, default: false, doc: "create new options on the fly?") attr(:max_items, :integer, default: nil, doc: "The maximum number of items that can be selected" ) attr(:event, :string, default: nil, doc: "The event name to trigger when searching for remote options. That event must return a li" ) attr(:target, :string, default: nil, doc: ~s|the target of the call for remote options. Will default to the current live view. For a live component, pass `remove_options_target={@myself}` if the event is handled on the live component.| ) attr(:remove_button_title, :string, default: "Remove this item", doc: "The title for the remove item button" ) attr(:placeholder, :string, default: "Select an option...", doc: "The placeholder text") attr(:tom_select_plugins, :map, default: %{clear_button: true}, doc: ~s|Which plugins should be activated? Pass a map that will be converted to a Javascript object via JSON. eg. `%{remove_button: %{title: "Remove!"}}`. See https://tom-select.js.org/plugins for available plugins.| ) attr(:tom_select_options, :map, default: %{}, doc: "Options to pass to Tom Select. Uses camel case. eg `%{maxOptions: 1000}`. See https://tom-select.js.org/docs for options." ) attr(:id, :any, default: nil, doc: "the id of the input. If not passed, it will be generated automatically from the field" ) attr(:name, :any, doc: "the name of the input. If not passed, it will be generated automatically from the field" ) attr(:value, :any, default: nil, doc: "the value of the input. If not passed, it will be generated automatically from the field" ) attr(:errors, :list, default: [], doc: "a list of errors to display. If not passed, it will be generated automatically from the field. Format is a list of strings." ) attr(:tom_select_options_global_variable, :string, default: nil, doc: ~s|for when you want to manually pass the options to Tom Select. eg. inside some script tags: `window.myOptions = { render: {...}}`. And in your component:`tom_select_options_global_variable="myOptions"`. It will merge the options with the existing ones.| ) attr(:rest, :global, include: ~w(autocomplete disabled form max maxlength min minlength list pattern placeholder readonly required size step value name multiple selected default year month day hour minute second builder options layout cols rows wrap checked accept), doc: "All other props go on the input" ) def autocomplete(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do assigns |> assign(field: nil, id: assigns.id || field.id) |> assign_new(:name, fn -> field.name end) |> assign_new(:value, fn -> field.value end) |> assign_new(:label, fn -> field.field end) |> autocomplete() end def autocomplete(assigns) do tom_select_options = %{ create: assigns.create, maxItems: if(assigns.multiple, do: assigns.max_items, else: 1) } |> Map.merge(assigns.tom_select_options) maybe_multiple_name = if assigns.multiple, do: assigns.name <> "[]", else: assigns.name assigns = assigns |> assign_new(:id, fn -> "combo-box-#{:rand.uniform(10_000_000) + 1}" end) |> assign(:tom_select_options_json, Jason.encode!(tom_select_options)) |> assign(:tom_select_plugins_json, Jason.encode!(assigns.tom_select_plugins)) |> assign(:maybe_multiple_name, maybe_multiple_name) ~H"""