Current section

Files

Jump to
sigma_kit lib components forms.ex
Raw

lib/components/forms.ex

defmodule SigmaKit.Components.Forms do
use Phoenix.LiveComponent
import SigmaKit.Components.Icons, only: [icon: 1]
import SigmaKit.Components.Forms.Autocomplete, only: [autocomplete: 1]
@doc """
Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label="Email"/>
<.input field={@form[:username]} label="Username" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
attr :for, :any, required: true, doc: "the data structure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global,
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="space-y-4 bg-white">
{render_slot(@inner_block, f)}
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
{render_slot(action, f)}
</div>
</div>
</.form>
"""
end
@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, doc: "the id of the input"
attr :name, :any, doc: "the name of the input"
attr :label, :string, default: nil, doc: "the label for the input"
attr :value, :any, doc: "the value of the input"
attr :type, :string,
default: "text",
values:
~w(switch checkbox color date datetime-local email file month number password
range search select tel text textarea time url week hidden radio-group autocomplete),
doc: "the type of the input"
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: [], doc: "A list of strings to display as field errors"
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(: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(:event, :string,
default: nil,
doc:
"The event name to trigger when searching for remote options. That event must return a li"
)
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 :help, doc: "A slot used to display help text under the input"
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(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
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div>
<label class={[
"flex items-center gap-4 text-sm leading-6 text-zinc-900"
]}>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
{@rest}
/>
{@label}
</label>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
</div>
"""
end
def input(%{type: "switch"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<label class={[
"flex items-center gap-4 leading-6 text-zinc-900 cursor-pointer"
]}>
<input name={@name} type="checkbox" value="true" checked={@checked} class="sr-only peer" />
<div class="relative min-w-[44px] h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-600">
</div>
<span class="ms-3 text-sm font-semibold text-gray-900 select-none">
{@label}
<div
:if={@help != []}
class={[
"text-sm mt-0.5 text-zinc-600"
]}
>
{render_slot(@help)}
</div>
</span>
</label>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<select
id={@id}
name={@name}
class={[
"h-[42px] px-2 py-2 mt-2 block w-full rounded focus:ring-0 sm:text-sm sm:leading-6 border !outline-none ring-3 box-border",
"text-zinc-900 bg-white focus:ring-4 focus:ring-primary-300 ",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value="">{@prompt}</option>
{Phoenix.HTML.Form.options_for_select(@options, @value)}
</select>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
<div
:if={@help != []}
class={[
"text-sm mt-1 ml-2 mb-2 text-zinc-600"
]}
>
{render_slot(@help)}
</div>
</div>
"""
end
def input(%{type: "autocomplete"} = assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<.autocomplete
id={@id}
name={@name}
value={@value}
options={@options}
target={@target}
event={@event}
/>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
<div
:if={@help != []}
class={[
"text-sm mt-1 ml-2 mb-2 text-zinc-600"
]}
>
{render_slot(@help)}
</div>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div>
<.label for={@id}>{@label}</.label>
<textarea
id={@id}
name={@name}
class={[
"px-2 py-2 mt-2 block w-full rounded focus:ring-0 sm:text-sm sm:leading-6 border !outline-none",
"mt-2 block w-full rounded-lg sm:text-sm sm:leading-6 min-h-[6rem] focus:border-primary-500",
"text-zinc-900 bg-white focus:ring-4 focus:ring-primary-300",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
<div
:if={@help != []}
class={[
"text-sm mt-1 ml-2 mb-2 text-zinc-600"
]}
>
{render_slot(@help)}
</div>
</div>
"""
end
def input(%{type: "radio-group"} = assigns) do
~H"""
<div>
<div :for={{label, value} <- @options}>
<label class={[
"flex items-center gap-4 text-sm leading-6 text-zinc-900 cursor-pointer hover:bg-gray-100 p-2 rounded"
]}>
<input
type="radio"
name={@name}
checked={value == @value || "#{value}" == "#{@value}"}
value={value}
class="rounded border-zinc-300 text-zinc-900 focus:ring-0 bg-white"
{@rest}
/>
{label}
</label>
</div>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div>
<.label for={@id}>
{@label}
</.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"px-2 py-2 h-[42px] mt-2 block w-full rounded sm:text-sm sm:leading-6 border focus:border-primary-500 !outline-none box-border",
"text-zinc-900 bg-white border-zinc-300 focus:ring-4 focus:ring-primary-300",
@errors != [] && "border-rose-400 focus:border-rose-400 !bg-red-100"
]}
{@rest}
/>
<.field_error :for={msg <- @errors}>{msg}</.field_error>
<div
:if={@help != []}
class={[
"text-sm mt-1 ml-2 mb-2 text-zinc-600"
]}
>
{render_slot(@help)}
</div>
</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 text-sm font-semibold leading-6 text-zinc-800"
]}
>
{render_slot(@inner_block)}
</label>
"""
end
slot :inner_block, required: true
def field_error(assigns) do
~H"""
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
{render_slot(@inner_block)}
</p>
"""
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
gt_module = Application.get_env(:sigma_kit, :gettext)
if count = opts[:count] do
Gettext.dngettext(gt_module, "errors", msg, msg, count, opts)
else
Gettext.dgettext(gt_module, "errors", msg, opts)
end
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
end