Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New variant for tags-mode #78

Merged
merged 11 commits into from
Dec 27, 2024
112 changes: 58 additions & 54 deletions lib/live_select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ defmodule LiveSelect do
@moduledoc ~S"""
The `LiveSelect` component is rendered by calling the `live_select/1` function and passing it a form field.
LiveSelect creates a text input field in which the user can type text, and hidden input field(s) that will contain the value of the selected option(s).

Whenever the user types something in the text input, LiveSelect triggers a `live_select_change` event for your LiveView or LiveComponent.
The message has a `text` parameter containing the current text entered by the user, as well as `id` and `field` parameters with the id of the
The message has a `text` parameter containing the current text entered by the user, as well as `id` and `field` parameters with the id of the
LiveSelect component and the name of the LiveSelect form field, respectively.
Your job is to handle the event, retrieve the list of selectable options and then call `Phoenix.LiveView.send_update/3`
to send the list of options to LiveSelect. See the "Examples" section below for details, and check out the
to send the list of options to LiveSelect. See the "Examples" section below for details, and check out the
[cheatsheet](https://hexdocs.pm/live_select/cheatsheet.html) for some useful tips.

Selection can happen either using the keyboard, by navigating the options with the arrow keys and then pressing enter, or by
Expand All @@ -23,12 +23,12 @@ defmodule LiveSelect do
In single mode, if the configuration option `allow_clear` is set, the user can manually clear the selection by clicking on the `x` button on the input field.
In tags mode, single tags can be removed by clicking on them.

## Single mode
## Single mode

<img alt="demo" src="https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/demo_single.gif" width="300" />

## Tags mode

<img alt="demo" src="https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/demo_tags.gif" width="300" />

When `:tags` mode is enabled `LiveSelect` allows the user to select multiple entries. The entries will be visible above the text input field as removable tags.
Expand All @@ -46,7 +46,7 @@ defmodule LiveSelect do

* _atoms, strings or numbers_: In this case, each element will be both label and value for the option
* _tuples_: `{label, value}` corresponding to label and value for the option
* _maps_: `%{label: label, value: value}` or `%{value: value}`
* _maps_: `%{label: label, value: value}` or `%{value: value}`
* _keywords_: `[label: label, value: value]` or `[value: value]`

In the case of maps and keywords, if only `value` is specified, it will be used as both value and label for the option.
Expand All @@ -61,23 +61,23 @@ defmodule LiveSelect do

Note that the option values, if they are not strings, will be JSON-encoded. Your LiveView will receive this JSON-encoded version in the `phx-change` and `phx-submit` events.

## Styling
## Styling

`LiveSelect` supports 3 styling modes:

* `tailwind`: uses standard tailwind utility classes (the default)
* `daisyui`: uses [daisyUI](https://daisyui.com/) classes.
* `none`: no styling at all.

Please see [the styling section](styling.md) for details
Please see [the styling section](styling.md) for details

## Alternative tag labels
Sometimes, in `:tags` mode, you might want to use alternative labels for the tags. For example, you might want the labels in the tags to be shorter

Sometimes, in `:tags` mode, you might want to use alternative labels for the tags. For example, you might want the labels in the tags to be shorter
in order to save space. You can do this by specifying an additional `tag_label` key when passing options as map or keywords. For example, passing these options:

```
[%{label: "New York", value: "NY", tag_label: "NY"}, %{label: "Barcelona", value: "BCN", tag_label: "BCN"}]
[%{label: "New York", value: "NY", tag_label: "NY"}, %{label: "Barcelona", value: "BCN", tag_label: "BCN"}]
```

will result in "New York" and "Barcelona" being used for the options in the dropdown, while "NY" and "BCN" will be used for the tags (and the values).
Expand All @@ -93,11 +93,14 @@ defmodule LiveSelect do
Now, whenever the selection contains "New York", the option will stick and the user won't be able to remove it.

## Slots

You can control how your options and tags are rendered by using the `:option` and `:tag` slots.
Both slots will be passed an option as argument. In the case of the `:option` slot, the option will have an
extra boolean field `:selected`, which will be set to `true` if the option has been selected by the user.

Let's say you want to show some fancy icons next to each option in the dropdown and the tags:

```elixir
```elixir
<.live_select
field={@form[:city_search]}
phx-target={@myself}
Expand All @@ -115,7 +118,7 @@ defmodule LiveSelect do
```

Here's the result:

<img alt="slots" src="https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/slots.png" width="200" />

## Controlling the selection programmatically
Expand All @@ -127,8 +130,8 @@ defmodule LiveSelect do
send_update(LiveSelect.Component, id: live_select_id, value: new_selection)
```

`new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared.
After updating the selection, `LiveSelect` will trigger a change event in the form.
`new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared.
After updating the selection, `LiveSelect` will trigger a change event in the form.

To set a custom id for the component to use with `Phoenix.LiveView.send_update/3`, you can pass the `id` assign to `live_select/1`.

Expand All @@ -148,23 +151,23 @@ defmodule LiveSelect do
_Template:_
```
<.form for={@form} phx-change="change">
<.live_select field={@form[:city_search]} />
<.live_select field={@form[:city_search]} />
</.form>
```

> #### Forms implemented in LiveComponents {: .warning}
>
>
> If your form is implemented in a LiveComponent and not in a LiveView, you have to add the `phx-target` attribute
> when rendering LiveSelect:
>
> ```elixir
> <.live_select field={@form[:city_search]} phx-target={@myself} />
> ```
> ```

_LiveView or LiveComponent that is the target of the form's events:_
```
@impl true
def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do
def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do
cities = City.search(text)
# cities could be:
# [ {"city name 1", [lat_1, long_1]}, {"city name 2", [lat_2, long_2]}, ... ]
Expand All @@ -173,13 +176,13 @@ defmodule LiveSelect do
# [ "city name 1", "city name 2", ... ]
#
# or:
# [ [label: "city name 1", value: [lat_1, long_1]], [label: "city name 2", value: [lat_2, long_2]], ... ]
# [ [label: "city name 1", value: [lat_1, long_1]], [label: "city name 2", value: [lat_2, long_2]], ... ]
#
# or even:
# ["city name 1": [lat_1, long_1], "city name 2": [lat_2, long_2]]

send_update(LiveSelect.Component, id: live_select_id, options: cities)

{:noreply, socket}
end

Expand All @@ -192,7 +195,7 @@ defmodule LiveSelect do
IO.puts("You selected city #{city_name} located at: #{city_coords}")

{:noreply, socket}
end
end
```

### Tags mode
Expand All @@ -203,7 +206,7 @@ defmodule LiveSelect do
_Template:_
```
<.form for={@form} phx-change="change">
<.live_select field={@form[:city_search]} mode={:tags} />
<.live_select field={@form[:city_search]} mode={:tags} />
</.form>
```

Expand All @@ -216,17 +219,17 @@ defmodule LiveSelect do
socket
) do
# list_of_coords will contain the list of the JSON-encoded coordinates of the selected cities, for example:
# ["[-46.565,-23.69389]", "[-48.27722,-18.91861]"]
# ["[-46.565,-23.69389]", "[-48.27722,-18.91861]"]

IO.puts("You selected cities located at: #{list_of_coords}")

{:noreply, socket}
end
end
```

### Multiple LiveSelect inputs in the same LiveView
### Multiple LiveSelect inputs in the same LiveView

If you have multiple LiveSelect inputs in the same LiveView, you can distinguish them based on the field id.
If you have multiple LiveSelect inputs in the same LiveView, you can distinguish them based on the field id.
For example:

_Template:_
Expand Down Expand Up @@ -255,10 +258,10 @@ defmodule LiveSelect do

## Using LiveSelect with associations and embeds

LiveSelect can also be used to display and select associations or embeds without too much effort.
Let's say you have the following schemas:
```
LiveSelect can also be used to display and select associations or embeds without too much effort.
Let's say you have the following schemas:

```
defmodule City do
@moduledoc false

Expand Down Expand Up @@ -292,7 +295,7 @@ defmodule LiveSelect do
end
end
```

Each city has a name and an array with coordinates - we want `LiveSelect` to display the name as label in the dropdown and in the tags, but we want
the entire data structure (name + coordinates) to be sent to the server when the user selects.

Expand Down Expand Up @@ -329,20 +332,20 @@ defmodule LiveSelect do
{:noreply, socket}
end
```

> #### IMPORTANT: the output of the `value_mapper/1` function should be JSON-encodable {: .warning}

Finally, in order to take care of (2) you need to decode the JSON-encoded list of options that's coming from the client before you can
Finally, in order to take care of (2) you need to decode the JSON-encoded list of options that's coming from the client before you can
cast them to create a changeset. To do so, `LiveSelect` offers a convenience function called `LiveSelect.decode/1`:
```
def handle_event("change", params, socket) do
# decode will JSON-decode the value in city_search, handling the type of selection
# decode will JSON-decode the value in city_search, handling the type of selection
# and taking care of special values such as "" and nil
params = update_in(params, ~w(city_search_form city_search), &LiveSelect.decode/1)

# now we can cast the params:
changeset = CitySearchForm.changeset(params)

{:noreply, assign(socket, form: to_form(changeset))}
end
```
Expand All @@ -352,12 +355,12 @@ defmodule LiveSelect do

@doc ~S"""
Renders a `LiveSelect` input in a form.

