Problem/Motivation

Currently there is no standardized method to know for a RenderElement if any child element has an error.

It becomes additionally difficult to get to know this for grouping elements like: details, fieldsets, fieldgroups and containers that have no direct children in the sense as registered in the #array_parents property of those children.

To clarify: it is possible to add elements to a group by providing a #group property (e.g. fieldsetgroup][fieldset][container) without being a descendent of this group in the array structure. The final renderable array will be assembled later in \Drupal\Core\Render\Element\RenderElement->preRenderGroup().

This issue is Major because it is a blocker for the Major issue: #2346773: Details form element should open when there are errors on child elements.
Additionally this can help stabilizing Inline Form Errors, a lot of form elements and widgets could use this extra info to get a good user experience. See #2504847: [meta] Roadmap for stabilizing Inline Form Errors module (IFE) for related issues.

Proposed resolution

Add a RenderElement property which stores all errors of their children and children children's, and so on. Including for grouping elements that have no children in the sense of #parents, but visually do have errors.

Proposed new property: #children_errors, see 'Data Model Changes'.

Remaining tasks

  • Create patch with basic functionality Done
  • Create a test which makes sure that the #children_errors is populated.

User interface changes

None. But this opens the way for a lot of user interface changes and bug fixes. See:

API changes

None.

Data model changes

Addition of a RenderElement property: #children_errors.

It should contain errors of all children errors keyed by #array_parents of the original problematic RenderElement/Field. Childrens don't need to be direct descendants, but traverse up the tree.

Comments

dmsmidt created an issue. See original summary.

dmsmidt’s picture

Issue summary: View changes
dmsmidt’s picture

And the first patch, doing as advertised in the proposed solution.

dmsmidt’s picture

Status: Active » Needs review
dmsmidt’s picture

Issue tags: +Rendering, +forms
dmsmidt’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 3: 2754977-3-renderelement_children_errors.patch, failed testing.

dmsmidt’s picture

Assigned: Unassigned » dmsmidt

Will dive into those test failures, woohoo! But still, any insight greatly appreciated.

dmsmidt’s picture

Status: Needs work » Needs review
StatusFileSize
new3.42 KB
new892 bytes

(sorry wrong interdiff extension)

Status: Needs review » Needs work

The last submitted patch, 9: interdiff-2754977-3-9-render_element_children_errors.patch, failed testing.

dmsmidt’s picture

Status: Needs work » Needs review

Real patch is green, woot!

skaught’s picture

using the combined patch from 2346773#57 and errorstyle.module (github) this does pass a manual test run. Details open with children error in Safari 9.0.3

mgifford’s picture

Ok, so can we remove the "Needs tests" tag now? I'm a bit confused by https://www.drupal.org/files/issues/interdiff-2754977-3-9-render_element...

What's the easiest way to test this? Can we get some screenshots up?

dmsmidt’s picture

Status: Needs review » Needs work

@mgifford, testing this can be done in combination with the patch in #2346773. That combination shows how this patch can help other issues move on. Otherwise you'll need to debug the $form structure to see the effect.

We still need a test for this patch.
The change in the interdiff you mention makes sure all changes are to the original $form, while $form_state never needs to be changed in this method.

Todo: Children errors of elements are now keyed by #parents, however we should key them by #array_parents, since we use it in the literal form structure and are not dealing with values in $form_state.

mohit_aghera’s picture

Status: Needs work » Needs review
StatusFileSize
new3.42 KB
new704 bytes

@dmsmidt
Fixing @Todo point and adding #array_parent as key.

Status: Needs review » Needs work

The last submitted patch, 15: 2754977-15-render_element_children_errors.patch, failed testing.

dmsmidt’s picture

@mohit_aghera, thank! Could you look into tests as well?

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

lokapujya’s picture

Whats this "Undefined index: #array_parents"?

dmsmidt’s picture

@lokapujya, I proposed using #array_parents instead of #parents as key in comment #14. Maybe mohit_aghera in #15 changed it without checking if it was possible, so someone should (re)do that and look into the test.

The last submitted patch, 15: 2754977-15-render_element_children_errors.patch, failed testing.

getekid’s picture

Issue tags: +Dublin2016
StatusFileSize
new7.52 KB

Hello,

I worked a bit on it during DrupalCon Dublin 2016. I checked the Test fail messages and added the '#array_parents' key to the tests. As I'm quite new to the difference between the #parents/#array_parrents, I simply copy-pasted the values. In case for test purposes other values should be used instead, please let me know.

