diff --git a/core/includes/form.inc b/core/includes/form.inc
index 87f8edf..c8d7f33 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -10,7 +10,9 @@
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Url;
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;
use Drupal\Core\Utility\Color;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -1083,6 +1085,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',
@@ -1090,6 +1093,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;
@@ -1193,6 +1197,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,
);
}
@@ -1343,6 +1349,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,
);
}
@@ -2552,6 +2560,36 @@ function form_pre_render_color($element) {
}
/**
+ * Preprocesses variables for theme_form().
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - element: An associative array containing the properties of the element.
+ *
+ * @ingroup themeable
+ */
+function template_preprocess_form(&$variables) {
+ if (!empty($variables['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 ($variables['element']['#errors'] as $key => $error) {
+ if ($element = FormElementHelper::getElementByName($key, $variables['element'])) {
+ $title = FormElementHelper::getElementTitle($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');
+ }
+ }
+}
+
+/**
* Returns HTML for a form.
*
* @param $variables
@@ -2761,8 +2799,17 @@ function theme_form_element($variables) {
if (!empty($element['#attributes']['disabled'])) {
$attributes['class'][] = 'form-disabled';
}
+ // Add a class if an error exists.
+ if (!empty($element['#errors'])) {
+ $attributes['class'][] = 'form-error';
+ }
$output = '
' . "\n";
+ // Display any error messages.
+ if (!empty($element['#errors']) && empty($element['#error_use_parent'])) {
+ $output .= ' ' . theme('form_error_message', array('element' => $element));
+ }
+
// If #title is not set, we don't display any label or required marker.
if (!isset($element['#title'])) {
$element['#title_display'] = 'none';
@@ -2873,6 +2920,25 @@ function theme_form_element_label($variables) {
}
/**
+ * Returns HTML for an inline error associated with a specific form element.
+ *
+ * @param array $variables
+ * An associative array containing:
+ * - element: An associative array containing the properties of the element.
+ * Properties used: '#error'.
+ *
+ * @return string
+ *
+ * @ingroup themeable
+ */
+function theme_form_error_message($variables) {
+ $output = '
';
+ $output .= '' . t('Error') . ': ' . $variables['element']['#errors'] . '';
+ $output .= '
';
+ return $output;
+}
+
+/**
* Sets a form element's class attribute.
*
* Adds 'required' and 'error' classes as needed.
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index ef54c93..679cea5 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -2698,6 +2698,9 @@ function drupal_common_theme() {
'form_element_label' => array(
'render element' => 'element',
),
+ 'form_error_message' => array(
+ 'render element' => 'element',
+ ),
'vertical_tabs' => array(
'render element' => 'element',
),
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 37c913a..1269569 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -907,6 +907,9 @@ public function validateForm($form_id, &$form, &$form_state) {
}
$form_state['values'] = $values;
}
+ if (!$form_state['programmed']) {
+ $form['#errors'] = $this->getErrors($form_state);
+ }
}
/**
@@ -1240,9 +1243,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');
- }
}
}
@@ -1770,15 +1770,6 @@ protected function watchdog($type, $message, array $variables = NULL, $severity
}
/**
- * Wraps drupal_set_message().
- *
- * @return array|null
- */
- protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
- return drupal_set_message($message, $type, $repeat);
- }
-
- /**
* Wraps drupal_html_class().
*
* @return string
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 @@
+ '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'),
+ );
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 3b7a97f..992995d 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -287,12 +287,6 @@ protected function menuGetItem() {
/**
* {@inheritdoc}
*/
- protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
- }
-
- /**
- * {@inheritdoc}
- */
protected function watchdog($type, $message, array $variables = NULL, $severity = WATCHDOG_NOTICE, $link = NULL) {
}