Last active
December 7, 2024 12:56
-
-
Save brainlid/9dcf78386e68ca03d279ae4a9c8c2373 to your computer and use it in GitHub Desktop.
Code snippets for Fly blog posts - https://fly.io/phoenix-files/making-a-checkboxgroup-input/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Books.Book do | |
use Ecto.Schema | |
import Ecto.Query, warn: false | |
import Ecto.Changeset | |
import MyApp.ChangesetHelpers | |
schema "books" do | |
field :name, :string | |
field :genres, {:array, :string}, default: [] | |
# ... | |
end | |
@genre_options [ | |
{"Fantasy", "fantasy"}, | |
{"Science Fiction", "sci-fi"}, | |
{"Dystopian", "dystopian"}, | |
{"Adventure", "adventure"}, | |
{"Romance", "romance"}, | |
{"Detective & Mystery", "mystery"}, | |
{"Horror", "horror"}, | |
{"Thriller", "thriller"}, | |
{"Historical Fiction", "historical-fiction"}, | |
{"Young Adult (YA)", "young-adult"}, | |
{"Children's Fiction", "children-fiction"}, | |
{"Memoir & Autobiography", "autobiography"}, | |
{"Biography", "biography"}, | |
{"Cooking", "cooking"}, | |
# ... | |
] | |
@valid_genres Enum.map(@genre_options, fn({_text, val}) -> val end) | |
def genre_options, do: @genre_options | |
def changeset(book, attrs) do | |
book | |
|> cast(attrs, [:name, :genres]) | |
|> common_validations() | |
end | |
defp common_validations(changeset) do | |
changeset | |
# ... | |
|> validate_required([:name]) | |
|> clean_and_validate_array(:genres, @valid_genres) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.ChangesetHelpers do | |
@moduledoc """ | |
Helper functions for working with changesets. | |
Includes common specialized validations for working with tags. | |
""" | |
import Ecto.Changeset | |
@doc """ | |
Remove the blank value from the array. | |
""" | |
def trim_array(changeset, field, blank_value \\ "") do | |
update_change(changeset, field, &Enum.reject(&1, fn item -> item == blank_value end)) | |
end | |
@doc """ | |
Validate that the array of string on the changeset are all in the set of valid | |
values. | |
NOTE: Could use `Ecto.Changeset.validate_subset/4` instead, however, it won't | |
give as helpful errors. | |
""" | |
def validate_array(changeset, field, valid_values) when is_list(valid_values) do | |
validate_change(changeset, field, fn ^field, new_values -> | |
if Enum.all?(new_values, &(&1 in valid_values)) do | |
[] | |
else | |
unsupported = new_values -- valid_values | |
[{field, "Only the defined values are allowed. Unsupported: #{inspect(unsupported)}"}] | |
end | |
end) | |
end | |
@doc """ | |
When working with a field that is an array of strings, this function sorts the | |
values in the array. | |
""" | |
def sort_array(changeset, field) do | |
update_change(changeset, field, &Enum.sort(&1)) | |
end | |
@doc """ | |
Clean and process the array values and validate the selected values against an | |
approved list. | |
""" | |
def clean_and_validate_array(changeset, field, valid_values, blank_value \\ "") do | |
changeset | |
|> trim_array(field, blank_value) | |
|> sort_array(field) | |
|> validate_array(field, valid_values) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyAppWeb.CoreComponents do | |
use Phoenix.Component | |
# ... | |
def input(%{type: "checkgroup"} = assigns) do | |
~H""" | |
<div phx-feedback-for={@name} class="text-sm"> | |
<.label for={@id}><%= @label %></.label> | |
<div class="mt-1 w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> | |
<div class="grid grid-cols-1 gap-1 text-sm items-baseline"> | |
<input type="hidden" name={@name} value="" /> | |
<div class="flex items-center" :for={{label, value} <- @options}> | |
<label | |
for={"#{@name}-#{value}"} class="font-medium text-gray-700"> | |
<input | |
type="checkbox" | |
id={"#{@name}-#{value}"} | |
name={@name} | |
value={value} | |
checked={value in @value} | |
class="mr-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 transition duration-150 ease-in-out" | |
{@rest} | |
/> | |
<%= label %> | |
</label> | |
</div> | |
</div> | |
</div> | |
<.error :for={msg <- @errors}><%= msg %></.error> | |
</div> | |
""" | |
end | |
# ... | |
@doc """ | |
Generate a checkbox group for multi-select. | |
""" | |
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[:email]" | |
attr :errors, :list | |
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" | |
attr :rest, :global, include: ~w(disabled form readonly) | |
attr :class, :string, default: nil | |
def checkgroup(assigns) do | |
new_assigns = | |
assigns | |
|> assign(:multiple, true) | |
|> assign(:type, "checkgroup") | |
input(new_assigns) | |
end | |
# ... | |
end |
In some cases the
value
field is also just a list of id strings, including the dummy value""
for an empty selection.to deal with these cases I wrote a helper:
defp pick_selected( assigns ) do assigns.value |> Enum.map(fn x-> case x do %Ecto.Changeset{ action: :update, data: data } -> data.id %Ecto.Changeset{} -> nil %{id: id} -> id x when is_binary(x) -> if x == "", do: nil, else: x _ -> nil end end) |> Enum.filter( &!is_nil(&1)) end
A bit shorter
defp pick_selected_checkgroup(assigns) do
assigns.value
|> Enum.map(fn
%Ecto.Changeset{action: :update, data: data} -> data.id
%Ecto.Changeset{} -> nil
%{id: id} -> id
x when is_binary(x) and x != "" -> x
_ -> nil
end)
|> Enum.reject(&is_nil/1)
end
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I replaced the line
checked={value in @value}
withchecked={@value && value in @value}
to avoid the protocol Enumerable not implemented for nil of type Atom error mentioned by @ciroque above.