Phoenix LiveView Under The Hood: The Form Function Component
Tags: elixir
Published: 2022-04-05 11:59:40

Thanks to HEEx and function components, LiveView provides developers with a sleek, ergonomic syntax for building and maintaining sophisticated interactive UIs. LiveView's `form/1` function component is a great example of this, making it easier than ever before to render complex forms within LiveView.

However, the `form/1` function component can feel a little mysterious to anyone unfamiliar with LiveView's function components.

In this post, we'll look under the hood of the `form/1` function component. We'll dive into the Phoenix Component functionality that underpins this function and explore features like component slots. When we're done, you'll know exactly how the `form/1` function renders your forms and you'll have a deeper understanding of LiveView components, setting you up to build your own components in the future.

## What Are Function Components in Phoenix LiveView?

Before we dive into the `form/1` function component, let's discuss LiveView's function components at a high level. Function components are defined in modules that use the `Phoenix.Component` behavior. Any function that takes in an argument of assigns and returns some markup wrapped in a HEEx template is a function component.

Let's take a look at a simple example. We'll define a `UserDetails` module that implements a function component, `contact_info`.

```elixir
defmodule MyAppLive.UserDetails do
  use Phoenix.Component

  def contact_info(assigns) do
    ~H"""
    <div class="contact-info">
      <p><strong>Phone:</strong><%= @user.phone_number %></strong></p>
      <p><strong>Email:</strong><%= @user.email %></strong></p>
    </div>
    """
  end
end
```

Now, we can call on this component with any HEEx template like this (assuming you have an available `@current_user` assigns):

```html
<UserDetails.contact_info user="{@current_user}" />
```

If you import the `UserDetails` module or if you're calling on this function component somewhere where the function is defined locally (i.e., elsewhere in the `UserDetails` module), you can leave off the module name from the function call:

```html
<.contact_info user={@current_user}>
```

With this basic understanding of function components under your belt, we're ready to dive into the `.form/1` function component that LiveView makes available to you. We'll take a look at some more advanced features of function components along the way.

## Calling the `form/1` Function Component

