.formatter.exs LICENSE lib/phx_component_helpers.ex lib/phx_component_helpers/css.ex lib/phx_component_helpers/forms.ex lib/phx_component_helpers/forward.ex lib/phx_component_helpers/set_attributes.ex lib/phx_view_helpers.ex mix.exs mix.lock test/phx_component_helpers_test.exs test/phx_view_helpers_test.exs test/test_helper.exs <<<<<< network # path=./cover/excoveralls.json {"source_files":[{"coverage":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,16,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,9,null,12,null,null,null,null,null,9,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,4,4,null,null,null,null,null,null,null,null,null,null,null,null,null,17,null,null,10,5,null,5,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,6,null,6,null,null,null,null,null,6,6,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,4,null,null,null,null,null,null,null,3,null,null,null,null,null,null,null,null,null,1,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,6,null,7,7,null,null,null,null,6,6,null,null,null,22,1,1,null,null,null,null,12,24,null,null,null,null],"name":"lib/phx_component_helpers.ex","source":"defmodule PhxComponentHelpers do\n @moduledoc \"\"\"\n `PhxComponentHelpers` are helper functions meant to be used within Phoenix\n LiveView live_components to make your components more configurable and extensible\n from your templates.\n\n It provides following features:\n\n * set HTML or data attributes from component assigns\n * set phx_* attributes from component assigns\n * set attributes with any custom prefix such as `@click` or `x-bind:` from [alpinejs](https://github.com/alpinejs/alpine)\n * encode attributes as JSON from an Elixir structure assign\n * validate mandatory attributes\n * set and extend CSS classes from component assigns\n \"\"\"\n\n import PhxComponentHelpers.{SetAttributes, CSS, Forms, Forward}\n import Phoenix.HTML, only: [html_escape: 1]\n import Phoenix.HTML.Form, only: [input_id: 2, input_name: 2, input_value: 2]\n\n @doc ~S\"\"\"\n Extends assigns with raw_* attributes that can be interpolated within\n your component markup.\n\n ## Parameters\n * `assigns` - your component assigns\n * `attributes` - a list of attributes (atoms) that will be fetched from assigns.\n Attributes can either be single atoms or tuples in the form `{:atom, default}` to\n provide default values.\n\n ## Options\n * `:required` - raises if required attributes are absent from assigns\n * `:json` - when true, will JSON encode the assign value\n * `:data` - when true, HTML attributes are prefixed with `data-`\n * `:into` - merges all assigns in a single one that can be interpolated at once\n\n ## Example\n ```\n assigns\n |> set_attributes(\n [:id, :name, label: \"default label\"],\n required: [:id, :name],\n into: :attributes\n )\n |> set_attributes([:value], json: true)\n ```\n\n `assigns` now contains `@raw_id`, `@raw_name`, `@raw_label` and `@raw_value`.\n It also contains `@raw_attributes` which holds the values if `:id`, `:name` and `:label`.\n \"\"\"\n def set_attributes(assigns, attributes, opts \\\\ []) do\n assigns\n |> do_set_attributes(attributes, opts)\n |> validate_required_attributes(opts[:required])\n end\n\n @doc ~S\"\"\"\n Extends assigns with prefixed attributes that can be interpolated within\n your component markup. It will automatically detect any attribute prefixed by\n any of the given prefixes from input assigns.\n\n Can be used for intance to easily map `alpinejs` html attributes.\n\n ## Parameters\n * `assigns` - your component assigns\n * `prefixes` - a list of prefix as binaries\n\n ## Options\n * `:init` - a list of attributes that will be initialized if absent from assigns\n * `:required` - raises if required attributes are absent from assigns\n * `:into` - merges all assigns in a single one that can be interpolated at once\n\n ## Example\n ```\n assigns\n |> set_prefixed_attributes(\n [\"@click\", \"x-bind:\"],\n required: [\"x-bind:class\"],\n into: :alpine_attributes\n )\n ```\n\n `assigns` now contains `@raw_click`, `@raw_x-bind:class` and `@raw_alpine_attributes`.\n \"\"\"\n def set_prefixed_attributes(assigns, prefixes, opts \\\\ []) do\n phx_attributes =\n prefixes\n |> Enum.flat_map(&find_assigns_with_prefix(assigns, &1))\n |> Enum.uniq()\n\n assigns\n |> do_set_attributes(phx_attributes, opts)\n |> set_empty_attributes(opts[:init])\n |> validate_required_attributes(opts[:required])\n end\n\n @doc ~S\"\"\"\n Just a convenient method built on top of `set_prefixed_attributes/3` for phx attributes.\n It will automatically detect any attribute prefixed by `phx_` from input assigns.\n By default, the `:into` option of `set_prefixed_attributes/3` is `:phx_attributes`\n\n ## Example\n ```\n assigns\n |> set_phx_attributes(required: [:phx_submit], init: [:phx_change])\n ```\n\n `assigns` now contains `@raw_phx_change`, `@raw_phx_submit` and `@raw_phx_attributes`.\n \"\"\"\n def set_phx_attributes(assigns, opts \\\\ []) do\n opts = Keyword.put_new(opts, :into, :phx_attributes)\n set_prefixed_attributes(assigns, [\"phx_\"], opts)\n end\n\n @doc ~S\"\"\"\n Validates that attributes are present in assigns.\n Raises an `ArgumentError` if any attribute is missing.\n\n ## Example\n ```\n assigns\n |> validate_required_attributes([:id, :label])\n ```\n \"\"\"\n def validate_required_attributes(assigns, required)\n def validate_required_attributes(assigns, nil), do: assigns\n\n def validate_required_attributes(assigns, required) do\n if Enum.all?(required, &Map.has_key?(assigns, &1)) do\n assigns\n else\n raise ArgumentError, \"missing required attributes\"\n end\n end\n\n @doc ~S\"\"\"\n Set assigns with class attributes.\n\n The class attribute will take provided `default_classes` as a default value and will\n be extended, on a class-by-class basis, by your assigns.\n\n This function will identify default classes to be replaced by assigns on a prefix basis:\n - \"bg-gray-200\" will be overwritten by \"bg-blue-500\" because they share the same \"bg-\" prefix\n - \"hover:bg-gray-200\" will be overwritten by \"hover:bg-blue-500\" because they share the same\n \"hover:bg-\" prefix\n - \"m-1\" would not be overwritten by \"mt-1\" because they don't share the same prefix (\"m-\" vs \"mt-\")\n\n ## Parameters\n * `assigns` - your component assigns\n * `default_classes` - the default classes that will be overridden by your assigns.\n This parameter can be a binary or a single parameter function that receives all assigns and\n returns a binary\n\n ## Options\n * `:attribute` - read & write css classes from & into this key\n * `:error_class` - extra class that will be added if assigns contain form/field keys\n and field is faulty.\n\n ## Example\n ```\n assigns\n |> extend_class(\"bg-blue-500 mt-8\")\n |> extend_class(\"py-4 px-2 divide-y-8 divide-gray-200\", attribute: :wrapper_class)\n |> extend_class(\"form-input\", error_class: \"form-input-error\", attribute: :input_class)\n |> extend_class(fn assigns ->\n default = \"p-2 m-4 text-sm \"\n if assigns[:active], do: default <> \"bg-indigo-500\", else: default <> \"bg-gray-200\"\n end)\n ```\n\n `assigns` now contains `@raw_class` and `@raw_wrapper_class`.\n\n If your input assigns were `%{class: \"mt-2\", wrapper_class: \"divide-none\"}` then:\n * `@raw_class` would contain `\"bg-blue-500 mt-2\"`\n * `@raw_wrapper_class` would contain `\"py-4 px-2 divide-none\"`\n \"\"\"\n def extend_class(assigns, default_classes, opts \\\\ []) do\n class_attribute_name = Keyword.get(opts, :attribute, :class)\n\n new_class =\n assigns\n |> handle_error_class_option(opts[:error_class], class_attribute_name)\n |> do_css_extend_class(default_classes, class_attribute_name)\n\n assigns\n |> Map.put(:\"#{class_attribute_name}\", new_class)\n |> Map.put(:\"raw_#{class_attribute_name}\", {:safe, \"class=#{escaped(new_class)}\"})\n end\n\n @doc ~S\"\"\"\n Extends assigns with form related attributes.\n\n If assigns contain `:form` and `:field` keys then it will set `:id`, `:name`, ':for',\n `:value`, and `:errors` from received `Phoenix.HTML.Form`.\n\n ## Parameters\n * `assigns` - your component assigns\n\n ## Example\n ```\n assigns\n |> set_form_attributes()\n ```\n \"\"\"\n def set_form_attributes(assigns) do\n with_form_fields(\n assigns,\n fn assigns, form, field ->\n assigns\n |> put_if_new_or_nil(:id, input_id(form, field))\n |> put_if_new_or_nil(:name, input_name(form, field))\n |> put_if_new_or_nil(:for, input_name(form, field))\n |> put_if_new_or_nil(:value, input_value(form, field))\n |> put_if_new_or_nil(:errors, form_errors(form, field))\n end,\n fn assigns ->\n assigns\n |> put_if_new_or_nil(:form, nil)\n |> put_if_new_or_nil(:field, nil)\n |> put_if_new_or_nil(:id, nil)\n |> put_if_new_or_nil(:name, nil)\n |> put_if_new_or_nil(:for, nil)\n |> put_if_new_or_nil(:value, nil)\n |> put_if_new_or_nil(:errors, [])\n end\n )\n end\n\n @doc ~S\"\"\"\n Forward and filter assigns to sub components.\n By default it doesn't forward anything unless you provide it with any combination\n of the options described below.\n\n ## Parameters\n\n * `assigns` - your component assigns\n\n ## Options\n * `prefix` - will only forward assigns prefixed by the given prefix. Forwarded assign key will no longer have the prefix\n * `take`- is a list of key (without prefix) that will be picked from assigns to be forwarded\n * `merge`- takes a map that will be merged as-is to the output assigns\n\n If both options are given at the same time, the resulting assigns will be the union of the two.\n\n\n ## Example\n Following will forward an assign map containing `%{button_id: 42, button_label: \"label\", phx_click: \"save\"}` as `%{id: 42, label: \"label\", phx_click: \"save\"}`\n ```\n forward_assigns(assigns, prefix: :button, take: [:phx_click])\n ```\n \"\"\"\n def forward_assigns(assigns, opts) do\n for option <- opts, reduce: %{} do\n acc ->\n assigns = handle_forward_option(assigns, option)\n Map.merge(acc, assigns)\n end\n end\n\n defp escaped(val) do\n {:safe, escaped_val} = html_escape(val)\n \"\\\"#{escaped_val}\\\"\"\n end\n\n defp put_if_new_or_nil(map, key, val) do\n Map.update(map, key, val, fn\n nil -> val\n current -> current\n end)\n end\n\n defp find_assigns_with_prefix(assigns, prefix) do\n for key <- Map.keys(assigns),\n key_s = to_string(key),\n String.starts_with?(key_s, prefix),\n do: key\n end\nend"},{"coverage":[null,null,null,null,null,6,6,null,null,null,null,2,2,null,null,null,8,null,1,7,null,null,8,8,null,8,16,null,16,null,16,6,null,null,null,null,null,8,null,null],"name":"lib/phx_component_helpers/css.ex","source":"defmodule PhxComponentHelpers.CSS do\n @moduledoc false\n\n @doc false\n def do_css_extend_class(assigns, default_classes, class_attribute_name) when is_map(assigns) do\n input_class = Map.get(assigns, class_attribute_name) || \"\"\n do_extend_class(assigns, input_class, default_classes)\n end\n\n @doc false\n def do_css_extend_class(options, default_classes, class_attribute_name) when is_list(options) do\n input_class = Keyword.get(options, class_attribute_name) || \"\"\n do_extend_class(options, input_class, default_classes)\n end\n\n defp do_extend_class(assigns_or_options, input_class, default_classes) do\n default_classes =\n case default_classes do\n _ when is_function(default_classes) -> default_classes.(assigns_or_options)\n _ -> default_classes\n end\n\n default_classes = String.split(default_classes, [\" \", \"\\n\"], trim: true)\n extend_classes = String.split(input_class, [\" \", \"\\n\"], trim: true)\n\n classes =\n for class <- default_classes, reduce: extend_classes do\n acc ->\n [class_prefix | _] = String.split(class, \"-\")\n\n if Enum.any?(extend_classes, &String.starts_with?(&1, \"#{class_prefix}-\")) do\n acc\n else\n [class | acc]\n end\n end\n\n Enum.join(classes, \" \")\n end\nend"},{"coverage":[null,null,null,null,4,null,null,2,2,1,1,1,null,1,null,null,null,null,null,null,6,6,null,6,5,null,1,null,null,null,null,null,3,null,null,null,2,2,null,null],"name":"lib/phx_component_helpers/forms.ex","source":"defmodule PhxComponentHelpers.Forms do\n @moduledoc false\n\n @doc false\n def handle_error_class_option(assigns, nil, _class_attribute_name), do: assigns\n\n def handle_error_class_option(assigns, error_class, class_attribute_name) do\n with_form_fields(assigns, fn assigns, form, field ->\n if errors?(form, field) do\n input_class = Map.get(assigns, class_attribute_name) || \"\"\n new_class = String.trim(input_class <> \" \" <> error_class)\n Map.put(assigns, class_attribute_name, new_class)\n else\n assigns\n end\n end)\n end\n\n @doc false\n def with_form_fields(assigns, fun, fallback \\\\ & &1) do\n form = assigns[:form]\n field = assigns[:field]\n\n if form && field do\n fun.(assigns, form, field)\n else\n fallback.(assigns)\n end\n end\n\n @doc false\n def form_errors(form, field) do\n Keyword.get_values(form.errors, field)\n end\n\n defp errors?(form, field) do\n errors = Keyword.get_values(form.errors, field)\n errors && !Enum.empty?(errors)\n end\nend"},{"coverage":[null,null,null,null,3,null,3,null,12,null,12,7,7,null,5,null,null,null,null,null,2,null,null,null,2,null,null],"name":"lib/phx_component_helpers/forward.ex","source":"defmodule PhxComponentHelpers.Forward do\n @moduledoc false\n\n def handle_forward_option(assigns, {:prefix, prefix}) do\n prefix = \"#{prefix}_\"\n\n for {key, val} <- assigns, reduce: %{} do\n acc ->\n key = to_string(key)\n\n if String.starts_with?(key, prefix) do\n forwarded_key = key |> String.replace_leading(prefix, \"\") |> String.to_atom()\n Map.put(acc, forwarded_key, val)\n else\n acc\n end\n end\n end\n\n def handle_forward_option(assigns, {:take, attributes}) do\n Map.take(assigns, attributes)\n end\n\n def handle_forward_option(_assigns, {:merge, assigns}) do\n assigns\n end\nend"},{"coverage":[null,null,null,null,null,null,null,null,25,null,null,21,21,21,null,21,null,1,null,null,null,null,4,null,null,16,null,null,null,null,25,null,null,null,null,null,7,null,null,2,null,2,2,null,null,null,null,20,6,6,null,14,14,null,null,null,null,21,6,null,15,null,null,null,20,6,null,null,null,null,null,26,null,null,22,null,null,3,3,3,null,null],"name":"lib/phx_component_helpers/set_attributes.ex","source":"defmodule PhxComponentHelpers.SetAttributes do\n @moduledoc false\n @json_library Jason\n\n import Phoenix.HTML, only: [html_escape: 1]\n\n @doc false\n def do_set_attributes(assigns, attributes, opts \\\\ []) do\n new_assigns =\n attributes\n |> Enum.reduce(%{}, fn attr, acc ->\n {attr, default} = attribute_key_and_default(attr)\n attr_key = raw_attribute_key(attr)\n attribute_fun = attribute_fun(opts)\n\n case {Map.get(assigns, attr), default} do\n {nil, nil} ->\n Map.put(acc, attr_key, {:safe, \"\"})\n\n {nil, default} ->\n acc\n |> Map.put(attr, default)\n |> Map.put(attr_key, {:safe, \"#{attribute_fun.(attr)}=#{escaped(default, opts)}\"})\n\n {val, _} ->\n Map.put(acc, attr_key, {:safe, \"#{attribute_fun.(attr)}=#{escaped(val, opts)}\"})\n end\n end)\n |> handle_into_option(opts[:into])\n\n Map.merge(assigns, new_assigns)\n end\n\n @doc false\n def set_empty_attributes(assigns, attributes)\n\n def set_empty_attributes(assigns, nil), do: assigns\n\n def set_empty_attributes(assigns, attributes) do\n for attr <- attributes, reduce: assigns do\n acc ->\n attr_key = raw_attribute_key(attr)\n Map.put_new(acc, attr_key, {:safe, \"\"})\n end\n end\n\n defp escaped(val, opts) do\n if opts[:json] do\n {:safe, escaped_val} = val |> @json_library.encode!() |> html_escape()\n \"\\\"#{escaped_val}\\\"\"\n else\n {:safe, escaped_val} = html_escape(val)\n \"\\\"#{escaped_val}\\\"\"\n end\n end\n\n defp attribute_fun(opts) do\n if opts[:data] do\n &data_attribute/1\n else\n &raw_attribute/1\n end\n end\n\n defp raw_attribute(attr), do: attr |> to_string() |> String.replace(\"_\", \"-\")\n defp data_attribute(attr), do: \"data-#{raw_attribute(attr)}\"\n\n defp attribute_key_and_default({attr, default}), do: {attr, default}\n defp attribute_key_and_default(attr), do: {attr, nil}\n\n defp raw_attribute_key(attr) do\n \"raw_#{attr}\" |> String.replace(\"@\", \"\") |> String.to_atom()\n end\n\n defp handle_into_option(assigns, nil), do: assigns\n\n defp handle_into_option(assigns, into) do\n into_assign = for({_key, {:safe, attr}} <- assigns, do: attr)\n attr_key = raw_attribute_key(into)\n Map.put(assigns, attr_key, {:safe, Enum.join(into_assign, \" \")})\n end\nend"},{"coverage":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,2,2,null,null],"name":"lib/phx_view_helpers.ex","source":"defmodule PhxViewHelpers do\n @moduledoc \"\"\"\n `PhxComponentHelpers` are helper functions meant to be used within Phoenix\n your views to facilitate usage of live_components inside templates.\n \"\"\"\n\n import PhxComponentHelpers.CSS\n\n @doc ~S\"\"\"\n Extends `Phoenix.HTML.Form.form_for/3` options to merge css class as with\n `PhxComponentHelpers.extend_class/2`.\n\n It's useful to define your own `form_for` functions with default css classes\n that still can be overriden from the template.\n\n ## Example\n ```\n def my_form_for(form_data, action, options) when is_list(options) do\n new_options = extend_form_class(options, \"mt-4 space-y-2\")\n form_for(form_data, action, new_options)\n end\n \"\"\"\n def extend_form_class(options, default_classes) do\n extended_classes = do_css_extend_class(options, default_classes, :class)\n Keyword.put(options, :class, extended_classes)\n end\nend"}]}<<<<<< EOF