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 ``` """ 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 ``` """ 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 ``` 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 """ 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