Skip to content

Instantly share code, notes, and snippets.

@joeesteves
Last active May 6, 2019 13:42
Show Gist options
  • Save joeesteves/372cf186665ab464413096fd3855f0ca to your computer and use it in GitHub Desktop.
Save joeesteves/372cf186665ab464413096fd3855f0ca to your computer and use it in GitHub Desktop.
Selection Panel Live View
<div>
<%= selection_panel(@conn, @f,
[ %{
key: :number,
panels: [
%{
title: "select_category_type",
collection: Mu.Misc.Constants.localized_category_types
}
]
}
]) %>
</div>
<div>
<%= selection_panel(@conn, @f,
[ %{
key: :number,
panels: [
%{
title: "select_event_type",
collection: Mu.Misc.Constants.localized_event_types
}
],
child_selectors: [
%{
key: :constraint_src,
active_when_parent_in: [2,4],
opts: %{exclusive_til: 255},
panels: [
%{
title: "select_categories_src",
collection: Mu.Misc.Constants.localized_category_types,
opts: %{
action: "inclusive_toggle",
color: "rgb(70,100,170)"
}
},
%{
title: "select_stages_src",
collection: Mu.Misc.Constants.localized_stages,
opts: %{
action: "inclusive_toggle",
color: "rgb(70,100,170)",
container_class: "w100 flex wrap"}
}
]
},
%{
key: :constraint_dst,
active_when_parent_in: [1,2],
opts: %{exclusive_til: 255},
panels: [
%{
title: "select_categories_dst",
collection: Mu.Misc.Constants.localized_category_types,
opts: %{action: "inclusive_toggle", color: "rgb(170,70,70)"}
},
%{
title: "select_stages_dst",
collection: Mu.Misc.Constants.localized_stages,
opts: %{action: "inclusive_toggle", color: "rgb(170,70,70)", container_class: "w100 flex wrap"}
}
]
}
]
}
]
)%>
</div>
<div>
<%= for selector <- @selectors do %>
<div class="<%= show_container(get_value(@form, selector[:parent_key]), selector[:active_when_parent_in]) %>">
<%= text_input @form, selector[:key], value: get_value(@form, selector[:key]), readonly: true, hidden: false %>
<%= for %{title: t, collection: c, opts: o} <- selector[:panels] do %>
<h3 class="w100"> <%= t %></h3>
<div class="<%= o[:container_class] %>">
<%= for {name, value} <- c do %>
<div
class="<%= o[:member_class] %> selection-group__member <%= get_value(@form, selector[:key]) |> active(value, ~s[selection-group__member-active]) %>"
style="background-color: <%= o[:color] %>"
phx-click="<%= o[:action] %>"
phx-value="<%= [selector[:key], value] |> Enum.join(~s[,]) %>"
>
<%= name %>
</div>
<% end %>
</div>
<%= error_tag @form, selector[:key] %>
<% end %>
</div>
<% end %>
</div>
defmodule MuWeb.SelectPanelView do
use MuWeb, :view
use Bitwise
@moduledoc """
This module provides a view helper to easily plug a Selection Panel (visuale keypad) for handling bitwise flags options.
It support excluxive, inclusive and mixed selections.
## Selector
Each selector contains:
* key targeting the hidden input field on the form.
* Panels (one ore more): the visual key pads that feed the input
* Child_Selectors: Selectors that are toggle based on parent value
Example of a simple exclusive (default) selection_panel. It shows buttons for the categories in collection (collecionts must be bit flats.. {1, category_1}, {2, cat_2},{4, cat_4})
<div>
<%= selection_panel(@conn, @f,
[ %{
key: :number,
panels: [
%{
title: "select_category_type",
collection: Mu.Misc.Constants.localized_category_types
}
]
}
]) %>
</div>
"""
# exclusive_til defautls to 4 bytes (32 bits)
# This option is for mixed selectors, where one part is exlusive and other is incluive
# Excluive panels must be on top of inclusive ones and pass tht boundaries as exlusive_til.
@default_selector_opts %{
exclusive_til: (1 <<< 32) - 1
}
@default_panel_opts %{
container_class: "w100 flex",
member_class: "w33",
color: "rgb(150, 170, 70)",
action: "exclusive_toggle"
}
# Selectors is a collection of button panels that affect input
def selection_panel(conn, form, selectors) do
selectors = parse_selectors(conn, selectors)
live_render(
conn,
MuWeb.SelectPanelLiveView,
session: [
form: form,
selectors: selectors,
selectors_opts: parse_selectors_opts(selectors)
]
)
end
defp active(target_value, member_value, class) when is_number(target_value) do
((target_value &&& member_value) != 0 && class) || ""
end
defp active(_, _, _), do: false
defp add_parent(child_selectors, parent_key) do
Enum.map(child_selectors, fn child_selector ->
Map.put(child_selector, :parent_key, parent_key)
end)
end
defp flatten_selectors_with_childs(selectors) do
Enum.flat_map(selectors, fn selector ->
List.flatten([
Map.delete(selector, :child_selectors),
(selector[:child_selectors] || []) |> add_parent(selector[:key])
])
end)
end
defp get_value(form, key) do
Ecto.Changeset.get_field(form.source, key)
end
defp merge_defaults(default_opts, opts) do
Map.merge(default_opts, opts)
end
defp parse_panels(panels) do
Enum.map(panels, fn panel ->
panel
|> Map.update!(:title, &translate/1)
|> Map.update(:opts, @default_panel_opts, &merge_defaults(@default_panel_opts, &1))
end)
end
defp parse_selectors(conn, selectors) do
selectors
|> flatten_selectors_with_childs
|> Enum.map(fn selector ->
selector
|> Map.update(:opts, @default_selector_opts, &merge_defaults(@default_selector_opts, &1))
|> Map.update!(:panels, &parse_panels/1)
end)
end
defp parse_selectors_opts(selectors) do
for selector <- selectors, into: %{}, do: {selector[:key], selector[:opts]}
end
defp show_container(_, nil), do: ""
defp show_container(value, active_when) do
(Enum.member?(active_when, value) && "") || "hide"
end
defp translate(string) do
Gettext.dgettext(MuWeb.Gettext, "labels", string)
end
defmacro __using__(opts) do
quote do
import MuWeb.SelectPanelView, only: [selection_panel: 3]
end
end
end
defmodule MuWeb.SelectPanelLiveView do
use Phoenix.LiveView
use Bitwise, only: :operators
def render(assigns) do
MuWeb.SelectPanelView.render("select_panel.html", assigns)
end
def mount(session, socket) do
{:ok,
assign(socket,
form: session[:form],
selectors: session[:selectors],
selectors_opts: session[:selectors_opts]
)}
end
defp decode_member_msg(member_msg) do
[key, value] =
member_msg
|> String.split(",")
{String.to_atom(key), String.to_integer(value)}
end
defp update_exclusibly(form, key, member_value, opts) do
exclusive_til = opts[:exclusive_til]
Map.update!(form, :source, fn prev_source ->
prev_value = Ecto.Changeset.get_field(prev_source, key)
new_value =
(is_nil(prev_value) && member_value) ||
(prev_value &&& ~~~exclusive_til) ||| member_value
Ecto.Changeset.change(prev_source, %{key => new_value})
end)
end
defp update_inclusibly(form, key, member_value) do
Map.update!(form, :source, fn prev_source ->
prev_value = Ecto.Changeset.get_field(prev_source, key)
new_value =
(is_nil(prev_value) && member_value) ||
prev_value ^^^ member_value
Ecto.Changeset.change(prev_source, %{key => new_value})
end)
end
def handle_event("exclusive_toggle", member_msg, socket) do
{key, member_value} = decode_member_msg(member_msg)
opts = socket.assigns.selectors_opts[key]
{
:noreply,
update(socket, :form, &update_exclusibly(&1, key, member_value, opts))
}
end
def handle_event("inclusive_toggle", member_msg, socket) do
{key, member_value} = decode_member_msg(member_msg)
{
:noreply,
update(socket, :form, &update_inclusibly(&1, key, member_value))
}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment