From 18113c3f4a7e4c230d61e7b46eabded74bfb0238 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 8 Jan 2026 10:26:32 -0500 Subject: [PATCH 01/20] add fieldset formwidget --- modules/backend/ServiceProvider.php | 2 + modules/backend/formwidgets/FieldSet.php | 80 +++++++++++++++++++ .../fieldset/assets/css/fieldset.css | 9 +++ .../fieldset/assets/less/fieldset.less | 52 ++++++++++++ .../fieldset/partials/_fieldset.php | 9 +++ modules/backend/widgets/Form.php | 17 ++++ 6 files changed, 169 insertions(+) create mode 100644 modules/backend/formwidgets/FieldSet.php create mode 100644 modules/backend/formwidgets/fieldset/assets/css/fieldset.css create mode 100644 modules/backend/formwidgets/fieldset/assets/less/fieldset.less create mode 100644 modules/backend/formwidgets/fieldset/partials/_fieldset.php diff --git a/modules/backend/ServiceProvider.php b/modules/backend/ServiceProvider.php index 338096a147..0f74e2d568 100644 --- a/modules/backend/ServiceProvider.php +++ b/modules/backend/ServiceProvider.php @@ -97,6 +97,7 @@ protected function registerAssetBundles() $combiner->registerBundle('~/modules/backend/formwidgets/codeeditor/assets/less/codeeditor.less'); $combiner->registerBundle('~/modules/backend/formwidgets/repeater/assets/less/repeater.less'); $combiner->registerBundle('~/modules/backend/formwidgets/codeeditor/assets/js/build.js'); + $combiner->registerBundle('~/modules/backend/formwidgets/fieldset/assets/less/fieldset.less'); $combiner->registerBundle('~/modules/backend/formwidgets/fileupload/assets/less/fileupload.less'); $combiner->registerBundle('~/modules/backend/formwidgets/nestedform/assets/less/nestedform.less'); $combiner->registerBundle('~/modules/backend/formwidgets/richeditor/assets/js/build-plugins.js'); @@ -248,6 +249,7 @@ protected function registerBackendWidgets() $manager->registerFormWidget(\Backend\FormWidgets\ColorPicker::class, 'colorpicker'); $manager->registerFormWidget(\Backend\FormWidgets\DataTable::class, 'datatable'); $manager->registerFormWidget(\Backend\FormWidgets\DatePicker::class, 'datepicker'); + $manager->registerFormWidget(\Backend\FormWidgets\FieldSet::class, 'fieldset'); $manager->registerFormWidget(\Backend\FormWidgets\FileUpload::class, 'fileupload'); $manager->registerFormWidget(\Backend\FormWidgets\IconPicker::class, 'iconpicker'); $manager->registerFormWidget(\Backend\FormWidgets\MarkdownEditor::class, 'markdown'); diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php new file mode 100644 index 0000000000..379cd14645 --- /dev/null +++ b/modules/backend/formwidgets/FieldSet.php @@ -0,0 +1,80 @@ + + */ +class FieldSet extends FormWidgetBase +{ + /** + * @inheritDoc + */ + protected $defaultAlias = 'fieldset'; + + /** + * @var array Form configuration + */ + public $form; + + /** + * @var Form form widget reference + */ + protected $formWidget; + + /** + * @inheritDoc + */ + public function init() + { + $this->fillFromConfig([ + 'form', + ]); + + if ($this->formField->disabled) { + $this->previewMode = true; + } + + $config = $this->makeConfig($this->form); + $config->model = $this->model; + $config->data = $this->getLoadValue(); + $config->alias = $this->alias . $this->defaultAlias; + // set arrayName from parent form to save fields to the model + $config->arrayName = $this->getParentForm()->arrayName; + $config->isNested = true; + + $this->formWidget = $widget = $this->makeWidget(Form::class, $config); + $widget->previewMode = $this->previewMode; + $widget->bindToController(); + } + + protected function loadAssets() + { + $this->addCss('css/fieldset.css', 'core'); + } + + public function getFormFields() + { + return $this->formWidget->getFields(); + } + + /** + * @inheritdoc + */ + public function render() + { + $this->prepareVars(); + return $this->makePartial('fieldset'); + } + + public function prepareVars() + { + $this->formWidget->previewMode = $this->previewMode; + } +} diff --git a/modules/backend/formwidgets/fieldset/assets/css/fieldset.css b/modules/backend/formwidgets/fieldset/assets/css/fieldset.css new file mode 100644 index 0000000000..7777594430 --- /dev/null +++ b/modules/backend/formwidgets/fieldset/assets/css/fieldset.css @@ -0,0 +1,9 @@ +.fieldset{position:relative;min-height:30px;border:1px solid #d1d6d9;border-radius:0.5rem;box-shadow:inset 0 1px 0 rgba(209,214,217,0.25),0 1px 0 rgba(255,255,255,.5);margin-top:5px;padding:1em 1.25em 0 1.25em;background:#f5f5f5} +@media (prefers-color-scheme:dark){.fieldset{background:#000}} +.fieldset>legend{border:none;margin:0;padding-inline:1rem;width:fit-content;font-size:18px;color:inherit} +.fieldset .control-tabs.primary-tabs .nav-tabs{margin:0} +.fieldset .control-tabs.primary-tabs .nav-tabs>li.active>a:before{background-color:#f5f5f5;border-color:#f5f5f5} +@media (prefers-color-scheme:dark){.fieldset .control-tabs.primary-tabs .nav-tabs>li.active>a:before{background-color:#000;border-color:#000}} +.fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:before, +.fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:after{background-color:#f5f5f5} +@media (prefers-color-scheme:dark){.fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:before,.fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:after{background-color:#000}} \ No newline at end of file diff --git a/modules/backend/formwidgets/fieldset/assets/less/fieldset.less b/modules/backend/formwidgets/fieldset/assets/less/fieldset.less new file mode 100644 index 0000000000..63f89ba56e --- /dev/null +++ b/modules/backend/formwidgets/fieldset/assets/less/fieldset.less @@ -0,0 +1,52 @@ +@import "../../../../assets/less/core/boot.less"; + +@panel-bg: #f5f5f5;; +@panel-bg-dark: #000; + +.fieldset { + position: relative; + min-height: 30px; + + border: 1px solid @input-border; + border-radius: 0.5rem; + box-shadow: @input-box-shadow; + + margin-top: 5px; + padding: 1em 1.25em 0 1.25em; + + background: @panel-bg; + @media (prefers-color-scheme: dark) { + background: @panel-bg-dark; + } + + & > legend { + border: none; + margin: 0; + padding-inline: 1rem; + width: fit-content; + font-size: 18px; + color: inherit; + } + .control-tabs.primary-tabs { + .nav-tabs { + margin: 0; + > li.active > a:before { + background-color: @panel-bg; + border-color: @panel-bg; + @media (prefers-color-scheme: dark) { + background-color: @panel-bg-dark; + border-color: @panel-bg-dark; + } + } + + > li > a > span.title { + &:before, &:after { + background-color: @panel-bg; + @media (prefers-color-scheme: dark) { + background-color: @panel-bg-dark; + } + } + } + } + } +} diff --git a/modules/backend/formwidgets/fieldset/partials/_fieldset.php b/modules/backend/formwidgets/fieldset/partials/_fieldset.php new file mode 100644 index 0000000000..2ca483ecf2 --- /dev/null +++ b/modules/backend/formwidgets/fieldset/partials/_fieldset.php @@ -0,0 +1,9 @@ +config, 'placeholder'); ?> + +
+ + + + + formWidget->render() ?> +
diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index 5598810d93..50dcf45996 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -1221,6 +1221,23 @@ public function getSaveData(): array continue; } + // get nested widget fields that should be saved to the model + if (method_exists($widget, 'getFormFields')) { + foreach ($widget->getFormFields() as $field) { + $parts = HtmlHelper::nameToArray($field->fieldName); + if (($value = $this->dataArrayGet($data, $parts)) !== null) { + /* + * Number fields should be converted to integers + */ + if ($field->type === 'number') { + $value = !strlen(trim($value)) ? null : (float) $value; + } + + $this->dataArraySet($result, $parts, $value); + } + } + } + // Exclude fields that didn't provide any value $fieldValue = $this->dataArrayGet($result, $parts, FormField::NO_SAVE_DATA); if ($fieldValue === FormField::NO_SAVE_DATA) { From b53bc996572069348280d7ab775852765e78cc90 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 8 Jan 2026 10:47:56 -0500 Subject: [PATCH 02/20] remove extra semicolon --- modules/backend/formwidgets/fieldset/assets/less/fieldset.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/backend/formwidgets/fieldset/assets/less/fieldset.less b/modules/backend/formwidgets/fieldset/assets/less/fieldset.less index 63f89ba56e..ced4194824 100644 --- a/modules/backend/formwidgets/fieldset/assets/less/fieldset.less +++ b/modules/backend/formwidgets/fieldset/assets/less/fieldset.less @@ -1,6 +1,6 @@ @import "../../../../assets/less/core/boot.less"; -@panel-bg: #f5f5f5;; +@panel-bg: #f5f5f5; @panel-bg-dark: #000; .fieldset { From bebc32ea3ff70f492bdac2179ec4ea702138188d Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 8 Jan 2026 12:23:57 -0500 Subject: [PATCH 03/20] use label instead of placeholder for fieldset legend --- modules/backend/formwidgets/FieldSet.php | 2 ++ modules/backend/formwidgets/fieldset/partials/_fieldset.php | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 379cd14645..e78e463eeb 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -23,6 +23,8 @@ class FieldSet extends FormWidgetBase */ public $form; + public $showLabels = false; + /** * @var Form form widget reference */ diff --git a/modules/backend/formwidgets/fieldset/partials/_fieldset.php b/modules/backend/formwidgets/fieldset/partials/_fieldset.php index 2ca483ecf2..23301b0317 100644 --- a/modules/backend/formwidgets/fieldset/partials/_fieldset.php +++ b/modules/backend/formwidgets/fieldset/partials/_fieldset.php @@ -1,8 +1,8 @@ -config, 'placeholder'); ?> +config, 'label'); ?>
- - + + formWidget->render() ?> From 9165f933638c0a643e8b3c5bee667aeb8c925d48 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 13 Jan 2026 21:59:19 -0500 Subject: [PATCH 04/20] add missing docblock --- modules/backend/formwidgets/FieldSet.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index e78e463eeb..00098ebd51 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -23,6 +23,10 @@ class FieldSet extends FormWidgetBase */ public $form; + + /** + * @var bool Determines if this form field should display comments and labels. + */ public $showLabels = false; /** From 2422ce1d0ce7748dbda1094745fc428107a5f76d Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Fri, 20 Feb 2026 03:11:10 -0600 Subject: [PATCH 05/20] Apply suggestion from @LukeTowers --- modules/backend/formwidgets/FieldSet.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 00098ebd51..9600b29a6f 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -23,7 +23,6 @@ class FieldSet extends FormWidgetBase */ public $form; - /** * @var bool Determines if this form field should display comments and labels. */ From b7cf6091a9616632432ca76d6e850586b6ff2935 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 20 Feb 2026 11:24:06 -0500 Subject: [PATCH 06/20] only use outside fields as this is only for defining the grouped fields --- modules/backend/formwidgets/FieldSet.php | 7 +++++-- .../backend/formwidgets/fieldset/partials/_fieldset.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 9600b29a6f..3db32e48ea 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -64,9 +64,12 @@ protected function loadAssets() $this->addCss('css/fieldset.css', 'core'); } - public function getFormFields() + /** + * return the internal formwidget's outside fields + */ + public function getFormFields(): array { - return $this->formWidget->getFields(); + return $this->formWidget->fields; } /** diff --git a/modules/backend/formwidgets/fieldset/partials/_fieldset.php b/modules/backend/formwidgets/fieldset/partials/_fieldset.php index 23301b0317..99e07bba3c 100644 --- a/modules/backend/formwidgets/fieldset/partials/_fieldset.php +++ b/modules/backend/formwidgets/fieldset/partials/_fieldset.php @@ -5,5 +5,5 @@ - formWidget->render() ?> + formWidget->render(['section' => 'outside']) ?>
From b7442e177117b0ddd7b4b052918b6dddcbaa3cb5 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 20 Feb 2026 12:10:50 -0500 Subject: [PATCH 07/20] escape the label. Co-authored-by: Luke Towers --- modules/backend/formwidgets/fieldset/partials/_fieldset.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/backend/formwidgets/fieldset/partials/_fieldset.php b/modules/backend/formwidgets/fieldset/partials/_fieldset.php index 99e07bba3c..6da5db39db 100644 --- a/modules/backend/formwidgets/fieldset/partials/_fieldset.php +++ b/modules/backend/formwidgets/fieldset/partials/_fieldset.php @@ -2,7 +2,7 @@
- + formWidget->render(['section' => 'outside']) ?> From b78bc053a2b8be03baa278283add6cc64b290781 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 20 Feb 2026 12:14:21 -0500 Subject: [PATCH 08/20] fix codestyle --- modules/backend/formwidgets/FieldSet.php | 1 + .../fieldset/assets/less/fieldset.less | 36 +++++++++---------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 3db32e48ea..9e8dde55f9 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -1,4 +1,5 @@ legend { - border: none; - margin: 0; - padding-inline: 1rem; - width: fit-content; - font-size: 18px; - color: inherit; + border: none; + margin: 0; + padding-inline: 1rem; + width: fit-content; + font-size: 18px; + color: inherit; } .control-tabs.primary-tabs { .nav-tabs { margin: 0; > li.active > a:before { - background-color: @panel-bg; - border-color: @panel-bg; - @media (prefers-color-scheme: dark) { - background-color: @panel-bg-dark; - border-color: @panel-bg-dark; - } + background-color: @panel-bg; + border-color: @panel-bg; + @media (prefers-color-scheme: dark) { + background-color: @panel-bg-dark; + border-color: @panel-bg-dark; + } } > li > a > span.title { - &:before, &:after { - background-color: @panel-bg; - @media (prefers-color-scheme: dark) { - background-color: @panel-bg-dark; + &:before, &:after { + background-color: @panel-bg; + @media (prefers-color-scheme: dark) { + background-color: @panel-bg-dark; + } } - } } } } From 02a0fdd76890e7c9354a5681f9ad55cac7990ff9 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Fri, 20 Feb 2026 12:34:51 -0500 Subject: [PATCH 09/20] we need fields, not just fields configs --- modules/backend/formwidgets/FieldSet.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 9e8dde55f9..03182babef 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -66,11 +66,11 @@ protected function loadAssets() } /** - * return the internal formwidget's outside fields + * return the internal formwidget's fields */ public function getFormFields(): array { - return $this->formWidget->fields; + return $this->formWidget->getFields(); } /** From 11faa45c7fc1d5ae447e50052f5b2f2687696f44 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 10 Mar 2026 17:04:17 -0400 Subject: [PATCH 10/20] add application exception for unsupported fields --- modules/backend/formwidgets/FieldSet.php | 10 +++++++++- modules/backend/lang/en/lang.php | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 03182babef..453c76261e 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -2,6 +2,7 @@ namespace Backend\FormWidgets; +use ApplicationException; use Backend\Classes\FormWidgetBase; use Backend\Widgets\Form; @@ -70,7 +71,14 @@ protected function loadAssets() */ public function getFormFields(): array { - return $this->formWidget->getFields(); + $fields = $this->formWidget->getFields(); + + foreach ($fields as $field) { + if ($this->model->hasRelation($field->fieldName)) { + throw new ApplicationException(trans('backend::lang.fieldset.relation-not-supported',['field' => $field->fieldName])); + } + } + return $fields; } /** diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index 151b11114d..a4cfdb4c4d 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -244,6 +244,9 @@ 'column_switch_true' => 'Yes', 'column_switch_false' => 'No', ], + 'fieldset' => [ + 'relation-not-supported' => ':field: model relation fields are not supported by the fieldset formwidget', + ], 'fileupload' => [ 'attachment' => 'Attachment', 'help' => 'Add a title and description for this attachment.', From 9c885c01cf5ac8ffc4fcb654d35f7504b571b0d3 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 10 Mar 2026 17:20:50 -0400 Subject: [PATCH 11/20] add missing space after coma --- modules/backend/formwidgets/FieldSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 453c76261e..79138ce66f 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -75,7 +75,7 @@ public function getFormFields(): array foreach ($fields as $field) { if ($this->model->hasRelation($field->fieldName)) { - throw new ApplicationException(trans('backend::lang.fieldset.relation-not-supported',['field' => $field->fieldName])); + throw new ApplicationException(trans('backend::lang.fieldset.relation-not-supported', ['field' => $field->fieldName])); } } return $fields; From 116c95c81b00321dabddf71cfc7de8a7bf7457ee Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 10 Mar 2026 18:43:33 -0400 Subject: [PATCH 12/20] add support for formwidgets as fieldset internal fields --- modules/backend/formwidgets/FieldSet.php | 24 +++++++++++++++++------- modules/backend/lang/en/lang.php | 3 --- modules/backend/widgets/Form.php | 10 ++++++---- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 79138ce66f..6792c042a1 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -3,6 +3,7 @@ namespace Backend\FormWidgets; use ApplicationException; +use Backend\Classes\FormField; use Backend\Classes\FormWidgetBase; use Backend\Widgets\Form; @@ -71,14 +72,15 @@ protected function loadAssets() */ public function getFormFields(): array { - $fields = $this->formWidget->getFields(); + return $this->formWidget->getFields(); + } - foreach ($fields as $field) { - if ($this->model->hasRelation($field->fieldName)) { - throw new ApplicationException(trans('backend::lang.fieldset.relation-not-supported', ['field' => $field->fieldName])); - } - } - return $fields; + /** + * return an internal formwidget's formWidget + */ + public function getFormWidget($field): FormWidgetBase + { + return $this->formWidget->getFormWidget($field); } /** @@ -94,4 +96,12 @@ public function prepareVars() { $this->formWidget->previewMode = $this->previewMode; } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + return $this->formWidget->getSaveValue($value); + } } diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index a4cfdb4c4d..151b11114d 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -244,9 +244,6 @@ 'column_switch_true' => 'Yes', 'column_switch_false' => 'No', ], - 'fieldset' => [ - 'relation-not-supported' => ':field: model relation fields are not supported by the fieldset formwidget', - ], 'fileupload' => [ 'attachment' => 'Attachment', 'help' => 'Add a title and description for this attachment.', diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index 50dcf45996..f328f3de9e 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -6,6 +6,7 @@ use Backend\Classes\FormWidgetBase; use Backend\Classes\WidgetBase; use Backend\Classes\WidgetManager; +use Backend\FormWidgets\FieldSet; use BackendAuth; use Exception; use Form as FormHelper; @@ -1222,20 +1223,21 @@ public function getSaveData(): array } // get nested widget fields that should be saved to the model - if (method_exists($widget, 'getFormFields')) { + if ($widget instanceof FieldSet) { foreach ($widget->getFormFields() as $field) { $parts = HtmlHelper::nameToArray($field->fieldName); if (($value = $this->dataArrayGet($data, $parts)) !== null) { - /* - * Number fields should be converted to integers - */ if ($field->type === 'number') { $value = !strlen(trim($value)) ? null : (float) $value; } + if ($field->type === 'widget') { + $value = $widget->getFormWidget($field->fieldName)->getSaveValue($value); + } $this->dataArraySet($result, $parts, $value); } } + continue; } // Exclude fields that didn't provide any value From f13f0249bd2b04477e7b48ef9de8043f1e36b343 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 10 Mar 2026 18:52:42 -0400 Subject: [PATCH 13/20] improve style --- modules/backend/formwidgets/FieldSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 6792c042a1..2e9c907fdb 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -57,7 +57,7 @@ public function init() $config->arrayName = $this->getParentForm()->arrayName; $config->isNested = true; - $this->formWidget = $widget = $this->makeWidget(Form::class, $config); + $widget = $this->formWidget = $this->makeWidget(Form::class, $config); $widget->previewMode = $this->previewMode; $widget->bindToController(); } From f133d71691b428534774dbf40fc82a6bf5674223 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 10 Mar 2026 18:56:37 -0400 Subject: [PATCH 14/20] use switch statement --- modules/backend/widgets/Form.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index f328f3de9e..97dfb0f597 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -1227,13 +1227,14 @@ public function getSaveData(): array foreach ($widget->getFormFields() as $field) { $parts = HtmlHelper::nameToArray($field->fieldName); if (($value = $this->dataArrayGet($data, $parts)) !== null) { - if ($field->type === 'number') { - $value = !strlen(trim($value)) ? null : (float) $value; + switch ($field->type) { + case 'number': + $value = !strlen(trim($value)) ? null : (float) $value; + break; + case 'widget': + $value = $widget->getFormWidget($field->fieldName)->getSaveValue($value); + break; } - if ($field->type === 'widget') { - $value = $widget->getFormWidget($field->fieldName)->getSaveValue($value); - } - $this->dataArraySet($result, $parts, $value); } } From 5458caeea8e276de432f3d0dd015b7a0bf35a397 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 10 Mar 2026 19:14:10 -0400 Subject: [PATCH 15/20] does not save anything itself --- modules/backend/formwidgets/FieldSet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 2e9c907fdb..cd733d8350 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -102,6 +102,6 @@ public function prepareVars() */ public function getSaveValue($value) { - return $this->formWidget->getSaveValue($value); + return FormField::NO_SAVE_DATA; } } From 4cfe2f83dc16e4e476f2fdf0a4bf0367adf3c158 Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Thu, 2 Jul 2026 16:55:14 -0400 Subject: [PATCH 16/20] only use fields config --- modules/backend/formwidgets/FieldSet.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index cd733d8350..9a7d46ca82 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -42,14 +42,14 @@ class FieldSet extends FormWidgetBase public function init() { $this->fillFromConfig([ - 'form', + 'fields', ]); if ($this->formField->disabled) { $this->previewMode = true; } - $config = $this->makeConfig($this->form); + $config = $this->makeConfig(['fields' => $this->fields]); $config->model = $this->model; $config->data = $this->getLoadValue(); $config->alias = $this->alias . $this->defaultAlias; From b6a5d3b200f90ffc74446c946d310b54bc32393d Mon Sep 17 00:00:00 2001 From: Meindert <89913092+AIC-BV@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:15:43 +0200 Subject: [PATCH 17/20] Add fields property to FieldSet class --- modules/backend/formwidgets/FieldSet.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 9a7d46ca82..30da54b084 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -26,6 +26,11 @@ class FieldSet extends FormWidgetBase */ public $form; + /** + * @var array Field configuration + */ + public $fields; + /** * @var bool Determines if this form field should display comments and labels. */ From dc73d97a9ed04b6b0df97eb2a59c4c446c03658d Mon Sep 17 00:00:00 2001 From: Marc Jauvin Date: Tue, 4 Aug 2026 07:18:21 -0400 Subject: [PATCH 18/20] remove unused $form property --- modules/backend/formwidgets/FieldSet.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 30da54b084..78ba88dd42 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -21,11 +21,6 @@ class FieldSet extends FormWidgetBase */ protected $defaultAlias = 'fieldset'; - /** - * @var array Form configuration - */ - public $form; - /** * @var array Field configuration */ From cbba547ed79b9f4965d6ad26ceb7916f649b2700 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 19 Aug 2026 15:38:43 -0600 Subject: [PATCH 19/20] Fix broken darkmode on fieldset formwidget --- .../formwidgets/fieldset/assets/css/fieldset.css | 3 --- .../formwidgets/fieldset/assets/less/fieldset.less | 11 ----------- 2 files changed, 14 deletions(-) diff --git a/modules/backend/formwidgets/fieldset/assets/css/fieldset.css b/modules/backend/formwidgets/fieldset/assets/css/fieldset.css index 7777594430..933d9b103f 100644 --- a/modules/backend/formwidgets/fieldset/assets/css/fieldset.css +++ b/modules/backend/formwidgets/fieldset/assets/css/fieldset.css @@ -1,9 +1,6 @@ .fieldset{position:relative;min-height:30px;border:1px solid #d1d6d9;border-radius:0.5rem;box-shadow:inset 0 1px 0 rgba(209,214,217,0.25),0 1px 0 rgba(255,255,255,.5);margin-top:5px;padding:1em 1.25em 0 1.25em;background:#f5f5f5} -@media (prefers-color-scheme:dark){.fieldset{background:#000}} .fieldset>legend{border:none;margin:0;padding-inline:1rem;width:fit-content;font-size:18px;color:inherit} .fieldset .control-tabs.primary-tabs .nav-tabs{margin:0} .fieldset .control-tabs.primary-tabs .nav-tabs>li.active>a:before{background-color:#f5f5f5;border-color:#f5f5f5} -@media (prefers-color-scheme:dark){.fieldset .control-tabs.primary-tabs .nav-tabs>li.active>a:before{background-color:#000;border-color:#000}} .fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:before, .fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:after{background-color:#f5f5f5} -@media (prefers-color-scheme:dark){.fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:before,.fieldset .control-tabs.primary-tabs .nav-tabs>li>a>span.title:after{background-color:#000}} \ No newline at end of file diff --git a/modules/backend/formwidgets/fieldset/assets/less/fieldset.less b/modules/backend/formwidgets/fieldset/assets/less/fieldset.less index b220451f27..3ae5a9e261 100644 --- a/modules/backend/formwidgets/fieldset/assets/less/fieldset.less +++ b/modules/backend/formwidgets/fieldset/assets/less/fieldset.less @@ -1,7 +1,6 @@ @import "../../../../assets/less/core/boot.less"; @panel-bg: #f5f5f5; -@panel-bg-dark: #000; .fieldset { position: relative; @@ -15,9 +14,6 @@ padding: 1em 1.25em 0 1.25em; background: @panel-bg; - @media (prefers-color-scheme: dark) { - background: @panel-bg-dark; - } & > legend { border: none; @@ -33,18 +29,11 @@ > li.active > a:before { background-color: @panel-bg; border-color: @panel-bg; - @media (prefers-color-scheme: dark) { - background-color: @panel-bg-dark; - border-color: @panel-bg-dark; - } } > li > a > span.title { &:before, &:after { background-color: @panel-bg; - @media (prefers-color-scheme: dark) { - background-color: @panel-bg-dark; - } } } } From eb4168c5c3f2dba2262e1149c81314e85234fbb5 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 19 Aug 2026 15:39:25 -0600 Subject: [PATCH 20/20] Fix fieldset saving logic and add tests --- modules/backend/formwidgets/FieldSet.php | 19 +- .../tests/widgets/FormFieldSetSaveTest.php | 196 ++++++++++++++++++ modules/backend/widgets/Form.php | 35 ++-- 3 files changed, 217 insertions(+), 33 deletions(-) create mode 100644 modules/backend/tests/widgets/FormFieldSetSaveTest.php diff --git a/modules/backend/formwidgets/FieldSet.php b/modules/backend/formwidgets/FieldSet.php index 78ba88dd42..70da8e0736 100644 --- a/modules/backend/formwidgets/FieldSet.php +++ b/modules/backend/formwidgets/FieldSet.php @@ -2,7 +2,6 @@ namespace Backend\FormWidgets; -use ApplicationException; use Backend\Classes\FormField; use Backend\Classes\FormWidgetBase; use Backend\Widgets\Form; @@ -68,19 +67,15 @@ protected function loadAssets() } /** - * return the internal formwidget's fields + * Returns the save data for the nested fields, to be merged into the parent + * form's data as if these fields were defined at that level. Reusing the nested + * form's getSaveData() ensures number casting, widget getSaveValue() handling, + * NO_SAVE_DATA exclusion and disabled/hidden skipping all behave identically to + * a regular field. */ - public function getFormFields(): array + public function getSaveData(): array { - return $this->formWidget->getFields(); - } - - /** - * return an internal formwidget's formWidget - */ - public function getFormWidget($field): FormWidgetBase - { - return $this->formWidget->getFormWidget($field); + return $this->formWidget->getSaveData(); } /** diff --git a/modules/backend/tests/widgets/FormFieldSetSaveTest.php b/modules/backend/tests/widgets/FormFieldSetSaveTest.php new file mode 100644 index 0000000000..988cba7f62 --- /dev/null +++ b/modules/backend/tests/widgets/FormFieldSetSaveTest.php @@ -0,0 +1,196 @@ +registerFormWidget(FieldSet::class, 'fieldset'); + WidgetManager::instance()->registerFormWidget(FieldSetSaveStubWidget::class, 'fieldsetsavestub'); + WidgetManager::instance()->registerFormWidget(FieldSetNoSaveStubWidget::class, 'fieldsetnosavestub'); + } + + protected function makeForm(): Form + { + return new Form(new Controller, [ + 'model' => new FormFieldSetSaveTestModel, + 'arrayName' => 'array', + 'fields' => [ + 'top_level' => [ + 'type' => 'text', + ], + 'group' => [ + 'type' => 'fieldset', + 'label' => 'Grouped Fields', + 'fields' => [ + 'nested_text' => [ + 'type' => 'text', + ], + 'nested_number' => [ + 'type' => 'number', + ], + 'nested_widget' => [ + 'type' => 'fieldsetsavestub', + ], + 'nested_nosave' => [ + 'type' => 'fieldsetnosavestub', + ], + 'nested_disabled' => [ + 'type' => 'text', + 'disabled' => true, + ], + ], + ], + ], + ]); + } + + /** + * getSaveData() reads its values with post(), which only returns real data when + * the request is genuinely a POST -- so simulate the postback first. + */ + protected function postForm(array $data): array + { + request()->setMethod('POST'); + request()->request->replace(['array' => $data]); + + return $this->makeForm()->getSaveData(); + } + + public function testNestedFieldSetFieldsAreCollectedToParentLevel() + { + $data = $this->postForm([ + 'top_level' => 'top', + 'nested_text' => 'hello', + 'nested_number' => '42', + ]); + + // Nested fields are hoisted to the parent save data... + $this->assertArrayHasKey('nested_text', $data); + $this->assertArrayHasKey('nested_number', $data); + $this->assertEquals('hello', $data['nested_text']); + + // ...and the top level field is unaffected. + $this->assertEquals('top', $data['top_level']); + + // The fieldset container itself must not be saved. + $this->assertArrayNotHasKey('group', $data); + } + + public function testNestedNumberFieldIsCastToFloat() + { + $data = $this->postForm([ + 'nested_number' => '42', + ]); + + $this->assertSame(42.0, $data['nested_number']); + } + + public function testNestedNumberEmptyStringBecomesNull() + { + $data = $this->postForm([ + 'nested_number' => ' ', + ]); + + $this->assertArrayHasKey('nested_number', $data); + $this->assertNull($data['nested_number']); + } + + public function testNestedWidgetFieldHasSaveValueApplied() + { + $data = $this->postForm([ + 'nested_widget' => 'abc', + ]); + + // The nested widget's getSaveValue() must be applied (stub upper-cases it), + // rather than the raw posted value being stored. + $this->assertArrayHasKey('nested_widget', $data); + $this->assertSame('ABC', $data['nested_widget']); + } + + public function testNestedWidgetReturningNoSaveDataDoesNotLeakSentinel() + { + $data = $this->postForm([ + 'nested_nosave' => 'ignore me', + ]); + + // A nested widget that returns NO_SAVE_DATA must behave exactly as it would + // at the top level of a form: the NO_SAVE_DATA sentinel (-1) must never be + // written to the model. + $this->assertNotSame(FormField::NO_SAVE_DATA, $data['nested_nosave'] ?? null); + } + + public function testDisabledNestedFieldIsNotSaved() + { + $data = $this->postForm([ + 'nested_text' => 'hello', + 'nested_disabled' => 'tampered', + ]); + + // Disabled fields are omitted from the save data, just like top-level fields. + $this->assertArrayHasKey('nested_text', $data); + $this->assertArrayNotHasKey('nested_disabled', $data); + } + + public function testMissingNestedValuesAreOmitted() + { + $data = $this->postForm([ + 'top_level' => 'top', + ]); + + $this->assertArrayNotHasKey('nested_text', $data); + $this->assertArrayNotHasKey('nested_number', $data); + } + } +} diff --git a/modules/backend/widgets/Form.php b/modules/backend/widgets/Form.php index 97dfb0f597..2fafa88a2f 100644 --- a/modules/backend/widgets/Form.php +++ b/modules/backend/widgets/Form.php @@ -1,18 +1,20 @@ -getFormFields() as $field) { - $parts = HtmlHelper::nameToArray($field->fieldName); - if (($value = $this->dataArrayGet($data, $parts)) !== null) { - switch ($field->type) { - case 'number': - $value = !strlen(trim($value)) ? null : (float) $value; - break; - case 'widget': - $value = $widget->getFormWidget($field->fieldName)->getSaveValue($value); - break; - } - $this->dataArraySet($result, $parts, $value); - } + foreach ($widget->getSaveData() as $key => $value) { + $this->dataArraySet($result, HtmlHelper::nameToArray($key), $value); } continue; } @@ -1439,8 +1432,8 @@ protected function objectMethodExists($object, $method) * * @param array $array * @param array $parts - * @param null $default - * @return array|null + * @param mixed $default + * @return mixed */ protected function dataArrayGet(array $array, array $parts, $default = null) {