64
lib/mv_web/live/components/sort_header_component.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
defmodule MvWeb.Components.SortHeaderComponent do
|
||||
@moduledoc """
|
||||
Sort Header that can be used as column header and sorts a table:
|
||||
Props:
|
||||
- field: atom() # Ash‑Field for sorting
|
||||
|
carla marked this conversation as resolved
Outdated
|
||||
- label: string() # Column Heading (can be aan heex templyte)
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
typo typo `templyte`
|
||||
- sort_field: atom() | nil # current sort-field from parent liveview
|
||||
- sort_order: :asc | :desc | nil # current sorting order
|
||||
"""
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok, assign(socket, assigns)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
aria-sort={aria_sort(@field, @sort_field, @sort_order)}
|
||||
class="btn btn-ghost select-none"
|
||||
phx-click="sort"
|
||||
phx-value-field={@field}
|
||||
phx-target={@myself}
|
||||
>
|
||||
{@label}
|
||||
<%= if @sort_field == @field do %>
|
||||
<.icon name={if @sort_order == :asc, do: "hero-chevron-up", else: "hero-chevron-down"} />
|
||||
<% else %>
|
||||
<.icon name="hero-chevron-up-down" class="opacity-40" />
|
||||
<% end %>
|
||||
|
||||
<!-- Screen Reader Info -->
|
||||
<span class="sr-only">
|
||||
({if @sort_field == @field do
|
||||
if @sort_order == :asc, do: gettext("ascending"), else: gettext("descending")
|
||||
else
|
||||
gettext("not sorted")
|
||||
end})
|
||||
</span>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||
send(self(), {:sort, field_str})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
carla marked this conversation as resolved
Outdated
moritz
commented
ambiguous Unicode character - in comment. ambiguous Unicode character - in comment.
|
||||
|
||||
# -------------------------------------------------
|
||||
# Hilfsfunktionen für ARIA‑Attribute & Icon‑SVG
|
||||
# -------------------------------------------------
|
||||
defp aria_sort(field, sort_field, dir) when field == sort_field do
|
||||
case dir do
|
||||
:asc -> "ascending"
|
||||
:desc -> "descending"
|
||||
end
|
||||
end
|
||||
|
||||
defp aria_sort(_, _, _), do: "none"
|
||||
end
|
||||
|
|
@ -5,18 +5,18 @@ defmodule MvWeb.MemberLive.Index do
|
|||
import MvWeb.TableComponents
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
members = Ash.read!(Mv.Membership.Member)
|
||||
sorted = Enum.sort_by(members, & &1.first_name)
|
||||
|
||||
{:ok,
|
||||
def mount(params, _session, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, gettext("Members"))
|
||||
|> assign(:query, "")
|
||||
|> assign(:sort_field, :first_name)
|
||||
|> assign(:sort_order, :asc)
|
||||
|> assign(:members, sorted)
|
||||
|> assign(:selected_members, [])}
|
||||
|> assign_new(:sort_field, fn -> :first_name end)
|
||||
|> assign_new(:sort_order, fn -> :asc end)
|
||||
|> assign(:selected_members, [])
|
||||
|
||||
# We call handle params to use the query from the URL
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html
"The life-cycle is: mount/3 -> handle_params/3 -> render/1"
Therefore handle_params is called twice, in your mount/3 and afterwards by the LiveView. Each handdle_params loads all the members. Is this line necessary at all? If I just remove it it still works for me.
|
||||
{:noreply, socket} = handle_params(params, nil, socket)
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
|
|
@ -45,6 +45,11 @@ defmodule MvWeb.MemberLive.Index do
|
|||
# Handle Events
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Handle Events
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# Delete a member
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
member = Ash.get!(Mv.Membership.Member, id)
|
||||
|
|
@ -67,32 +72,7 @@ defmodule MvWeb.MemberLive.Index do
|
|||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
end
|
||||
|
||||
# Sorts the list of members according to a field, when you click on the column header
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||
members = socket.assigns.members
|
||||
field = String.to_existing_atom(field_str)
|
||||
|
||||
new_order =
|
||||
if socket.assigns.sort_field == field do
|
||||
toggle_order(socket.assigns.sort_order)
|
||||
else
|
||||
:asc
|
||||
end
|
||||
|
||||
sorted_members =
|
||||
members
|
||||
|> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order))
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:sort_field, field)
|
||||
|> assign(:sort_order, new_order)
|
||||
|> assign(:members, sorted_members)}
|
||||
end
|
||||
|
||||
# Selects all members in the list of members
|
||||
|
||||
@impl true
|
||||
def handle_event("select_all", _params, socket) do
|
||||
members = socket.assigns.members
|
||||
|
|
@ -109,8 +89,123 @@ defmodule MvWeb.MemberLive.Index do
|
|||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Handle Infos from Child Components
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
# Sorts the list of members according to a field, when you click on the column header
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
Best practice: instead of hardcoded paths, render path dynamically: Also the Best practice: instead of hardcoded paths, render path dynamically:
```
current_path = socket.view.__live__() |> elem(0)
new_path = current_path <> "?" <> URI.encode_query(query_params)
```
Also the `~p` sigil could be used for creating a path.
carla
commented
I tried that already before and did not find a simple solution for getting the current path. So I would keep that static for now. I tried that already before and did not find a simple solution for getting the current path. So I would keep that static for now.
|
||||
@impl true
|
||||
def handle_info({:sort, field_str}, socket) do
|
||||
field = String.to_existing_atom(field_str)
|
||||
|
||||
{new_order, new_field} =
|
||||
if socket.assigns.sort_field == field do
|
||||
{toggle_order(socket.assigns.sort_order), field}
|
||||
else
|
||||
{:asc, field}
|
||||
end
|
||||
|
||||
active_id = :"sort_#{new_field}"
|
||||
|
||||
|
moritz marked this conversation as resolved
Outdated
moritz
commented
This function ignores the sorting. This leads to reset the sorting while searching. This function ignores the sorting. This leads to reset the sorting while searching.
To avoid duplicate code I would recommend to integrate the search logic into `load_members`.
|
||||
# Update the SortHeader to
|
||||
send_update(MvWeb.Components.SortHeaderComponent,
|
||||
id: active_id,
|
||||
sort_field: new_field,
|
||||
sort_order: new_order
|
||||
)
|
||||
|
||||
# Build the URL with queries
|
||||
query_params = %{
|
||||
"sort_field" => Atom.to_string(new_field),
|
||||
"sort_order" => Atom.to_string(new_order)
|
||||
}
|
||||
|
||||
# "/members" is the path you defined in router.ex
|
||||
new_path = "/members?" <> URI.encode_query(query_params)
|
||||
|
||||
# Push the new URL
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: new_path,
|
||||
# replace true
|
||||
replace: true
|
||||
)}
|
||||
end
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Handle Params from the URL
|
||||
# -----------------------------------------------------------------
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> maybe_update_sort(params)
|
||||
|> load_members()
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# FUNCTIONS
|
||||
# -------------------------------------------------------------
|
||||
# Load members eg based on a query for sorting
|
||||
defp load_members(socket) do
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> Ash.Query.new()
|
||||
|> Ash.Query.select([
|
||||
:id,
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code,
|
||||
:city,
|
||||
:phone_number,
|
||||
:join_date
|
||||
])
|
||||
|> maybe_sort(socket.assigns.sort_field, socket.assigns.sort_order)
|
||||
|
||||
members = Ash.read!(query)
|
||||
assign(socket, :members, members)
|
||||
end
|
||||
|
||||
defp maybe_update_sort(socket, %{"sort_field" => sf, "sort_order" => so}) do
|
||||
field =
|
||||
try do
|
||||
String.to_existing_atom(sf)
|
||||
rescue
|
||||
ArgumentError -> socket.assigns.sort_field
|
||||
end
|
||||
|
||||
order = if so in ["asc", "desc"], do: String.to_atom(so), else: socket.assigns.sort_order
|
||||
|
||||
IO.inspect(order)
|
||||
|
||||
socket
|
||||
|> assign(:sort_field, field)
|
||||
|> assign(:sort_order, order)
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Helper Functions
|
||||
# -------------------------------------------------------------
|
||||
|
||||
# Functions to toggle sorting order
|
||||
defp toggle_order(:asc), do: :desc
|
||||
defp toggle_order(:desc), do: :asc
|
||||
defp sort_fun(:asc), do: &<=/2
|
||||
defp sort_fun(:desc), do: &>=/2
|
||||
defp toggle_order(nil), do: :asc
|
||||
|
||||
# Function to turn a string into an atom only if it already exists
|
||||
defp maybe_atom(nil), do: nil
|
||||
defp maybe_atom(atom) when is_atom(atom), do: atom
|
||||
defp maybe_atom(str) when is_binary(str), do: String.to_existing_atom(str)
|
||||
|
||||
# Function to sort the column if needed
|
||||
defp maybe_sort(query, nil, _), do: query
|
||||
defp maybe_sort(query, field, :asc), do: Ash.Query.sort(query, [{field, :asc}])
|
||||
defp maybe_sort(query, field, :desc), do: Ash.Query.sort(query, [{field, :desc}])
|
||||
# no changes
|
||||
defp maybe_update_sort(socket, _), do: socket
|
||||
end
|
||||
|
|
|
|||
|
|
@ -52,23 +52,139 @@
|
|||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
sort_button(%{
|
||||
field: :first_name,
|
||||
label: gettext("Name"),
|
||||
sort_field: @sort_field,
|
||||
sort_order: @sort_order
|
||||
})
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_first_name}
|
||||
field={:first_name}
|
||||
label={gettext("First name")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.first_name} {member.last_name}
|
||||
</:col>
|
||||
<:col :let={member} label={gettext("Email")}>{member.email}</:col>
|
||||
<:col :let={member} label={gettext("Street")}>{member.street}</:col>
|
||||
<:col :let={member} label={gettext("House Number")}>{member.house_number}</:col>
|
||||
<:col :let={member} label={gettext("Postal Code")}>{member.postal_code}</:col>
|
||||
<:col :let={member} label={gettext("City")}>{member.city}</:col>
|
||||
<:col :let={member} label={gettext("Phone Number")}>{member.phone_number}</:col>
|
||||
<:col :let={member} label={gettext("Join Date")}>{member.join_date}</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_email}
|
||||
field={:email}
|
||||
label={gettext("Email")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.email}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_street}
|
||||
field={:street}
|
||||
label={gettext("Street")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.street}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_house_number}
|
||||
field={:house_number}
|
||||
label={gettext("House Number")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.house_number}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_postal_code}
|
||||
field={:postal_code}
|
||||
label={gettext("Postal Code")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.postal_code}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_city}
|
||||
field={:city}
|
||||
label={gettext("City")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.city}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_phone_number}
|
||||
field={:phone_number}
|
||||
label={gettext("Phone Number")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.phone_number}
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.live_component
|
||||
module={MvWeb.Components.SortHeaderComponent}
|
||||
id={:sort_join_date}
|
||||
field={:join_date}
|
||||
label={gettext("Join Date")}
|
||||
sort_field={@sort_field}
|
||||
sort_order={@sort_order}
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
{member.join_date}
|
||||
</:col>
|
||||
|
||||
<:action :let={member}>
|
||||
<div class="sr-only">
|
||||
|
|
|
|||
ambiguous Unicode character
-in comment.