Skip to content
Open
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
42 changes: 35 additions & 7 deletions guides/fields/readonly.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,52 @@
# Readonly

Fields can be configured to be readonly. In edit view, these fields are rendered with the additional HTML attributes `readonly` and `disabled`, ensuring that users cannot interact with the field or change its value.
Fields can be configured to be readonly. In edit view, readonly fields prevent users from interacting with the field or changing its value, while still displaying the current value.

In index view, if readonly and index editable are both set to true, forms will be rendered with the `readonly` HTML attribute.

## Supported fields

On index view, readonly is supported for all fields with the index editable option (see [Index Edit](index-edit.md)).

On edit view, readonly is supported for:
- `Backpex.Fields.Date`
- `Backpex.Fields.DateTime`
- `Backpex.Fields.Number`
On edit view, `readonly` is a global field option defined on `Backpex.Field`, so every built-in field type inherits it. It accepts either a `boolean` or a function `(assigns -> boolean)`.

Built-in fields render readonly using one of three strategies:

**Native `readonly` (text-like inputs)**

These fields render the browser's native `readonly` attribute on their input, so the value is still focusable and selectable but cannot be changed:

- `Backpex.Fields.Text`
- `Backpex.Fields.Textarea`
- `Backpex.Fields.Number`
- `Backpex.Fields.Date`
- `Backpex.Fields.DateTime`
- `Backpex.Fields.Time`
- `Backpex.Fields.Email`
- `Backpex.Fields.URL`
- `Backpex.Fields.Currency`

**`disabled` (control-style inputs)**

Native `readonly` does not apply to these control types, so they render as `disabled` instead:

- `Backpex.Fields.Select`
- `Backpex.Fields.MultiSelect`
- `Backpex.Fields.Boolean` — renders as a disabled toggle
- `Backpex.Fields.BelongsTo`
- `Backpex.Fields.HasMany` — dropdown is rendered as an inert element; selected badges lose the remove control

**Custom readonly rendering**

A few fields need tailored behavior beyond a single attribute:

- `Backpex.Fields.Upload` — the drop target and "Upload a file" link are disabled, the cancel/remove buttons on pending and existing entries are hidden, and the existing-file list is still displayed so users can see what is attached.
- `Backpex.Fields.InlineCRUD` — nested row fields become readonly, and the per-row delete checkbox and the add-row control are hidden entirely.
- `Backpex.Fields.HasManyThrough` — the Actions column (edit/remove buttons) is hidden, the "new relational" button is disabled, and pivot and select inputs inside the modal are rendered as disabled.

## Configuration

To enable readonly for a field, you need to set the `readonly` option to true in the field configuration. This key must contain either a boolean value or a function that returns a boolean value.
To enable readonly for a field, you need to set the `readonly` option in the field configuration. This key must contain either a boolean value or a function that returns a boolean value.

