A few simple HTML functional components that can be used in a form.
- construction process
- passing data via hidden input
- a reusable input
- a single select
- a dynamic datalist
Construction process
- usage: a function that uses "attributes={@assign}". For example:
<.html_function attrib1={@assign1} attr2={@assign2}... />
declare the attributes used for compile-time checks:
attr(:attrb1, :string) ...
check for the assigns in
mount/1
orrender/1
build the function component to render an HTML element using the attributes. For example:
def html_function(assigns) do
~H"""
<label for={@attrib1}>
<input id={@attrib1} value={attrib2} name=.../>
</label>
...
"""
end
- if a "change" with a changeset is used, update the socket assigns with the changed values
Example of passing a hidden input
Suppose we have an assign distance: 1234
that we want to pass to the formData. We don't want to use an explicit input so we can use the hidden
attribute.
The "plain HTML" way is just a hidden input in the form:
<input type="hidden" name="my_form[radius]" value={@distance} />
On submit, we get a formdata:
%{"my_form" => %{"radius" => 1234}}
We illustrate the process with a functional component.
- usage: import a module with a function say
pass_assign
with an attribute - sayradius
- that uses the assigndistance
we want to pass.
<.pass_assign radius={@distance} />
- the attribute is declared:
attr(:radius, :float, doc: false)
- in a module, we define a component
pass_assign
. It uses the attributeradius
as an assign to set the HTMLinput#value
:
def pass_assign(assigns) do
~H"""
<input type="hidden" value={@radius}
name="my_form[radius]"
/>
"""
end
- there is no change handler for this value
Example of a reusable date input
An example a bit more interesting when we want to reuse an input of type date.
- usage: import the module with a function say
date
. We use it with two different assigns:
@date_class "w-15 rounded-ml"
<.date date={@start_date} name="start"
class_date={@date_class} />
<.date date={@end_date} name="end"
class_date={@date_class} />
You have 3 attributes and 2 assigns.
- in the live_component function
mount/1
, instantiate the assigns:
# def mount(socket)
{:ok, assign(socket,
...
start_date: Date.utc_today(),
end_date: Date.utc_today() |> Date.add(1)
}
- declare the attributes used in the component:
attr(:name, :string)
attr(:date, :string)
attr(:class_date, :string)
- build the function
date/1
and use these attributes:
def date(assigns) do
~H"""
<label for={@name}> <%= @name %>
<input type="date" id={@name}
name={"my_form[#{@name}_date]"}
value={@date}
class={@class_date}
/>
</label>
"""
end
- update the changed assigns in the "change" event handler:
socket = socket
|> assign(:changeset, changeset)
|> assign(:start_date, params["start_date"])
|> assign(:end_date, params["end_date"])
where the submitted params (formdata) are:
%{"my_form" => %{
"start_date" => 01/01/2023,
"end_date" => 02/01/2023
}
}
TODO: example with dynamic number of attributes:
@rest
,
Conditional classes
We want the text color to be blue if the user's status is "admin".
Since the attribute class
accepts a list, we can do:
def date(assigns) do
~H"""
<input type="date" id={@name}
name={"myform[#{@name}_date]"}
value={@date}
class={[@user.status == "admin" && "text-blue-700", @class_date]}
/>
"""
end
Integrate errors
- We want to integrate validation errors. We can design a component that adds a class to the error tag to help the positioning in the DOM. We can also pass an attribute name say "attribute" - to extract the errors of a given component from the whole changeset of the form.
<.date_err name="my_form[start_date]"
class="w-30" date={@start_date}
class_err="mt-1" errors={@changeset.errors}
attribute={:start_date}
/>
- We add the extra attributes:
attr(:errors, :list)
attr(:class_err, :string)
attr(:attribute, :atom)
def date_err(assigns) do
attribute = assigns.attribute
messages =
assigns.errors
|> Enum.filter(fn {attr,_} -> attr == attribute end)
|> Enum.map(fn {_k, {msg, _}} -> msg end)
assigns = assign(assigns, :messages, messages)
~H"""
<label for={@name}>
<input type="date" id={@name} name={@name}
value={@date} class={@class}
/>
<span class={["text-red-700", @class_err]}
:for={error <- @messages}
>
<%= error %>
</span>
</label>
"""
end
Example of a single Select menu
A better example: we can easily capture the selected value from the menu in a single select functional component. We use the boolean HTML attribute selected
to capture the chosen "option" from the menu and pass it to the formData "on change".
note: if the case of multiple selects, even if we use a list instead of a single string, we need to manage the possible changes in this list.
- usage: we declare a function
selectl
with two inputs - the menu list and the initial choice - and four attributes (could have more attributes, such as class ...).
<.select options={@menu} choice={@status}
name="my_form[status]" class="form-select w-20"
/>
- we instantiate the assigns in the
mount/1
:
# mount(socket)
{:ok,
assign(socket,
changeset: MyForm.changeset(%MyForm{}),
menu: ["a", "b", "c"],
status: "",
[...]
}
- we declare the attributes:
attr(:options, :list)
attr(:choice, :string)
attr(:name, :string)
atttr(:class, :string)
- we build the function HTML component
select/1
with the attributes:
def select(assigns) do
~H"""
<select name={@name} class={@class} id={@name}>
<option
:for={option <- @options}
selected={option == @choice}
>
<%= option %>
</option>
</select>
"""
end
and commit (after the "changeset" validation) the new value with the event handler "change" into the assigns:
socket =
socket
|> assign(:changeset, changeset)
|> assign(:status, params["status"])
Example of a dynamic Datalist
An example with a dynamically populated datalist
.
We use the associated input
to pass the value to the formData.
We want a datalist to get populated when the user starts typing.
- usage: the datalist component can be:
<.datalist users={@users} user={@user}/>
assigns: we instantiate the two assigns
users: []
anduser: ""
inmount/1
,
or setuser: assign.current_user
inrender/1
if needed/desired.attributes: we declare the two attributes
attr(:users, :list)
attr(:user, :string)
- the function component is:
def datalist(assigns) do
~H"""
<input list="datalist" id="datalist-input"
name="my_form[user]"
phx-change="search-email"
value={@user}
/>
<datalist id="datalist">
<option :for={user <- @users} value={user}>
<%= user %>
</option>
</datalist>
"""
end
and the "change" handler can be like:
def handle_event("search-email", %{"my_form" => %{"user" => string}}, socket) do
datalist =
MyApp.User.search(string) |> Enum.map(& &1.email)
{:noreply, assign(socket, users: datalist, user: string}
end
Top comments (0)