Skip to content

Instantly share code, notes, and snippets.

@cblavier
Last active April 28, 2021 17:37
Show Gist options
  • Save cblavier/52a4ea88cde6069f079add93f0a3600e to your computer and use it in GitHub Desktop.
Save cblavier/52a4ea88cde6069f079add93f0a3600e to your computer and use it in GitHub Desktop.
Table live_component with phx_component_helpers
= live_component @socket, Table,
headers: [id: "ID", last_name: "Nom et email", actions: ""],
sortable: [:id, :last_name],
selectable: :checkbox,
mobile_hidden: [:id, :actions],
current_sorting: @table_sorting,
row_count: @user_count,
selected_row_count: @selected_user_count,
sticky_header: true do
= for user <- @users do
= live_component @socket, TableRow, id: "user-table-row-#{user.id}",
select_id: user.id, selectable: @selectable, selected: user.selected do
= live_component @socket, TableCell, class: "hidden md:table-cell" do
= user.id
= live_component @socket, TableCell do
.text-default-txt= "#{user.first_name} #{user.last_name}"
.text-sm.text-default-txt-informative= user.email
= live_component @socket, TableButtonCell, class: "hidden md:table-cell" do
= live_component @socket, TableButton, icon: "fas fa-trash text-red-400",
phx_click: "delete_user", phx_value_user_id: "#{user.id}",
confirm: "Confirmer la suppression ?"
defmodule Storybook.Components.Table do
use Storybook, :live_component
alias Storybook.Components.IllustrationPlaceholder
@default_wrapper_class "grid gap-y-2 md:px-2 pt-2"
@default_table_class "rounded-md shadow border-b border-default-border min-w-full divide-y divide-default-border"
@default_thead_class "bg-default-bg"
@default_selection_input_class "table-select-all h-4 w-4 mt-0.5 cursor-pointer text-primary-txt rounded border-default-border focus:ring-primary-btn-focus"
@default_th_class "bg-default-bg px-2 md:px-6 py-3 text-left text-xs font-medium text-default-txt uppercase tracking-wider"
@default_tbody_class ""
def update(assigns, socket) do
assigns =
assigns
|> extend_class(@default_table_class)
|> extend_class(@default_wrapper_class, attribute: :wrapper_class)
|> extend_class(@default_thead_class, attribute: :thead_class)
|> extend_class(@default_selection_input_class, attribute: :selection_input_class)
|> extend_class(&default_th_class/1, attribute: :th_class)
|> extend_class(@default_tbody_class, attribute: :tbody_class)
|> set_attributes([
phx_sort_event: "sort_table",
phx_select_all_event: "select_all_table",
placeholder_name: :empty,
placeholder_text: "Aucun résultat",
id: "table-component"
)
|> set_headers()
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~L"""
<%= if assigns[:row_count] == 0 do %>
<%= live_component @socket, IllustrationPlaceholder, forward_assigns(assigns, prefix: :placeholder) %>
<% else %>
<div <%= @raw_wrapper_class %> <%= @raw_id %> phx-hook="TableHook">
<table <%= @raw_class %>>
<%= if Enum.any?(@headers) do %>
<thead <%= @raw_thead_class%>>
<tr>
<%= for header <- @headers do %>
<th scope="col" class="<%= th_class(header, assigns)%>" colspan="<%= th_colspan(header, assigns) %>">
<%= render_header(header, assigns) %>
</th>
<% end %>
</tr>
</thead>
<% end %>
<tbody <%= @tbody_class%>>
<%= render_block(@inner_block, selectable: assigns[:selectable]) %>
</tbody>
</table>
</div>
<% end %>
"""
end
defp default_th_class(assigns) do
if assigns[:sticky_header] do
@default_th_class <> " sticky -top-2"
else
@default_th_class
end
end
defp th_class(:select_all, assigns), do: assigns[:th_class]
defp th_class({name, _label}, assigns) do
if assigns[:mobile_hidden] && Enum.member?(assigns[:mobile_hidden], name) do
assigns[:th_class] <> " hidden md:table-cell"
else
assigns[:th_class]
end
end
defp th_colspan(:select_all, assigns) do
if length(assigns.headers) == 1, do: 100, else: 1
end
defp th_colspan(_, _assigns), do: 1
defp render_header(:select_all, assigns) do
selected_status =
cond do
assigns[:selected_row_count] == 0 -> "unchecked"
assigns[:selected_row_count] == assigns[:row_count] -> "checked"
true -> "indeterminate"
end
~L"""
<input type="checkbox" <%= @raw_selection_input_class %> phx-click="<%= @phx_select_all_event %>"
data-checked="<%= selected_status %>">
</input>
"""
end
defp render_header({name, label}, assigns) do
~L"""
<span><%= label %><span>
<%= render_header_sorting_status(name, assigns) %>
"""
end
defp render_header_sorting_status(name, assigns) do
if assigns[:sortable] && Enum.member?(assigns[:sortable], name) do
sorting = assigns[:current_sorting]
cond do
sorting && sorting[name] == :asc ->
render_sort_icon("fa-sort-up", name, "desc", assigns)
sorting && sorting[name] == :desc ->
render_sort_icon("fa-sort-down", name, "none", assigns)
true ->
render_sort_icon("fa-sort", name, "asc", assigns)
end
else
""
end
end
defp render_sort_icon(icon, name, sort_order, assigns) do
~L"""
<i class="fas <%= icon %> cursor-pointer text-default-txt hover:text-default-txt-hover"
phx-click="<%= @phx_sort_event %>"
phx-value-attribute="<%= name %>"
phx-value-order="<%= sort_order %>"
</i>
"""
end
defp set_headers(assigns) do
headers = Map.get(assigns, :headers, [])
headers =
if assigns[:selectable] == :checkbox do
[:select_all | headers]
else
headers
end
Map.put(assigns, :headers, headers)
end
end
defmodule Storybook.Components.Table.TableButton do
use Storybook, :live_component
def update(assigns, socket) do
assigns =
assigns
|> extend_class(
"-ml-px relative inline-flex items-center px-2 py-2 first:rounded-l-md last:rounded-r-md\
text-sm font-medium text-default-txt-secondary hover:bg-primary-bg-hover"
)
|> set_phx_attributes()
|> set_attributes([:id, :icon], required: [:icon])
|> set_attributes([:confirm], data: true)
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~L"""
<button <%= @raw_id %> type="button" <%= @raw_class %> <%= @raw_phx_attributes %> <%= @raw_confirm %>>
<i class="<%= @icon %>"></i>
</button>
"""
end
end
defmodule Storybook.Components.Table.TableButtonCell do
use Storybook, :live_component
def update(assigns, socket) do
assigns =
assigns
|> extend_class("text-right w-40 px-2 md:px-4 whitespace-nowrap text-sm font-medium text-default-txt")
|> extend_class("hidden group-hover:inline relative rounded-md", attribute: :button_group_class)
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~L"""
<td <%= @raw_class %>>
<span <%= @raw_button_group_class %>>
<%= render_block(@inner_block) %>
</span>
</td>
"""
end
end
defmodule Storybook.Components.Table.TableCell do
use Storybook, :live_component
def update(assigns, socket) do
assigns =
assigns
|> extend_class("px-2 md:px-6 py-2 md:py-4 whitespace-nowrap text-sm font-medium text-default-txt")
|> set_attributes([:colspan])
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~L"""
<td <%= @raw_class %> <%= @raw_colspan %>>
<%= render_block(@inner_block) %>
</td>
"""
end
end
defmodule Storybook.Components.Table.TableRow do
use Storybook, :live_component
alias Storybook.Components.Table.TableCell
def update(assigns, socket) do
assigns =
assigns
|> extend_class(
"even:bg-default-bg odd:bg-white hover:bg-primary-bg border-l-2 border-transparent\
hover:border-primary flex-none group cursor-pointer"
)
|> extend_class(&default_selection_input_class/1, attribute: :selection_input_class)
|> set_attributes([:id, phx_select_event: "select_row"])
|> set_phx_attributes()
{:ok, assign(socket, assigns)}
end
def render(assigns) do
~L"""
<tr <%= @raw_class %> <%= @raw_id %> <%= @raw_phx_attributes %>>
<%= render_selection_cell(assigns) %>
<%= render_block(@inner_block) %>
</tr>
"""
end
defp render_selection_cell(assigns) do
case assigns[:selectable] do
:checkbox -> render_selection_input(assigns, "checkbox")
:radio -> render_selection_input(assigns, "radio")
_ -> nil
end
end
# setting a useless data attribute (checked should be enough) on input
# otherwise liveview diff ignores the checked update
defp render_selection_input(assigns, type) do
selected = assigns[:selected] == true
raw_checked = if selected, do: {:safe, "checked=\"true\""}, else: ""
~L"""
<%= live_component nil, TableCell do %>
<input type="<%= type %>" <%= @raw_selection_input_class %>
phx-click="<%= @phx_select_event %>" phx-value-id="<%= @select_id %>"
<%= raw_checked %> data-selected="<%= assigns[:selected] %>">
</input>
<% end %>
"""
end
defp default_selection_input_class(assigns) do
class = "h-4 w-4 mt-0.5 cursor-pointer text-primary-txt border-default-border\
focus:ring-primary-btn-focus"
case assigns[:selectable] do
:checkbox -> class <> " rounded"
_ -> class
end
end
end
export const TableHook = {
mounted () {
renderTable(this.el, this)
},
updated () {
renderTable(this.el, this)
}
}
function renderTable (el, hook) {
const selectAllCheckbox = el.querySelector('input.table-select-all')
if (selectAllCheckbox) {
if (selectAllCheckbox.dataset.checked == "checked") {
selectAllCheckbox.checked = true;
} else if (selectAllCheckbox.dataset.checked == "unchecked") {
selectAllCheckbox.checked = false;
} else {
selectAllCheckbox.indeterminate = true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment