Skip to content

Support readonly for all fields#1677

Open
gmazzamuto wants to merge 13 commits intonaymspace:developfrom
gmazzamuto:feature/readonly-fields
Open

Support readonly for all fields#1677
gmazzamuto wants to merge 13 commits intonaymspace:developfrom
gmazzamuto:feature/readonly-fields

Conversation

@gmazzamuto
Copy link
Copy Markdown
Contributor

Support :readonly option for all fields.

The :readonly field option, being shared among all fields, has been moved to the @config_schema of Backpex.Field. This should not be a breaking change.

Example of readonly fields:

Screenshot 2025-11-25 at 01-24-43 Edit User · Backpex

@Flo0807 Flo0807 added the enhancement Changes that are not breaking label Nov 28, 2025
Comment thread lib/backpex/html/core_components.ex Outdated
Comment thread lib/backpex/html/form.ex Outdated
Comment on lines +50 to +55
assigns =
if Map.has_key?(assigns.rest, :disabled) do
assigns
else
put_in(assigns, [:rest, :disabled], Map.get(assigns.rest, :readonly) || false)
end
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.

Suggested change
assigns =
if Map.has_key?(assigns.rest, :disabled) do
assigns
else
put_in(assigns, [:rest, :disabled], Map.get(assigns.rest, :readonly) || false)
end

Why do we want to set disabled based on the readonly attribute? If you want to set the input to disabled, you should explicitly pass the disabled attribute, in my opinion.

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.

See https://github.com/naymspace/backpex/blob/0.16.3/lib/backpex/fields/text.ex#L58

I noticed that we do not set dsiabled in every field (e.g. Backpex.Fields.Select)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah you're right, I'll review your comments to this PR asap (some time has passed and I can't remember right now). Anyways I was confused by the fact that in Backpex almost all fields have

<BackpexForm.input
  ...
  readonly={@readonly}
  disabled={@readonly}
>

Do you think it makes sense to have two separate options, :readonly and :disabled? After all, they serve different purposes. If that's the case I can work on that and add the :disabled option to Backpex.Field.

Comment thread lib/backpex/html/form.ex Outdated
Comment on lines +215 to +220
assigns =
if Map.has_key?(assigns.rest, :disabled) do
assigns
else
put_in(assigns, [:rest, :disabled], Map.get(assigns.rest, :readonly) || false)
end
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.

Comment thread lib/backpex/html/core_components.ex Outdated
Comment on lines +68 to +80
trigger_class = (assigns.trigger && assigns.trigger[:class]) || ""

trigger_class =
if assigns.readonly do
["cursor-not-allowed bg-base-200"] ++
(trigger_class
|> Enum.join(" ")
|> String.split()
|> List.delete("bg-transparent")
|> List.delete("input"))
else
trigger_class
end
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.

This looks complicated. Can't we just add additional classes in the markup in case the dropdown is readonly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here is why. This is how the dropdown is rendered with the code above:

Screenshot_20251223_173629

Whereas this code:

    trigger_class =
      if assigns.readonly do
        ["cursor-not-allowed bg-base-200"] ++ trigger_class
      else
        trigger_class
      end

results in this, and the field can be even highlighted on mouse click (although the dropdown does not open):

Screenshot_20251223_173556

</: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

Comment thread lib/backpex/html/form.ex Outdated
Comment on lines +179 to +184
assigns =
if assigns[:readonly] == true do
assigns
else
assign(assigns, :readonly, Backpex.Field.readonly?(field_options, assigns))
end
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.

For which cases do we need this?

And can we simplify the if-condition?

Suggested change
assigns =
if assigns[:readonly] == true do
assigns
else
assign(assigns, :readonly, Backpex.Field.readonly?(field_options, assigns))
end
assigns =
if assigns[:readonly] do
assigns
else
assign(assigns, :readonly, Backpex.Field.readonly?(field_options, assigns))
end

Copy link
Copy Markdown
Contributor Author

@gmazzamuto gmazzamuto Dec 23, 2025

Choose a reason for hiding this comment

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

Sure, the if condition can be simplified.

The reason for this code is because the :readonly assign is not always defined when calling the callback Backpex.Field.render_form/1 and also to allow individual fields in Backpex.Fields.InlineCRUD to be set readonly when the parent InlineCRUD field is not readonly. I will add a comment in the code to clarify this.

The `:readonly` field option, being shared among all fields, has been
moved to the `@config_schema` of `Backpex.Field`.
@gmazzamuto gmazzamuto force-pushed the feature/readonly-fields branch from 30ef1e3 to c2ab647 Compare December 23, 2025 17:27
Flo0807 added 11 commits April 22, 2026 15:40
Previously the readonly dropdown kept role="button", tabindex=0, and
aria-haspopup="true" while stripping the menu, producing a focusable
element that announces a popup that isn't there. Also removed the
fragile class-token-surgery that reached into caller class lists to
delete "input" and "bg-transparent" strings; callers now build their
own readonly-aware class lists using Phoenix's list syntax.
The drop target's phx-drop-target attribute was not gated on readonly,
so drag-and-drop still started uploads. Cancel buttons for in-progress
and existing entries remained clickable, letting users mutate file
state. The "Upload a file" <a> rendered as a dead link in readonly
mode — now a plain <span> with the interactive classes removed.
text-base-content/40 on bg-base-200 falls below 4.5:1 on stock
daisyUI themes. Bumped to /60 to match the codebase's standard
muted-text token (used in help_text / labels).
HTML's readonly and disabled have different a11y semantics: disabled
removes the element from the tab order and is frequently skipped by
screen readers in form-review mode, making the readonly value
invisible. For inputs that natively support readonly (text, number,
date, date_time, time, textarea, email, url, currency), keeping only
readonly={@readonly} is correct. Controls without native readonly
(select, multi_select, boolean, relations, upload) continue to use
disabled as before.
Previously the readonly branch removed the delete checkbox but left
an orphaned <label for> pointing at nothing, wrapping a <div
class="btn btn-disabled"> with sr-only "Delete" text. Screen readers
announced a non-interactive "Delete" with a dangling label
association. Same problem for "Add entry" — rendered as a disabled
checkbox-masquerading-as-button. In readonly mode a user has no use
for either control, so they are now omitted entirely.
select_relational_field/1 and pivot_field/1 previously did not
receive the parent field's readonly flag. The main UI already hid
the triggers that invoke them, so no live regression — but any
future path that opens the relation modal (e.g. programmatic
push_event, pre-populated newest_relational) would render an
editable select on a readonly field. Now both components declare
readonly as an explicit attr and forward it to their inputs.
The guide listed only Date, DateTime, Number, Text, Textarea as
readonly-capable. With this PR, readonly is a global Backpex.Field
option and most field types support it. Documented the three
rendering strategies (native-readonly, disabled, custom) and the
per-field quirks for Upload, InlineCRUD, HasManyThrough, Boolean.
…ledocs

These three fields render readonly with custom UI changes that hide
or disable elements beyond the standard readonly/disabled attribute.
Added short Readonly sections to the moduledocs so users discover
this behavior without reading the guide.
Covers the readonly-branch HTML surface of both components:
dropdown has no role/tabindex/aria-haspopup/menu when readonly;
multi_select prompt uses /60 contrast and badges render without
primary color or remove controls. Sets up test/html/ as the home
for future HTML component tests (the repo previously had no
component-level coverage).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Changes that are not breaking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants