Phoenix Nested Form with LiveView
May 28, 2019 ยท View on GitHub
This is an example repo of how to work with nested forms while using Phoenix LiveView. (It's still in progress as of writing this, but check out Phoenix LiveView! Very fun framework!)
I spent awhile trying to get nested forms to work properly with, so I put this together to share what I got working. The conditions of this problem:
- One-to-many association
- Preload associations for edit form
- Create multiple nested associations dynamically (via LiveView) in one form
- Adding more of an association dynamically does not disrupt/reset form fields
- Utilize LiveView (no additional JS!)
I wanted a form where upon creating/updating you can add a variable number of a nested association. Each button press adds an additional association field to the form. Check out the repo for details, but here's a short description of the key parts.
Schema
I use a one-many posts to comments association for this example. Add cast_assoc/3 to Post.changeset/2 so the association is included in the changeset.
def changeset(post, attrs) do
post
|> cast(attrs, [:text])
|> cast_assoc(:comments)
|> validate_required([:text])
end
Change the default Example.change_post/1 to change_post/2 so we can add pass attributes through.
def change_post(%Post{} = post, attrs \\ %{}) do
Post.changeset(post, attrs)
end
Edit Example.get_post!/1 to preload comments
def get_post!(id) do
Repo.get!(Post, id)
|> Repo.preload(:comments)
end
Template
Use phx_change tag in form
Utilizing LiveView's phx_change tag is necessary! Else the form fields resets when a new comment is added to the post. As of writing this, there is little LiveView documentation on phx-change. What this example is doing: anytime a form field recieves a change, the event "validate" is sent, which is caught with handle_event/3.
<%= form_for @post_changeset, "#", [phx_change: :validate, phx_submit: :save], fn f -> %>
Nested Form
Use Phoenix.HTML.Form.inputs_for/4 to create form inputs for the nested association.
<%= inputs_for f, :comments, fn cf -> %>
<h3>Comment</h3>
<%= label cf, :text %>
<%= text_input cf, :text %>
<%= error_tag cf, :text %>
<% end %>
add_comment button
<button phx-click="add_comment">Add comment</button>
LiveView
add_comment
Catch that "add_comment" event via PostLive.New.handle_event/3. First add a new comment changeset to the list of current comment changesets. Use Map.get/3 to set a default empty list. Then update the post changeset and update the socket assigns.
def handle_event("add_comment", _, socket) do
comment_changeset = %Comment{} |> Example.change_comment
comments = Map.get(socket.assigns.post_changeset.changes, :comments, []) ++ [comment_changeset]
post_changeset = socket.assigns.post_changeset
|> Map.put(:changes, %{comments: comments})
{:noreply, assign(socket, post_c def handle_event("validate", %{"post" => params}, socket) do
post_changeset = %Post{}
|> Example.change_post(params)
{:noreply, assign(socket, post_changeset: post_changeset)}
endhangeset: post_changeset)}
end
validate
Catch the "validate" event from the phx-change mentioned above. Here the changeset is updated with every change of the form. Now when add_comment is handled and the changeset is updated, the form fields are not overwritten as empty.
def handle_event("validate", %{"post" => params}, socket) do
post_changeset = %Post{}
|> Example.change_post(params)
{:noreply, assign(socket, post_changeset: post_changeset)}
end
PostLive.Edit is similarly written to PostLive.New.
Test It!
To start your Phoenix server:
- Install dependencies with
mix deps.get - Create and migrate your database with
mix ecto.setup - Install Node.js dependencies with
cd assets && npm install - Start Phoenix endpoint with
mix phx.server
Now you can visit localhost:4000/posts from your browser.