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.

Comments

chx’s picture

Assigned: Unassigned » tim.plunkett
Issue summary: View changes

Before attempting any patching, I would like to hear from tim.plunkett on this.

tim.plunkett’s picture

In 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.

tim.plunkett’s picture

Status: Active » Postponed (maintainer needs more info)
tim.plunkett’s picture

Assigned: tim.plunkett » Unassigned
Status: Postponed (maintainer needs more info) » Closed (cannot reproduce)
sweetchuck’s picture

Status: Closed (cannot reproduce) » Needs review

"#parents and #array_parents aren't populated until much later."
@tim.plunkett Yes, this is true, but the #limit_validation_errors and 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 #parents will 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?

$element['my_text'] = array(
  '#type' => 'checkbox',
);

$element['my_action'] = array(
  '#type' => 'submit',
  '#value' => t('Add new'),
  '#ajax' => array(
    'callback' => 'my_callback',
    'wrapper' => 'my-id',
  ),
  '#states' => array(
    'visible' => array(
      ':input[name="? ][my_text"]' => array('checked' => TRUE),
    ),
  ),
  '#limit_validation_errors' => array(
    array(/* ? */),
  ),
);
tim.plunkett’s picture

Status: Needs review » Postponed (maintainer needs more info)

In which of those examples is $form_state missing?

Once I get the answer to this, we can move forward.

sweetchuck’s picture

OK. 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:

When a pluggable from is building then the plugins does not have any informations about the main form and their position in the main form.

I think the code snippet in the #5 is good example for the problem. The $form_state itself is does not help.

tim.plunkett’s picture

What kind of plugin is that supposed to be? That is not enough code to understand the problem.

sweetchuck’s picture

Status: Postponed (maintainer needs more info) » Needs work

@tim.plunkett Please write me a sample code which explain me how can I use the #limit_validation_errors and #states inside

  • \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)
  • Maybe
  • \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 #process callback or create a new element type with hook_element_info()?
I think this is not improve the DX.

tim.plunkett’s picture

Please 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.

chx’s picture

As 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.

chx’s picture

Title: Reduced FormAPI possibilities in the plugin system » Partial forms can not use all form API properties
Issue summary: View changes
Status: Needs work » Active

So 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

chx’s picture

Title: Partial forms can not use all form API properties » Pass #parents to form element plugin methods
Category: Bug report » Task
Issue summary: View changes
chx’s picture

Issue summary: View changes
tim.plunkett’s picture

Priority: Normal » Major
Issue tags: +beta target
tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

Working on this.

tim.plunkett’s picture

Title: Pass #parents to form element plugin methods » Pass #parents to PluginFormInterface
Status: Active » Needs review
StatusFileSize
new28.96 KB

Here 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.)

Status: Needs review » Needs work

The last submitted patch, 17: plugin-form-parents-2065485-17.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new851 bytes
new29.79 KB

Fixed test

tim.plunkett’s picture

StatusFileSize
new1.71 KB
new31.51 KB

Rerolled for the recently added plugins.

Status: Needs review » Needs work

The last submitted patch, 20: plugin-form-parents-2065485-20.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
Issue tags: +Page Manager
StatusFileSize
new1.31 KB
new31.5 KB
tim.plunkett’s picture

Issue summary: View changes
xano’s picture

Status: Needs review » Needs work

@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:

  1. This approach creates an even closer dependency on Form API to PluginFormInterface.
  2. The current solution to solving this problem is making buildConfigurationForm() return an element with a #process callback. By the time the callback is invoked, #parents is 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.
  3. From the point of view of the calling code, passing on the parents to 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 #process as 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 PluginFormInterface documentation with the suggestion developers use #process to solve their nesting problems.

xano’s picture

Title: Pass #parents to PluginFormInterface » Document that PluginFormInterface should use #process to solve nesting issues
Assigned: tim.plunkett » xano

We agreed on updating the PluginFormInterface documentation. I'll update a patch shortly.

