Skip to content

Conversation

@mjauvin
Copy link
Member

@mjauvin mjauvin commented Jan 8, 2026

This formwidget wraps form elements with a <fieldset> HTML tag.

The fields are visually grouped but saved as if they were regular fields at the parent level.

The field's label config is used for the <fieldset> legend.

The Form widget's getSaveData() method now checks for a getFormFields() method on widgets in order to pull child fields from the widget.

image

The field config is akin to a nestedform:

myFieldSet:
  type: fieldset
  label: Grouped Fields
  form:
    fields:
      ...

Summary by CodeRabbit

  • New Features

    • Adds a FieldSet form widget to embed a nested form section with an optional legend and nested form behavior.
  • Style

    • Introduces themed styling (including dark-mode) and improved tab visuals for the FieldSet widget.
  • Bug Fixes

    • Ensures fields exposed by nested widgets are collected and saved correctly during form submission.

✏️ Tip: You can customize this high-level summary in your review settings.

@mjauvin mjauvin requested a review from LukeTowers January 8, 2026 15:35
@mjauvin mjauvin self-assigned this Jan 8, 2026
@mjauvin mjauvin added enhancement PRs that implement a new feature or substantial change needs review Issues/PRs that require a review from a maintainer labels Jan 8, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 8, 2026

Walkthrough

Adds a FieldSet backend form widget (class, template, CSS/LESS, asset registration) and extends backend form save logic to collect values from nested widgets that expose getFormFields().

Changes

Cohort / File(s) Summary
FieldSet Form Widget
modules/backend/formwidgets/FieldSet.php
New FormWidget class (FieldSet) that embeds a Form widget, binds model/data/alias, supports nested/preview mode, exposes getFormFields(), loads assets, and renders via a partial.
FieldSet Widget Template
modules/backend/formwidgets/fieldset/partials/_fieldset.php
New partial wrapping the embedded form widget in a <fieldset> and rendering an optional translated label as a legend.
FieldSet Widget Styling
modules/backend/formwidgets/fieldset/assets/css/fieldset.css, modules/backend/formwidgets/fieldset/assets/less/fieldset.less
Adds CSS/LESS for .fieldset appearance, legend, tab visuals and dark-mode variants; LESS imports core boot variables.
Service Registration / Assets
modules/backend/ServiceProvider.php
Registers Backend\FormWidgets\FieldSet::class as alias 'fieldset' and adds .../formwidgets/fieldset/assets/less/fieldset.less to backend asset bundles.
Form Save Handling
modules/backend/widgets/Form.php
getSaveData() extended to detect widgets exposing getFormFields(), extract their submitted values (including numeric conversion for number fields), and merge them into the save payload before standard per-field finalization.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BackendForm as Backend Form
  participant FieldSet as FieldSet Widget
  participant EmbeddedForm as Embedded Form Widget
  participant FormSave as Form.getSaveData()
  participant Model

  User->>BackendForm: Open form (render)
  BackendForm->>FieldSet: init()/prepareVars()
  FieldSet->>EmbeddedForm: create/bind Form widget (model/data/alias)
  EmbeddedForm-->>FieldSet: provides form fields & rendering
  FieldSet-->>BackendForm: render() (includes EmbeddedForm output)

  User->>BackendForm: Submit form
  BackendForm->>FormSave: getSaveData()
  FormSave->>FieldSet: detect widget with getFormFields()
  FieldSet->>FormSave: expose nested field definitions
  FormSave->>FormSave: extract nested values, convert types, merge payload
  FormSave->>Model: persist merged save data
  Model-->>User: save result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add fieldset formwidget' clearly and concisely describes the main change: introducing a new FieldSet form widget. It is specific, meaningful, and directly summarizes the primary feature addition without vague language or noise.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bebc32e and 9165f93.

📒 Files selected for processing (1)
  • modules/backend/formwidgets/FieldSet.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/backend/formwidgets/FieldSet.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: ubuntu-latest / PHP 8.2
  • GitHub Check: windows-latest / PHP 8.2
  • GitHub Check: windows-latest / PHP 8.1
  • GitHub Check: windows-latest / PHP 8.3
  • GitHub Check: ubuntu-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.4
  • GitHub Check: ubuntu-latest / PHP 8.3
  • GitHub Check: windows-latest / PHP 8.4
  • GitHub Check: windows-latest / JavaScript

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @modules/backend/formwidgets/fieldset/assets/less/fieldset.less:
- Line 3: The LESS variable declaration for @panel-bg has a double semicolon
("@panel-bg: #f5f5f5;;"); remove the extra semicolon so the declaration is a
single semicolon terminated statement (update the @panel-bg variable in
fieldset.less).
🧹 Nitpick comments (4)
modules/backend/widgets/Form.php (2)

