Problem/Motivation
Related, but independent to #3398974: Follow-up for #3382510: FormStateInterface::setErrorByName() needs not #name but a variation.
Configuration UIs should present things in a way that makes sense for the end user's mental model.
🆚
Configuration should store things in a way that makes things as simple as possible for the module developer AND optimizes for git diff
.
This means that config UIs CAN and arguably SHOULD not have a 1:1 relationship between UI/form elements and the underlying config. #3382510: Introduce a new #config_target Form API property to make it super simple to use validation constraints on simple config forms, and adopt it in several core config forms does assume that, which is a reasonable default, but it must not get in the way. Unfortunately it does.
A validation error for some specific config property path should be associated with the closest containing form element. And that does not happen today: if a validation error for a property path does not have a 1:1 relationship to a form element, a PHP error appears:
Attempt to read property "elementName" on null in Drupal\Core\Form\ConfigFormBase->validateForm() (line 204 of core/lib/Drupal/Core/Form/ConfigFormBase.php).
Steps to reproduce
- Install Drupal 10.2
- Install the CDN module, and apply #3394172-6: [PP-1] Adopt Drupal core 10.2 config validation infrastructure
- Choose "only files", do not specify any file extensions, click "Save configuration".
- You will see:
There are a few examples in core of forms whose logic is a bit too complicated for the 1:1 use case preferred by #config_target
:
- \Drupal\language\Form\NegotiationBrowserForm
- \Drupal\locale\Form\LocaleSettingsForm (fixed here)
- \Drupal\language\Form\NegotiationConfigureForm
Proposed resolution
Root cause:
$map["$config_name:$property_path"]
is assumed to exist. But … this will ONLY exist if there's a 1:1 relationship between property paths and form elements. It may very well NOT exist.
So instead, try to find a parent property path, if it exists.
- The 1:N case
- (1 form element, N property paths
- Allow passing multiple property paths to
ConfigTarget
- If multiple property paths are indeed passed, then
fromConfig
andtoConfig
callables become required, not optional.
- Allow passing multiple property paths to
- The N:1 case
- (N form elements, 1 property path — see #27 for an example)
-
Allow making the necessary conditional decisions:
- Pass the
toConfig
callable aFormStateInterface
parameter. - Allow the
toConfig
callable to return one of two special values:ToConfig::NoOp
, to indicate that the given form value does not need to be mapped onto the Config objectToConfig::DeleteKey
to indicate that the targeted property path should be deleted from config.
- Pass the
- Use case for this: This allows for the scenario of a
input[type=radios]
(withmode
as the target) to choose the simple or advanced way to configure something in the UI.
When the user chooses "simple", it would be mapped tomode: { style: automatic }
(i.e. full config subtree set)
When the user chooses "advanced", a conditionally displayedinput[type=text]
would appear in the UI (target:mode.something_very_advanced
).That conditionally displayed form element's
toConfig
callable would default to returningToConfig::NoOp
, unless it can see in theFormStateInterface
object that the radio button is set to "advanced"👈 that's why those 2 pieces are needed!See #27 for a concrete example with accompanying code. As well as the explicit test coverage for the conditional use case plus the infrastructure and how to use it.
All of this complexity is encapsulated in ConfigTarget
— ConfigFormBase
becomes simpler😊
Remaining tasks
Reviews.
User interface changes
None.
API changes
ConfigTarget
(new in 10.2
!):
- Allow passing multiple property paths to
ConfigTarget
- If multiple property paths are indeed passed, then
fromConfig
andtoConfig
callables become required, not optional. - Allow the
toConfig
callable to specify aFormStateInterface
parameter, detect this using reflection, and if detected, pass it. - Allow the
toConfig
callable to returnToConfig::NoOp
(no-op) orToConfig::DeleteKey
(delete the targeted property path's key).
Data model changes
None.
Release notes snippet
None.