xano’s picture

Status: Needs work » Needs review
StatusFileSize
new1.05 KB

Status: Needs review » Needs work

The last submitted patch, 26: drupal_2065485_26.patch, failed testing.

xano’s picture

Status: Needs work » Needs review
Issue tags: -beta target, -Page Manager
StatusFileSize
new925 bytes

Since this has become just a docs update, I don't think the tags apply anymore.

sweetchuck’s picture

Status: Needs review » Needs work

To 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.

xano’s picture

What callback do you mean?

xano’s picture

drunken monkey’s picture

Issue summary: View changes

Yes, 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.)

Status: Needs work » Needs review

Xano queued 28: drupal_2065485_28.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 28: drupal_2065485_28.patch, failed testing.

The last submitted patch, 28: drupal_2065485_28.patch, failed testing.

xano’s picture

Status: Needs work » Needs review
StatusFileSize
new951 bytes

Re-roll.

Xano queued 36: drupal_2065485_36.patch for re-testing.

Xano queued 36: drupal_2065485_36.patch for re-testing.

jhodgdon’s picture

I 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?

xano’s picture

StatusFileSize
new1.54 KB
new1.14 KB
jhodgdon’s picture

Status: Needs review » Reviewed & tested by the community

That 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.

sweetchuck’s picture

I 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

  • plugins
    • plugin_1_instance_1
      • my_action_1
    • plugin_1_instance_2
      • my_action_1

I am still sure the original problem is cannot be solved by documentation.

xano’s picture

That is not really a nesting issue, but a naming issue. You should be able to resolve that by prefixing the button's #name value 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.

sweetchuck’s picture

This is also a problem:

I have never found any documentation about how to fill the "#name" property of a submit button.

And this:

Because the triggering_element detection does not care about the nested buttons.

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.

jhodgdon’s picture

Are 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!

xano’s picture

I think it's worth documenting the inconsistent behavior for buttons' #name properties, but that is not related to this issue. Both are in a way about nesting, but the #name issue isn't about nesting render arrays in render arrays, which is what this issue is about.

sweetchuck’s picture

Status: Reviewed & tested by the community » Needs work

I do not want documentation. What I want is to make it possible to use the #states, #limit_validation_errors, #name and #parents on a developer friendly way.

We have two options

  1. Documenting the current API is not works well
  2. Improve the API

And we really choice the first one?

If a custom #process callback really solve the problem (I don't think so), then I would like to see a working example.

xano’s picture

Assigned: xano » Unassigned
Status: Needs work » Reviewed & tested by the community

@Sweetchuck: The #name issue is about Form API and has nothing to do with PluginFormInterface in specific. It is far more generic. Please open a new issue for that.

sweetchuck’s picture

This 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."

xano’s picture

This issue is opened because the #states and #limit_validation_errors is unusable. There is no kind of documentation which would help this problem.

I agree that Form API has limitations that get in developers' ways. Trust me, I've battled with this before. However, using a #process callback is the only way to solve this right now.

The recommendation to add a new custom #process callback is very weird. That means: "You have to alter your own form elements."

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.

sweetchuck’s picture

I do not really understand why we can't fix this before 8.0.0 this issue is more than 1 year old.

xano’s picture

Form API is large, complex, and somewhat unwieldy.

If you would like me to help you getting #states and #limit_validation_errors working using #process, just ping me on IRC and/or take a look at \Drupal\payment\Plugin\Payment\MethodSelector\AdvancedPaymentMethodSelectorBase::buildSelector().

catch’s picture

@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.

sweetchuck’s picture

@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?

xano’s picture

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.

This is the original report. #states and #limit_validation_errors can 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 #parents and #array_parents properties 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.

  • catch committed fdb67c2 on 8.0.x
    Issue #2065485 by tim.plunkett, Xano: Document that PluginFormInterface...
catch’s picture

Status: Reviewed & tested by the community » Fixed

So 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!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.