Add creatable option to ComboBox (#3169)

* Add creatable option to ComboBox

This commit changes the ComboBox component to allow a `creatable`
option. This option enables users to create new options along with
choosing existing options.

* Test ComboBox class parameter

* Use display_value instead of input

* Change scroll block to nearest to prevent glitches
This commit is contained in:
Vini Brasil 2023-07-21 14:58:50 +01:00 committed by GitHub
parent 9ed79542f2
commit 16846b16c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 7 deletions

View File

@ -16,7 +16,7 @@ let suggestionsDropdown = function(id) {
},
scrollTo(idx) {
this.$refs[`dropdown-${this.id}-option-${idx}`]?.scrollIntoView(
{ block: 'center', behavior: 'smooth', inline: 'start' }
{ block: 'nearest', behavior: 'smooth', inline: 'start' }
)
},
focusNext() {

View File

@ -48,12 +48,15 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
attr(:submit_value, :string, default: "")
attr(:suggest_mod, :atom, required: true)
attr(:suggestions_limit, :integer)
attr(:class, :string, default: "")
attr(:required, :boolean, default: false)
attr(:creatable, :boolean, default: false)
def render(assigns) do
~H"""
<div
id={"input-picker-main-#{@id}"}
class="mb-3"
class={@class}
x-data={"window.suggestionsDropdown('#{@id}')"}
x-on:keydown.arrow-up="focusPrev"
x-on:keydown.arrow-down="focusNext"
@ -77,6 +80,7 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
value={@display_value}
class="border-none py-1 px-1 p-0 w-full inline-block rounded-md focus:outline-none focus:ring-0 text-sm"
style="background-color: inherit;"
required={@required}
/>
<.dropdown_anchor id={@id} />
@ -91,7 +95,14 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
</div>
</div>
<.dropdown ref={@id} suggest_mod={@suggest_mod} suggestions={@suggestions} target={@myself} />
<.dropdown
ref={@id}
suggest_mod={@suggest_mod}
suggestions={@suggestions}
target={@myself}
creatable={@creatable}
display_value={@display_value}
/>
</div>
"""
end
@ -123,6 +134,8 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
attr(:suggestions, :list, default: [])
attr(:suggest_mod, :atom, required: true)
attr(:target, :any)
attr(:creatable, :boolean, required: true)
attr(:display_value, :string, required: true)
def dropdown(assigns) do
~H"""
@ -149,8 +162,18 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
ref={@ref}
/>
<.option
:if={@creatable && String.length(@display_value) > 0}
idx={length(@suggestions)}
submit_value={@display_value}
display_value={@display_value}
target={@target}
ref={@ref}
creatable
/>
<div
:if={@suggestions == []}
:if={@suggestions == [] && !@creatable}
class="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300"
>
No matches found. Try searching for something different.
@ -160,10 +183,11 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
end
attr(:display_value, :string, required: true)
attr(:submit_value, :integer, required: true)
attr(:submit_value, :string, required: true)
attr(:ref, :string, required: true)
attr(:target, :any)
attr(:idx, :integer, required: true)
attr(:creatable, :boolean, default: false)
def option(assigns) do
assigns = assign(assigns, :suggestions_limit, suggestions_limit(assigns))
@ -183,7 +207,11 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
class="block py-2 px-3"
>
<span class="block truncate">
<%= if @creatable do %>
Create "<%= @display_value %>"
<% else %>
<%= @display_value %>
<% end %>
</span>
</a>
</li>
@ -217,6 +245,13 @@ defmodule PlausibleWeb.Live.Components.ComboBox do
input = params[target]
input_len = input |> String.trim() |> String.length()
socket =
if socket.assigns[:creatable] do
assign(socket, display_value: input, submit_value: input)
else
socket
end
if input_len > 0 do
suggestions =
input

View File

@ -64,7 +64,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
class: "mt-6 block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
) %>
<div :for={step_idx <- @step_ids} class="flex">
<div :for={step_idx <- @step_ids} class="flex mb-3">
<div class="w-2/5 flex-1">
<.live_component
submit_name="funnel[steps][][goal_id]"

View File

@ -105,6 +105,34 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
assert text_of_element(doc, "#dropdown-test-component") ==
"No matches found. Try searching for something different."
end
test "dropdown suggests user input when creatable" do
doc =
render_sample_component([{"USD", "US Dollar"}, {"EUR", "Euro"}],
creatable: true,
display_value: "Brazilian Real"
)
assert text_of_element(doc, "#dropdown-test-component-option-0") == "US Dollar"
assert text_of_element(doc, "#dropdown-test-component-option-1") == "Euro"
assert text_of_element(doc, "#dropdown-test-component-option-2") ==
~s(Create "Brazilian Real")
refute text_of_element(doc, "#dropdown-test-component") ==
"No matches found. Try searching for something different."
end
test "makes the html input required when required option is passed" do
input_query = "input[type=text][required]"
assert render_sample_component([], required: true) |> element_exists?(input_query)
refute render_sample_component([]) |> element_exists?(input_query)
end
test "adds class to html element when class option is passed" do
assert render_sample_component([], class: "animate-spin")
|> element_exists?("#input-picker-main-test-component.animate-spin")
end
end
describe "integration" do
@ -165,6 +193,71 @@ defmodule PlausibleWeb.Live.Components.ComboBoxTest do
end
end
describe "creatable integration" do
defmodule CreatableView do
use Phoenix.LiveView
def render(assigns) do
~H"""
<.live_component
submit_name="some_submit_name"
module={PlausibleWeb.Live.Components.ComboBox}
suggest_mod={ComboBox.StaticSearch}
id="test-creatable-component"
options={for i <- 1..20, do: {i, "Option #{i}"}}
creatable
/>
"""
end
end
test "stores selected value from suggestion", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
type_into_combo(lv, "test-creatable-component", "option 20")
doc =
lv
|> element("li#dropdown-test-creatable-component-option-0 a")
|> render_click()
assert element_exists?(doc, "input[type=hidden][name=some_submit_name][value=20]")
end
test "suggests creating custom value", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
assert lv
|> type_into_combo("test-creatable-component", "my new option")
|> text_of_element("li#dropdown-test-creatable-component-option-0 a") ==
~s(Create "my new option")
end
test "stores new value by clicking on the dropdown custom option", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
type_into_combo(lv, "test-creatable-component", "my new option")
doc =
lv
|> element("li#dropdown-test-creatable-component-option-0 a")
|> render_click()
assert element_exists?(
doc,
"input[type=hidden][name=some_submit_name][value=\"my new option\"]"
)
end
test "stores new value while typing", %{conn: conn} do
{:ok, lv, _html} = live_isolated(conn, CreatableView, session: %{})
assert lv
|> type_into_combo("test-creatable-component", "my new option")
|> element_exists?(
"input[type=hidden][name=some_submit_name][value=\"my new option\"]"
)
end
end
defp render_sample_component(options, extra_opts \\ []) do
render_component(
ComboBox,