Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -125,6 +125,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 @@ -198,15 +208,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,11 @@
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
end
1 change: 1 addition & 0 deletions guides/fields/what-is-a-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Backpex provides the following built-in field types:

- `Backpex.Fields.BelongsTo`
- `Backpex.Fields.Boolean`
- `Backpex.Fields.Checkgroup`
- `Backpex.Fields.Currency`
- `Backpex.Fields.DateTime`
- `Backpex.Fields.Date`
Expand Down
93 changes: 93 additions & 0 deletions lib/backpex/fields/checkgroup.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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
],
readonly: [
doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]}
]
]

@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

@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)}
readonly={@readonly}
/>
</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
34 changes: 32 additions & 2 deletions lib/backpex/html/form.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,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, FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]"
Expand Down Expand Up @@ -148,6 +148,35 @@ defmodule Backpex.HTML.Form do
"""
end

def input(%{type: "checkgroup"} = assigns) do
assigns = assign_new(assigns, :readonly, fn -> Map.get(assigns.rest, :readonly, false) end)

~H"""
<fieldset class={["fieldset py-0", @class]}>
<legend :if={@label} class="label mb-1">{@label}</legend>
<input type="hidden" name={@name <> "[]"} value="" tabindex="-1" aria-hidden="true" />
<div>
<label :for={{label, value} <- @options} class="flex min-h-11 cursor-pointer items-center space-x-2 md:min-h-8">
<input
type="checkbox"
id={"#{@id}-#{value}"}
name={@name <> "[]"}
value={value}
checked={to_string(value) in Enum.map(List.wrap(@value), &to_string/1)}
disabled={@readonly}
class={["checkbox checkbox-sm checkbox-primary", @errors != [] && "checkbox-error"]}
aria-invalid={@errors != [] && "true"}
aria-describedby={@help_text && "#{@id}-help"}
/>
<span class="text-sm">{label}</span>
</label>
</div>
<.error :for={msg <- @errors} :if={not @hide_errors}>{msg}</.error>
<.help_text :if={@help_text} id={"#{@id}-help"}>{@help_text}</.help_text>
</fieldset>
"""
end

def input(assigns) do
~H"""
<div class={["fieldset py-0", @class]}>
Expand Down Expand Up @@ -274,13 +303,14 @@ defmodule Backpex.HTML.Form do
"""
@doc type: :component

attr :id, :string, default: nil
attr :class, :string, default: nil

slot :inner_block, required: true

def help_text(assigns) do
~H"""
<p class={["text-base-content/60", @class]}>
<p id={@id} class={["text-base-content/60", @class]}>
{render_slot(@inner_block)}
</p>
"""
Expand Down