1224-1239: Consider variable naming to avoid shadowing.

The inner foreach loop reuses the variable name $field (line 1226), which shadows the outer loop variable $field from line 1216. While this doesn't cause functional issues due to scoping, it can reduce code clarity.

♻️ Suggested refactor
             // 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) {
+                foreach ($widget->getFormFields() as $nestedField) {
+                    $parts = HtmlHelper::nameToArray($nestedField->fieldName);
+                    if (($value = $this->dataArrayGet($data, $parts)) !== null) {
                         /*
                          * Number fields should be converted to integers
                          */
-                        if ($field->type === 'number') {
+                        if ($nestedField->type === 'number') {
                             $value = !strlen(trim($value)) ? null : (float) $value;
                         }
 
                         $this->dataArraySet($result, $parts, $value);
                     }
                 }
             }

1228-1237: Consider extracting field value processing logic.

The field value processing logic (lines 1228-1237) duplicates the same logic from lines 1201-1210. While the duplication is minimal and the context differs slightly, extracting this into a helper method could improve maintainability.

♻️ Optional refactor to reduce duplication

Consider adding a protected helper method:

protected function processFieldSaveValue($field, $data, &$result)
{
    $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);
        return true;
    }
    return false;
}

Then both locations could use:

foreach ($this->allFields as $field) {
    if ($field->disabled || $field->hidden) {
        continue;
    }
    $this->processFieldSaveValue($field, $data, $result);
}

// ...and later...

if (method_exists($widget, 'getFormFields')) {
    foreach ($widget->getFormFields() as $nestedField) {
        $this->processFieldSaveValue($nestedField, $data, $result);
    }
}
modules/backend/formwidgets/fieldset/assets/css/fieldset.css (1)

1-9: Note: Recompile after fixing LESS source.

This appears to be the compiled CSS output from the LESS file. After fixing the double semicolon syntax error in fieldset.less (line 3), remember to recompile this CSS file to ensure consistency.

modules/backend/formwidgets/FieldSet.php (1)

21-29: Consider adding type hints to properties.

Adding type declarations would improve IDE support and code clarity.

📝 Suggested type hints
 /**
  * @var array Form configuration
  */
-public $form;
+public array $form;

 /**
  * @var Form form widget reference
  */
-protected $formWidget;
+protected Form $formWidget;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a7f74b and 18113c3.

📒 Files selected for processing (6)
  • modules/backend/ServiceProvider.php
  • modules/backend/formwidgets/FieldSet.php
  • modules/backend/formwidgets/fieldset/assets/css/fieldset.css
  • modules/backend/formwidgets/fieldset/assets/less/fieldset.less
  • modules/backend/formwidgets/fieldset/partials/_fieldset.php
  • modules/backend/widgets/Form.php
🧰 Additional context used
🧬 Code graph analysis (3)
modules/backend/formwidgets/fieldset/partials/_fieldset.php (1)
modules/backend/formwidgets/FieldSet.php (1)
  • render (70-74)
modules/backend/widgets/Form.php (1)
modules/backend/formwidgets/FieldSet.php (1)
  • getFormFields (62-65)
modules/backend/ServiceProvider.php (2)
modules/system/classes/CombineAssets.php (1)
  • registerBundle (660-697)
modules/backend/formwidgets/FieldSet.php (1)
  • FieldSet (14-80)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: windows-latest / PHP 8.2
  • GitHub Check: ubuntu-latest / PHP 8.4
  • GitHub Check: ubuntu-latest / PHP 8.2
  • GitHub Check: windows-latest / PHP 8.4
  • GitHub Check: windows-latest / PHP 8.3
  • GitHub Check: ubuntu-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.3
  • GitHub Check: windows-latest / PHP 8.1
  • GitHub Check: windows-latest / JavaScript
🔇 Additional comments (5)
modules/backend/formwidgets/fieldset/assets/less/fieldset.less (1)

1-9: LGTM!

The partial cleanly renders the fieldset with optional translated legend and embedded form widget. The implementation is straightforward and follows Winter CMS conventions.

modules/backend/ServiceProvider.php (1)

100-100: LGTM!

Both registrations follow the existing patterns correctly:

  • The asset bundle is properly registered for the FieldSet widget
  • The form widget registration maintains alphabetical ordering and follows the established convention

Also applies to: 252-252

modules/backend/formwidgets/FieldSet.php (3)

