Initial Commit
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
defmodule RiverConnectWeb.Components.UI.Helpers do
|
||||
@moduledoc false
|
||||
use Phoenix.Component
|
||||
|
||||
import Phoenix.Component
|
||||
|
||||
@doc """
|
||||
Prepare input assigns for use in a form. Extract required attribute from the Form.Field struct and update current assigns.
|
||||
"""
|
||||
def prepare_assign(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns
|
||||
|> assign(field: nil, id: assigns[:id] || field.id)
|
||||
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|
||||
|> assign(
|
||||
:name,
|
||||
assigns[:name] || if(assigns[:multiple], do: field.name <> "[]", else: field.name)
|
||||
)
|
||||
|> assign(:value, assigns[:value] || field.value)
|
||||
|> prepare_assign()
|
||||
end
|
||||
|
||||
# use default value if value is not provided or empty
|
||||
def prepare_assign(assigns) do
|
||||
value =
|
||||
if assigns[:value] in [nil, "", []] do
|
||||
assigns[:"default-value"]
|
||||
else
|
||||
assigns[:value]
|
||||
end
|
||||
|
||||
assign(assigns, value: value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function return list of error message from form field
|
||||
"""
|
||||
def field_errors(%Phoenix.HTML.FormField{} = field) do
|
||||
Enum.map(field.errors, &translate_error(&1))
|
||||
end
|
||||
|
||||
def field_errors(_), do: []
|
||||
|
||||
@doc """
|
||||
This function return true if the field has error
|
||||
"""
|
||||
def has_error?(%Phoenix.HTML.FormField{} = field) do
|
||||
not Enum.empty?(field.errors)
|
||||
end
|
||||
|
||||
def has_error?(_field), do: false
|
||||
|
||||
# Helper function to add event mappings
|
||||
def add_event_mapping(map \\ %{}, assigns, event, key) do
|
||||
if assigns[key] do
|
||||
Map.put(map, event, assigns[key])
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
# Helper to encode data to JSON
|
||||
def json(data) do
|
||||
Phoenix.json_library().encode!(data)
|
||||
end
|
||||
|
||||
# normalize_integer
|
||||
def normalize_integer(value) when is_integer(value), do: value
|
||||
|
||||
def normalize_integer(value) when is_binary(value) do
|
||||
case Integer.parse(value) do
|
||||
{integer, _} -> integer
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_integer(_), do: nil
|
||||
|
||||
def normalize_boolean(value) do
|
||||
case value do
|
||||
"true" -> true
|
||||
"false" -> false
|
||||
true -> true
|
||||
false -> false
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Normalize id to be used in HTML id attribute
|
||||
It will replace all non-alphanumeric characters with `-` and downcase the string
|
||||
"""
|
||||
def id(id) do
|
||||
id
|
||||
|> String.replace(~r/[^a-zA-Z0-9]/, "-")
|
||||
|> String.downcase()
|
||||
end
|
||||
|
||||
@variants %{
|
||||
variant: %{
|
||||
"default" => "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
"destructive" =>
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
"outline" =>
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
"secondary" => "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
"ghost" => "hover:bg-accent hover:text-accent-foreground",
|
||||
"link" => "text-primary underline-offset-4 hover:underline"
|
||||
},
|
||||
size: %{
|
||||
"default" => "h-9 px-4 py-2",
|
||||
"sm" => "h-8 rounded-md px-3 text-xs",
|
||||
"lg" => "h-10 rounded-md px-8",
|
||||
"icon" => "h-9 w-9"
|
||||
}
|
||||
}
|
||||
|
||||
@default_variants %{
|
||||
variant: "default",
|
||||
size: "default"
|
||||
}
|
||||
|
||||
@doc """
|
||||
Reuseable button variant helper. Support 2 variant
|
||||
- size: `default|sm|lg|icon`
|
||||
- variant: `default|destructive|outline|secondary|ghost|link`
|
||||
"""
|
||||
def button_variant(props \\ %{}) do
|
||||
variants = Map.take(props, ~w(variant size)a)
|
||||
variants = Map.merge(@default_variants, variants)
|
||||
|
||||
variation_classes = Enum.map_join(variants, " ", fn {key, value} -> @variants[key][value] end)
|
||||
|
||||
shared_classes =
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50"
|
||||
|
||||
"#{shared_classes} #{variation_classes}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Common function for building variant
|
||||
|
||||
## Examples
|
||||
|
||||
```elixir
|
||||
config =
|
||||
%{
|
||||
variants: %{
|
||||
variant: %{
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: %{
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
default_variants: %{
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
|
||||
class_input = %{variant: "outline", size: "lg"}
|
||||
variant_class(config, class_input)
|
||||
```
|
||||
|
||||
"""
|
||||
def variant_class(config, class_input) do
|
||||
variants = Map.get(config, :variants, %{})
|
||||
default_variants = Map.get(config, :default_variants, %{})
|
||||
|
||||
variants
|
||||
|> Map.keys()
|
||||
|> Enum.map(fn variant_key ->
|
||||
# Get the variant value from input or use default
|
||||
variant_value =
|
||||
Map.get(class_input, variant_key) ||
|
||||
Map.get(default_variants, variant_key)
|
||||
|
||||
# Get the variant options map
|
||||
variant_options = Map.get(variants, variant_key, %{})
|
||||
|
||||
# Get the CSS classes for this variant value
|
||||
Map.get(variant_options, String.to_existing_atom(variant_value))
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Enum.join(" ")
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function build css style string from map of css style
|
||||
|
||||
## Examples
|
||||
|
||||
```elixir
|
||||
css_style = %{
|
||||
"background-color": "red",
|
||||
"color": "white",
|
||||
"font-size": "16px",
|
||||
}
|
||||
|
||||
style(css_style)
|
||||
|
||||
# => "background-color: red; color: white; font-size: 16px;"
|
||||
```
|
||||
"""
|
||||
def style(items) when is_list(items) do
|
||||
{acc_map, acc_list} =
|
||||
Enum.reduce(items, {%{}, []}, fn item, {acc_map, acc_list} ->
|
||||
cond do
|
||||
is_map(item) ->
|
||||
{Map.merge(acc_map, item), acc_list}
|
||||
|
||||
is_binary(item) ->
|
||||
{acc_map, [item | acc_list]}
|
||||
|
||||
true ->
|
||||
{acc_map, [item | acc_list]}
|
||||
end
|
||||
end)
|
||||
|
||||
style = Enum.map_join(acc_map, "; ", fn {k, v} -> "#{k}: #{v}" end) <> ";"
|
||||
Enum.join([style | acc_list], "; ")
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function build js script to invoke JS stored in given attribute.
|
||||
Similar to JS.exec/2 but this function target the nearest ancestor element.
|
||||
|
||||
## Examples
|
||||
|
||||
```heex
|
||||
<button click={exec_closest("phx-hide-sheet", ".ancestor_class")}>
|
||||
Close
|
||||
</button>
|
||||
```
|
||||
"""
|
||||
def exec_closest(attribute, ancestor_selector) do
|
||||
"""
|
||||
var el = this.closest("#{ancestor_selector}"); liveSocket.execJS(el, el.getAttribute("#{attribute}"));
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
This component is used to render dynamic tag based on the `tag` attribute. `tag` attribute can be a string or a function component.
|
||||
|
||||
This is just a wrapper around `dynamic_tag` function from Phoenix LiveView which only support string tag.
|
||||
|
||||
## Examples
|
||||
|
||||
```heex
|
||||
<.dynamic tag={@tag} class="bg-primary text-primary-foreground">
|
||||
Hello World
|
||||
</.dynamic>
|
||||
```
|
||||
"""
|
||||
def dynamic(%{tag: name} = assigns) when is_function(name, 1) do
|
||||
assigns = Map.delete(assigns, :tag)
|
||||
name.(assigns)
|
||||
end
|
||||
|
||||
def dynamic(assigns) do
|
||||
name = assigns[:tag] || "div"
|
||||
|
||||
assigns =
|
||||
assigns
|
||||
|> Map.delete(:tag)
|
||||
|> assign(:tag_name, name)
|
||||
|> assign(:name, name)
|
||||
|
||||
dynamic_tag(assigns)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This component mimic behavior of `asChild` attribute from shadcn/ui.
|
||||
It works by passing all attribute from `as_child` tag to `tag` function component, add pass `child` attribute to the `as` attribute of the `tag` function component.
|
||||
|
||||
The `tag` function component should accept `as_tag` attribute to render the child component.
|
||||
|
||||
## Examples
|
||||
|
||||
```heex
|
||||
<.as_child tag={&dropdown_menu_trigger/1} child={&sidebar_menu_button/1} class="bg-primary text-primary-foreground">
|
||||
Hello World
|
||||
</.as_child>
|
||||
```
|
||||
|
||||
Normally this can be archieved by using `dropdown_menu_trigger` component directly but this will fire copile warning.
|
||||
|
||||
```heex
|
||||
<.dropdown_menu_trigger as={&sidebar_menu_button/1} class="bg-primary text-primary-foreground">
|
||||
Hello World
|
||||
</.dropdown_menu_trigger>
|
||||
"""
|
||||
def as_child(%{tag: tag, child: child_tag} = assigns) when is_function(tag, 1) do
|
||||
assigns
|
||||
|> Map.drop([:tag, :child])
|
||||
|> assign(:as, child_tag)
|
||||
|> tag.()
|
||||
end
|
||||
|
||||
# Translate error message
|
||||
# borrowed from https://github.com/petalframework/petal_components/blob/main/lib/petal_components/field.ex#L414
|
||||
defp translate_error({msg, opts}) do
|
||||
config_translator = get_translator_from_config() || (&fallback_translate_error/1)
|
||||
|
||||
config_translator.({msg, opts})
|
||||
end
|
||||
|
||||
defp fallback_translate_error({msg, opts}) do
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
try do
|
||||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
rescue
|
||||
e ->
|
||||
IO.warn(
|
||||
"""
|
||||
the fallback message translator for the form_field_error function cannot handle the given value.
|
||||
|
||||
Hint: you can set up the `error_translator_function` to route all errors to your application helpers:
|
||||
|
||||
config :salad_ui, :error_translator_function, {MyAppWeb.CoreComponents, :translate_error}
|
||||
|
||||
Given value: #{inspect(value)}
|
||||
|
||||
Exception: #{Exception.message(e)}
|
||||
""",
|
||||
__STACKTRACE__
|
||||
)
|
||||
|
||||
"invalid value"
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_translator_from_config do
|
||||
case Application.get_env(:salad_ui, :error_translator_function) do
|
||||
{module, function} -> &apply(module, function, [&1])
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user