Commit 01ac7634 authored by alexpott's avatar alexpott

Issue #2754977 by dmsmidt, marcvangend, mohit_aghera, Getekid, tim.plunkett,...

Issue #2754977 by dmsmidt, marcvangend, mohit_aghera, Getekid, tim.plunkett, ndf: Enhance formErrorHandler to include children errors on RenderElements
parent a43af1c4
......@@ -2,6 +2,7 @@
namespace Drupal\Core\Form;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Render\Element;
/**
......@@ -43,21 +44,115 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
}
/**
* Stores the errors of each element directly on the element.
* Stores errors and a list of child element errors directly on each element.
*
* We must provide a way for non-form functions to check the errors for a
* specific element. The most common usage of this is a #pre_render callback.
* Grouping elements like containers, details, fieldgroups and fieldsets may
* 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.
*
* @param array $elements
* An associative array containing the structure of a form element.
* 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'] = [
* '#type' => 'textfield',
* '#title' => $this->t('Street'),
* '#group' => 'address',
* '#required' => TRUE,
* ];
* $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 = [
* '#errors' => {Drupal\Core\StringTranslation\TranslatableMarkup},
* '#children_errors' => [],
* ];
* // The address detail element.
* $element = [
* '#errors' => null,
* '#children_errors' => [
* 'street' => {Drupal\Core\StringTranslation\TranslatableMarkup}
* ],
* ];
* @endcode
*
* 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
* the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $elements
* 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.
*/
protected function setElementErrorsFromFormState(array &$elements, FormStateInterface &$form_state) {
// Recurse through all children.
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
// the complete form is needed.
if (empty($elements)) {
$elements = &$form;
}
// Recurse through all element children.
foreach (Element::children($elements) as $key) {
if (isset($elements[$key]) && $elements[$key]) {
$this->setElementErrorsFromFormState($elements[$key], $form_state);
if (!empty($elements[$key])) {
// 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 = [];
// Inherit all recorded "children errors" of the direct child.
if (!empty($child['#children_errors'])) {
$children_errors = $child['#children_errors'];
}
// Additionally store the errors of the direct child itself, keyed by
// it's parent elements structure.
if (!empty($child['#errors'])) {
$child_parents = implode('][', $child['#array_parents']);
$children_errors[$child_parents] = $child['#errors'];
}
if (!empty($elements['#children_errors'])) {
$elements['#children_errors'] += $children_errors;
}
else {
$elements['#children_errors'] = $children_errors;
}
// If this direct child belongs to a group populate the grouping element
// with the children errors.
if (!empty($child['#group'])) {
$parents = explode('][', $child['#group']);
$group_element = NestedArray::getValue($form, $parents);
if (isset($group_element['#children_errors'])) {
$group_element['#children_errors'] = $group_element['#children_errors'] + $children_errors;
}
else {
$group_element['#children_errors'] = $children_errors;
}
NestedArray::setValue($form, $parents, $group_element);
}
}
}
......
......@@ -50,25 +50,30 @@ public function testDisplayErrorMessagesInline() {
$form = [
'#parents' => [],
'#array_parents' => [],
];
$form['test1'] = [
'#type' => 'textfield',
'#title' => 'Test 1',
'#parents' => ['test1'],
'#array_parents' => ['test1'],
'#id' => 'edit-test1',
];
$form['test2'] = [
'#type' => 'textfield',
'#title' => 'Test 2 & a half',
'#parents' => ['test2'],
'#array_parents' => ['test2'],
'#id' => 'edit-test2',
];
$form['fieldset'] = [
'#parents' => ['fieldset'],
'#array_parents' => ['fieldset'],
'test3' => [
'#type' => 'textfield',
'#title' => 'Test 3',
'#parents' => ['fieldset', 'test3'],
'#array_parents' => ['fieldset', 'test3'],
'#id' => 'edit-test3',
],
];
......@@ -76,18 +81,21 @@ public function testDisplayErrorMessagesInline() {
'#type' => 'textfield',
'#title' => 'Test 4',
'#parents' => ['test4'],
'#array_parents' => ['test4'],
'#id' => 'edit-test4',
'#error_no_message' => TRUE,
];
$form['test5'] = [
'#type' => 'textfield',
'#parents' => ['test5'],
'#array_parents' => ['test5'],
'#id' => 'edit-test5',
];
$form['test6'] = [
'#type' => 'value',
'#title' => 'Test 6',
'#parents' => ['test6'],
'#array_parents' => ['test6'],
'#id' => 'edit-test6',
];
$form_state = new FormState();
......@@ -114,11 +122,13 @@ public function testSetElementErrorsFromFormState() {
$form = [
'#parents' => [],
'#array_parents' => [],
];
$form['test'] = [
'#type' => 'textfield',
'#title' => 'Test',
'#parents' => ['test'],
'#array_parents' => ['test'],
'#id' => 'edit-test',
];
$form_state = new FormState();
......
......@@ -41,37 +41,44 @@ public function testDisplayErrorMessages() {
$form = [
'#parents' => [],
'#array_parents' => [],
];
$form['test1'] = [
'#type' => 'textfield',
'#title' => 'Test 1',
'#parents' => ['test1'],
'#array_parents' => ['test1'],
'#id' => 'edit-test1',
];
$form['test2'] = [
'#type' => 'textfield',
'#title' => 'Test 2 & a half',
'#parents' => ['test2'],
'#array_parents' => ['test2'],
'#id' => 'edit-test2',
];
$form['fieldset'] = [
'#parents' => ['fieldset'],
'#array_parents' => ['fieldset'],
'test3' => [
'#type' => 'textfield',
'#title' => 'Test 3',
'#parents' => ['fieldset', 'test3'],
'#array_parents' => ['fieldset', 'test3'],
'#id' => 'edit-test3',
],
];
$form['test5'] = [
'#type' => 'textfield',
'#parents' => ['test5'],
'#array_parents' => ['test5'],
'#id' => 'edit-test5',
];
$form['test6'] = [
'#type' => 'value',
'#title' => 'Test 6',
'#parents' => ['test6'],
'#array_parents' => ['test6'],
'#id' => 'edit-test6',
];
$form_state = new FormState();
......@@ -96,17 +103,91 @@ public function testSetElementErrorsFromFormState() {
$form = [
'#parents' => [],
'#array_parents' => [],
];
$form['test'] = [
'#type' => 'textfield',
'#title' => 'Test',
'#parents' => ['test'],
'#array_parents' => ['test'],
'#id' => 'edit-test',
];
$form['details'] = [
'#type' => 'details',
'#title' => 'Details grouping test',
'#parents' => ['details'],
'#array_parents' => ['details'],
'#id' => 'edit-details',
];
$form['grouping_test'] = [
'#type' => 'textfield',
'#title' => 'Grouping test',
'#parents' => ['grouping_test'],
'#array_parents' => ['grouping_test'],
'#id' => 'edit-grouping-test',
'#group' => 'details',
];
$form['grouping_test2'] = [
'#type' => 'textfield',
'#title' => 'Grouping test 2',
'#parents' => ['grouping_test2'],
'#array_parents' => ['grouping_test2'],
'#id' => 'edit-grouping-test2',
'#group' => 'details',
];
$form['details2'] = [
'#type' => 'details',
'#title' => 'Details grouping test 2',
'#parents' => ['details2'],
'#array_parents' => ['details2'],
'#id' => 'edit-details2',
];
$form['grouping_test3'] = [
'#type' => 'textfield',
'#title' => 'Grouping test 3',
'#parents' => ['grouping_test3'],
'#array_parents' => ['grouping_test3'],
'#id' => 'edit-grouping-test3',
'#group' => 'details2',
];
$form['fieldset'] = [
'#type' => 'fieldset',
'#parents' => ['fieldset'],
'#array_parents' => ['fieldset'],
'#id' => 'edit-fieldset',
'nested_test' => [
'#type' => 'textfield',
'#title' => 'Nested test',
'#parents' => ['fieldset', 'nested_test'],
'#array_parents' => ['fieldset', 'nested_test'],
'#id' => 'edit-nested_test',
],
'nested_test2' => [
'#type' => 'textfield',
'#title' => 'Nested test2',
'#parents' => ['fieldset', 'nested_test2'],
'#array_parents' => ['fieldset', 'nested_test2'],
'#id' => 'edit-nested_test2',
],
];
$form_state = new FormState();
$form_state->setErrorByName('test', 'invalid');
$form_state->setErrorByName('grouping_test', 'invalid');
$form_state->setErrorByName('grouping_test2', 'invalid');
$form_state->setErrorByName('fieldset][nested_test', 'invalid');
$form_state->setErrorByName('fieldset][nested_test2', 'invalid2');
$form_error_handler->handleFormErrors($form, $form_state);
$this->assertSame('invalid', $form['test']['#errors']);
$this->assertSame([
'grouping_test' => 'invalid',
'grouping_test2' => 'invalid',
], $form['details']['#children_errors']);
$this->assertSame([
'fieldset][nested_test' => 'invalid',
'fieldset][nested_test2' => 'invalid2',
], $form['fieldset']['#children_errors']);
$this->assertEmpty($form['details2']['#children_errors'], 'Children errors are empty for grouping element.');
$this->assertCount(5, $form['#children_errors']);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment