Current section

Files

Jump to
cinder lib cinder table url_sync.ex
Raw

lib/cinder/table/url_sync.ex

defmodule Cinder.Table.UrlSync do
@moduledoc """
Simple URL synchronization helper for Table components.
This module provides an easy way to enable URL state synchronization
for Table components with minimal setup.
## Usage
1. Add `use Cinder.Table.UrlSync` to your LiveView
2. Call `Cinder.Table.UrlSync.handle_params/3` in your `handle_params/3` callback
3. Pass `url_state={@url_state}` to your Table component
4. That's it! The helper handles all URL updates automatically.
## Example
defmodule MyAppWeb.UsersLive do
use MyAppWeb, :live_view
use Cinder.Table.UrlSync
def mount(_params, _session, socket) do
{:ok, assign(socket, :current_user, get_current_user())}
end
def handle_params(params, uri, socket) do
socket = Cinder.Table.UrlSync.handle_params(params, uri, socket)
{:noreply, socket}
end
def render(assigns) do
~H\"\"\"
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
url_state={@url_state}
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
</Cinder.Table.table>
\"\"\"
end
# Or with a pre-configured query:
def render_with_query(assigns) do
~H\"\"\"
<Cinder.Table.table
query={MyApp.User |> Ash.Query.filter(active: true)}
actor={@current_user}
url_state={@url_state}
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
<:col :let={user} field="email" filter>{user.email}</:col>
</Cinder.Table.table>
\"\"\"
end
end
The helper automatically:
- Handles `:table_state_change` messages from Table components
- Updates the URL with new table state
- Preserves other URL parameters
- Works with any number of Table components on the same page
"""
import Phoenix.Component, only: [assign: 3]
@doc """
Adds URL sync support to a LiveView.
This macro injects the necessary `handle_info/2` callback to handle
table state changes and update the URL accordingly.
"""
defmacro __using__(_opts) do
quote do
@doc """
Handles table state changes and updates the URL.
This function is automatically injected by `use Cinder.Table.UrlSync`.
It handles `:table_state_change` messages from Table components.
"""
def handle_info({:table_state_change, _table_id, encoded_state}, socket) do
current_uri = get_in(socket.assigns, [:url_state, :uri])
{:noreply, Cinder.Table.UrlSync.update_url(socket, encoded_state, current_uri)}
end
end
end
@doc """
Updates the LiveView socket with new URL parameters.
This function preserves the current path and updates only the query parameters
with the new table state.
## Parameters
- `socket` - The LiveView socket
- `encoded_state` - Map of URL parameters from table state
- `current_uri` - Optional current URI string to use for path resolution
## Returns
Updated socket with URL changed via `push_patch/2`
"""
def update_url(socket, encoded_state, current_uri \\ nil) do
# Convert encoded state to string keys and remove empty params
new_params =
encoded_state
|> Enum.map(fn {k, v} -> {to_string(k), v} end)
|> Enum.into(%{})
|> remove_empty_params()
# Get the current path from the URI if provided, otherwise use relative path
if current_uri do
uri = URI.parse(current_uri)
current_path = uri.path || "/"
new_url =
if map_size(new_params) > 0 do
query_string = URI.encode_query(new_params)
"#{current_path}?#{query_string}"
else
current_path
end
Phoenix.LiveView.push_patch(socket, to: new_url)
else
# Fallback: Extract path from socket's url_state or use root
fallback_uri = get_in(socket.assigns, [:url_state, :uri]) || "/"
current_path =
if is_binary(fallback_uri) do
URI.parse(fallback_uri).path || "/"
else
"/"
end
if map_size(new_params) > 0 do
query_string = URI.encode_query(new_params)
Phoenix.LiveView.push_patch(socket, to: "#{current_path}?#{query_string}")
else
# No parameters - clear query string but keep current path
Phoenix.LiveView.push_patch(socket, to: current_path)
end
end
end
@doc """
Extracts complete URL state from URL parameters for use with table components.
This function can be used in `handle_params/3` to initialize
URL state for table components.
## Example
def handle_params(params, _uri, socket) do
url_state = Cinder.Table.UrlSync.extract_url_state(params)
socket = assign(socket, :url_state, url_state)
{:noreply, socket}
end
"""
def extract_url_state(params) when is_map(params) do
# Handle case where sort_by might be an empty list (causing UrlManager error)
safe_params =
Map.update(params, "sort", nil, fn
[] -> nil
# Convert lists to nil
sort when is_list(sort) -> nil
sort -> sort
end)
# Use empty columns list since we don't have column context here
Cinder.UrlManager.decode_state(safe_params, [])
end
@doc """
Extracts table state from URL parameters using empty columns list.
This function provides a simplified extraction that works without column
metadata. It preserves page and sort information but may not fully decode
filters (which is why we also preserve raw params in handle_params).
## Parameters
- `params` - URL parameters map
## Returns
Map with `:filters`, `:current_page`, and `:sort_by` keys
"""
def extract_table_state(params) when is_map(params) do
# Handle case where sort_by might be an empty list (causing UrlManager error)
safe_params =
Map.update(params, "sort", nil, fn
[] -> nil
# Convert lists to nil
sort when is_list(sort) -> nil
sort -> sort
end)
# Use empty columns list - this will preserve page/sort but may lose filter details
# That's why we also preserve raw params in the url_state
Cinder.UrlManager.decode_state(safe_params, [])
end
# Private helper functions
defp remove_empty_params(params) do
params
|> Enum.reject(fn {k, v} ->
is_nil(v) or v == "" or (v == "1" and String.contains?(to_string(k), "page"))
end)
|> Enum.into(%{})
end
@doc """
Helper function to handle table state in LiveView handle_params.
This function extracts URL parameters and creates a URL state object that
can be passed to Table components. It should be called from your LiveView's
`handle_params/3` callback.
## Parameters
- `params` - URL parameters from `handle_params/3`
- `uri` - Current URI from `handle_params/3` (optional but recommended)
- `socket` - The LiveView socket
## Returns
Updated socket with `:url_state` assign containing:
- `filters` - Raw URL parameters for proper filter decoding
- `current_page` - Current page number
- `sort_by` - Sort configuration
- `uri` - Current URI for URL generation
## Example
def handle_params(params, uri, socket) do
socket = Cinder.Table.UrlSync.handle_params(params, uri, socket)
{:noreply, socket}
end
def render(assigns) do
~H\"\"\"
<Cinder.Table.table
resource={MyApp.User}
actor={@current_user}
url_state={@url_state}
>
<:col :let={user} field="name" filter sort>{user.name}</:col>
</Cinder.Table.table>
\"\"\"
end
The `@url_state` assign will be available for use with the Table component.
"""
def handle_params(params, uri \\ nil, socket) do
table_state = extract_table_state(params)
url_state = %{
# Raw params for proper filter decoding
filters: params,
current_page: table_state.current_page,
sort_by: table_state.sort_by,
uri: uri
}
assign(socket, :url_state, url_state)
end
@doc """
Helper to get the URL state for passing to table components.
Use this to get the URL state object created by `handle_params/3`.
## Example
def render(assigns) do
~H\"\"\"
<Cinder.Table.table
resource={Album}
actor={@current_user}
url_state={@url_state}
theme="minimal"
>
<:col :let={album} field="name" filter="text">{album.name}</:col>
<:col :let={album} field="artist.name" filter="text">{album.artist.name}</:col>
</Cinder.Table.table>
\"\"\"
end
The URL state object contains:
- filters: Raw URL parameters for proper filter decoding
- current_page: Current page number
- sort_by: Sort configuration
- uri: Current URI
"""
def get_url_state(socket_assigns) do
Map.get(socket_assigns, :url_state, nil)
end
end