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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A bit shorter