Phoenix has nice helpers to generate forms. Let’s see how we can leverage its capacities to dynamically add fields to a form, without reloading the page.

Let’s consider a user model that has a list of attached addresses:

web/model/user.ex

schema "user" do

has_many :addresses, MyApp.Address

end

The template for the form allowing to create a user might look like this:

web/templates/user/new.html.eex

<%= form_for @changeset, user_path(@conn, :create), fn f -> %>

<ul>

<%= inputs_for f, :addresses, fn a -> %>

<li><%= text_input a, :address %></li>

<% end %>

</ul>

<% end %>

This template is going to render a li tag containing an input for each address.

But how do we deal with adding new address inputs to the form ? Strategies using javascript to clone addresses from the dom are fairly complicated and risky. Let’s see what we can do using the same lines of code that rendered the template in the first place: our Phoenix app.

Here is the plan:

Create a live-html channel where clients can ask for templates and receive them back over socket — lightning fast. Put the part of the form that we need dynamically loaded in a separate template so that we can render it alone. Render this template in a channel and send it back to the client.

1. The live-html channel.

First, let’s add a channel to our user socket.

web/channels/user_socket.ex

# Channels

channel "live-html", MyApp.LiveHtmlChannel

Then let’s define this LiveHtmlChannel module so that our app can handle client requests:

web/channels/live_html_channel.ex

defmodule MyApp.LiveHtmlChannel do

def join("live-html", _message, socket) do

{:ok, socket}

end def handle_in("new_address", _params, socket) do

html = ... # we are going to define this later on

push socket "new_address", %{html: html}

end

end

2. The separate template.

In order to be able to render the addresses separately, let’s split our for template in two parts:

First, the component to render the addresses

web/views/user/addresses.html.eex

<%= inputs_for @f, :addresses, fn a -> %>

<li><%= text_input a, :address %></li>

<% end %>

Then the form :

web/views/user/new.html.eex

<%= form_for @changeset, user_path(@conn, :create), fn f -> %>

<ul>

<%= render MyApp.UserView, "addresses.html", f: f %>

</ul>

<% end %>

Notice :

how we are including a template, using the render function, taking advantage of the functional nature of Phoenix.

how we pass the form as assign to our template. It is the @f that you find at the first line of the template.

3. Rendering in the channel

Now that we have our live html channel and that the template for the addresses inputs is split appart, we can finally work on rendering the addresses dynamically.

The difficulty comes from the @f form assign that the template needs to render :

web/templates/user/addresses.html.eex

<%= inputs_for @f, :addresses, fn a -> %> ...

Where to get it from ? This form was previously provided to our template as the f variable in the form_for callback like so :

web/templates/user/new.html.eex

<%= form_for @changeset, user_path(@conn, :create), fn f -> %> ...

But this form_form helper renders a template, and we just would like to have this f form available to provide it to our addresses.html.eex template.

Let’s have a glance at the source code for form_for :

def form_for(form_data, action, options \\ [], fun) when is_function(fun, 1) do

form = Phoenix.HTML.FormData.to_form(form_data, options) |> normalize_form

html_escape [form_tag(action, form.options), fun.(form), raw("</form>")] end

Sweet, the form that form_for provides to the template is the output of this Phoenix.HTML.FormData.to_form function. Let’s use it !

The form_data argument provided to this function must be an object implementing the FormData protocol. Conn and Changeset do this. Let’s see what we can do with a changeset, and this new plan:

get the actual state of the client form over socket

add an input field

create a changeset with those data

provide this changeset to form_for

use the generated form as assign to render our addresses.html.eex

send it back to the client

celebrate

Here is what our new live html channel might look like :

web/channels/live_html_channel.ex

defmodule MyApp.LiveHtmlChannel do

alias MyApp.User

alias MyApp.Address

alias Phoenix.HTML.FormData

alias MyApp.UserView

alias Phoenix.View

# ... def handle_in("new_address", %{"form" => client_form}, socket) do updated_client_form = client_form |> append_address f =

%User{}

|> User.changeset(updated_client_form)

|> FormData.to_form([]) html = View.render_to_string UserView, "addresses.html", f: f

push socket "new_address", %{html: html}

end



defp append_address(user_form_data)

user_form_data |> Map.update(:addresses, [], fn(addresses) -> addresses ++ [%{}] end)

end end

Great !

We are receiving the current form state, updating it to what we would like to send back, generating the form data from it through changeset and rendering our template with it.

To get the current state of the form from the client, I am using serializeObject sending it over the channel. We could also send only parts of it to optimise.

The interest of passing all the addresses as parameter is that the inputs_for method from the addresses.html.eex component will re-render all of them plus the new one with the correct indexes : we can just replace the old list in the dom by the new one.

Note the magic happening there : as we pass the current state of the form to the changeset, the addresses in addresses.html.eex will be re-rendered with their values preserved!

Note also how generic this approach is : we could have rendered just any inputs from the form this way.

Hint

If you want the above changeset manipulation to work, don’t forget to use cast_assoc in the user changeset so that the user changeset takes the addresses in acount:

web/models/user.ex

def changeset(struct, params \\ %{}) do

struct

|> cast(params, @fields)

|> cast_assoc(:addresses)

Conclusion

We have found a way to render any part of a form with Phoenix, even a nested one, from the current state of the form on the client side — or any other arbitrary state.

Combined to the super-fast rendering of the phoenix templates and the ease of use of channels, this gives us the flexibility to manage complex form interactions on the client side without the need for a complex fronted technology.