Created
September 11, 2022 08:10
-
-
Save 0xGGGGG/c41e22d306d1675b87dcc77badc24d68 to your computer and use it in GitHub Desktop.
Small utility classes to work css for the new function component API in Phoenix LiveView >= 0.18
This file contains hidden or 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 UI.Helpers do | |
@moduledoc """ | |
Provides common helpers and js commands to work with | |
UI components. | |
""" | |
@doc """ | |
Utility function that pipes into `assigns` and | |
helps with class concatenation inside live_views | |
and heex components. | |
Accepts nested `class_list_or_str`, reduces it to | |
a class list string and assigns it under the given | |
`class_key` inside the `assigns`. | |
Prepends the reduced class list string to | |
comply with the cascading aspect of CSS. | |
In case where `{predicate :: term(), class}` is passed as a part of the argument, | |
it only adds the `class` if `predicate` is a truthy value. | |
This helps with conditional styling inside the components. | |
Essentially, it emulates the idea from notorious | |
[classnames](https://www.npmjs.com/package/classnames) package | |
that has become a very common pattern in component based frontend frameworks. | |
### Examples | |
iex> concat_class(%{}, :class, "mb-3 px-1 text-white") | |
%{class: "mb-3 px-1 text-white"} | |
iex> concat_class(%{}, "mb-3") | |
%{class: "mb-3"} | |
iex> concat_class(%{class: "bg-white"}, :class, "") | |
%{class: "bg-white"} | |
iex> concat_class(%{class: "text-primary"}, :class, "text-white mb-3") | |
%{class: "text-white mb-3 text-primary"} | |
iex> concat_class(%{class: "bg-primary"}, :other_class, "bg-white") | |
%{class: "bg-primary", other_class: "bg-white"} | |
iex> concat_class(%{class: "bg-white p-5"}, ["m-3", "text-primary"]) | |
%{class: "m-3 text-primary bg-white p-5"} | |
iex> concat_class(%{class: "m-5"}, [{nil, "invisible"}, "p-5", {true, "visible"}, {:truthy, "focus:visible"}]) | |
%{class: "p-5 visible focus:visible m-5"} | |
""" | |
@type concatable :: | |
String.t() | |
| {predicate :: term(), String.t()} | |
| list(String.t() | {predicate :: term(), String.t()}) | |
@spec concat_class( | |
assigns :: map(), | |
class_key :: atom(), | |
class_list_or_str :: concatable() | |
) :: | |
map() | |
def concat_class(assigns, class_key \\ :class, class_list_or_str) do | |
assigned_class = Map.get(assigns, class_key) | |
class = Enum.reduce([class_list_or_str, assigned_class], "", &do_concat_class/2) | |
Map.put(assigns, class_key, class) | |
end | |
defp do_concat_class(class, "") when is_binary(class), | |
do: class | |
defp do_concat_class("", accumulated_class), | |
do: accumulated_class | |
defp do_concat_class(class, accumulated_class) when is_binary(class), | |
do: accumulated_class <> " " <> class | |
defp do_concat_class({false, _class}, accumulated_class), | |
do: accumulated_class | |
defp do_concat_class({nil, _class}, accumulated_class), | |
do: accumulated_class | |
defp do_concat_class({_predicate, class}, accumulated_class), | |
do: do_concat_class(class, accumulated_class) | |
defp do_concat_class(class, accumulated_class) when is_list(class) do | |
Enum.reduce(class, accumulated_class, &do_concat_class/2) | |
end | |
defp do_concat_class(_class, accumulated_class), | |
do: accumulated_class | |
@doc ~s''' | |
Utility function that pipes into `assigns` and | |
helps with concatenation of preset classes. | |
Presets are a way of bundling different styling aspects of a component | |
under a defined preset key e.g. `:xl`, `:lg`, `:naked`, `:rounded`, `:primary` etc. | |
This function requires a `class` attr to be set in the `assigns` | |
and is usable alongside with `concat_class`. | |
## How to use? | |
Inside your component, define your presets, and add them by | |
piping `concat_presets` into your assigns: | |
```ex | |
@presets %{ | |
sm: "text-sm", | |
md: "text-md font-semibold", | |
lg: "text-lg font-bold", | |
rounded: "border rounded-md", | |
} | |
attr :class, :string | |
attr :presets, :list | |
def my_component(assigns) do | |
assigns = concat_presets(assigns, @presets) | |
~H""" | |
<div class={@class}></div> | |
""" | |
end | |
``` | |
Then pass `presets` attr to your component: | |
```heex | |
<.my_component presets={[:lg, :rounded]} /> | |
``` | |
As a result, `class` attribute will have concatenated css classes as follows: | |
``` | |
assigns.class == "text-lg font-bold border rounded-md" | |
``` | |
# Examples | |
iex> concat_presets(%{presets: [:primary, :xl]}, %{rounded: "rounded", xl: "font-xl p-6", primary: "text-white bg-pink-600"}) | |
%{presets: [:primary, :xl], class: "text-white bg-pink-600 font-xl p-6"} | |
iex> concat_presets(%{presets: [:rounded]}, %{rounded: "rounded", xl: "font-xl p-6", primary: "text-white bg-pink-600"}) | |
%{presets: [:rounded], class: "rounded"} | |
iex> concat_presets(%{presets: []}, %{rounded: "rounded", xl: "font-xl p-6", primary: "text-white bg-pink-600"}) | |
%{presets: [], class: ""} | |
iex> concat_presets(%{presets: [:primary]}, %{rounded: "rounded", xl: "font-xl p-6"}) | |
%{presets: [:primary], class: ""} | |
iex> concat_presets(%{presets: [:primary]}, %{}) | |
%{presets: [:primary], class: ""} | |
''' | |
@spec concat_presets(assigns :: map(), presets :: map()) :: map() | |
def concat_presets(%{presets: given_presets} = assigns, presets) | |
when is_list(given_presets) and is_map(presets) do | |
preset_classes = | |
for preset when is_map_key(presets, preset) <- assigns.presets do | |
Map.get(presets, preset) | |
end | |
concat_class(assigns, preset_classes) | |
end | |
def concat_presets(_assigns, _presets) do | |
raise ArgumentError, """ | |
`assigns` passed to `concat_presets` does not have the required `:presets` key. | |
Please make sure you are passing `:presets` list to your `assigns` in all cases. | |
Either by (Preferred): | |
`attr :presets, :list, default: []` | |
or by: | |
`assign_new(assigns, :presets, fn -> [] end)`. | |
""" | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment