analytics/lib/plausible_web/live/components/form.ex
hq1 b3ff695797
Improve goal settings UX (#3293)
* Add Heroicons dependency

* Add name_of/1 html helper

Currently with Floki there's no way to query for
`[name=foo[some]]` selector

* Update changelog

* Make goal deletion possible with only goal id

* Remove stale goal controllers

* Improve ComboBox component

- make sure the list options are always of the parent input width
- allow passing a suggestion function instead of a module

* Stale fixup

* Update routes

* Use the new goals route in funnel settings

* Use a function in the funnel combo

* Use function in the props combo

* Remove old goals form

* Implement new goal settings

* Update moduledoc

* Fix revenue switch in dark mode

* Connect live socket on goal settings page

* Fixup

* Use Heroicons.trash icon

* Tweak goals search input

* Remove unused alias

* Fix search/button alignment

* Fix backspace icon alignment

* Delegate :superadmin check to get_for_user/3

I'll do props settings separately, it's work in progress
in a branch on top of this one already. cc @ukutaht

* Rename socket assigns

* Fixup to 5c9f58e

* Fixup

* Render ComboBox suggestions asynchronously

This commit:
  - prevents redundant work by checking the socket connection
  - allows passing no options to the ComboBox component,
    so that when combined with the `async` option, the options
    are asynchronously initialized post-render
  - allows updating the suggestions asynchronously with the
    `async` option set to `true` - helpful in case of DB
    queries used for suggestions

* Update tests

* Throttle comboboxes

* Update tests

* Dim the search input

* Use debounce=200 in ComboBox component

* Move creatable option to the top

* Ensure there's always a leading slash for goals

* Test pageview goals with leading / missing

* Make the modal scrollable on small viewports
2023-09-04 13:44:22 +02:00

111 lines
3.0 KiB
Elixir

defmodule PlausibleWeb.Live.Components.Form do
@moduledoc """
Generic components stolen from mix phx.new templates
"""
use Phoenix.Component
@doc """
Renders an input with label and error messages.
A `Phoenix.HTML.FormField` may be passed as argument,
which is used to retrieve the input name, id, and values.
Otherwise all attributes may be passed explicitly.
## Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr(:id, :any, default: nil)
attr(:name, :any)
attr(:label, :string, default: nil)
attr(:value, :any)
attr(:type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
)
attr(:field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
)
attr(:errors, :list, default: [])
attr(:checked, :boolean, doc: "the checked flag for checkbox inputs")
attr(:prompt, :string, default: nil, doc: "the prompt for select inputs")
attr(:options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2")
attr(:multiple, :boolean, default: false, doc: "the multiple flag for select inputs")
attr(:rest, :global,
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
multiple pattern placeholder readonly required rows size step)
)
slot(:inner_block)
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}>
<%= @label %>
</.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
{@rest}
/>
<.error :for={msg <- @errors}>
<%= msg %>
</.error>
</div>
"""
end
@doc """
Renders a label.
"""
attr(:for, :string, default: nil)
slot(:inner_block, required: true)
def label(assigns) do
~H"""
<label for={@for} class="block font-medium dark:text-gray-100">
<%= render_slot(@inner_block) %>
</label>
"""
end
@doc """
Generates a generic error message.
"""
slot(:inner_block, required: true)
def error(assigns) do
~H"""
<p class="flex gap-3 text-sm leading-6 text-red-500 phx-no-feedback:hidden">
<%= render_slot(@inner_block) %>
</p>
"""
end
def translate_error({msg, opts}) do
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
end)
end
end