Skip to content

Instantly share code, notes, and snippets.

@0xGGGGG
Created September 11, 2022 08:10
Show Gist options
  • Save 0xGGGGG/c41e22d306d1675b87dcc77badc24d68 to your computer and use it in GitHub Desktop.
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
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