Skip to content

Instantly share code, notes, and snippets.

@brainlid
Last active April 1, 2024 04:29
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brainlid/9dcf78386e68ca03d279ae4a9c8c2373 to your computer and use it in GitHub Desktop.
Save brainlid/9dcf78386e68ca03d279ae4a9c8c2373 to your computer and use it in GitHub Desktop.
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
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
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
@petrus-jvrensburg
Copy link

petrus-jvrensburg commented Apr 1, 2024

I replaced the line checked={value in @value} with checked={@value && value in @value} to avoid the protocol Enumerable not implemented for nil of type Atom error mentioned by @ciroque above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment