DEV Community

NDREAN
NDREAN

Posted on • Updated on

Notes on HTML function Components with Phoenix

A few simple HTML functional components that can be used in a form.

Construction process

  • usage: a function that uses "attributes={@assign}". For example:
<.html_function attrib1={@assign1} attr2={@assign2}... />
Enter fullscreen mode Exit fullscreen mode
  • declare the attributes used for compile-time checks:
    attr(:attrb1, :string) ...

  • check for the assigns in mount/1 or render/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
Enter fullscreen mode Exit fullscreen mode
  • 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} />
Enter fullscreen mode Exit fullscreen mode

On submit, we get a formdata:

%{"my_form" => %{"radius" => 1234}}
Enter fullscreen mode Exit fullscreen mode

We illustrate the process with a functional component.

  • usage: import a module with a function say pass_assign with an attribute - say radius - that uses the assign distance we want to pass.
<.pass_assign radius={@distance} />
Enter fullscreen mode Exit fullscreen mode
  • the attribute is declared:
attr(:radius, :float, doc: false)
Enter fullscreen mode Exit fullscreen mode
  • in a module, we define a component pass_assign. It uses the attribute radius as an assign to set the HTML input#value:
def pass_assign(assigns) do
  ~H"""
    <input type="hidden" value={@radius} 
      name="my_form[radius]"
    />
  """
end
Enter fullscreen mode Exit fullscreen mode
  • 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} />
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
  • declare the attributes used in the component:
attr(:name, :string)
attr(:date, :string)
attr(:class_date, :string)
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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"])
Enter fullscreen mode Exit fullscreen mode

where the submitted params (formdata) are:

%{"my_form" => %{
    "start_date" => 01/01/2023, 
    "end_date" => 02/01/2023
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode
  • We add the extra attributes:
attr(:errors, :list)
attr(:class_err, :string)
attr(:attribute, :atom)
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode
  • we instantiate the assigns in the mount/1:
# mount(socket)
{:ok,
   assign(socket,
     changeset: MyForm.changeset(%MyForm{}),
     menu: ["a", "b", "c"],
     status: "",
     [...]
}
Enter fullscreen mode Exit fullscreen mode
  • we declare the attributes:
attr(:options, :list)
attr(:choice, :string)
attr(:name, :string)
atttr(:class, :string)
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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}/>
Enter fullscreen mode Exit fullscreen mode
  • assigns: we instantiate the two assigns users: [] and user: "" in mount/1,
    or set user: assign.current_user in render/1 if needed/desired.

  • attributes: we declare the two attributes

attr(:users, :list)
attr(:user, :string)
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)