3

I have a couple of different forms on one page; each has a 'name' field.

Sensibly, on submission drupal processes the submitted form, but in what seems a completely barmy way of doing things, form_set_error() just takes a field name, and appears to have no knowledge of which form was submitted.

So if the name field is invalid on the submitted form, it gets marked as invalid on all the other forms that use a field called 'name'.

Does this mean that every field needs a unique field name, e.g. name-somemodule1 and name-somemod2? This will break user convenience (browser probably knows what to pre-fill for 'name').

Sure I've missed something, but have read a lot of FAPI pages and code comments and am still confused.

In the screenshot below, you'll see 2 forms. The one on the left is the submitted one, with 'invalid-name' in a field called 'name'. The one on the right is not submitted (can only physically click one submit button at once!) and yet it's 'name' field (with 'admin' in it) is also marked invalid.

These are entirely separate forms.

Screenshot showing problem

Here's my modules' code (email field omitted, some attrs omitted just to reduce post size)

function mymod_contact_form() {
  $form['comment'] = array(
    '#title'         => t('Enter query'),
    '#default_value' => '',
    '#type'          => 'textarea',
  );                                                             

  $form['name'] = array(
    '#title'         => t('Enter name'),
    '#default_value' => '',
    '#type'          => 'textfield',
  );

  $form['submit'] = array(
    '#type'  => 'submit',
    '#value' => 'Send',
  );

  return $form;
}

function mymod_contact_form_validate($form, &$form_state) {
  $name = trim($form_state['values']['name']); 
  if (strpos($name,' ') === FALSE) {
    form_set_error('name', t('Please enter your full name'));
  }
}

function mymod_contact_form_submit($form, &$form_state) {
  $vals = $form_state['values'];
  // …
}

2 Answers 2

5
+50

There is definitely a limitation in Drupal core that is causing form fields with with the same name to be styled as "error" even through the validation error happened on a different form that was submitted on the same page. Looking at the code for Drupal version 6,7,8 (as of now) - they all have this limitation. The good news is that the patch I put together (for Drupal 6) to fix this problem is fairly minimal and it involves using the form_id to properly scope the form element with the validation error. I've haven't done much testing with it, so use it at your own risk. The changes required for Drupal 7 should be similar.

