How to update URL params in a Phoenix LiveView
I was working on a project that displayed data tables that need to be filtered in a number of different ways. I added a filter toggle to make it easy to select a state to filter the table by and then wanted a way to update the URL with the selected filter state. Updating the URL is important because then the page can be refreshed or shared with the filter(s) in tact. Alongside the filter toggle, I also had a search field that allowed the user to search for values in the data. Both needed to work together, search and state filtering.
I first reached for JS.patch/1
which seemed to be what I needed, but it requires the full path to be passed in and doesn’t have a mechanism for manipulating the query string parameters. This makes it a bit more difficult to build multiple independent components that don’t have knowledge of the page they’re operating in. Also, I needed a way to add or update a single query parameter regardless of how many other parameters are in the URL.
Leaning on how Phoenix handles javascript events, I added my own click handler
at-click
and then added custom attributes at-param
and at-value
. The at-
prefix is just my initials and borrows from the way Phoenix uses the
phx-
prefix for its attributes.
This now allows for the following code:
<button at-click="update-param" at-param="state" at-value="active">Update param</button>
The Javascript event handler
This is where most of the magic happens. A global event listener listens for clicks and checks if the click target has the required at-click
, at-param
, and
at-value
attributes. If it does, then it manipulates the URL querystring to add or edit the parameter and sends the new URL via the livesocket so it can be processed by handle_params/3
within the LiveView.
I also added support for list/array parameters by checking if the parameter name ends in []
. If it does, then the value is added to the list. This is useful for tags or other multi-select fields.
Paste this into your app.js
file just below other window.addEventListener
handlers.
// Handle at-click events
// at- is a prefix for custom attributes (like phx- is for Phoenix attributes)
window.addEventListener('click', (event) => {
const target = event.target;
const click = target.getAttribute('at-click');
switch (click) {
case 'update-param': {
const param = target.getAttribute('at-param');
const value = target.getAttribute('at-value');
// Must have both param and value to update the URL
if (!param || !value) {
return;
}
const queryString = new URLSearchParams(window.location.search);
// If a list parameter, append the value to the list
if (param.endsWith('[]')) {
let values = queryString.getAll(param);
values.push(value);
values = values.filter((v, i, a) => a.indexOf(v) === i);
queryString.delete(param);
values.forEach((v) => queryString.append(param, v));
} else {
// Otherwise, set the value
queryString.set(param, value);
}
// Update the url with the new query string
const url = new URL(window.location);
url.search = queryString.toString();
// Push the new URL which will invoke handle_params/3 in the LiveView
liveSocket.pushHistoryPatch(url.toString(), 'push');
}
default:
// no-op
undefined;
}
});
A live example
This entire post is an active LiveView that demonstrates the filter toggle and tag list components. The code for the components and the handle_params code is below.
Click on a filter state to see the state
parameter update in the URL (the default state is "active").
ID | Name | State |
---|---|---|
2 | Lysandra | active |
5 | Thalassa | active |
9 | Elowen | active |
Click on the available tags and see the tags[]
parameters added to the URL and the tags list grow.
The LiveView components
@doc """
## Example:
<.filter_toggle param="state" value={@state}>
<:button value="all">All</:button>
<:button value="pending">Pending</:button>
<:button value="active">Active</:button>
<:button value="inactive">Inactive</:button>
<:button value="deleted">Deleted</:button>
</.filter_toggle>
"""
attr :param, :string, required: true, doc: "the parameter to update"
attr :value, :string, required: true, doc: "the current value of the parameter"
attr :class, :string, default: nil
slot :button, required: true do
attr :value, :string, required: true, doc: "the value to set the parameter to"
end
def filter_toggle(assigns) do
~H"""
<div class={[
"inline-flex overflow-hidden text-xs border rounded-full filter-toggle whitespace-nowrap",
@class
]}>
<button
:for={button <- @button}
type="button"
class={["px-3 py-2", if(@value == button[:value], do: "bg-blue-200")]}
at-click="update-param"
at-value={button[:value]}
at-param={@param}
>
<%= render_slot button %>
</button>
</div>
"""
end
@doc """
## Example:
<.tag_list param="tag[]" tags={@tags} available_tags={~w(elixir phoenix liveview javascript)} />
"""
attr :param, :string, required: true, doc: "the parameter to update"
attr :tags, :list, required: true, doc: "the current selected tags"
attr :available_tags, :list, required: true, doc: "the avaiable tags to select"
def tag_list(assigns) do
~H"""
<div class="flex gap-2">
Selected tags:
<div class="flex gap-1">
<span :for={tag <- @tags} class="px-2 py-1 text-xs bg-blue-200 border border-blue-400 rounded">
<%= tag %>
</span>
</div>
</div>
<div class="text-sm">
Select tags:
<button
:for={tag <- @available_tags}
type="button"
at-click="update-param"
at-param="tags[]"
at-value={tag}
class="px-2 py-1 text-white bg-gray-500 border rounded"
>
<%= tag %>
</button>
</div>
"""
end
The LiveView handle_params function
This adds the parameter values to the socket assigns with default values when no parameter exists.
def handle_params(params, _uri, socket) do
{:noreply,
assign(socket,
state: Map.get(params, "state", "active"),
tags: Map.get(params, "tags", [])
)}
end
Conclusion
I hope this was useful to you. If anything in this post isn’t clear, please send me an email and I’ll do my best to help—and update the post from the feedback ;-).