57-65: LGTM!

The loadAssets() and getFormFields() methods are correctly implemented. The getFormFields() method provides the necessary integration point for the Form widget's getSaveData() enhancement mentioned in the PR.


67-79: LGTM!

The render() and prepareVars() methods follow standard form widget patterns. Syncing preview mode in prepareVars() ensures the embedded form respects the current state even if it changes after initialization.


44-46: Verify whether getLoadValue() is appropriate for the parent-level fieldset data structure.

The pattern of setting $config->data = $this->getLoadValue() follows the same approach as NestedForm.php, but since FieldSet saves fields at the parent level (using parent form's arrayName), the data structure implications differ. It's unclear whether getLoadValue() returns a compatible structure for fields saved at the parent level rather than nested. This needs verification against actual form value handling.

Additionally, line 49 directly accesses $this->getParentForm()->arrayName without null safety, unlike the defensive pattern used in DataTable.php (which checks isset($this->getParentForm()->arrayName)). While NestedForm similarly accesses getParentForm() unsafely, consider adding a null check for consistency and robustness.

@damsfx
Copy link
Contributor

damsfx commented Jan 8, 2026

@mjauvin How is the responsive design being managed?
Does the span left/right/auto or custom Css with span:storm still works ?

@mjauvin
Copy link
Member Author

mjauvin commented Jan 8, 2026

@mjauvin How is the responsive design being managed? Does the span left/right/auto or custom Css with span:storm still works ?

It's pretty much derived from a nestedform ... so it renders a form within the <fieldset> tag.

The main difference is that fields are exported to the parent when saving (does not use a JSON field).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @modules/backend/formwidgets/FieldSet.php:
- Line 26: Add a PHPDoc docblock above the public property $showLabels on the
FieldSet class that explains its purpose, type and default value (bool, defaults
to false), describes what enabling it changes (e.g., whether child field labels
within the fieldset are displayed), and any usage notes or expected values;
reference the property name $showLabels and ensure the docblock follows PSR-5
style so IDEs and static analyzers can pick up the type and description.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b53bc99 and bebc32e.

📒 Files selected for processing (2)
  • modules/backend/formwidgets/FieldSet.php
  • modules/backend/formwidgets/fieldset/partials/_fieldset.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/backend/formwidgets/fieldset/partials/_fieldset.php
🧰 Additional context used
🧬 Code graph analysis (1)
modules/backend/formwidgets/FieldSet.php (3)
modules/backend/widgets/Form.php (7)
  • Form (23-1496)
  • init (127-143)
  • bindToController (152-156)
  • loadAssets (161-164)
  • getFields (1032-1035)
  • render (181-229)
  • prepareVars (303-311)
modules/system/traits/AssetMaker.php (1)
  • addCss (157-176)
modules/system/traits/ViewMaker.php (1)
  • makePartial (100-119)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: ubuntu-latest / PHP 8.1
  • GitHub Check: windows-latest / PHP 8.2
  • GitHub Check: ubuntu-latest / PHP 8.4
  • GitHub Check: windows-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.3
  • GitHub Check: windows-latest / PHP 8.4
  • GitHub Check: ubuntu-latest / PHP 8.2
  • GitHub Check: windows-latest / PHP 8.3
  • GitHub Check: windows-latest / JavaScript
🔇 Additional comments (3)
modules/backend/formwidgets/FieldSet.php (3)

48-48: Verify data initialization for the nested form.

Setting $config->data = $this->getLoadValue() passes the fieldset field's stored value to the nested Form widget. Since fieldset fields are saved at the parent level (not nested), the fieldset itself shouldn't have a stored value. The nested Form should load field values from the model (set on line 47), not from any value stored under the fieldset's name.

If getLoadValue() returns non-null data (e.g., legacy data or misconfiguration), the nested Form might use incorrect values instead of loading from the model.

Consider removing or setting to null:

-        $config->data = $this->getLoadValue();
+        $config->data = null;

Or verify that the Form widget correctly prioritizes model values over the data config when both are provided.


64-67: LGTM!

This delegation method correctly exposes the nested form's fields to the parent form's save process, enabling the fields inside the fieldset to be saved at the parent level.


51-51: No action required.

The getParentForm() method is defined in FormWidgetBase (line 99) as a public method, and FieldSet extends FormWidgetBase, making the method available for use at line 51.

@mjauvin mjauvin marked this pull request as draft January 14, 2026 15:17
@mjauvin mjauvin marked this pull request as ready for review January 14, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement PRs that implement a new feature or substantial change needs review Issues/PRs that require a review from a maintainer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants