From 21cfefe86f48d84896afbe8e97a30b2d7cd087ab Mon Sep 17 00:00:00 2001 From: Giacomo Mazzamuto Date: Tue, 25 Nov 2025 00:28:03 +0100 Subject: [PATCH 1/2] Support readonly for all fields The `:readonly` field option, being shared among all fields, has been moved to the `@config_schema` of `Backpex.Field`. --- lib/backpex/field.ex | 5 +++++ lib/backpex/fields/belongs_to.ex | 1 + lib/backpex/fields/boolean.ex | 1 + lib/backpex/fields/currency.ex | 1 + lib/backpex/fields/date.ex | 4 ---- lib/backpex/fields/date_time.ex | 4 ---- lib/backpex/fields/email.ex | 4 ---- lib/backpex/fields/has_many.ex | 12 ++++++++++-- lib/backpex/fields/has_many_through.ex | 12 +++++++++--- lib/backpex/fields/inline_crud.ex | 3 ++- lib/backpex/fields/multi_select.ex | 1 + lib/backpex/fields/number.ex | 4 ---- lib/backpex/fields/select.ex | 1 + lib/backpex/fields/text.ex | 4 ---- lib/backpex/fields/textarea.ex | 4 ---- lib/backpex/fields/time.ex | 4 ---- lib/backpex/fields/upload.ex | 14 +++++++++---- lib/backpex/fields/url.ex | 1 + lib/backpex/html/core_components.ex | 22 +++++++++++++++++++-- lib/backpex/html/form.ex | 27 ++++++++++++++++++++++++-- lib/backpex/html/resource.ex | 8 +++++++- 21 files changed, 94 insertions(+), 43 deletions(-) diff --git a/lib/backpex/field.ex b/lib/backpex/field.ex index 7dc82554d..d34c41c72 100644 --- a/lib/backpex/field.ex +++ b/lib/backpex/field.ex @@ -11,6 +11,11 @@ defmodule Backpex.Field do type: :string, required: true ], + readonly: [ + doc: "Sets the field to readonly. Also see the [panels](/guides/fields/readonly.md) guide.", + type: {:or, [:boolean, {:fun, 1}]}, + default: false + ], class: [ type: {:or, [:string, {:fun, 1}]}, doc: """ diff --git a/lib/backpex/fields/belongs_to.ex b/lib/backpex/fields/belongs_to.ex index 20e9a2493..271b2a462 100644 --- a/lib/backpex/fields/belongs_to.ex +++ b/lib/backpex/fields/belongs_to.ex @@ -143,6 +143,7 @@ defmodule Backpex.Fields.BelongsTo do field={@form[@owner_key]} options={@options} prompt={@prompt} + 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)} diff --git a/lib/backpex/fields/boolean.ex b/lib/backpex/fields/boolean.ex index 9345db88d..3f0e71e33 100644 --- a/lib/backpex/fields/boolean.ex +++ b/lib/backpex/fields/boolean.ex @@ -42,6 +42,7 @@ defmodule Backpex.Fields.Boolean do - + <:trigger class={[ "input block h-fit w-full p-2", @@ -154,12 +154,13 @@ defmodule Backpex.Fields.HasMany do aria_labelledby={Map.get(assigns, :aria_labelledby)} >
-

{@prompt}

+

{@prompt}

<.badge :for={{label, value} <- @selected} live_resource={@live_resource} label={label} value={value} + readonly={@readonly} name={@name} />
@@ -302,10 +303,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""" + {@label} + """ + end + defp badge(assigns) do ~H"""
diff --git a/lib/backpex/fields/has_many_through.ex b/lib/backpex/fields/has_many_through.ex index c78ae55b5..6fccaac50 100644 --- a/lib/backpex/fields/has_many_through.ex +++ b/lib/backpex/fields/has_many_through.ex @@ -259,7 +259,7 @@ defmodule Backpex.Fields.HasManyThrough do > {label} - + {Backpex.__("Actions", @live_resource)} @@ -289,7 +289,7 @@ defmodule Backpex.Fields.HasManyThrough do {assigns} /> - +
diff --git a/lib/backpex/fields/inline_crud.ex b/lib/backpex/fields/inline_crud.ex index a0388ec62..6768dce51 100644 --- a/lib/backpex/fields/inline_crud.ex +++ b/lib/backpex/fields/inline_crud.ex @@ -187,7 +187,7 @@ defmodule Backpex.Fields.InlineCRUD do class="hidden" /> -
+
{Backpex.__("Delete", @live_resource)}
@@ -199,6 +199,7 @@ defmodule Backpex.Fields.InlineCRUD do
diff --git a/lib/backpex/html/core_components.ex b/lib/backpex/html/core_components.ex index 8d31e6858..c8e065bd8 100644 --- a/lib/backpex/html/core_components.ex +++ b/lib/backpex/html/core_components.ex @@ -38,6 +38,7 @@ defmodule Backpex.HTML.CoreComponents do """ attr :id, :string, required: true, doc: "unique identifier for the dropdown" + attr :readonly, :boolean, default: false attr :class, :any, default: nil, doc: "additional classes for the outer container element" slot :trigger, doc: "the trigger element to be used to toggle the dropdown menu" do @@ -64,8 +65,24 @@ defmodule Backpex.HTML.CoreComponents do _trigger -> nil end) + 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 + + assigns = assign(assigns, trigger_class: trigger_class) + ~H""" -
+
{render_slot(@trigger)}
prepare_field_assigns(field, assigns.translate_error_fun) |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) @@ -205,6 +212,13 @@ defmodule Backpex.HTML.Form do multiple pattern placeholder readonly required rows size step) def currency_input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do + assigns = + if Map.has_key?(assigns.rest, :disabled) do + assigns + else + put_in(assigns, [:rest, :disabled], Map.get(assigns.rest, :readonly) || false) + end + assigns |> prepare_field_assigns(field, assigns.translate_error_fun) |> assign_new(:name, fn -> field.name end) @@ -286,6 +300,7 @@ defmodule Backpex.HTML.Form do @doc type: :component attr :prompt, :string, required: true, doc: "string that will be shown when no option is selected" + attr :readonly, :boolean, default: false attr :help_text, :string, default: nil, doc: "help text to be displayed below input" attr :not_found_text, :string, required: true, doc: "string that will be shown when there are no options" attr :options, :list, required: true, doc: "a list of options for the select" @@ -309,7 +324,7 @@ defmodule Backpex.HTML.Form do ~H"""
- <.dropdown id={"multi-select-#{@field.id}"} class="w-full"> + <.dropdown id={"multi-select-#{@field.id}"} class="w-full" readonly={@readonly}> <:trigger aria_label={@prompt} aria_labelledby={Map.get(assigns, :aria_labelledby)} @@ -320,13 +335,14 @@ defmodule Backpex.HTML.Form do ]} >
-

{@prompt}

+

{@prompt}

<.multi_select_badge :for={{label, value} <- @selected} live_resource={@live_resource} label={label} value={value} event_target={@event_target} + readonly={@readonly} />
@@ -384,10 +400,17 @@ defmodule Backpex.HTML.Form do end attr :live_resource, :atom, required: true + attr :readonly, :boolean, default: false attr :label, :string, required: true attr :value, :any, required: true attr :event_target, :any, required: true + defp multi_select_badge(%{readonly: true} = assigns) do + ~H""" + {@label} + """ + end + defp multi_select_badge(assigns) do ~H"""
diff --git a/lib/backpex/html/resource.ex b/lib/backpex/html/resource.ex index 27c0b98a3..6d6f5c126 100644 --- a/lib/backpex/html/resource.ex +++ b/lib/backpex/html/resource.ex @@ -175,7 +175,13 @@ defmodule Backpex.HTML.Resource do |> assign(:field, field) |> assign(:field_options, field_options) |> assign(:type, :form) - |> assign(:readonly, Backpex.Field.readonly?(field_options, assigns)) + + assigns = + if assigns[:readonly] == true do + assigns + else + assign(assigns, :readonly, Backpex.Field.readonly?(field_options, assigns)) + end ~H""" <.live_component From c2ab6477fc67dea7c6649d0d921b8cb60312d1a8 Mon Sep 17 00:00:00 2001 From: Giacomo Mazzamuto Date: Tue, 23 Dec 2025 17:26:10 +0100 Subject: [PATCH 2/2] Support readonly for all fields: changes for PR review --- lib/backpex/fields/belongs_to.ex | 1 + lib/backpex/fields/boolean.ex | 2 +- lib/backpex/fields/currency.ex | 1 + lib/backpex/fields/inline_crud.ex | 1 + lib/backpex/fields/select.ex | 1 + lib/backpex/fields/url.ex | 1 + lib/backpex/html/core_components.ex | 2 +- lib/backpex/html/form.ex | 16 +--------------- lib/backpex/html/resource.ex | 3 ++- 9 files changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/backpex/fields/belongs_to.ex b/lib/backpex/fields/belongs_to.ex index 271b2a462..192a2e112 100644 --- a/lib/backpex/fields/belongs_to.ex +++ b/lib/backpex/fields/belongs_to.ex @@ -144,6 +144,7 @@ defmodule Backpex.Fields.BelongsTo do 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)} diff --git a/lib/backpex/fields/boolean.ex b/lib/backpex/fields/boolean.ex index 3f0e71e33..c33f1782b 100644 --- a/lib/backpex/fields/boolean.ex +++ b/lib/backpex/fields/boolean.ex @@ -42,7 +42,7 @@ defmodule Backpex.Fields.Boolean do
diff --git a/lib/backpex/html/core_components.ex b/lib/backpex/html/core_components.ex index c8e065bd8..b9c70dbba 100644 --- a/lib/backpex/html/core_components.ex +++ b/lib/backpex/html/core_components.ex @@ -38,7 +38,7 @@ defmodule Backpex.HTML.CoreComponents do """ attr :id, :string, required: true, doc: "unique identifier for the dropdown" - attr :readonly, :boolean, default: false + attr :readonly, :boolean, default: false, doc: "whether the dropdown is readonly" attr :class, :any, default: nil, doc: "additional classes for the outer container element" slot :trigger, doc: "the trigger element to be used to toggle the dropdown menu" do diff --git a/lib/backpex/html/form.ex b/lib/backpex/html/form.ex index a141eef4e..9ef09fa2a 100644 --- a/lib/backpex/html/form.ex +++ b/lib/backpex/html/form.ex @@ -47,13 +47,6 @@ defmodule Backpex.HTML.Form do slot :inner_block def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do - assigns = - if Map.has_key?(assigns.rest, :disabled) do - assigns - else - put_in(assigns, [:rest, :disabled], Map.get(assigns.rest, :readonly) || false) - end - assigns |> prepare_field_assigns(field, assigns.translate_error_fun) |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) @@ -212,13 +205,6 @@ defmodule Backpex.HTML.Form do multiple pattern placeholder readonly required rows size step) def currency_input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do - assigns = - if Map.has_key?(assigns.rest, :disabled) do - assigns - else - put_in(assigns, [:rest, :disabled], Map.get(assigns.rest, :readonly) || false) - end - assigns |> prepare_field_assigns(field, assigns.translate_error_fun) |> assign_new(:name, fn -> field.name end) @@ -300,7 +286,7 @@ defmodule Backpex.HTML.Form do @doc type: :component attr :prompt, :string, required: true, doc: "string that will be shown when no option is selected" - attr :readonly, :boolean, default: false + attr :readonly, :boolean, default: false, doc: "whether the dropdown is readonly" attr :help_text, :string, default: nil, doc: "help text to be displayed below input" attr :not_found_text, :string, required: true, doc: "string that will be shown when there are no options" attr :options, :list, required: true, doc: "a list of options for the select" diff --git a/lib/backpex/html/resource.ex b/lib/backpex/html/resource.ex index 6d6f5c126..107f203ed 100644 --- a/lib/backpex/html/resource.ex +++ b/lib/backpex/html/resource.ex @@ -176,8 +176,9 @@ defmodule Backpex.HTML.Resource do |> assign(:field_options, field_options) |> assign(:type, :form) + # this is needed to apply `:readonly` to individual fields in `Backpex.Fields.InlineCRUD` assigns = - if assigns[:readonly] == true do + if assigns[:readonly] do assigns else assign(assigns, :readonly, Backpex.Field.readonly?(field_options, assigns))