In this article we talk about syncing LiveView state with URL. Fly.io is a great place to run your Phoenix LiveView applications! Check out how to get started!
Problem
Say you have a simple posts search form LiveView like this one:
Whenever users change the title or author on the search form you perform the search just fine but whenever you hit the refresh button your filters are emptied, poof. We did not persist those in any way, that’s why.
How can we persist our filters so that a page refresh would not make us have to do it all over again?
Solution
We will use a simple trick to sync your URL query string with you LiveView filters using nothing but push_patch/2
and handle_params/3
.
The initial state
It’s very likely you used Phoenix’s generators so your code must look almost like this on your index.ex
:
defmodule FormUrlRecipeWeb.PostLive.Index do
use FormUrlRecipeWeb, :live_view
alias FormUrlRecipe.Blog
alias FormUrlRecipe.Blog.Post
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, posts: [], authors: [])}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
# Other actions omitted here, lets focus on the list
defp apply_action(socket, :index, _params) do
changeset = Blog.change_post(%Post{})
socket
|> assign(:page_title, "Listing Posts")
|> assign(:post, nil)
|> assign(:posts, Blog.search_posts(%{}))
|> assign(:authors, Blog.list_authors())
|> assign(:changeset, changeset)
end
@impl true
def handle_event("search_posts", %{"post" => attrs}, socket) do
changeset = Blog.change_post(%Post{}, attrs)
{:noreply,
socket
|> assign(:changeset, changeset)
|> assign(:posts, Blog.search_posts(attrs))
}
end
# ...
end
As you can see there’s two places who search for posts using Blog.search_posts
. The first is inside apply_action/3
which runs from handle_params/3
, those happen when you open the page, and the other one is inside the handle_event("search_posts", ...)
which is triggered from our form component.
We can simplify this code and centralize where posts are loaded from on apply_action/3
by simply making our handle_event/3
just send you back to the same LiveView through push_patch/3
like this:
@impl true
def handle_event("search_posts", %{"post" => attrs}, socket) do
{:noreply,
socket
|> push_patch(to: Routes.post_index_path(socket, :index, attrs))
}
end
Assume we change the author to Michael, you will be sent to /posts?author=Michael&title=
. Now we just need to start parsing the URL into a attributes we can fill our changeset and our search function:
defp apply_action(socket, :index, params) do
# Changed these two lines below
attrs = Map.take(params, ["title", "author"])
changeset = Blog.change_post(%Post{}, attrs)
socket
|> assign(:page_title, "Listing Posts")
|> assign(:post, nil)
# Changed this line below
|> assign(:posts, Blog.search_posts(attrs))
|> assign(:authors, Blog.list_authors())
|> assign(:changeset, changeset)
end
And that’s it! We now sync URL with our forms plus we refactored our LiveView to deduplicate the search being run. Don’t believe me? Here’s the commit URL.
Notes
It’s worth mentioning Phoenix will understand any empty field as empty string so your params might look like %{"title" => "", "author" => "Lubien"}
. For my search functionality to work the way I wanted I’ve filtered any nil
or ""
inside the Blog.search_posts function.