[INSERT LVATTRDOCS]

## Styling attributes

* See [the styling section](styling.md) for details
* See [the styling section](styling.md) for details
"""
@doc type: :component

Expand All @@ -370,15 +373,16 @@ defmodule LiveSelect do
~S(an id to assign to the component. If none is provided, `#{form_name}_#{field}_live_select_component` will be used)

attr :mode, :atom,
values: [:single, :tags],
values: [:single, :tags, :quick_tags],
default: Component.default_opts()[:mode],
doc: "either `:single` (for single selection), or `:tags` (for multiple selection using tags)"
doc:
"either `:single` (for single selection), `:tags` (for multiple selection using tags), or :quick_tags (multiple selection but tags can be selected/deselected in quick succession)"

attr :options, :list,
doc:
~s(initial available options to select from. Note that, after the initial rendering of the component, options can only be updated using `Phoenix.LiveView.send_update/3` - See the "Options" section for details)

attr :value, :any, doc: "used to manually set a selection - overrides any values from the form.
attr :value, :any, doc: "used to manually set a selection - overrides any values from the form.
Must be a single element in `:single` mode, or a list of elements in `:tags` mode."

attr :max_selectable, :integer,
Expand Down Expand Up @@ -479,27 +483,27 @@ defmodule LiveSelect do

@doc ~S"""
Decodes the selection from the client. This has to be used when the values in the selection aren't simple integers or strings.
Let's say you receive your params in the variable `params`, and your `LiveSelect` field is called `my_field` and belongs to the form `my_form`. Then you should

Let's say you receive your params in the variable `params`, and your `LiveSelect` field is called `my_field` and belongs to the form `my_form`. Then you should
decode like this:

```
params = update_in(params, ~w(my_form my_field), &LiveSelect.decode/1)
```
## Examples:

## Examples:

iex> decode(nil)
[]

iex> decode("")
nil

iex> decode("{\"name\":\"Berlin\",\"pos\":[13.41053,52.52437]}")
%{"name" => "Berlin","pos" => [13.41053,52.52437]}

iex> decode(["{\"name\":\"New York City\",\"pos\":[-74.00597,40.71427]}","{\"name\":\"Stockholm\",\"pos\":[18.06871,59.32938]}"])
[%{"name" => "New York City","pos" => [-74.00597,40.71427]}, %{"name" => "Stockholm","pos" => [18.06871,59.32938]}]
[%{"name" => "New York City","pos" => [-74.00597,40.71427]}, %{"name" => "Stockholm","pos" => [18.06871,59.32938]}]
"""
def decode(selection) do
case selection do
Expand Down
Loading
Loading