```elixir
# in your resource configuration file
Expand Down Expand Up @@ -67,7 +96,6 @@ def render_form(assigns) do
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
readonly={@readonly}
disabled={@readonly}
/>
</Layout.field_container>
</div>
Expand Down
5 changes: 5 additions & 0 deletions lib/backpex/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ defmodule Backpex.Field do
type: :string,
required: true
],
readonly: [
doc: "Sets the field to readonly. Also see the [readonly](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]},
default: false
],
class: [
type: {:or, [:string, {:fun, 1}]},
doc: """
Expand Down
2 changes: 2 additions & 0 deletions lib/backpex/fields/belongs_to.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ defmodule Backpex.Fields.BelongsTo do
field={@form[@owner_key]}
options={@options}
prompt={@prompt}
readonly={@readonly}
disabled={@readonly}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
help_text={Backpex.Field.help_text(@field_options, assigns)}
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/fields/boolean.ex
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ defmodule Backpex.Fields.Boolean do
<BackpexForm.input
type="toggle"
field={@form[@name]}
disabled={@readonly}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
help_text={Backpex.Field.help_text(@field_options, assigns)}
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/fields/currency.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ defmodule Backpex.Fields.Currency do
<BackpexForm.currency_input
type="text"
field={@form[@name]}
readonly={@readonly}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
help_text={Backpex.Field.help_text(@field_options, assigns)}
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
Expand Down
5 changes: 0 additions & 5 deletions lib/backpex/fields/date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ defmodule Backpex.Fields.Date do
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
],
readonly: [
doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]}
]
]

Expand Down Expand Up @@ -113,7 +109,6 @@ defmodule Backpex.Fields.Date do
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
readonly={@readonly}
disabled={@readonly}
aria-labelledby={Map.get(assigns, :aria_labelledby)}
/>
</Layout.field_container>
Expand Down
5 changes: 0 additions & 5 deletions lib/backpex/fields/date_time.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ defmodule Backpex.Fields.DateTime do
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
],
readonly: [
doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]}
]
]

Expand Down Expand Up @@ -113,7 +109,6 @@ defmodule Backpex.Fields.DateTime do
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
readonly={@readonly}
disabled={@readonly}
aria-labelledby={Map.get(assigns, :aria_labelledby)}
/>
</Layout.field_container>
Expand Down
5 changes: 0 additions & 5 deletions lib/backpex/fields/email.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ defmodule Backpex.Fields.Email do
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
],
readonly: [
doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]}
]
]

Expand Down Expand Up @@ -55,7 +51,6 @@ defmodule Backpex.Fields.Email do
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
readonly={@readonly}
disabled={@readonly}
aria-labelledby={Map.get(assigns, :aria_labelledby)}
/>
</Layout.field_container>
Expand Down
20 changes: 15 additions & 5 deletions lib/backpex/fields/has_many.ex
Original file line number Diff line number Diff line change
Expand Up @@ -144,22 +144,25 @@ defmodule Backpex.Fields.HasMany do
<Layout.input_label as="span" text={@field_options[:label]} />
</:label>

<Backpex.HTML.CoreComponents.dropdown id={"has-many-dropdown-#{@name}"} class="w-full">
<Backpex.HTML.CoreComponents.dropdown id={"has-many-dropdown-#{@name}"} class="w-full" readonly={@readonly}>
<:trigger
class={[
"input block h-fit w-full p-2",
@errors == [] && "bg-transparent",
@errors != [] && "input-error bg-error/10"
"block h-fit w-full p-2",
not @readonly && "input",
not @readonly && @errors == [] && "bg-transparent",
not @readonly && @errors != [] && "input-error bg-error/10",
@readonly && "cursor-not-allowed bg-base-200"
]}
aria_labelledby={Map.get(assigns, :aria_labelledby)}
>
<div class="flex h-full w-full flex-wrap items-center gap-1 px-2">
<p :if={@selected == []} class="p-0.5 text-sm">{@prompt}</p>
<p :if={@selected == []} class={["p-0.5 text-sm", @readonly && "text-base-content/60"]}>{@prompt}</p>
<.badge
:for={{label, value} <- @selected}
live_resource={@live_resource}
label={label}
value={value}
readonly={@readonly}
name={@name}
/>
</div>
Expand Down Expand Up @@ -302,10 +305,17 @@ defmodule Backpex.Fields.HasMany do
end

attr :live_resource, :atom, required: true
attr :readonly, :boolean, default: false
attr :name, :string, required: true
attr :label, :string, required: true
attr :value, :string, required: true

defp badge(%{readonly: true} = assigns) do
~H"""
<span class="badge badge-sm badge-soft">{@label}</span>
"""
end

defp badge(assigns) do
~H"""
<div class="badge badge-sm badge-soft badge-primary pointer-events-auto pr-0">
Expand Down
41 changes: 37 additions & 4 deletions lib/backpex/fields/has_many_through.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ defmodule Backpex.Fields.HasManyThrough do
end

The field requires a [`Ecto.Schema.has_many/3`](https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3) relation with a mandatory `through` option in the main schema. Any extra column in the pivot table besides the relational id's must be mapped in the `pivot_fields` option or given a default value.

## Readonly

When the field is readonly, the Actions column (edit/remove) is hidden, the "new relational"
button is disabled, and any pivot and select inputs inside the edit-relation modal are disabled.
See the [readonly](/guides/fields/readonly.md) guide for details.
"""
use Backpex.Field, config_schema: @config_schema
import Ecto.Query
Expand Down Expand Up @@ -259,7 +265,7 @@ defmodule Backpex.Fields.HasManyThrough do
>
{label}
</th>
<th>
<th :if={not @readonly}>
<span class="sr-only">{Backpex.__("Actions", @live_resource)}</span>
</th>
</tr>
Expand Down Expand Up @@ -289,7 +295,7 @@ defmodule Backpex.Fields.HasManyThrough do
{assigns}
/>
</td>
<td>
<td :if={not @readonly}>
<div class="flex items-center space-x-2">
<button
class="cursor-pointer"
Expand Down Expand Up @@ -345,8 +351,15 @@ defmodule Backpex.Fields.HasManyThrough do
field_options={@field}
owner_key={@owner_key}
options={@options}
readonly={@readonly}
/>
<.pivot_field
:for={{name, _field_options} <- @field_options.pivot_fields}
name={name}
form={e}
readonly={@readonly}
{assigns}
/>
<.pivot_field :for={{name, _field_options} <- @field_options.pivot_fields} name={name} form={e} {assigns} />
</div>
<div class="bg-base-200 flex justify-end space-x-4 px-6 py-3">
<button
Expand All @@ -360,7 +373,13 @@ defmodule Backpex.Fields.HasManyThrough do
</div>
</.modal>

<button type="button" class="btn btn-sm btn-outline btn-primary" phx-click="new-relational" phx-target={@myself}>
<button
disabled={@readonly}
type="button"
class="btn btn-sm btn-outline btn-primary"
phx-click="new-relational"
phx-target={@myself}
>
{@relational_title}
</button>

Expand Down Expand Up @@ -448,6 +467,10 @@ defmodule Backpex.Fields.HasManyThrough do
@impl Backpex.Field
def association?(_field), do: true

attr :name, :atom, required: true
attr :form, :any, required: true
attr :readonly, :boolean, default: false

defp pivot_field(assigns) do
name = assigns.name

Expand Down Expand Up @@ -537,6 +560,14 @@ defmodule Backpex.Fields.HasManyThrough do
items
end

attr :form, :any, required: true
attr :hide_label, :boolean, required: true
attr :label, :string, required: true
attr :field_options, :any, required: true
attr :owner_key, :atom, required: true
attr :options, :list, required: true
attr :readonly, :boolean, default: false

defp select_relational_field(assigns) do
~H"""
<Layout.field_container>
Expand All @@ -547,6 +578,8 @@ defmodule Backpex.Fields.HasManyThrough do
type="select"
field={@form[@owner_key]}
options={@options}
disabled={@readonly}
aria-disabled={@readonly}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
Expand Down
9 changes: 8 additions & 1 deletion lib/backpex/fields/inline_crud.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ defmodule Backpex.Fields.InlineCRUD do
}
]
end

## Readonly

When the field is readonly, each nested row's child fields render as readonly, the per-row delete
checkbox is hidden entirely, and the add-row control is hidden entirely. See the
[readonly](/guides/fields/readonly.md) guide for details.
"""
use Backpex.Field, config_schema: @config_schema

Expand Down Expand Up @@ -209,7 +215,7 @@ defmodule Backpex.Fields.InlineCRUD do
)}
</div>

<div class={if f_nested.index == 0, do: "mt-5", else: nil}>
<div :if={not @readonly} class={if f_nested.index == 0, do: "mt-5", else: nil}>
<label for={"#{@name}-checkbox-delete-#{f_nested.index}"}>
<input
id={"#{@name}-checkbox-delete-#{f_nested.index}"}
Expand All @@ -231,6 +237,7 @@ defmodule Backpex.Fields.InlineCRUD do
<input type="hidden" name={"change[#{@name}_delete][]"} tabindex="-1" aria-hidden="true" />
</div>
<input
:if={not @readonly}
name={"change[#{@name}_order][]"}
type="checkbox"
aria-label={Backpex.__("Add entry", @live_resource)}
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/fields/multi_select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ defmodule Backpex.Fields.MultiSelect do
</:label>
<Form.multi_select
field={@form[@name]}
readonly={@readonly}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

undefined attribute "readonly" for component Backpex.HTML.Form.multi_select/1

prompt={@prompt}
not_found_text={@not_found_text}
options={@options}
Expand Down
5 changes: 0 additions & 5 deletions lib/backpex/fields/number.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ defmodule Backpex.Fields.Number do
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
],
readonly: [
doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]}
]
]

Expand Down Expand Up @@ -55,7 +51,6 @@ defmodule Backpex.Fields.Number do
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
readonly={@readonly}
disabled={@readonly}
aria-labelledby={Map.get(assigns, :aria_labelledby)}
/>
</Layout.field_container>
Expand Down
2 changes: 2 additions & 0 deletions lib/backpex/fields/select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ defmodule Backpex.Fields.Select do
field={@form[@name]}
options={@options}
prompt={@prompt}
readonly={@readonly}
disabled={@readonly}
translate_error_fun={Backpex.Field.translate_error_fun(@field_options, assigns)}
help_text={Backpex.Field.help_text(@field_options, assigns)}
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
Expand Down
5 changes: 0 additions & 5 deletions lib/backpex/fields/text.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ defmodule Backpex.Fields.Text do
throttle: [
doc: "Timeout value (in milliseconds) or function that receives the assigns.",
type: {:or, [:pos_integer, {:fun, 1}]}
],
readonly: [
doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.",
type: {:or, [:boolean, {:fun, 1}]}
]
]

Expand Down Expand Up @@ -55,7 +51,6 @@ defmodule Backpex.Fields.Text do
phx-debounce={Backpex.Field.debounce(@field_options, assigns)}
phx-throttle={Backpex.Field.throttle(@field_options, assigns)}
readonly={@readonly}
disabled={@readonly}
aria-labelledby={Map.get(assigns, :aria_labelledby)}
/>
</Layout.field_container>
Expand Down
Loading