diff --git a/core/includes/form.inc b/core/includes/form.inc index 5b292f5..2d083d4 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -11,6 +11,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; use Drupal\Core\Database\Database; +use Drupal\Core\Form\FormElementHelper; use Drupal\Core\Language\Language; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; @@ -1151,6 +1152,7 @@ function form_process_password_confirm($element) { '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], '#required' => $element['#required'], '#attributes' => array('class' => array('password-field')), + '#error_use_parent' => TRUE, ); $element['pass2'] = array( '#type' => 'password', @@ -1158,6 +1160,7 @@ function form_process_password_confirm($element) { '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], '#required' => $element['#required'], '#attributes' => array('class' => array('password-confirm')), + '#error_use_parent' => TRUE, ); $element['#element_validate'] = array('password_confirm_validate'); $element['#tree'] = TRUE; @@ -1261,6 +1264,8 @@ function form_process_radios($element) { '#parents' => $element['#parents'], '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + // Errors should only be shown on the parent radios element. + '#error_use_parent' => TRUE, '#weight' => $weight, ); } @@ -1335,6 +1340,7 @@ function form_pre_render_conditional_form_element($element) { if (isset($element['#title']) || isset($element['#description'])) { // @see #type 'fieldgroup' + $element['#theme_wrappers'][] = 'form_element'; $element['#theme_wrappers'][] = 'fieldset'; $element['#attributes']['class'][] = 'fieldgroup'; $element['#attributes']['class'][] = 'form-composite'; @@ -1414,6 +1420,8 @@ function form_process_checkboxes($element) { '#default_value' => isset($value[$key]) ? $key : NULL, '#attributes' => $element['#attributes'], '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + // Errors should only be shown on the parent checkboxes element. + '#error_use_parent' => TRUE, '#weight' => $weight, ); } @@ -2689,6 +2697,25 @@ function template_preprocess_form(&$variables) { } $variables['attributes'] = $element['#attributes']; $variables['children'] = $element['#children']; + + if (!empty($element['#errors'])) { + $error_links = array(); + // Loop through all form errors, and display a link for each error that + // is associated with a specific form element. + foreach ($element['#errors'] as $key => $error) { + if ($form_element = FormElementHelper::getElementByName($key, $element)) { + $title = FormElementHelper::getElementTitle($form_element); + $error_links[] = l($title, '', array('fragment' => 'edit-' . str_replace('_', '-', $key), 'external' => TRUE)); + } + else { + drupal_set_message($error, 'error'); + } + } + + if (!empty($error_links)) { + drupal_set_message(format_plural(count($error_links), '1 error has been found', '@count errors have been found') . ': ' . implode(', ', $error_links), 'error'); + } + } } /** @@ -2882,6 +2909,14 @@ function template_preprocess_form_element(&$variables) { $variables['attributes']['class'][] = 'form-disabled'; } + // Display any error messages. + $variables['errors'] = NULL; + if (!empty($element['#errors']) && empty($element['#error_use_parent'])) { + // Add a class if an error exists. + $variables['attributes']['class'][] = 'form-error'; + $variables['errors'] = $element['#errors']; + } + // If #title is not set, we don't display any label or required marker. if (!isset($element['#title'])) { $element['#title_display'] = 'none'; diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index aa8369a..0b42dbc 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -875,6 +875,9 @@ public function validateForm($form_id, &$form, &$form_state) { } $form_state['values'] = $values; } + if (!$form_state['programmed']) { + $form['#errors'] = $this->getErrors($form_state); + } } /** @@ -1185,6 +1188,16 @@ public function executeHandlers($type, &$form, &$form_state) { */ public function setErrorByName($name, array &$form_state, $message = '') { if (!isset($form_state['errors'][$name])) { + // This is only used by errors set in submit handlers. + // @todo Unlike errors set during validation, these errors will not + // directly correspond to their input element, and will not interrupt + // submission. We should consider limiting usage of form errors to + // validation only, and encourage usage of drupal_set_message() in + // submit handlers. + if ($message && isset($form_state['build_info']['form_id']) && !empty($this->validatedForms[$form_state['build_info']['form_id']])) { + $this->drupalSetMessage($message, 'error'); + } + $record = TRUE; if (isset($form_state['limit_validation_errors'])) { // #limit_validation_errors is an array of "sections" within which user @@ -1212,9 +1225,6 @@ public function setErrorByName($name, array &$form_state, $message = '') { if ($record) { $form_state['errors'][$name] = $message; $this->request->attributes->set('_form_errors', TRUE); - if ($message) { - $this->drupalSetMessage($message, 'error'); - } } } diff --git a/core/lib/Drupal/Core/Form/FormElementHelper.php b/core/lib/Drupal/Core/Form/FormElementHelper.php new file mode 100644 index 0000000..ec5616e --- /dev/null +++ b/core/lib/Drupal/Core/Form/FormElementHelper.php @@ -0,0 +1,68 @@ + + {% if errors %} +
+ {{ errors }} +
+ {% endif %} {% if label_display in ['before', 'invisible'] %} {{ label }} {% endif %} diff --git a/core/tests/Drupal/Tests/Core/Form/FormElementHelperTest.php b/core/tests/Drupal/Tests/Core/Form/FormElementHelperTest.php new file mode 100644 index 0000000..58973bd --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Form/FormElementHelperTest.php @@ -0,0 +1,81 @@ + 'FormElementHelper test', + 'description' => 'Tests the form element helper.', + 'group' => 'Form API', + ); + } + + /** + * Tests the getElementByName() method. + * + * @covers ::getElementByName() + * + * @dataProvider getElementByNameProvider + */ + public function testGetElementByName($name, $form, $expected) { + $this->assertSame($expected, FormElementHelper::getElementByName($name, $form)); + } + + /** + * Provides test data. + */ + public function getElementByNameProvider() { + return array( + array('id', array(), array()), + array('id', array('id' => array('#title' => 'ID')), array('#title' => 'ID')), + array('id', array('fieldset' => array('id' => array('#title' => 'ID'))), array('#title' => 'ID')), + array('fieldset', array('fieldset' => array('id' => array('#title' => 'ID'))), array('id' => array('#title' => 'ID'))), + ); + } + + /** + * Tests the getElementTitle() method. + * + * @covers ::getElementTitle() + * + * @dataProvider getElementTitleProvider + */ + public function testGetElementTitle($name, $form, $expected) { + $element = FormElementHelper::getElementByName($name, $form); + $this->assertSame($expected, FormElementHelper::getElementTitle($element)); + } + + /** + * Provides test data. + */ + public function getElementTitleProvider() { + return array( + array('id', array(), ''), + array('id', array('id' => array('#title' => 'ID')), 'ID'), + array('id', array('fieldset' => array('id' => array('#title' => 'ID'))), 'ID'), + array('fieldset', array('fieldset' => array('id' => array('#title' => 'ID'))), 'ID'), + ); + } + +}