Skip to content
Open
2 changes: 1 addition & 1 deletion demo/lib/demo/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule Demo.User do
field :full_name, :string, virtual: true
field :age, :integer
field :role, Ecto.Enum, values: [:user, :admin]
field :permissions, {:array, :string}
field :permissions, {:array, :string}, default: []
field :avatar, :string
field :deleted_at, :utc_datetime

Expand Down
19 changes: 10 additions & 9 deletions demo/lib/demo_web/live/user_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ defmodule DemoWeb.UserLive do
options: [Admin: "admin", User: "user"],
prompt: "Choose role..."
},
permissions: %{
module: Backpex.Fields.Checkgroup,
label: "Permissions",
options: [
{"Create Posts", "create_posts"},
{"Edit Posts", "edit_posts"},
{"Delete Posts", "delete_posts"},
{"Manage Users", "manage_users"}
]
},
posts: %{
module: Backpex.Fields.HasMany,
label: "Posts",
Expand Down Expand Up @@ -193,15 +203,6 @@ defmodule DemoWeb.UserLive do
label: "Notes"
}
]
},
permissions: %{
module: Backpex.Fields.MultiSelect,
label: "Permissions",
options: [
{"Can access admin panel", "can_access_admin_panel"},
{"Item actions", [{"Delete", "delete"}, {"Edit", "edit"}, {"Show", "show"}]},
{"Other actions", [{"Can send email", "can_send_email"}]}
]
}
]
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Demo.Repo.Migrations.SetDefaultForPermissions do
use Ecto.Migration

def up do
# Set default for new records
execute "ALTER TABLE users ALTER COLUMN permissions SET DEFAULT '{}'::text[]"

# Update existing NULL values to empty array
execute "UPDATE users SET permissions = '{}' WHERE permissions IS NULL"
end

def down do
execute "ALTER TABLE users ALTER COLUMN permissions DROP DEFAULT"
end
Comment on lines +12 to +14

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using that?

end
91 changes: 91 additions & 0 deletions lib/backpex/fields/checkgroup.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
defmodule Backpex.Fields.Checkgroup do
@config_schema [
options: [
doc: "List of options or function that receives the assigns.",
type: {:or, [{:list, :any}, {:fun, 1}]},
required: true
]
]

@moduledoc """
A field for handling multiple checkboxes with predefined options.

This field stores selected values as an array.

## Field-specific options

See `Backpex.Field` for general field options.

#{NimbleOptions.docs(@config_schema)}

## Example

@impl Backpex.LiveResource
def fields do
[
roles: %{
module: Backpex.Fields.Checkgroup,
label: "Roles",
options: [{"Admin", "admin"}, {"User", "user"}, {"Editor", "editor"}]
}
]
end
"""
use Backpex.Field, config_schema: @config_schema

alias Backpex.HTML

Comment thread
Flo0807 marked this conversation as resolved.
Outdated
@impl Backpex.Field
def render_value(assigns) do
options = get_options(assigns)
labels = get_labels(assigns.value, options)

assigns = assign(assigns, :labels, labels)

~H"""
<p class={@live_action in [:index, :resource_action] && "truncate"}>
{if @labels == [], do: raw("&mdash;"), else: @labels |> Enum.map(&HTML.pretty_value/1) |> Enum.join(", ")}
</p>
"""
end

@impl Backpex.Field
def render_form(assigns) do
options = get_options(assigns)

assigns = assign(assigns, :options, options)

~H"""
<div>
<Layout.field_container>
<:label :if={not @hide_label} align={Backpex.Field.align_label(@field_options, assigns)}>
<Layout.input_label as="span" text={@field_options[:label]} />
</:label>
<BackpexForm.input
type="checkgroup"
field={@form[@name]}
options={@options}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
help_text={Backpex.Field.help_text(@field_options, assigns)}
aria-labelledby={Map.get(assigns, :aria_labelledby)}
/>
</Layout.field_container>
</div>
"""
end

defp get_labels(value, options) do
values = List.wrap(value) |> Enum.map(&to_string/1)

options
|> Enum.filter(fn {_label, option_value} -> to_string(option_value) in values end)
|> Enum.map(fn {label, _value} -> label end)
end

defp get_options(assigns) do
case Map.get(assigns.field_options, :options) do
options when is_function(options) -> options.(assigns)
options -> options
end
end
end
27 changes: 26 additions & 1 deletion lib/backpex/html/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ defmodule Backpex.HTML.Form do

attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
values: ~w(checkbox checkgroup color date datetime-local email file hidden month number password
range radio search select tel text textarea time toggle url week)

attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]"
Expand Down Expand Up @@ -142,6 +142,31 @@ defmodule Backpex.HTML.Form do
"""
end

def input(%{type: "checkgroup"} = assigns) do
~H"""
<div class={["fieldset py-0", @class]}>
<span :if={@label} class="label mb-1">{@label}</span>
<div class="space-y-2">
<input type="hidden" name={@name} value="" tabindex="-1" aria-hidden="true" />
<label :for={{label, value} <- @options} for={"#{@id}-#{value}"} class="flex cursor-pointer items-center space-x-2">
<input
type="checkbox"
id={"#{@id}-#{value}"}
name={@name <> "[]"}
value={value}
checked={value in List.wrap(@value)}
Comment thread
Flo0807 marked this conversation as resolved.
Outdated
class={["checkbox checkbox-sm checkbox-primary", @errors != [] && "checkbox-error"]}
{@rest}
/>
<span class="text-sm">{label}</span>
</label>
</div>
<.error :for={msg <- @errors} :if={not @hide_errors}>{msg}</.error>
<.help_text :if={@help_text}>{@help_text}</.help_text>
</div>
"""
end

def input(assigns) do
~H"""
<div class={["fieldset py-0", @class]}>
Expand Down