I also tried to write a test by asserting the 'children_errors' property on a parent element on the form (in FormErrorHandlerTest under the module's test class) but I keep having "Undefined index" errors.

Hope it helps anyways

mohit_aghera’s picture

Status: Needs work » Needs review

Changing status to trigger test bot

tim.plunkett’s picture

dmsmidt’s picture

Issue summary: View changes

@tim, ah that one again, a must read for everyone working with forms. I clarified #14 a bit and updated the description to reflect this.

@getekid, nice to see you working on this again. Hopefully I can review soon. For next time could you also provide an interdiff? It makes our live easier.

dmsmidt’s picture

Status: Needs review » Needs work

Happy to see that the tests work again, thanks @getekid!
I created an interdiff, to see what you changed.

One nit, which should be removed in the next patch.

+   * @param array $elements
+   *   An associative array containing the structure of a form element. This
+   *   should be left empty in the case this method isn't called by itself.

Next up: creating a test for the added functionality.

Update: @param was oke.

dmsmidt’s picture

StatusFileSize
new4.91 KB
ndf’s picture

+++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
@@ -43,21 +44,64 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
+  protected function setElementErrorsFromFormState(array &$form, FormStateInterface $form_state, array &$elements = array()) {

Is it possible to typehint $form and $elements with interfaces?

+++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
@@ -43,21 +44,64 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
+  protected function setElementErrorsFromFormState(array &$form, FormStateInterface $form_state, array &$elements = array()) {
+    if (empty($elements)) {
+      $elements =& $form;
+    }
+
     // Recurse through all children.
     foreach (Element::children($elements) as $key) {
       if (isset($elements[$key]) && $elements[$key]) {
...
+        $child =& $elements[$key];
+
+        // Call self to traverse up the form tree.
+        $this->setElementErrorsFromFormState($form, $form_state, $child);

What do you think of chainElement...?
or setElementErrorsOnForm($form, ...)

Don't get it yet with $elements, $child and $form

+++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
@@ -43,21 +44,64 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
     // Recurse through all children.
     foreach (Element::children($elements) as $key) {
...
+        $child =& $elements[$key];
...
+          $elements['#children_errors'] = $child['#children_errors'];
...
+          $parents = implode('][', $child['#array_parents']);
+          $elements['#children_errors'][$parents] = $child['#errors'];
...
+          $parents = explode('][', $child['#group']);
...
+            $group_element['#children_errors'] = array_merge($group_element['#children_errors'], $elements['#children_errors']);
...
+          NestedArray::setValue($form, $parents, $group_element);

Can we document the end-result? What will be the structure of the final array?

BTW Only read the code, did not tested yet with your other patches. Stay tuned :)

dmsmidt’s picture

@ndf

1. We can't type $form and $elements because the formBuilder just returns an array. Not an typed object.
2. I don't get your second point about chainElement/setElementErrorsOnForm.
3. The extra $form parameter is needed to keep a reference to the original $form structure. Without it we have a recursive function (traversing up the form tree) that only knows about the current element, and nothing about the complete form structure. We need the complete form structure to populate the grouping elements (like the details form element) with info about the errors of their children, see: $group_element = NestedArray::getValue($form, $parents);. Because those children aren't
"real children" in the nested form array structure.

I'll make the code more readable.

dmsmidt’s picture

Assigned: dmsmidt » Unassigned
Status: Needs work » Needs review
StatusFileSize
new4.91 KB
new9.28 KB

Here is a hopefully more understandable patch. Test still needed.

dmsmidt’s picture

Issue summary: View changes
dmsmidt’s picture

Tests added! All should be complete now, please review.

(Minor fix for a screw up when rewriting part of the patch in #30).

dmsmidt’s picture

Issue tags: -Needs tests

Removing 'needs tests' tag.

marcvangend’s picture

Some minor remarks on the (otherwise excellent) inline docs:

  1. +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
    @@ -43,21 +44,112 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
    +   * Children errors are always keyed by the #array_parents of the element
    +   * with the error.
    

    I'm afraid this line of documentation is hard to understand.

  2. +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
    @@ -43,21 +44,112 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
    +   * @param array $elements
    +   *   An associative array containing the part of the form structure that will
    +   *   be processed while traversing up the tree. This should be left empty in
    +   *   the case this method isn't called by itself.
    

    The "in the case the method isn't called by itself" confused me, I had to read it 5 times before I got it. I suggest a more direct instruction: "An associative array containing the part of the form structure that will be processed while traversing up the tree. For recursion only; leave empty when calling this method."

Coming up: actual testing (test results will be in #2346773: Details form element should open when there are errors on child elements because they must be tested together).

marcvangend’s picture

Assigned: Unassigned » marcvangend
Status: Needs review » Needs work

I found a problem, posted it in #2346773-77: Details form element should open when there are errors on child elements, but the problem turns out to be in this patch here. New patch coming up.

dmsmidt’s picture

Thanks, can you add the case also to the automated test?

marcvangend’s picture

Here's the patch with just the bugfix, I'll see what I can do for the tests.

marcvangend’s picture

Assigned: marcvangend » Unassigned

Re #37, I added a test for this in #2346773-83: Details form element should open when there are errors on child elements. It really was a case of different form elements interacting with each other because of a mistake in the code; I'm not sure if / how that could be covered by the unit tests.

tim.plunkett’s picture

All tests for this change need to be in this issue, we can't rely on a follow-up for that.

marcvangend’s picture

I agree, but afaic the separation of those two related issues doesn't help anyway. Can't we merge the issues and fix it in one go?

dmsmidt’s picture

@marcvangend, this issue is a bit more generic and could potentially be used to fix other IFE issues. I think we are far enough progressed to finish this issue and quickly solve the follow-up later.

We need to enhance/add to the tests provided in this issue's patch to cover the changes in #37.
So a patch without the fix and including the improved test to show that it fails, and a patch with the improved test and the fix of #37 to show that it is fixed.

xjm’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Marking NW for #41. Thanks all!

As a reminder, this issue is a blocker for Inline Form Errors being marked stable, because it blocks a required fix for that module. We should aim to make progress on this issue before February 1 since it must change stable core code, and therefore can't be changed in patch releases the way that IFE itself can. See #2504847: [meta] Roadmap for stabilizing Inline Form Errors module (IFE) for background.

dmsmidt’s picture

Assigned: Unassigned » dmsmidt
dmsmidt’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new12.38 KB
new2.47 KB
new4.63 KB

@marcvangend found that I made an error, that consequentially leaked children errors to grouping elements without children errors. Hereby a patch with improved tests that cover this scenario (test only, should fail).
Furthermore I improved the inline docs as suggested.

Status: Needs review » Needs work
dmsmidt’s picture

Status: Needs work » Needs review

Woop, green (note to self: upload test only patch first).

chr.fritsch’s picture

Status: Needs review » Reviewed & tested by the community

Looks really good from my point of view

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

pierremarcel’s picture

Issue tags: +SprintWeekend2017

Reviewed it and applied. Also applied patch from https://www.drupal.org/node/2346773#comment-11897477 and so far it's working as expected. More details on https://www.drupal.org/node/2346773#comment-11897477

mgifford’s picture

Assigned: dmsmidt » Unassigned
Status: Reviewed & tested by the community » Needs review

The bot should test in in 8.4. I wasn't able to retest it from #44 so setting this to Needs review to see if that helps bump things. No concerns with the RTBC.

kattekrab’s picture

Status: Needs review » Reviewed & tested by the community

So that test only patch SHOULD fail. Patch itself is green. Back to RTBC!

dmsmidt’s picture

Manually added 8.4.x tests, also pass!

tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
@@ -43,21 +44,114 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
     foreach (Element::children($elements) as $key) {
-      if (isset($elements[$key]) && $elements[$key]) {
...
+      if (!empty($elements[$key])) {

This is the only weird part of the patch that jumps out at me, when would Element::children ever return something that wouldn't pass this check? But it's part of the original code anyway, so not a big deal.

Everything else looks good to me, I feel comfortable signing off on this as a subsystem maintainer.
Thanks for the incredible work and persistence @dmsmidt, and everyone else who pushed this along!

pierremarcel’s picture

Yes as I mentioned, I can assure it's working as well, I'll try to add a bit more comment to here too. I tested it last Saturday for #SprintWeekend. I tested it manually.

I also went ahead and tested issue https://www.drupal.org/node/2346773 which is postponed by this current issue. I also can ensure that patch has worked and so far we didn't see anything weird in the code. It seems we are getting closer.

Thanks guys for the hard work.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I tried to re-arrange the code to not change the signature of the protected method and to keep it simple - but recursive methods are never simple :). The approach in the patch looks okay and has the sign-off from a sub system maintainer.

Committed and pushed 01ac763 to 8.4.x and 8d8bb34 to 8.3.x. Thanks!

diff --git a/core/lib/Drupal/Core/Form/FormErrorHandler.php b/core/lib/Drupal/Core/Form/FormErrorHandler.php
index 4efe593..9783cb5 100644
--- a/core/lib/Drupal/Core/Form/FormErrorHandler.php
+++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
@@ -44,50 +44,51 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
   }
 
   /**
-   * Stores errors and a list of children errors directly on the element.
+   * Stores errors and a list of child element errors directly on each element.
    *
    * Grouping elements like containers, details, fieldgroups and fieldsets may
-   * need error info of their children to be able to accessibly show form
-   * problems to a user. A good example is a details element which should be
-   * opened when children have errors.
+   * need error info from their child elements to be able to accessibly show
+   * form error messages to a user. For example, a details element should be
+   * opened when child elements have errors.
    *
-   * Groupings example:
+   * Grouping example:
    * Assume you have a 'street' element somewhere in a form, which is displayed
    * in a details element 'address'. It might be:
    * @code
-   * $form['street'] = array(
+   * $form['street'] = [
    *   '#type' => 'textfield',
    *   '#title' => $this->t('Street'),
    *   '#group' => 'address',
    *   '#required' => TRUE,
-   * );
-   * $form['address'] = array(
+   * ];
+   * $form['address'] = [
    *   '#type' => 'details',
    *   '#title' => $this->t('Address'),
-   * );
+   * ];
    * @endcode
    *
    * When submitting an empty street field, the generated error is available to
    * the different render elements like so:
    * @code
    * // The street textfield element.
-   * $element = array(
+   * $element = [
    *   '#errors' => {Drupal\Core\StringTranslation\TranslatableMarkup},
-   *   '#children_errors' => array(),
-   * );
+   *   '#children_errors' => [],
+   * ];
    * // The address detail element.
-   * $element = array(
+   * $element = [
    *   '#errors' => null,
-   *   '#children_errors' => array(
+   *   '#children_errors' => [
    *      'street' => {Drupal\Core\StringTranslation\TranslatableMarkup}
-   *   ),
-   * );
+   *   ],
+   * ];
    * @endcode
    *
-   * The list of children errors of an element is an associative array. A child
-   * error is keyed with the #array_parents value of the respective element.
-   * The key is formed by imploding this value with '][' as glue.
-   * E.g. a value ['contact_info', 'name'] becomes 'contact_info][name'.
+   * The list of child element errors of an element is an associative array. A
+   * child element error is keyed with the #array_parents value of the
+   * respective element. The key is formed by imploding this value with '][' as
+   * glue. For example, a value ['contact_info', 'name'] becomes
+   * 'contact_info][name'.
    *
    * @param array $form
    *   An associative array containing a reference to the complete structure of
@@ -99,7 +100,7 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
    *   be processed while traversing up the tree. For recursion only; leave
    *   empty when calling this method.
    */
-  protected function setElementErrorsFromFormState(array &$form, FormStateInterface $form_state, array &$elements = array()) {
+  protected function setElementErrorsFromFormState(array &$form, FormStateInterface $form_state, array &$elements = []) {
     // At the start of traversing up the form tree set the to be processed
     // elements to the complete form structure by reference so that we can
     // modify the original form. When processing grouped elements a reference to
@@ -111,14 +112,14 @@ protected function setElementErrorsFromFormState(array &$form, FormStateInterfac
     // Recurse through all element children.
     foreach (Element::children($elements) as $key) {
       if (!empty($elements[$key])) {
-        // Get the child by reference so that we can update the orignial form.
+        // Get the child by reference so that we can update the original form.
         $child = &$elements[$key];
 
         // Call self to traverse up the form tree. The current element's child
         // contains the next elements to be processed.
         $this->setElementErrorsFromFormState($form, $form_state, $child);
 
-        $children_errors = array();
+        $children_errors = [];
 
         // Inherit all recorded "children errors" of the direct child.
         if (!empty($child['#children_errors'])) {

Some comment fixes on commit. Reviewed with @dmsmidt on IRC.

  • alexpott committed 01ac763 on 8.4.x
    Issue #2754977 by dmsmidt, marcvangend, mohit_aghera, Getekid, tim....

  • alexpott committed 8d8bb34 on 8.3.x
    Issue #2754977 by dmsmidt, marcvangend, mohit_aghera, Getekid, tim....

Status: Fixed » Closed (fixed)

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