diff --git a/includes/form.inc b/includes/form.inc
index 1529c19..4052133 100644
--- a/includes/form.inc
+++ b/includes/form.inc
@@ -798,29 +798,41 @@ function form_execute_handlers($type, &$form, &$form_state) {
  *   The error message to present to the user.
  * @param $reset
  *   Reset the form errors static cache.
+ * @param $scoped
+ *   Returns the errors scoped with the form_id
  * @return
  *   Never use the return value of this function, use form_get_errors and
  *   form_get_error instead.
  */
-function form_set_error($name = NULL, $message = '', $reset = FALSE) {
+function form_set_error($name = NULL, $message = '', $reset = FALSE,$scoped = FALSE) {
   static $form = array();
+  static $form_scoped = array();
   if ($reset) {
     $form = array();
+    $form_scoped = array();
   }
   if (isset($name) && !isset($form[$name])) {
     $form[$name] = $message;
+    if(isset($_POST['form_id'])) {
+      $name = $_POST['form_id'] . "::" . $name; // Scope it based on form_id
+    } 
+    $form_scoped[$name] = $message;   
     if ($message) {
       drupal_set_message($message, 'error');
     }
   }
-  return $form;
+  if($scoped) return $form_scoped;
+  else return $form;
 }

 /**
  * Return an associative array of all errors.
+ * 
+ * * @param $scoped
+ *   Returns the errors scoped with the form_id
  */
-function form_get_errors() {
-  $form = form_set_error();
+function form_get_errors($scoped = FALSE) {
+  $form = form_set_error(NULL,'',FALSE,$scoped);
   if (!empty($form)) {
     return $form;
   }
@@ -828,16 +840,29 @@ function form_get_errors() {

 /**
  * Return the error message filed against the form with the specified name.
- */
-function form_get_error($element) {
-  $form = form_set_error();
-  $key = $element['#parents'][0];
-  if (isset($form[$key])) {
-    return $form[$key];
-  }
-  $key = implode('][', $element['#parents']);
-  if (isset($form[$key])) {
-    return $form[$key];
+ * 
+ * * @param $scoped
+ *   Returns the error scoped with the form_id
+ */
+function form_get_error($element,$scoped = FALSE) {
+  if($scoped && isset($element['#form_id'])) {
+    $form_id = $element['#form_id']; // Try to make the error form specific
+  } 
+  $form = form_set_error(NULL,'',FALSE,$scoped);
+    
+  for($i=0;$i<2;$i++) {
+    if($i==0) $key = $element['#parents'][0];
+    else $key = implode('][', $element['#parents']);
+     
+    if(isset($form_id)) {
+      $key0 = "$form_id::$key"; // Provide the scope based on form_id
+      if(isset($form[$key0])) {
+        return $form[$key0];
+      }
+    } 
+    if (isset($form[$key])) {
+      return $form[$key]; // If form_id not found then try without it
+    }
   }
 }

@@ -896,6 +921,7 @@ function form_builder($form_id, $form, &$form_state) {
   $count = 0;
   foreach (element_children($form) as $key) {
     $form[$key]['#post'] = $form['#post'];
+    $form[$key]['#form_id'] = $form_id;
     $form[$key]['#programmed'] = $form['#programmed'];
     // Don't squash an existing tree value.
     if (!isset($form[$key]['#tree'])) {
@@ -2246,7 +2272,7 @@ function _form_set_class(&$element, $class = array()) {
   if ($element['#required']) {
     $class[] = 'required';
   }
-  if (form_get_error($element)) {
+  if (form_get_error($element,TRUE)) {
     $class[] = 'error';
   }
   if (isset($element['#attributes']['class'])) {
8
  • Nice work, have you submitted that patch to the issue queue on d.o as well?
    – Clive
    Commented May 8, 2012 at 16:44
  • No, not yet. Is there an existing issue# for this? This patch is for Drupal 6, btw.
    – ttk
    Commented May 8, 2012 at 16:46
  • I'm not sure to be honest, there was one for Drupal 5, but nothing that I've found for D6/7 yet. Seems strange that this problem hasn't popped up more often
    – Clive
    Commented May 8, 2012 at 16:50
  • 2
    Looked at the patches in that issue for Drupal 5. Dries didn't like the approach taken there. My patch takes a different approach, and requires no modification to any forms to work. I'll post to d.o to get some community feedback on the patch.
    – ttk
    Commented May 8, 2012 at 17:00
  • Submitted the patch to the issue queue.
    – ttk
    Commented May 8, 2012 at 17:21
1

Now, form_set_error is indeed a nasty piece of work and I take the full blame for not fixing it in D7 -- but then again, you have drupal_static. Just call drupal_static_reset('form_set_error'); some time between putting out the two forms and be done.

Edit: as the comments point out this is not so simple because we first assemble the page array and then render it and only during render time we add the errors to the form fields. So then you need a wrapper around drupal_get_form which calls it, records the errors somewhere (a global or another drupal_static), resets the errors, adds a #pre_render callback to the top level of the returned form structure. This callback needs to read the errors from our storage and set it back into the form_set_error drupal_static. Yuck! This perhaps would be a nice contributed module.

5
  • "some time between putting out the 2 forms?" Don't get it. Wouldn't I have to know which was going to get create first? :-/ Commented May 11, 2012 at 15:13
  • You could figure that out but even easier to just reset it before each drupal_get_form call.
    – user49
    Commented May 11, 2012 at 16:19
  • Rewrite the hook_menu to not use drupal_get_form, instead create a wrapper function that calls this reset? Might try this. Or hack core and put the call at the top of drupal_get_form to be done with it. Commented May 12, 2012 at 12:52
  • Well, if hook_menu uses drupal_get_form then 100% it's the first.
    – user49
    Commented May 12, 2012 at 16:19
  • 2
    Nope. This erases ALL errors from all forms. I used function drupal_get_form_fix() { $args = func_get_args(); drupal_static_reset('form_set_error'); return call_user_func_array('drupal_get_form', $args);} As in, yes there's no errors on the wrong form, but there's also no errors on the submitted form! Commented May 14, 2012 at 8:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.