Drupal Form API (FAPI) allows to easily build complex forms with nested fields in fieldsets, details, etc.:
$form['name'] = [
'#title' => $this->t('Name'),
'#description' => $this->t('Enter your name.'),
'#type' => 'textfield',
'#default_value' => $this->options['name'],
];
Typically, this is done by nesting the fields in the form array in a field group like this:
$form['contact'] = [
'#type' => 'details',
'#title' => t('Contact details'),
'#open' => TRUE,
];
$form['contact']['name'] = [
'#title' => $this->t('Name'),
'#description' => $this->t('Enter your name.'),
'#type' => 'textfield',
'#default_value' => $this->options['name'],
];
This is also what the documentation page on drupal.org explains: https://www.drupal.org/node/262758
The problematic part is, that this also affects the field structure when submitting the form. While this might be expected and even helpful in some cases, there are other cases, like settings forms (e.g. module setting pages, views style settings, ...) where you only want to wrap the fields visually but not touch the data structure!
Using the #group attribute instead of nested fields
I found this helpful blog article: https://zanzarra.com/blog/group-attribute-drupal-8-form-api-explained which describes a similar problem and shows how to use the #group
attribute as a workaround. So instead of nesting the fields into the fieldgroup, the fields simply reference the group id:
$form['contact'] = [
'#type' => 'details',
'#title' => t('Contact details'),
'#open' => TRUE,
];
$form['name'] = [
'#title' => $this->t('Name'),
'#description' => $this->t('Enter your name.'),
'#type' => 'textfield',
'#group' => 'contact',
'#default_value' => $this->options['name'],
];
So the fields data structure is preserved! :)
Sadly I couldn't get it working in Drupal 9.3, and I guess it might be due to this core issue: https://www.drupal.org/project/drupal/issues/2190333#comment-14502294
So if you run into the same issue, please follow & help to fix the core issue and if I'm doing something wrong, please tell me. As I ran into this requirement several times, I thought it would be time to create a blog issue and have a deeper look for possible solutions.
Mad workaround: massageFormValues()
As a mad workaround for a field widget that heavily uses field grouping, I implemented a flattening funtionality in massageFormValues
:
public function massageFormValues(array $values, array $form, FormStateInterface $form_state)
{
// Turn the nested array structure into a flat key => value array.
$values_flat = [];
if (!empty($values)) {
foreach ($values as $delta => $field) {
$it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($field));
foreach ($it as $k => $v) {
$values_flat[$delta][$k] = $v;
}
}
}
return $values_flat;
}
See https://git.drupalcode.org/project/drowl_paragraphs/-/blob/4.x/src/Plug…
But I really dislike that approach and think Drupal Core should provide a clean way to wrap fields without changing the field structure.