Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An example of table pagination, sorting, and filtering using LiveView #20

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions lib/demo/country.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Demo.Country do

def list do
response = HTTPotion.get "https://restcountries.eu/rest/v2/all"
response.body |> Jason.decode!
end

end
132 changes: 132 additions & 0 deletions lib/demo_web/live/table_live.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
defmodule DemoWeb.TableLive do
use Phoenix.LiveView

def render(assigns) do
~L"""
<form phx-change="search"><input type="text" name="query" value="<%= @query %>" placeholder="Search..." /></form>

<table>
<thead>
<tr>
<th phx-click="sort" phx-value="name">
Name <%= sort_order_icon("name", @sort_by, @sort_order) %>
</th>
<th phx-click="sort" phx-value="population">
Population <%= sort_order_icon("population", @sort_by, @sort_order) %>
</th>
<th phx-click="sort" phx-value="region">
Region <%= sort_order_icon("region", @sort_by, @sort_order) %>
</th>
</tr>
</thead>

<tbody>
<%= for row <- rows(assigns) do %>
<tr>
<td><%= row["name"] %></td>
<td><%= row["population"] %></td>
<td><%= row["region"] %></td>
</tr>
<% end %>
</tbody>
</table>

<nav class="float-left">
<%= for page <- (1..number_of_pages(assigns)) do %>
<%= if page == @page do %>
<strong><%= page %></strong>
<% else %>
<a href="#" phx-click="goto-page" phx-value=<%= page %>><%= page %></a>
<% end %>
<% end %>
</nav>

<form phx-change="change-page-size" class="float-right">
<select name="page_size">
<%= for page_size <- [5, 10, 25, 50] do %>
<option value="<%= page_size %>" <%= page_size == @page_size && "selected" || "" %>>
<%= page_size %> per page
</option>
<% end %>
</select>
</form>
"""
end

def mount(_session, socket) do
{:ok, assign(socket, data: Demo.Country.list(), query: nil, sort_by: "name", sort_order: :desc, page: 1, page_size: 10)}
end

def handle_params(params, _url, socket) do
query = params["query"]
sort_by =
case params["sort_by"] do
sort_by when sort_by in ~w(name population region) ->
params["sort_by"]
_ ->
"name"
end
sort_order = params["sort_order"] == "asc" && :asc || :desc
page = String.to_integer(params["page"] || "1")
page_size = String.to_integer(params["page_size"] || "10")
{:noreply, assign(socket, query: query, sort_by: sort_by, sort_order: sort_order, page: page, page_size: page_size)}
end

def handle_event("search", %{"query" => query}, socket) do
{:noreply, redirect_with_attrs(socket, query: query, page: 1)}
end

# When the column that is used for sorting is clicked again, we reverse the sort order
def handle_event("sort", column, %{assigns: %{sort_by: sort_by, sort_order: :asc}} = socket) when column == sort_by do
{:noreply, assign(socket, sort_by: sort_by, sort_order: :desc)}
end
def handle_event("sort", column, %{assigns: %{sort_by: sort_by, sort_order: :desc}} = socket) when column == sort_by do
{:noreply, redirect_with_attrs(socket, sort_by: sort_by, sort_order: :asc)}
end

# A new column has been clicked
def handle_event("sort", column, socket) do
{:noreply, redirect_with_attrs(socket, sort_by: column)}
end

def handle_event("goto-page", page, socket) do
{:noreply, redirect_with_attrs(socket, page: String.to_integer(page))}
end

def handle_event("change-page-size", %{"page_size" => page_size}, socket) do
{:noreply, redirect_with_attrs(socket, page_size: String.to_integer(page_size), page: 1)}
end

defp redirect_with_attrs(socket, attrs) do
query = attrs[:query] || socket.assigns[:query]
sort_by = attrs[:sort_by] || socket.assigns[:sort_by]
sort_order = attrs[:sort_order] || socket.assigns[:sort_order]
page = attrs[:page] || socket.assigns[:page]
page_size = attrs[:page_size] || socket.assigns[:page_size]

live_redirect(socket, to: DemoWeb.Router.Helpers.live_path(socket, __MODULE__, query: query, sort_by: sort_by, sort_order: sort_order, page: page, page_size: page_size))
end

defp rows(%{data: data, query: query, sort_by: sort_by, sort_order: sort_order, page: page, page_size: page_size}) do
data |> filter(query) |> sort(sort_by, sort_order) |> paginate(page, page_size)
end

defp filter(rows, query) do
rows |> Enum.filter(&(String.match?(&1["name"], ~r/#{query}/i)))
end

defp sort(rows, sort_by, :asc), do: rows |> Enum.sort(&(&1[sort_by] > &2[sort_by]))
defp sort(rows, sort_by, :desc), do: rows |> Enum.sort(&(&1[sort_by] <= &2[sort_by]))

defp paginate(rows, page, page_size), do: rows |> Enum.slice((page - 1) * page_size, page_size)


defp number_of_pages(%{data: data, query: query, page_size: page_size}) do
number_of_rows = data |> filter(query) |> length
(number_of_rows / page_size) + 1 |> trunc
end

defp sort_order_icon(column, sort_by, :asc) when column == sort_by, do: "▲"
defp sort_order_icon(column, sort_by, :desc) when column == sort_by, do: "▼"
defp sort_order_icon(_, _, _), do: ""
end
1 change: 1 addition & 0 deletions lib/demo_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defmodule DemoWeb.Router do
live "/users/new", UserLive.New
live "/users/:id", UserLive.Show
live "/users/:id/edit", UserLive.Edit
live "/table", TableLive

resources "/plain/users", UserController
end
Expand Down
1 change: 1 addition & 0 deletions lib/demo_web/templates/page/index.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<li><%= link "Top", to: Routes.live_path(@conn, DemoWeb.TopLive) %></li>
<li><%= link "CRUD Users with Live pagination", to: Routes.live_path(@conn, DemoWeb.UserLive.Index) %></li>
<li><%= link "Presence Example", to: Routes.live_path(@conn, DemoWeb.UserLive.PresenceIndex, "user#{System.unique_integer([:positive])}") %></li>
<li><%= link "Table Example (pagination, sorting, and filtering)", to: Routes.live_path(@conn, DemoWeb.TableLive) %>
</ul>
</article>
</section>
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ defmodule Demo.Mixfile do
{:jason, "~> 1.0"},
{:cowboy, "~> 2.0"},
{:calendar, "~> 0.17.5"},
{:httpotion, "~> 3.1.0"}
]
end

Expand Down
4 changes: 3 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpotion": {:hex, :httpotion, "3.1.2", "50e3e559c2ffe8c8908c97e4ffb01efc1c18e8547cc7ce5dd173c9cf0a573a3b", [:mix], [{:ibrowse, "== 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: false]}], "hexpm"},
"ibrowse": {:hex, :ibrowse, "4.4.0", "2d923325efe0d2cb09b9c6a047b2835a5eda69d8a47ed6ff8bc03628b764e991", [:rebar3], [], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
Expand All @@ -21,7 +23,7 @@
"phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "9b1be2772e7d7886c78dc1039d6961c3f124630c", []},
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view.git", "5cec2cf8b151afb559b1929ea69819b13149451b", []},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down