Last active
April 12, 2021 17:49
-
-
Save keatz55/fd015ab6136c059c56fa1243550d11b4 to your computer and use it in GitHub Desktop.
LiveView Query Helper Module
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule ExampleWeb.ArticleLive.Index do | |
alias Example.{Articles, Tags} | |
alias ExampleWeb.{ArticleLive, ComponentLive, Query} | |
use ExampleWeb, :live_view | |
@impl true | |
def render(assigns) do | |
~L""" | |
<div class="container max-w-screen-md mx-auto pt-6"> | |
<!-- Title --> | |
<h1 class="mt-2 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl mb-6 text-center"> | |
Articles | |
</h1> | |
<!-- Text Filter --> | |
<div class="mb-6 shadow sm:rounded-lg"> | |
<%= live_component(@socket, ComponentLive.TextFilter, | |
id: "article-search", | |
param: "s", | |
query: @article_suquery | |
) %> | |
</div> | |
</div> | |
<div class="container max-w-screen-lg mx-auto"> | |
<div class="md:grid md:grid-cols-4 md:gap-6"> | |
<div class="md:col-span-1"> | |
<div class="px-4 sm:px-0"> | |
<!-- Tag Filter --> | |
<%= live_component(@socket, ComponentLive.MultiselectFilter, | |
id: "tags-filter", | |
options: @tag_opts, | |
param: "tags", | |
query: @article_suquery, | |
text_filter: [param: "tag_s"], | |
title: "Tags" | |
) %> | |
</div> | |
</div> | |
<div class="mt-5 md:mt-0 md:col-span-3"> | |
<!-- Article List --> | |
<%= for article <- @page.entries do %> | |
<div class="bg-white border border-gray-100 shadow overflow-hidden sm:rounded-lg mb-6"> | |
<div class="px-6 py-4"> | |
<h2 class="font-bold text-xl mb-2"><%= article.title %></h2> | |
<p class="text-gray-700 text-base"><%= article.description %></p> | |
</div> | |
<div class="px-6 pt-4 pb-2"> | |
<%= for tag <- @tags do %> | |
<%= live_component(@socket, ComponentLive.Chip, text: tag) %> | |
<% end %> | |
</div> | |
</div> | |
<% end %> | |
<!-- Pagination --> | |
<%= live_component(@socket, ComponentLive.Pagination, | |
page: @page, | |
query: @article_suquery | |
) %> | |
<span><%= live_patch "New", to: Routes.live_path(@socket, ArticleLive.New) %></span> | |
</div> | |
</div> | |
</div> | |
""" | |
end | |
@impl true | |
def mount(_params, session, socket), do: {:ok, assign_defaults(socket, session)} | |
@impl true | |
def handle_params(query, uri, socket) do | |
article_subquery = Query.init(query, uri, "articles") | |
{:ok, page} = article_subquery |> Query.to_map() |> Articles.list() | |
{:ok, tags} = article_subquery |> Query.to_map() |> Tags.list() | |
{:noreply, | |
assign(socket, | |
article_suquery: article_subquery, | |
page: page, | |
tags: tags | |
)} | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule ExampleWeb.ComponentLive.MultiselectFilter do | |
@moduledoc """ | |
Enables multi select filtering via URL query params. | |
""" | |
alias ExampleWeb.{ComponentLive, Query} | |
use ExampleWeb, :live_component | |
@default_text_filter_class "bg-gray-50 border-t border-b border-gray-100" | |
@impl true | |
def render(assigns) do | |
~L""" | |
<div | |
x-data={show:<%= @default_expanded? %>} | |
class="bg-white border border-gray-100 shadow overflow-hidden sm:rounded-lg mb-6" | |
> | |
<!-- Header --> | |
<div class="flex py-2 px-3 items-center text-gray-700 font-semibold"> | |
<!-- Title --> | |
<%= @title %> | |
<div class="flex-1"></div> | |
<!-- Clear Btn --> | |
<%= if @clear_btn.show? do %> | |
<%= live_patch( | |
to: @clear_btn.link, | |
class: "text-xs py-1 px-2 border border-gray-400 text-gray-600 flex items-center rounded bg-gray-100" | |
) do %> | |
clear <i class="fas fa-times ml-1"></i> | |
<% end %> | |
<% end %> | |
<!-- Collapse Btn --> | |
<div @click="show=!show" class="py-1 pl-2 cursor-pointer"> | |
<i x-show="!show" class="fas fa-chevron-down"></i> | |
<i x-show="show" class="fas fa-chevron-up"></i> | |
</div> | |
</div> | |
<!-- Content --> | |
<div x-show="show"> | |
<!-- Text Filter --> | |
<%= if assigns[:text_filter] do %> | |
<%= live_component(@socket, ComponentLive.TextFilter, @text_filter) %> | |
<% end %> | |
<!-- Options --> | |
<div | |
class="overflow-y-auto py-1" | |
style="max-height:150px;" | |
> | |
<%= if !Enum.any?(@options) do %> | |
<div class="py-2 px-2 text-gray-700 text-sm flex items-center"> | |
No results to display | |
</div> | |
<% end %> | |
<%= for opt <- @options do %> | |
<%= live_patch( | |
to: opt.link, | |
class: "py-1 px-2 text-gray-700 text-sm flex items-center" | |
) do %> | |
<input | |
class="cursor-pointer mr-3" | |
type="checkbox" | |
<%= if opt.checked?, do: "checked" %> | |
> | |
<%= opt.label %> | |
<% end %> | |
<% end %> | |
</div> | |
</div> | |
</div> | |
""" | |
end | |
@impl true | |
def update(%{options: options, param: param, query: query} = assigns, socket) do | |
selected = query |> Query.get(param, []) |> MapSet.new() | |
{:ok, | |
socket | |
|> assign(assigns) | |
|> assign( | |
clear_btn: get_clear_btn(assigns), | |
default_expanded?: Map.get(assigns, :default_expanded?, true), | |
options: Enum.map(options, &normalize_option(&1, assigns, selected)), | |
text_filter: normalize_text_filter_assigns(assigns) | |
)} | |
end | |
defp get_clear_btn(%{param: param, query: query}) do | |
link = query |> Query.delete([param, "pg"]) |> Query.to_link() | |
%{link: link, show?: Query.has_param?(query, param)} | |
end | |
defp normalize_option(option, assigns, selected) do | |
option |> with_checked?(selected) |> with_link(assigns) | |
end | |
defp with_checked?(option, selected) do | |
checked? = MapSet.member?(selected, option.value) | |
Map.put(option, :checked?, checked?) | |
end | |
defp with_link(option, %{param: param, query: query}) do | |
link = query |> build_option_query(param, option) |> Query.delete("pg") |> Query.to_link() | |
Map.put(option, :link, link) | |
end | |
defp build_option_query(query, param, %{checked?: false, value: value}) do | |
Query.update(query, param, [value], &[value | &1]) | |
end | |
defp build_option_query(query, param, %{checked?: true, value: value}) do | |
Query.update(query, param, [], &List.delete(&1, value)) | |
end | |
defp normalize_text_filter_assigns(%{text_filter: child_assigns} = assigns) do | |
child_assigns |> Map.new() |> with_class() |> with_id(assigns) |> with_query(assigns) | |
end | |
defp with_class(%{class: _} = child_assigns), do: child_assigns | |
defp with_class(child_assigns), do: Map.put(child_assigns, :class, @default_text_filter_class) | |
defp with_id(%{id: _} = child_assigns, _parent_assigns), do: child_assigns | |
defp with_id(child_assigns, %{id: id}), do: Map.put(child_assigns, :id, "#{id}-text-filter") | |
defp with_query(%{query: _} = child_assigns, _parent_assigns), do: child_assigns | |
defp with_query(child_assigns, %{query: query}), do: Map.put(child_assigns, :query, query) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule ExampleWeb.Query do | |
@moduledoc """ | |
Query helper library. | |
""" | |
defstruct data: nil, path: nil, subquery_param: nil | |
@doc """ | |
Initializes a query struct. | |
## Examples | |
iex> init(query, uri) | |
%Query{} | |
iex> init(query, uri, subquery_param) | |
%Query{} | |
""" | |
def init(query, uri), do: %__MODULE__{data: query, path: URI.parse(uri).path} | |
def init(query, uri, subquery_param) do | |
%__MODULE__{data: query, path: URI.parse(uri).path, subquery_param: subquery_param} | |
end | |
@doc """ | |
Returns param from query or subquery data. | |
## Examples | |
iex> get(query, param) | |
value | |
""" | |
def get(%__MODULE__{} = query, param, default \\ nil) do | |
query |> get_data() |> Map.get(param, default) | |
end | |
@doc """ | |
Puts param value in query or subquery data. | |
## Examples | |
iex> put(query, param, value) | |
%Query{} | |
""" | |
def put(%__MODULE__{} = query, param, value) do | |
query |> get_data() |> Map.put(param, value) |> put_data(query) | |
end | |
@doc """ | |
Applies update function on query or subquery data param. | |
## Examples | |
iex> update(query, param, default, fun) | |
%Query{} | |
""" | |
def update(%__MODULE__{} = query, param, default, fun) do | |
query |> get_data() |> Map.update(param, default, fun) |> put_data(query) | |
end | |
@doc """ | |
Deletes param from query or subquery data if exists. | |
## Examples | |
iex> delete(query, param(s)) | |
%Query{} | |
""" | |
def delete(%__MODULE__{} = query, param) when is_bitstring(param), do: delete(query, [param]) | |
def delete(%__MODULE__{} = query, params) do | |
query |> get_data() |> Map.drop(params) |> put_data(query) | |
end | |
@doc """ | |
Checks if param exists in query or subquery data. | |
## Examples | |
iex> has_param?(query, param) | |
true | |
""" | |
def has_param?(%__MODULE__{} = query, param), do: query |> get_data() |> Map.has_key?(param) | |
@doc """ | |
Returns a query-encoded link. | |
## Examples | |
iex> to_link(query) | |
"http://localhost:4000?param=value" | |
""" | |
def to_link(%__MODULE__{data: data, path: path}), do: path <> encode_query(data) | |
defp encode_query(data) do | |
with qs when qs != "" <- Plug.Conn.Query.encode(data) do | |
"?" <> qs | |
end | |
end | |
@doc """ | |
Returns a query or subquery data as a map. | |
## Examples | |
iex> to_map(query) | |
%{} | |
""" | |
def to_map(%__MODULE__{} = query), do: get_data(query) | |
defp get_data(%__MODULE__{data: data, subquery_param: nil}), do: data | |
defp get_data(%__MODULE__{data: data, subquery_param: param}), do: Map.get(data, param, %{}) | |
defp put_data(data, %__MODULE__{subquery_param: nil} = query) do | |
%__MODULE__{query | data: data} | |
end | |
defp put_data(subquery, %__MODULE__{data: data, subquery_param: subquery_param} = query) do | |
%__MODULE__{query | data: Map.put(data, subquery_param, subquery)} | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule ExampleWeb.ComponentLive.TextFilter do | |
@moduledoc """ | |
Enables text based filtering via URL query params. | |
""" | |
alias ExampleWeb.Query | |
alias Phoenix.LiveView.Socket | |
use ExampleWeb, :live_component | |
@default_class "focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm bg-white border border-gray-300 rounded-md" | |
@default_icon "fas fa-search" | |
@default_placeholder "Search" | |
@impl true | |
def render(assigns) do | |
~L""" | |
<div class="<%= @class %>"> | |
<div class="relative text-gray-600 text-sm "> | |
<!-- Icon --> | |
<%= if assigns[:icon] do %> | |
<div class="absolute top-0 bottom-0 left-0 py-0 px-3 flex items-center"> | |
<i class="<%= @icon %>"></i> | |
</div> | |
<% end %> | |
<!-- Form --> | |
<%= f = form_for(:filter, "#", | |
class: "flex-1", | |
phx_change: "filter", | |
phx_submit: "filter", | |
phx_target: @myself | |
) %> | |
<%= text_input(f, :text, | |
phx_debounce: 350, | |
placeholder: @placeholder, | |
class: "w-full py-2 px-10 bg-transparent border-none focus:outline-none focus:shadow-none", | |
value: @value | |
) %> | |
</form> | |
<!-- Clear Btn --> | |
<%= if @clear_btn.show? do %> | |
<%= live_patch( | |
to: @clear_btn.link, | |
class: "absolute top-0 right-0 bottom-0 py-0 px-3 flex items-center" | |
) do %> | |
<i class="fas fa-times"></i> | |
<% end %> | |
<% end %> | |
</div> | |
</div> | |
""" | |
end | |
@impl true | |
def update(%{param: param, query: query} = assigns, socket) do | |
{:ok, | |
socket | |
|> assign(assigns) | |
|> assign( | |
class: Map.get(assigns, :class, @default_class), | |
clear_btn: get_clear_btn(assigns), | |
icon: Map.get(assigns, :icon, @default_icon), | |
placeholder: Map.get(assigns, :placeholder, @default_placeholder), | |
value: Query.get(query, param, "") | |
)} | |
end | |
@impl true | |
def handle_event("filter", %{"filter" => %{"text" => value}}, socket) do | |
apply_filter(String.trim(value), socket) | |
end | |
defp apply_filter(value, %Socket{assigns: %{param: param, query: query}} = socket) do | |
link = query |> update_query(param, value) |> Query.delete("pg") |> Query.to_link() | |
{:noreply, push_patch(socket, to: link)} | |
end | |
defp update_query(query, param, ""), do: Query.delete(query, param) | |
defp update_query(query, param, value), do: Query.put(query, param, value) | |
defp get_clear_btn(%{param: param, query: query}) do | |
link = query |> Query.delete([param, "pg"]) |> Query.to_link() | |
%{link: link, show?: Query.has_param?(query, param)} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment