Change record status: 
Project: 
Introduced in branch: 
8.2.x
Introduced in version: 
8.2.0
Description: 

Drupal 8 introduced PluginFormInterface, which was the first official support for embedded forms, or subforms, in Drupal core. These subforms always received the top-level/complete form state, with all submitted form values instead of just the ones belonging to the subform, and as such had to use sometimes difficult and unreliable methods to extract their own data from the form state.

Drupal 8.2.0 introduces SubformStateInterface which should be passed on to subforms instead of FormState. It wraps subforms' parent form states, which are often FormState, but can be SubformStateInterface as well, and allows subforms to access this state without knowing anything about the parent form, keeping code clean and simple, and reducing the risk of collissions between top-level forms and subforms.

Example problem in Drupal 8.1.0 and lower

abstract class ConditionPluginBase extends ExecutablePluginBase implements ConditionInterface {

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $this->configuration['negate'] = $form_state->getValue('negate');
  }
}

This assumes the condition plugin's form is embedded at the top level in the complete parent form. If the parent form decides to add its own negate element, it will collide with the plugin form's element of the same name. If another parent form decides to embed the condition's form in a child element condition, the condition's form will no longer be able to get its value from the form state, because that would be in $form_state->getValue(['condition', 'negate']) instead of $form_state->getValue('negate'). SubformStateInterface allows the condition's form to remain the same, while fixing these problems.

Usage in Drupal 8.2.0

Embedded forms do not have to do anything, except assume that their $form and $form_state parameters belong together. Parent forms must instead not pass on their own form states to embedded forms, but create a new subform state for every subform they embed:

$form['plugin_configuration'] = [];
$subform_state = SubformState::createForSubform($form['plugin_configuration'], $form, $form_state);
$form['plugin_form'] = $plugin_instance->buildConfigurationForm($form['plugin_configuration'], $subform_state);
Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done

Comments

Anonymous’s picture

Don't forget to use #tree => TRUE in the form element or the whole parent form, otherwise no values are received when calling $form_state->getValue()/->getValues().

Also when using ajax callback in the plugin/subform, the form state will have the original/parent form structure so if the subform is under 'settings' key, in the ajax handler this key has to be used which kind of makes this not working as desired :$

aken.niels@gmail.com’s picture

I'm suspecting I'm experiencing the same problems as you describe in the last paragraph. Using `ajax` handlers and my subform having elements in 'settings' is not working as expected. Did you find any resolution @ivanjaros?

aken.niels@gmail.com’s picture

FWIW: That first stackexchange question was my own and did not report back here, but that was my eventual fix. :-)

geek-merlin’s picture