Current section
45 Versions
Jump to
Current section
45 Versions
Compare versions
6
files changed
+129
additions
-80
deletions
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/maxmarcon/live_select">>}]}. |
| 2 2 | {<<"name">>,<<"live_select">>}. |
| 3 | - {<<"version">>,<<"1.4.4">>}. |
| 3 | + {<<"version">>,<<"1.5.0">>}. |
| 4 4 | {<<"description">>,<<"Dynamic (multi)selection field for LiveView">>}. |
| 5 5 | {<<"elixir">>,<<"~> 1.14">>}. |
| 6 6 | {<<"app">>,<<"live_select">>}. |
| @@ -6,12 +6,12 @@ defmodule LiveSelect do | |
| 6 6 | @moduledoc ~S""" |
| 7 7 | The `LiveSelect` component is rendered by calling the `live_select/1` function and passing it a form field. |
| 8 8 | 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). |
| 9 | - |
| 9 | + |
| 10 10 | Whenever the user types something in the text input, LiveSelect triggers a `live_select_change` event for your LiveView or LiveComponent. |
| 11 | - 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 |
| 11 | + 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 |
| 12 12 | LiveSelect component and the name of the LiveSelect form field, respectively. |
| 13 13 | Your job is to handle the event, retrieve the list of selectable options and then call `Phoenix.LiveView.send_update/3` |
| 14 | - to send the list of options to LiveSelect. See the "Examples" section below for details, and check out the |
| 14 | + to send the list of options to LiveSelect. See the "Examples" section below for details, and check out the |
| 15 15 | [cheatsheet](https://hexdocs.pm/live_select/cheatsheet.html) for some useful tips. |
| 16 16 | |
| 17 17 | Selection can happen either using the keyboard, by navigating the options with the arrow keys and then pressing enter, or by |
| @@ -23,12 +23,12 @@ defmodule LiveSelect do | |
| 23 23 | 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. |
| 24 24 | In tags mode, single tags can be removed by clicking on them. |
| 25 25 | |
| 26 | - ## Single mode |
| 26 | + ## Single mode |
| 27 27 | |
| 28 28 | <img alt="demo" src="https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/demo_single.gif" width="300" /> |
| 29 29 | |
| 30 30 | ## Tags mode |
| 31 | - |
| 31 | + |
| 32 32 | <img alt="demo" src="https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/demo_tags.gif" width="300" /> |
| 33 33 | |
| 34 34 | 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. |
| @@ -46,7 +46,7 @@ defmodule LiveSelect do | |
| 46 46 | |
| 47 47 | * _atoms, strings or numbers_: In this case, each element will be both label and value for the option |
| 48 48 | * _tuples_: `{label, value}` corresponding to label and value for the option |
| 49 | - * _maps_: `%{label: label, value: value}` or `%{value: value}` |
| 49 | + * _maps_: `%{label: label, value: value}` or `%{value: value}` |
| 50 50 | * _keywords_: `[label: label, value: value]` or `[value: value]` |
| 51 51 | |
| 52 52 | In the case of maps and keywords, if only `value` is specified, it will be used as both value and label for the option. |
| @@ -61,23 +61,23 @@ defmodule LiveSelect do | |
| 61 61 | |
| 62 62 | 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. |
| 63 63 | |
| 64 | - ## Styling |
| 65 | - |
| 64 | + ## Styling |
| 65 | + |
| 66 66 | `LiveSelect` supports 3 styling modes: |
| 67 67 | |
| 68 68 | * `tailwind`: uses standard tailwind utility classes (the default) |
| 69 69 | * `daisyui`: uses [daisyUI](https://daisyui.com/) classes. |
| 70 70 | * `none`: no styling at all. |
| 71 71 | |
| 72 | - Please see [the styling section](styling.md) for details |
| 72 | + Please see [the styling section](styling.md) for details |
| 73 73 | |
| 74 74 | ## Alternative tag labels |
| 75 | - |
| 76 | - 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 |
| 75 | + |
| 76 | + 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 |
| 77 77 | 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: |
| 78 78 | |
| 79 79 | ``` |
| 80 | - [%{label: "New York", value: "NY", tag_label: "NY"}, %{label: "Barcelona", value: "BCN", tag_label: "BCN"}] |
| 80 | + [%{label: "New York", value: "NY", tag_label: "NY"}, %{label: "Barcelona", value: "BCN", tag_label: "BCN"}] |
| 81 81 | ``` |
| 82 82 | |
| 83 83 | 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). |
| @@ -93,11 +93,14 @@ defmodule LiveSelect do | |
| 93 93 | Now, whenever the selection contains "New York", the option will stick and the user won't be able to remove it. |
| 94 94 | |
| 95 95 | ## Slots |
| 96 | - |
| 96 | + |
| 97 97 | You can control how your options and tags are rendered by using the `:option` and `:tag` slots. |
| 98 | + Both slots will be passed an option as argument. In the case of the `:option` slot, the option will have an |
| 99 | + extra boolean field `:selected`, which will be set to `true` if the option has been selected by the user. |
| 100 | + |
| 98 101 | Let's say you want to show some fancy icons next to each option in the dropdown and the tags: |
| 99 102 | |
| 100 | - ```elixir |
| 103 | + ```elixir |
| 101 104 | <.live_select |
| 102 105 | field={@form[:city_search]} |
| 103 106 | phx-target={@myself} |
| @@ -115,7 +118,7 @@ defmodule LiveSelect do | |
| 115 118 | ``` |
| 116 119 | |
| 117 120 | Here's the result: |
| 118 | - |
| 121 | + |
| 119 122 | <img alt="slots" src="https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/slots.png" width="200" /> |
| 120 123 | |
| 121 124 | ## Controlling the selection programmatically |
| @@ -127,8 +130,8 @@ defmodule LiveSelect do | |
| 127 130 | send_update(LiveSelect.Component, id: live_select_id, value: new_selection) |
| 128 131 | ``` |
| 129 132 | |
| 130 | - `new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared. |
| 131 | - After updating the selection, `LiveSelect` will trigger a change event in the form. |
| 133 | + `new_selection` must be a single element in `:single` mode, a list in `:tags` mode. If it's `nil`, the selection will be cleared. |
| 134 | + After updating the selection, `LiveSelect` will trigger a change event in the form. |
| 132 135 | |
| 133 136 | 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`. |
| 134 137 | |
| @@ -148,23 +151,23 @@ defmodule LiveSelect do | |
| 148 151 | _Template:_ |
| 149 152 | ``` |
| 150 153 | <.form for={@form} phx-change="change"> |
| 151 | - <.live_select field={@form[:city_search]} /> |
| 154 | + <.live_select field={@form[:city_search]} /> |
| 152 155 | </.form> |
| 153 156 | ``` |
| 154 | - |
| 157 | + |
| 155 158 | > #### Forms implemented in LiveComponents {: .warning} |
| 156 | - > |
| 159 | + > |
| 157 160 | > If your form is implemented in a LiveComponent and not in a LiveView, you have to add the `phx-target` attribute |
| 158 161 | > when rendering LiveSelect: |
| 159 162 | > |
| 160 163 | > ```elixir |
| 161 164 | > <.live_select field={@form[:city_search]} phx-target={@myself} /> |
| 162 | - > ``` |
| 163 | - |
| 165 | + > ``` |
| 166 | + |
| 164 167 | _LiveView or LiveComponent that is the target of the form's events:_ |
| 165 168 | ``` |
| 166 169 | @impl true |
| 167 | - def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do |
| 170 | + def handle_event("live_select_change", %{"text" => text, "id" => live_select_id}, socket) do |
| 168 171 | cities = City.search(text) |
| 169 172 | # cities could be: |
| 170 173 | # [ {"city name 1", [lat_1, long_1]}, {"city name 2", [lat_2, long_2]}, ... ] |
| @@ -173,13 +176,13 @@ defmodule LiveSelect do | |
| 173 176 | # [ "city name 1", "city name 2", ... ] |
| 174 177 | # |
| 175 178 | # or: |
| 176 | - # [ [label: "city name 1", value: [lat_1, long_1]], [label: "city name 2", value: [lat_2, long_2]], ... ] |
| 179 | + # [ [label: "city name 1", value: [lat_1, long_1]], [label: "city name 2", value: [lat_2, long_2]], ... ] |
| 177 180 | # |
| 178 181 | # or even: |
| 179 182 | # ["city name 1": [lat_1, long_1], "city name 2": [lat_2, long_2]] |
| 180 183 | |
| 181 184 | send_update(LiveSelect.Component, id: live_select_id, options: cities) |
| 182 | - |
| 185 | + |
| 183 186 | {:noreply, socket} |
| 184 187 | end |
| 185 188 | |
| @@ -192,7 +195,7 @@ defmodule LiveSelect do | |
| 192 195 | IO.puts("You selected city #{city_name} located at: #{city_coords}") |
| 193 196 | |
| 194 197 | {:noreply, socket} |
| 195 | - end |
| 198 | + end |
| 196 199 | ``` |
| 197 200 | |
| 198 201 | ### Tags mode |
| @@ -203,7 +206,7 @@ defmodule LiveSelect do | |
| 203 206 | _Template:_ |
| 204 207 | ``` |
| 205 208 | <.form for={@form} phx-change="change"> |
| 206 | - <.live_select field={@form[:city_search]} mode={:tags} /> |
| 209 | + <.live_select field={@form[:city_search]} mode={:tags} /> |
| 207 210 | </.form> |
| 208 211 | ``` |
| 209 212 | |
| @@ -216,17 +219,22 @@ defmodule LiveSelect do | |
| 216 219 | socket |
| 217 220 | ) do |
| 218 221 | # list_of_coords will contain the list of the JSON-encoded coordinates of the selected cities, for example: |
| 219 | - # ["[-46.565,-23.69389]", "[-48.27722,-18.91861]"] |
| 222 | + # ["[-46.565,-23.69389]", "[-48.27722,-18.91861]"] |
| 220 223 | |
| 221 224 | IO.puts("You selected cities located at: #{list_of_coords}") |
| 222 225 | |
| 223 226 | {:noreply, socket} |
| 224 | - end |
| 227 | + end |
| 225 228 | ``` |
| 229 | + ### Quick tags mode |
| 226 230 | |
| 227 | - ### Multiple LiveSelect inputs in the same LiveView |
| 231 | + Quick tags work like tags except that the dropdown stays open after selection. This allows the user to add elements to the selection |
| 232 | + in quick succession. Also, the user can deselect already selected options via the dropdown. The dropdown closes when the LiveSelect component |
| 233 | + loses focus. |
| 228 234 | |
| 229 | - If you have multiple LiveSelect inputs in the same LiveView, you can distinguish them based on the field id. |
| 235 | + ### Multiple LiveSelect inputs in the same LiveView |
| 236 | + |
| 237 | + If you have multiple LiveSelect inputs in the same LiveView, you can distinguish them based on the field id. |
| 230 238 | For example: |
| 231 239 | |
| 232 240 | _Template:_ |
| @@ -255,10 +263,10 @@ defmodule LiveSelect do | |
| 255 263 | |
| 256 264 | ## Using LiveSelect with associations and embeds |
| 257 265 | |
| 258 | - LiveSelect can also be used to display and select associations or embeds without too much effort. |
| 259 | - Let's say you have the following schemas: |
| 260 | - |
| 261 | - ``` |
| 266 | + LiveSelect can also be used to display and select associations or embeds without too much effort. |
| 267 | + Let's say you have the following schemas: |
| 268 | + |
| 269 | + ``` |
| 262 270 | defmodule City do |
| 263 271 | @moduledoc false |
| 264 272 | |
| @@ -292,7 +300,7 @@ defmodule LiveSelect do | |
| 292 300 | end |
| 293 301 | end |
| 294 302 | ``` |
| 295 | - |
| 303 | + |
| 296 304 | 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 |
| 297 305 | the entire data structure (name + coordinates) to be sent to the server when the user selects. |
| 298 306 | |
| @@ -329,20 +337,20 @@ defmodule LiveSelect do | |
| 329 337 | {:noreply, socket} |
| 330 338 | end |
| 331 339 | ``` |
| 332 | - |
| 340 | + |
| 333 341 | > #### IMPORTANT: the output of the `value_mapper/1` function should be JSON-encodable {: .warning} |
| 334 342 | |
| 335 | - 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 |
| 343 | + 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 |
| 336 344 | cast them to create a changeset. To do so, `LiveSelect` offers a convenience function called `LiveSelect.decode/1`: |
| 337 345 | ``` |
| 338 346 | def handle_event("change", params, socket) do |
| 339 | - # decode will JSON-decode the value in city_search, handling the type of selection |
| 347 | + # decode will JSON-decode the value in city_search, handling the type of selection |
| 340 348 | # and taking care of special values such as "" and nil |
| 341 349 | params = update_in(params, ~w(city_search_form city_search), &LiveSelect.decode/1) |
| 342 | - |
| 350 | + |
| 343 351 | # now we can cast the params: |
| 344 352 | changeset = CitySearchForm.changeset(params) |
| 345 | - |
| 353 | + |
| 346 354 | {:noreply, assign(socket, form: to_form(changeset))} |
| 347 355 | end |
| 348 356 | ``` |
| @@ -352,12 +360,12 @@ defmodule LiveSelect do | |
| 352 360 | |
| 353 361 | @doc ~S""" |
| 354 362 | Renders a `LiveSelect` input in a form. |
| 355 | - |
| 363 | + |
| 356 364 | [INSERT LVATTRDOCS] |
| 357 365 | |
| 358 366 | ## Styling attributes |
| 359 367 | |
| 360 | - * See [the styling section](styling.md) for details |
| 368 | + * See [the styling section](styling.md) for details |
| 361 369 | """ |
| 362 370 | @doc type: :component |
| 363 371 | |
| @@ -370,15 +378,16 @@ defmodule LiveSelect do | |
| 370 378 | ~S(an id to assign to the component. If none is provided, `#{form_name}_#{field}_live_select_component` will be used) |
| 371 379 | |
| 372 380 | attr :mode, :atom, |
| 373 | - values: [:single, :tags], |
| 381 | + values: [:single, :tags, :quick_tags], |
| 374 382 | default: Component.default_opts()[:mode], |
| 375 | - doc: "either `:single` (for single selection), or `:tags` (for multiple selection using tags)" |
| 383 | + doc: |
| 384 | + "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)" |
| 376 385 | |
| 377 386 | attr :options, :list, |
| 378 387 | doc: |
| 379 388 | ~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) |
| 380 389 | |
| 381 | - attr :value, :any, doc: "used to manually set a selection - overrides any values from the form. |
| 390 | + attr :value, :any, doc: "used to manually set a selection - overrides any values from the form. |
| 382 391 | Must be a single element in `:single` mode, or a list of elements in `:tags` mode." |
| 383 392 | |
| 384 393 | attr :max_selectable, :integer, |
| @@ -479,27 +488,27 @@ defmodule LiveSelect do | |
| 479 488 | |
| 480 489 | @doc ~S""" |
| 481 490 | Decodes the selection from the client. This has to be used when the values in the selection aren't simple integers or strings. |
| 482 | - |
| 483 | - 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 |
| 491 | + |
| 492 | + 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 |
| 484 493 | decode like this: |
| 485 494 | |
| 486 495 | ``` |
| 487 496 | params = update_in(params, ~w(my_form my_field), &LiveSelect.decode/1) |
| 488 497 | ``` |
| 489 | - |
| 490 | - ## Examples: |
| 498 | + |
| 499 | + ## Examples: |
| 491 500 | |
| 492 501 | iex> decode(nil) |
| 493 502 | [] |
| 494 | - |
| 503 | + |
| 495 504 | iex> decode("") |
| 496 505 | nil |
| 497 | - |
| 506 | + |
| 498 507 | iex> decode("{\"name\":\"Berlin\",\"pos\":[13.41053,52.52437]}") |
| 499 508 | %{"name" => "Berlin","pos" => [13.41053,52.52437]} |
| 500 | - |
| 509 | + |
| 501 510 | iex> decode(["{\"name\":\"New York City\",\"pos\":[-74.00597,40.71427]}","{\"name\":\"Stockholm\",\"pos\":[18.06871,59.32938]}"]) |
| 502 | - [%{"name" => "New York City","pos" => [-74.00597,40.71427]}, %{"name" => "Stockholm","pos" => [18.06871,59.32938]}] |
| 511 | + [%{"name" => "New York City","pos" => [-74.00597,40.71427]}, %{"name" => "Stockholm","pos" => [18.06871,59.32938]}] |
| 503 512 | """ |
| 504 513 | def decode(selection) do |
| 505 514 | case selection do |
| @@ -78,7 +78,7 @@ defmodule LiveSelect.Component do | |
| 78 78 | none: [] |
| 79 79 | ] |
| 80 80 | |
| 81 | - @modes ~w(single tags)a |
| 81 | + @modes ~w(single tags quick_tags)a |
| 82 82 | |
| 83 83 | @impl true |
| 84 84 | def mount(socket) do |
| @@ -429,25 +429,43 @@ defmodule LiveSelect.Component do | |
| 429 429 | |
| 430 430 | defp maybe_select(%{assigns: %{active_option: -1}} = socket, _extra_params), do: socket |
| 431 431 | |
| 432 | + defp maybe_select( |
| 433 | + %{assigns: %{active_option: active_option, options: options, selection: selection}} = |
| 434 | + socket, |
| 435 | + extra_params |
| 436 | + ) |
| 437 | + when active_option >= 0 do |
| 438 | + option = Enum.at(options, active_option) |
| 439 | + |
| 440 | + if already_selected?(option, selection) do |
| 441 | + pos = get_selection_index(option, selection) |
| 442 | + unselect(socket, pos) |
| 443 | + else |
| 444 | + select(socket, option, extra_params) |
| 445 | + end |
| 446 | + end |
| 447 | + |
| 432 448 | defp maybe_select(socket, extra_params) do |
| 433 449 | select(socket, Enum.at(socket.assigns.options, socket.assigns.active_option), extra_params) |
| 434 450 | end |
| 435 451 | |
| 452 | + defp get_selection_index(option, selection) do |
| 453 | + Enum.find_index(selection, fn %{label: label} -> label == option.label end) |
| 454 | + end |
| 455 | + |
| 436 456 | defp select(socket, selected, extra_params) do |
| 437 457 | selection = |
| 438 | - case socket.assigns.mode do |
| 439 | - :tags -> |
| 440 | - socket.assigns.selection ++ [selected] |
| 441 | - |
| 442 | - _ -> |
| 443 | - [selected] |
| 458 | + if socket.assigns.mode in [:tags, :quick_tags] do |
| 459 | + socket.assigns.selection ++ [selected] |
| 460 | + else |
| 461 | + [selected] |
| 444 462 | end |
| 445 463 | |
| 446 464 | socket |
| 447 465 | |> assign( |
| 448 | - active_option: -1, |
| 466 | + active_option: if(quick_tags_mode?(socket), do: socket.assigns.active_option, else: -1), |
| 449 467 | selection: selection, |
| 450 | - hide_dropdown: true |
| 468 | + hide_dropdown: not quick_tags_mode?(socket) |
| 451 469 | ) |
| 452 470 | |> maybe_save_selection() |
| 453 471 | |> client_select(Map.merge(%{input_event: true}, extra_params)) |
| @@ -521,7 +539,7 @@ defmodule LiveSelect.Component do | |
| 521 539 | List.wrap(normalize_selection_value(value, options ++ current_selection, value_mapper)) |
| 522 540 | end |
| 523 541 | |
| 524 | - defp update_selection(value, current_selection, options, :tags, value_mapper) do |
| 542 | + defp update_selection(value, current_selection, options, _mode, value_mapper) do |
| 525 543 | value = if Enumerable.impl_for(value), do: value, else: [value] |
| 526 544 | |
| 527 545 | Enum.map(value, &normalize_selection_value(&1, options ++ current_selection, value_mapper)) |
| @@ -665,7 +683,11 @@ defmodule LiveSelect.Component do | |
| 665 683 | defp encode(value), do: Jason.encode!(value) |
| 666 684 | |
| 667 685 | defp already_selected?(option, selection) do |
| 668 | - option.label in Enum.map(selection, & &1.label) |
| 686 | + Enum.any?(selection, fn item -> item.label == option.label end) |
| 687 | + end |
| 688 | + |
| 689 | + defp quick_tags_mode?(socket) do |
| 690 | + socket.assigns.mode == :quick_tags |
| 669 691 | end |
| 670 692 | |
| 671 693 | defp next_selectable(%{ |
| @@ -676,10 +698,17 @@ defmodule LiveSelect.Component do | |
| 676 698 | when max_selectable > 0 and length(selection) >= max_selectable, |
| 677 699 | do: active_option |
| 678 700 | |
| 679 | - defp next_selectable(%{options: options, active_option: active_option, selection: selection}) do |
| 701 | + defp next_selectable(%{ |
| 702 | + options: options, |
| 703 | + active_option: active_option, |
| 704 | + selection: selection, |
| 705 | + mode: mode |
| 706 | + }) do |
| 680 707 | options |
| 681 708 | |> Enum.with_index() |
| 682 | - |> Enum.reject(fn {opt, _} -> active_option == opt || already_selected?(opt, selection) end) |
| 709 | + |> Enum.reject(fn {opt, _} -> |
| 710 | + active_option == opt || (mode != :quick_tags && already_selected?(opt, selection)) |
| 711 | + end) |
| 683 712 | |> Enum.map(fn {_, idx} -> idx end) |
| 684 713 | |> Enum.find(active_option, &(&1 > active_option)) |
| 685 714 | end |
| @@ -692,11 +721,18 @@ defmodule LiveSelect.Component do | |
| 692 721 | when max_selectable > 0 and length(selection) >= max_selectable, |
| 693 722 | do: active_option |
| 694 723 | |
| 695 | - defp prev_selectable(%{options: options, active_option: active_option, selection: selection}) do |
| 724 | + defp prev_selectable(%{ |
| 725 | + options: options, |
| 726 | + active_option: active_option, |
| 727 | + selection: selection, |
| 728 | + mode: mode |
| 729 | + }) do |
| 696 730 | options |
| 697 731 | |> Enum.with_index() |
| 698 732 | |> Enum.reverse() |
| 699 | - |> Enum.reject(fn {opt, _} -> active_option == opt || already_selected?(opt, selection) end) |
| 733 | + |> Enum.reject(fn {opt, _} -> |
| 734 | + active_option == opt || (mode != :quick_tags && already_selected?(opt, selection)) |
| 735 | + end) |
| 700 736 | |> Enum.map(fn {_, idx} -> idx end) |
| 701 737 | |> Enum.find(active_option, &(&1 < active_option || active_option == -1)) |
| 702 738 | end |
| @@ -8,10 +8,8 @@ | |
| 8 8 | data-field={@field.id} |
| 9 9 | data-debounce={@debounce} |
| 10 10 | > |
| 11 | - <%= if @mode == :tags && Enum.any?(@selection) do %> |
| 12 | - <div class={ |
| 13 | - class(@style, :tags_container, @tags_container_class, @tags_container_extra_class) |
| 14 | - }> |
| 11 | + <div class={class(@style, :tags_container, @tags_container_class, @tags_container_extra_class)}> |
| 12 | + <%= if (@mode in [:tags, :quick_tags]) && Enum.any?(@selection) do %> |
| 15 13 | <%= for {option, idx} <- Enum.with_index(@selection) do %> |
| 16 14 | <div class={class(@style, :tag, @tag_class, @tag_extra_class)}> |
| 17 15 | <%= if @tag == [] do %> |
| @@ -40,8 +38,9 @@ | |
| 40 38 | </button> |
| 41 39 | </div> |
| 42 40 | <% end %> |
| 43 | - </div> |
| 44 | - <% end %> |
| 41 | + <% end %> |
| 42 | + </div> |
| 43 | + |
| 45 44 | <div> |
| 46 45 | <%= text_input(@field.form, @text_input_field, |
| 47 46 | class: |
| @@ -122,12 +121,17 @@ | |
| 122 121 | ) |
| 123 122 | ) |
| 124 123 | } |
| 125 | - data-idx={unless already_selected?(option, @selection), do: idx} |
| 124 | + data-idx={ |
| 125 | + if @mode == :quick_tags or not already_selected?(option, @selection), do: idx |
| 126 | + } |
| 126 127 | > |
| 127 128 | <%= if @option == [] do %> |
| 128 129 | <%= option.label %> |
| 129 130 | <% else %> |
| 130 | - <%= render_slot(@option, option) %> |
| 131 | + <%= render_slot( |
| 132 | + @option, |
| 133 | + Map.merge(option, %{selected: already_selected?(option, @selection)}) |
| 134 | + ) %> |
| 131 135 | <% end %> |
| 132 136 | </div> |
| 133 137 | </li> |
| @@ -4,7 +4,7 @@ defmodule LiveSelect.MixProject do | |
| 4 4 | def project do |
| 5 5 | [ |
| 6 6 | app: app(), |
| 7 | - version: "1.4.4", |
| 7 | + version: "1.5.0", |
| 8 8 | elixir: "~> 1.14", |
| 9 9 | description: "Dynamic (multi)selection field for LiveView", |
| 10 10 | elixirc_paths: elixirc_paths(Mix.env()), |
Loading more files…