Problem/Motivation
PluginFormInterface enables a plugin to be used as a part of a larger form.
It is best practice for the parent form to assign the plugin's sub-form into a single array key:
$form['some_key'] = $plugin->buildConfigurationForm(array(), $form_state);
However, if the plugin form needs to use #parents, #states, or #limit_validation_errors, it does not know were it is located in the parent form.
Proposed resolution
Document the recommended solution for this problem, i.e., returning a single element with a #process callback from buildConfigurationForm() and constructing the real configuration form in that callback (where $element['#parents'] can be used to calculate the config form's position in the containing form).
Remaining tasks
N/A
User interface changes
N/A
API changes
N/A
Original report by @Sweetchuck
A number of plugins provide form elements to a parent form. These can't use #states or #limit_validation_errors because those need the whole #parents array and the child the form misses the beginning of it.
Suggested resolution:
$form['data'] = $this->imageEffect->getForm();
change to
$form['data'] = $this->imageEffect->getForm(array('data'));
While it's arguably the whole of $form and $form_state would be also useful to these, being context free is even more useful :)
Some of the actual usage actually put the returned elements in the root like BookManagerInterface::addFormElements do or add the whole thing together like $form += $this->blockForm($form, $form_state). These quite probably don't need an empty array for #parents, just comment that they will land in the root.
| Comment | File | Size | Author |
|---|---|---|---|
| #40 | drupal_2065485_40.patch | 1.14 KB | xano |
| #40 | interdiff.txt | 1.54 KB | xano |
Comments
Comment #1
chx commentedBefore attempting any patching, I would like to hear from tim.plunkett on this.
Comment #2
tim.plunkettIn which of those examples is $form_state missing?
And how is this different than any other form?
#parents and #array_parents aren't populated until much later.
Comment #3
tim.plunkettComment #4
tim.plunkettComment #5
sweetchuck"
#parentsand#array_parentsaren't populated until much later."@tim.plunkett Yes, this is true, but the
#limit_validation_errorsand the$e['#sates']['something'][':input[name="settings[cache][max_age]"]'](selector for the name of an input element) so often filled before the #parents is populated.Is the host form builder can predict what the
#parentswill be?Why this information is not passed to the plugins?
If I would have a Field Formatter plugin with settings form and
'#ajax'in it,then how can I fill the
'#limit_validation_errors'property?Comment #6
tim.plunkettOnce I get the answer to this, we can move forward.
Comment #7
sweetchuckOK. Now I do not found where the $form_state is missing. Maybe nowhere, but why it is so important for the answer?
My real problem is:
I think the code snippet in the #5 is good example for the problem. The $form_state itself is does not help.
Comment #8
tim.plunkettWhat kind of plugin is that supposed to be? That is not enough code to understand the problem.
Comment #9
sweetchuck@tim.plunkett Please write me a sample code which explain me how can I use the
#limit_validation_errorsand#statesinside\Drupal\Core\Field\FieldItemInterface::settingsForm(array $form, array &$form_state, $has_data)\Drupal\Core\Field\FieldItemInterface::instanceSettingsForm(array $form, array &$form_state)\Drupal\Core\Field\FormatterInterface::settingsForm(array $form, array &$form_state)\Drupal\Core\Field\WidgetInterface::formElement(FieldItemListInterface $items, $delta, array $element, array &$form, array &$form_state)\Drupal\Core\Field\WidgetInterface::settingsForm(array $form, array &$form_state)\Drupal\Core\ImageToolkit\ImageToolkitInterface::settingsForm()\Drupal\image\ConfigurableImageEffectInterface::getForm()\Drupal\block\BlockPluginInterface::blockForm($form, &$form_state)\Drupal\ckeditor\CKEditorPluginConfigurableInterface::settingsForm(array $form, array &$form_state, Editor $editor)\Drupal\editor\Plugin\EditorPluginInterface::settingsForm(array $form, array &$form_state, Editor $editor)\Drupal\entity_reference\Plugin\Type\Selection\SelectionInterface::settingsForm(FieldDefinitionInterface $field_definition)\Drupal\filter\Plugin\FilterInterface::settingsForm(array $form, array &$form_state)\Drupal\book\BookManagerInterface::addFormElements(array $form, array &$form_state, NodeInterface $node, AccountInterface $account, $collapsed = TRUE)\Drupal\config_translation\FormElement\ElementInterface::getFormElement(array $definition, Language $language, $value)Is the only way to achieve this by adding a custom
#processcallback or create a new element type with hook_element_info()?I think this is not improve the DX.
Comment #10
tim.plunkettPlease show me an example of D7 code that used hook_field_instance_settings_form with either #limit_validation_errors or #states, and I will try to rewrite it for D8.
Comment #11
chx commentedAs I see an issue status ping-pong and I want to make sure everyone has a pleasant experience contributing to Drupal, please find me on IRC if necessary and explain the problem to me and I will help. It's often easier than doing it in issue.
Comment #12
chx commentedSo this is not a regression but something missing from the form API in D7 already and exacerbated with the more widespread and canonicalized nature of partial forms -- there are properties like #limit_validation_errors and #states because those need #parents from the root
Comment #13
chx commentedComment #14
chx commentedComment #15
tim.plunkettComment #16
tim.plunkettWorking on this.
Comment #17
tim.plunkettHere we are. This is only currently needed by blocks, but will needed by image styles after #2251223: ConfigurableImageEffectInterface should use ConfigurablePluginInterface and PluginFormInterface.
Anything not using PluginFormInterface that needs this support needs to switch to it (Views, fields, etc.)
Comment #19
tim.plunkettFixed test
Comment #20
tim.plunkettRerolled for the recently added plugins.
Comment #22
tim.plunkettComment #23
tim.plunkettComment #24
xano@tim.plunkett and I just discussed this in Austin and I was asked to post my views here.
I don't think this solution is a good one:
PluginFormInterface.buildConfigurationForm()return an element with a#processcallback. By the time the callback is invoked,#parentsis already set by Form API and the problem this issue tries to solve is no longer a problem. It takes a few more lines of code, but it re-uses an existing API rather than extending another one.buildConfigurationForm()is not always possible, or at the very least limits the possibilities when altering the form. Passing on an empty array works when the plugin form is top-level. When it is not, the actual parents must be passed on. When these are hardcoded, they must either be changed when altering the form and (re-)nesting the form elements, or the parent form must use#processas well to pass on the parents as they are computed by Form API at build time.In short, the only two solid solutions require us to use
#process. If we have to use that anyway, let's go with the solution that requires no other changes and works just as well, which means this issue is won't fix.Instead, I suggest we update the
PluginFormInterfacedocumentation with the suggestion developers use#processto solve their nesting problems.Comment #25
xanoWe agreed on updating the
PluginFormInterfacedocumentation. I'll update a patch shortly.Comment #26
xanoComment #28
xanoSince this has become just a docs update, I don't think the tags apply anymore.
Comment #29
sweetchuckTo add a new callback function to the #process array is too late.
Perhaps it works for the #states, but I am sure it does not work well for the #limit_validation_errors.
Comment #30
xanoWhat callback do you mean?
Comment #31
xanoFor a code example of how to use #process and #parents, see \Drupal\payment\Plugin\Payment\MethodConfiguration\Basic::buildConfigurationForm() and \Drupal\payment\Plugin\Payment\MethodConfiguration\Basic::submitConfigurationForm()
Comment #32
drunken monkeyYes, documenting this solution would indeed be helpful. However, I think it would be better to include this in the documentation for
buildConfigurationForm(), not for the interface, since that is where plugin authors look when adding a config form to their plugin – the documentation of the methods they are implementing, not the documentation for every single interface they are implementing.(Also, updating the summary to the new proposed resolution.)
Comment #36
xanoRe-roll.
Comment #39
jhodgdonI was asked to review this.
A few wording suggestions/questions:
a) contained by other forms ==> contained in other forms
b) "In order to know where the plugin form is located in the parent form..."
I didn't understand this on the first read. I think this should say something like:
When a plugin form is being built, it generally needs to know where it is located in the parent form (#parents and #array_parents properties). In order for this to happen, ...
Thoughts?
Comment #40
xanoComment #41
jhodgdonThat looks good to me! At least, the docs are clear. Marking RTBC so others involved in the issue have a chance to respond before committing.
Comment #42
sweetchuckI have never found any documentation about how to fill the "#name" property of a submit button.
The fact is that only a "flat" name is works.
I mean this is works:
<input type="submit" name="my_action_1" />But this is not works:
<input type="submit" name="my[action][1]" />Because the triggering_element detection does not care about the nested buttons.
So if create a settings form for a plugin (e.g. field formatter ) and you leave empty the "#name" and/or "#parents" properties of a submit button then those will be automatically filled based on the position in the
$form. (assume that the #tree is TRUE).If you fill the "#name" property directly with a flat name (my_action_1) then it can cause a name conflict and overwrite something in the main form. (two or more input element with the same name)
Take into account that there could be a case when the same plugin settings form can be embeded into a main form multiple times.
$form structure
I am still sure the original problem is cannot be solved by documentation.
Comment #43
xanoThat is not really a nesting issue, but a naming issue. You should be able to resolve that by prefixing the button's
#namevalue with your module's machine name and by writing a piece of code that generates names for your button and keeps track of the names that are already in use by the form.Comment #44
sweetchuckThis is also a problem:
And this:
If you click on a button which has a name like this
my[action][1]then the FormAPI ignores it and take the first button and use it as a triggering_element without any warning.Comment #45
jhodgdonAre these issues with #name relevant to the discussion on this issue?
If so, and if we need to add documentation to the topic of this issue, please mark the issue "needs work" and explain what docs need to be added.
Otherwise, please open a new issue or discuss elsewhere. Thanks!
Comment #46
xanoI think it's worth documenting the inconsistent behavior for buttons'
#nameproperties, but that is not related to this issue. Both are in a way about nesting, but the#nameissue isn't about nesting render arrays in render arrays, which is what this issue is about.Comment #47
sweetchuckI do not want documentation. What I want is to make it possible to use the
#states,#limit_validation_errors,#nameand#parentson a developer friendly way.We have two options
And we really choice the first one?
If a custom
#processcallback really solve the problem (I don't think so), then I would like to see a working example.Comment #48
xano@Sweetchuck: The
#nameissue is about Form API and has nothing to do withPluginFormInterfacein specific. It is far more generic. Please open a new issue for that.Comment #49
sweetchuckThis issue is opened because the #states and #limit_validation_errors is unusable. There is no kind of documentation which would help this problem.
The recommendation to add a new custom #process callback is very weird. That means: "You have to alter your own form elements."
Comment #50
xanoI agree that Form API has limitations that get in developers' ways. Trust me, I've battled with this before. However, using a
#processcallback is the only way to solve this right now.It's not ideal, but it's also not weird. It's how Form API works right now. It's also not about altering your own elements, but about adding to them, depending on how you look at it.
Again, I agree the way Form API does this right now sucks, but we're not going to be able to fix this before 8.0.0, so the best we can do is teach people how to work around this problem.
Comment #51
sweetchuckI do not really understand why we can't fix this before 8.0.0 this issue is more than 1 year old.
Comment #52
xanoForm API is large, complex, and somewhat unwieldy.
If you would like me to help you getting
#statesand#limit_validation_errorsworking using#process, just ping me on IRC and/or take a look at\Drupal\payment\Plugin\Payment\MethodSelector\AdvancedPaymentMethodSelectorBase::buildSelector().Comment #53
catch@Sweetchuck please open a new issue. If there was a backwards compatible way to enable this, then I'd be fine with adding that to a minor 8.x release, but better to have accurate documentation since there's no viable solution for that.
Comment #54
sweetchuck@catch The problem what this issue is opened for is still exists and the recently attached patch is not solve that problem.
The issue title and description has been rewritten to "Improve the documentation".
Why the "Improve the documentation" problem was not moved to another issue in order to keep clean this issue?
Comment #55
xanoThis is the original report.
#statesand#limit_validation_errorscan be used perfectly fine with Form API in its current state (and even in Drupal 7), as I have explained in several comments on this issue and in several contributed modules.There is a good reason the required
#parentsand#array_parentsproperties do not exist during the initial form build phase. It's far from ideal, but the fix is going to be rather complex and we can't be sure it will be able to get into Drupal 8 at this point, which is right now documenting how to solve your problem is better than spending weeks or months on fixing it with the chance of the fix being rejected.If the solution we proposed here does not solve your problem, please explain how it doesn't. You keep saying it doesn't solve your problem, but it looks like neither @catch, @jhodgdon, nor me understand how it doesn't. If you simply don't like it, I'm sorry, but that doesn't mean this solution isn't a good one right now.
Comment #57
catchSo I also really dislike #process, but as stated already it's the only way to handle this case - when you're building a plugin form element you don't have access to the whole form. It's a chicken and egg situation and #process gets around it.
Committed/pushed to 8.0.x, thanks!