LiveView implements a function component, [`form/1`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Helpers.html#form/1). This function component is defined in `Phoenix.LiveView.Helper` and imported into all of your live views. We'll start with the entry point of the `form/1` code flow—the caller.

In this example, we'll construct a form for a book record that has a title, description, and author, like this:

```html
<.form
    let={f}
    for={@changeset}
    id="book-form"
    phx-change="validate"
    phx-submit="save">

  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <%= label f, :description %>
  <%= textarea f, :description %>
  <%= error_tag f, :description %>

  <%= label f, :author %>
  <%= select f, :author_id, Enum.map(@authors, &{&1.full_name, &1.id}) %>
  <%= error_tag f, :author_id %>

  <div>
    <%= submit "Save", phx_disable_with: "Saving..." %>
  </div>
</.form>
```

Already, we can see that the `form/1` function component offers a clean and easy-to-read syntax for describing forms. Let's break down the function invocation here. Then, we'll trace what's happening under the hood.

The `form/1` function component, like all function components, is called with one argument of the assigns. Some of the attributes passed as part of the assigns probably look familiar from working with `Phoenix.HTML.form_for/4`. We pass in the changeset, an ID, and some LiveView form bindings. In fact, we can optionally pass in any of the same options you would give to `Phoenix.HTML.form_for/4`, and the `form/3` function component will use those options in the same way.

In addition to the form options we're giving to assigns, we also use the `let` assigns to tell the function component to give a value back to the caller. More on this in a bit.

Lastly, you'll notice that we're actually calling the `form/1` function component with an opening and closing `<.form>` tag, and we've enclosed the content of our form within those tags. This eloquent, declarative syntax is made possible by the Phoenix Component's `render_slot/2` functionality. We'll see that in action later on in this post.

Now that we've seen how to call on the `form/1` function component, let's dive into the function's implementation.

## The `form/1` Component Under the Hood

You already know that a function component takes in an argument of some assigns and returns some markup wrapped in a HEEx template. So, you won't be surprised to see that at its core, the `form/1` function component sets up a `Phoenix.HTML.Form` struct and renders it in a HEEx template that returns an HTML form.

We'll break down this process one step at a time, beginning with the creation of the form struct.

### Creating the `Phoenix.Form`

The first thing that the `form/1` function does under the hood is to construct a `Phoenix.HTML.Form` struct. This is constructed using the data in the assigns that we passed into our function call. [Let's take a look at the code](https://github.com/phoenixframework/phoenix_live_view/blob/30ee942b3a18a9e2e1f222a76a707bfba7bd94f7/lib/phoenix_live_view/helpers.ex#L1034-L1044):

```elixir
# Extract options and then to the same call as form_for
action = assigns[:action] || "#"
form_for = assigns[:for] || raise ArgumentError, "missing :for assign to form"
form_options = assigns_to_attributes(assigns, [:action, :for])

# Since FormData may add options, read the actual options from form
%{options: opts} =
  form = %Phoenix.HTML.Form{
    Phoenix.HTML.FormData.to_form(form_for, form_options)
    | action: action
  }
```

First, it casts the data from the assigns into the same format that `Phoenix.HTML.form_for/4` uses to construct form structs. Then, it initializes a new struct.

Next up, the function constructs the form method, CSRF token, and multi-part setting from the data in the form struct, [like this](https://github.com/phoenixframework/phoenix_live_view/blob/30ee942b3a18a9e2e1f222a76a707bfba7bd94f7/lib/phoenix_live_view/helpers.ex#L1046-L1059):

```elixir
# And then process method, csrf_token, and multipart as in form_tag
{method, opts} = Keyword.pop(opts, :method, "post")
{method, hidden_method} = form_method(method)

{csrf_token, opts} =
  Keyword.pop_lazy(opts, :csrf_token, fn ->
    if method == "post", do: Plug.CSRFProtection.get_csrf_token_for(action)
  end)

opts =
  case Keyword.pop(opts, :multipart, false) do
    {false, opts} -> opts
    {true, opts} -> Keyword.put(opts, :enctype, "multipart/form-data")
  end
```

With the form struct and options in place, we then move on to constructing the assigns that will be [rendered in the HEEx template here](https://github.com/phoenixframework/phoenix_live_view/blob/30ee942b3a18a9e2e1f222a76a707bfba7bd94f7/lib/phoenix_live_view/helpers.ex#L1062-L1068):

```elixir
assigns =
  LiveView.assign(assigns,
    form: form,
    csrf_token: csrf_token,
    hidden_method: hidden_method,
    attrs: [action: action, method: method] ++ opts
  )
```

For LiveView to track changes to assigns values rendered by a function component, it _must_ render a valid `assigns` either passed in as the only argument given to the function or created via a call to `Phoenix.LiveView.assign/3` or `Phoenix.LiveView.assign_new/3`. So, the function component uses `LiveView.assign/3` here to construct a new set of assigns to render in the HEEx template.

With the assigns in place, the function will then render the template. Let's take a look at that now.

### Rendering the HEEx Template

The `form/1` function component, like all function components, returns some markup wrapped in a HEEx template. Let's take a look at [that return value](https://github.com/phoenixframework/phoenix_live_view/blob/30ee942b3a18a9e2e1f222a76a707bfba7bd94f7/lib/phoenix_live_view/helpers.ex#L1070-L1080) now:

```elixir
 ~H"""
<form {@attrs}>
  <%= if @hidden_method && @hidden_method not in ~w(get post) do %>
    <input name="_method" type="hidden" value={@hidden_method}>
  <% end %>
  <%= if @csrf_token do %>
    <input name="_csrf_token" type="hidden" value={@csrf_token}>
  <% end %>
  <%= render_slot(@inner_block, @form) %>
</form>
"""
```

Unsurprisingly, the `form/1` function component simply returns a HEEx template wrapping a call to an HTML `<form>`. Let's dig into how the template uses some of the assigns established in the previous step.

First up, you can see that the `@attrs` assigns is interpolated directly into the opening `<form>` tag. This will render the tag with the appropriate `method=` and `action=` attributes. Next up, you can see that the `@hidden_method` assigns determines if and how to populate the hidden input. Then the `@csrf` token, if present, is added to the HTML form in another hidden input.

Most of this template has been pretty straightforward so far. The HEEx template renders, and HTML is constructed from various assigns values. Next up, we'll look at how the form fields we specified between our opening and closing `<.form>` tags are rendered into the template with the component slot functionality.

### Render the Inner Block with Phoenix Component Slots

Phoenix Components implement a feature called ["slots"](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#module-slots). Slots enable us to give blocks to our function component calls, nesting them within opening and closing function component tags like regular HTML tags. Slots are the reason for the `form/1` syntax we used above:

```html
<.form ...assigns>
  <!-- form fields -->
</.form>
```

Here's how it works. Suppose you call on a function component with opening and closing function component tags. That function component's template renders the content in those tags with the help of the `render_slot/2` function.

Let's take a look at an example. Recall the `UserDetails.contact_info/1` function component we defined earlier:

```elixir
defmodule MyAppLive.UserDetails do
  use Phoenix.Component

  def contact_info(assigns) do
    ~H"""
    <div>
      <p><strong>Phone:</strong><%= @user.phone_number %></p>
      <p><strong>Email:</strong><%= @user.email %></p>
    </div>
    """
  end
end
```

We can refactor it to use slots like this:

```elixir
defmodule MyAppLive.UserDetails do
  use Phoenix.Component

  def contact_info(assigns) do
    ~H"""
    <div class="contact-info">
      <%= render_slot(@inner_block) %>
    </div>
    """
  end
end
```

Now, we have a dynamic function component we can use to render any type of contact info:

```html
<UserDetails.contact_info user="{@current_user}">
  <p><strong>Phone:</strong><%= @current_user.phone_number %></p>
</UserDetails.contact_info>
```

Or:

```html
<UserDetails.contact_info user={@current_user}>
  <p>
    <strong>Address:</strong>
    <ul>
      <li><%= @current_user.street_address %></li>
      <li><%= @current_user.city %></li>
      <li><%= @current_user.zip_code %></li>
      <li><%= @current_user.country %></li>
    </ul>
  </p>
</UserDetails.contact_info>
```

The HTML markup we include between the opening and closing function component tags becomes available in the function component as the `@inner_block` assigns. The `render_slot/2` function renders that content for us.

We can clean this up even further with the `let` assigns to yield a variable from the function component's template back to the calling template. Let's take a look.

First, we'll call the function component with an assigns of `let` set equal to a variable, `user`:

```html
<UserDetails.contact_info let="{user}" user="{@current_user}">
  <!-- coming soon -->
</UserDetails.contact_info>
```

Then, we'll update the function component's call to `render_slot/2` by invoking it with a second argument of the `@user` assignment, like this:

```elixir
def contact_info(assigns) do
  ~H"""
  <div class="contact-info">
    <%= render_slot(@inner_block, @user) %>
  </div>
  """
end
```

The function component sets the variable we specified in the `let` assignment equal to the value of whatever is passed in as the second argument to `render_slot/2`. Now, the inner content between the function component's opening and closing tags in the calling template has access to the `user` variable. We can update our inner block to look like this:

```html
<UserDetails.contact_info let="{user}" user="{@current_user}">
  <p><strong>Phone:</strong><%= user.phone_number %></p>
</UserDetails.contact_info>
```

With this basic understanding of slots in place, let's revisit the `form/1` template:

```html
~H"""
<form {@attrs}>
  <%= if @hidden_method && @hidden_method not in ~w(get post) do %>
  <input name="_method" type="hidden" value="{@hidden_method}" />
  <% end %> <%= if @csrf_token do %>
  <input name="_csrf_token" type="hidden" value="{@csrf_token}" />
  <% end %> <%= render_slot(@inner_block, @form) %>
</form>
"""
```

Here, its calling `render_slot/2` with the `@inner_block` assignment and the `@form` assignment. Recall that we invoked `form/1` like this:

```html
<.form
    let={f}
    for={@changeset}
    id="book-form"
    phx-change="validate"
    phx-submit="save">

  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <!-- ... -->
</.form>
```

So, the form fields render as the `@inner_block` assignment. The call to `<.form>`
establishes a variable `f`, which gets set to a value of the second argument passed to `render_slot/2`. The second argument to `render_slot/2` is the `@form` assignment, pointing to our `Phoenix.HTML.Form` struct. So, the inner content in our calling template can use the `f` variable to reference the form struct and build out the Phoenix form to render.

And that's it!

## Wrap Up: Demystifying Phoenix LiveView's Form Function Component

While the call to the `form/1` function component can seem mysterious, tracing the code under the hood isn't too daunting.

To recap: we can see that the function establishes a Phoenix form and assembles some assigns. Then, it returns a HEEx template that renders an HTML form with the assigns. The HEEx template uses Phoenix Component's slot functionality to render the inner content of our specified form fields, and yields the Phoenix form back to the calling template where we construct those fields.

I hope that this dive under the hood of the `form/1` function component has not only demystified that function, but also given you a deeper understanding of how you can use function components and slots in your own live views. Now, you're ready to build out your own extensible function component that dynamically renders different inner blocks of content.

Happy coding!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post](/elixir-alchemy)!**