I have a custom component in my LiveView, which is basically a group of checkboxes.
I want to add a new attribute to my custom component, however no matter what I do, I always get nil
when accessing said attribute.
core_components.ex
@doc """
Generate a checkbox group for multi-select.
## Examples
<.checkgroup
field={@form[:genres]}
label="Genres"
options={[{"Fantasy", "fantasy"}, {"Science Fiction", "sci-fi"}]}
selected={[{"Fantasy", "fantasy"}]}
/>
"""
attr :id, :any
attr :name, :any
attr :label, :string, default: nil
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:genres]"
attr :errors, :list
attr :required, :boolean, default: false
attr :rest, :global, include: ~w(form readonly)
attr :class, :string, default: nil
attr :options, :list, default: [], doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :selected, :list, default: [], doc: "the currently selected options, to know which boxes are checked"
attr :meow, :any, default: "meow", doc: "a very cool new attribute!"
def checkgroup(assigns) do
new_assigns =
assigns
|> assign(:multiple, true)
|> assign(:type, "checkgroup")
input(new_assigns)
end
def input(%{type: "checkgroup"} = assigns) do
~H"""
<p> <%= "Cat says: #{@meow}" %></p>
<div class="mt-2">
<%= for opt <- @options do %>
<div class="relative flex gap-x-3">
<div class="flex h-6 items-center">
<input id={opt.id} name={@name} type="checkbox" value={opt.id} checked={opt in @selected} disabled={false} />
</div>
<div class="text-sm leading-6">
<label for={opt.id} ><%= opt.name %></label>
</div>
</div>
<% end %>
</div>
"""
end
Here I am using a customer component to render a stylish checkbox group. It works fine, except for the :meow
attribute, which somehow is always nil
when inside the input
function.
Using this code as a sample:
<.checkgroup field={@form[:my_form]} label="SUper question!" options={@sounds} selected={@selected_sounds} meow={[1, 2]} required />
I get the following error upon rendering this code:
** (exit) an exception was raised:
** (KeyError) key :meow not found in: %{
__changed__: nil,
__given__: %{
__changed__: nil,
__given__: %{
__changed__: nil,
__given__: %{
__changed__: nil,
field: %Phoenix.HTML.FormField{
id: "my_form",
name: "my_form",
errors: [],
field: :my_form,
form: %Phoenix.HTML.Form{
source: %{"some_command" => []},
impl: Phoenix.HTML.FormData.Map,
id: nil,
name: nil,
data: %{},
hidden: [],
params: %{"some_command" => []},
errors: [],
options: [],
index: nil,
action: nil
},
value: nil
},
label: "SUper question!",
meow: [1, 2],
options: [1, 2, 3, 4],
# .... more things follow
As you can see, meow: [1, 2],
is clearly present. Yet, @meow
returns nil
and using assigns.meow
outright crashes the application.
What am I doing wrong here and how can I fix it?
After a lot of digging around, i realized that all of the input
functions incore_components.ex
modify assigns
. The best example of this is this intermediary function, called before any input component functions:
def input(%{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_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
This means I do not fully control what is going on. I decided to move away from using/modifying the input
functions and created my own component from scratch:
@doc """
Generate a checkbox group for multi-select.
## Examples
<.checkgroup
field={@form[:genres]}
label="Genres"
options={[{"Fantasy", "fantasy"}, {"Science Fiction", "sci-fi"}]}
selected={[{"Fantasy", "fantasy"}]}
/>
"""
attr :name, :any
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:genres]"
attr :required, :boolean, default: false
attr :options, :list,
default: [],
doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :disabled, :list, default: [], doc: "the list of options that are disabled"
attr :selected, :list,
default: [],
doc: "the currently selected options, to know which boxes are checked"
def checkgroup(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns =
assigns
|> assign(:name, "#{field.name}[]")
~H"""
<div class="mt-2">
<%= for opt <- @options do %>
<div class="relative flex gap-x-3">
<div class="flex h-6 items-center">
<input
id={opt.id}
name={@name}
type="checkbox"
value={opt.id}
class={
if opt in @disabled do
"h-4 w-4 rounded border-gray-300 text-gray-300 focus:ring-indigo-600"
else
"h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
end
}
checked={opt in @selected}
disabled={opt in @disabled}
/>
</div>
<div class="text-sm leading-6">
<label
for={opt.id}
class={
if opt in @disabled do
"text-base font-semibold text-gray-300"
else
"text-base font-semibold text-gray-900"
end
}
>
<%= opt.name %>
</label>
</div>
</div>
<% end %>
</div>
"""
end
which works as expected and all the assigns
not have the expected values.