You are hereBlogs / rahul's blog / Drupal - Changing form field values in validation/submit handlers

Drupal - Changing form field values in validation/submit handlers


rahul's picture

By rahul - Posted on 17 February 2010

All Drupal module developers would probably be aware of Drupal Form API's form_set_value() method, which allows you to "change submitted form values during the form processing cycle."

You can use this method in the form validate handler to process a different value in the form submit handler, than what was submitted by the browser. However, remember the new value would be used for processing only, and would not be rendered to the browser if you have not specified a redirect url for the form, and the same form renders again after submission.

Now, here was a big problem for me, while working on the next version of my open source Drupal module, Take Control. In the Admin section of this module's upcoming version, there is an option to generate new Public/Private key pair to be used by module in async Ajax calls.

I needed to generate new key pairs in the submit handler (not validate handler) of this form, and somehow get the new keys generated to be rendered to the browser instead of the original ones. An extensive Google search threw nothing up. However, a small hint in this comment, was enough to validate that what I wanted can be achieved, albeit with some work.

You can set 'rebuild'=TRUE on your form state to ask Drupal to build your form from scratch.

What this meant was that I can generate new Key Pair in the submit handler, ask Drupal to rebuild the form, and use the new Pair during the rebuild process, as default values for the form fields.

The last piece in the puzzle (rather second last) was how to decide while form rebuilding, which key pair to use, original one or the new one. The solution was simple here, presence of "rebuild" attribute in the form state with a value of true meant that the new pair was to be used.

The absolutely final part of the solution for the problem here was a bit complex. Remember, after Drupal rebuilt your form, all data submitted by the user would be over-written by default values you specify during rebuilding. This is desirable for the actual fields that you want to change, but highly non-user friendly for other fields, where user submitted data should have persisted.

This obviously meant you would need to copy $form_state to your $form fields manually. And this process needed to be recursive due to the probability of presence of fields having #tree=> TRUE which meant that the submitted data in $form_state would be nested.

So, here's the step-by-step solution to be above problems (the code has been extracted in bits and pieces from the Take Control module as suitable):

1) You need to overwrite data of some fields in the form's submit handler.

 

function take_control_generate_new_handler($form, &$form_state) {
  /// Other code
  $keys = take_control_new_keys($algo, $passphrase);
  form_set_value($form['key']['take_control_private_key'], $keys['priv'], $form_state);
  form_set_value($form['key']['take_control_public_key'], $keys['pub'], $form_state);

  $form_state['rebuild'] = TRUE;

  drupal_set_message('New Public /Private Key Pair generated.', 'status');
}

There are 2 important things to note above, i) we still use form_set_value() to overwrite submitted data in $form_state (this data would be used later for rebuilding the form), and ii) we ask Drupal to rebuild the form by setting: $form_state['rebuild']=TRUE.

 

2) You need to detect manually in your module_form() hook, whether this was en explicit request for rebuild:

 

function take_control_settings_form(&$form_state = array()) {
/// Other code

  $rebuild = FALSE;
  if (!empty($form_state['rebuild'])) {
    $rebuild = $form_state['rebuild'];
  }
  if ($rebuild == TRUE) {
    // Trigger recursive state restore. Remember, our values overwritten in $form_state would be used for appropriate fields.
    take_control_restore_user_input($form, $form_state);
    $form_state['rebuild'] = FALSE;
  }

  return system_settings_form($form);
}

3) Restore the form values recursively. You can simply copy-paste the code below for your purposes, just changing the method names:

 

function take_control_restore_user_input(&$form, &$form_state) {
  $values = $form_state['values'];
  foreach ($form as $key => &$parent) {
    if (!empty($parent['#type'])) {
      if (!empty($parent['#tree']) && $parent['#tree'] == FALSE) {
        $values = $values[$key];
      }
      take_control_restore_user_input_recursive($key, $parent, $values);
    }
  }
}

function take_control_restore_user_input_recursive($key, &$parent, &$values) {
  $value = $values[$key];
  if (!empty($value)) {
    $parent['#default_value'] = $value;
  }
  foreach ($parent as $key => &$child) {
    if (is_array($child) && !empty($child['#type'])) {
      if (!empty($child['#tree']) && $child['#tree'] == FALSE) {
        $values = $values[$key];
      }
      take_control_restore_user_input_recursive($key, $child, $values);
    }
  }
}

Hmmm, well, that is lots of code for what should have been a simple method call in my opinion. But as you know, frameworks have their ways of doing things, and its best to stick to their ways for what you are trying to do, for maintenance and consistency.

 

 

 

Hello!

Code looks nice and I'm trying to implement it in a solution of mine. I get an error however on line 4 of your last code snippet saying:

"Fatal error: Cannot use object of type stdClass as array..."

I've been trying to remove the empty() methods and fiddling around a bit but with no success.

I'm using hook_form_alter() to make customisations to the form rather then using hook_form(). Could this be a problem you think?

rahul's picture

Hmmm, not sure, but that could be an issue (using hook_form_alter). One thing is for sure, that the approach I presented above works, as the Take Control module already released on Drupal.org uses it, and works fine.

Now, one thing that is not too clear too me is why $form_state should contain an object of type stdClass. It should have been a nested associative array in my understanding. Why don't you try a print_r() on $form_state and see which stdClass object it contains? You can also try to step-through and see the exact state where the error is generated.

It might be very possible that another custom module is adding the object to $form_state. You could also try disabling all non-core modules, and then move forward with your development.

rahul's picture

You can also add a check for is_array at line 4 of the last snippet before is_empty (where you are saying the error is generated), and move forward only if the variable is an array. Here's a quick snippet:

 

function take_control_restore_user_input(&$form, &$form_state) {
  $values = $form_state['values'];
  foreach ($form as $key => &$parent) {
    if (is_array($parent) && !empty($parent['#type'])) {
      if (!empty($parent['#tree']) && $parent['#tree'] == FALSE) {
        $values = $values[$key];
      }
      take_control_restore_user_input_recursive($key, $parent, $values);
    }
  }
}

Notice the 4th line above, it has an extra check for is_array on $parent.

 

Thank you very much for your detailed answers and support, I will try your advice as soon as I get a chance. Will report back with a successstory I hope :]

Hi ,

I came across your post. Very helpful, I must say...

But one issue which I am unable to resolve is:

I want to add a javascript to my form fields:

example -     $form['text_status'] = array(
      '#default_value'  => $node->text_status,
      '#title'          => t('Status Text'),
      '#type'           => 'textfield',
    );

 

These type of text fields, when they are filled in with some values by the user, then a javascript will automatically show a preview on a right hand block. for ex:

http://papermashup.com/demos/widget-builder/

 

I have the javascript also:

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Papermashup.com | jQuery Form Preview Tool</title>
<link href="../style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$('#preview').hide();   
$("#photo").change(update);
$("#title").keyup(update);
});
   
function update(){       
       
$('#preview').slideDown('slow');
var title = $("#title").val();
var photo = $("#photo").val();
$('#Displaytitle').html(title);
$('#image').html('<img src="'+photo+'"/>');
}



</script>



<style>
.left {
    width:400px;
    float:left;
    font-size:13px;
    color:#333;
    margin-right:20px;
}
.right {
    width:320px;
    float:left;
    margin-right:20px;
}
#preview {
    display:none;
    min-height:247px;
    background-color:#FFC;
    padding:10px;
    font-size:12px;
    color:#999;
    border:1px solid #FF9;
}
#title {
    margin-top:10px;
    padding:5px;
    font-size:13px;
    color:#000;
    border:1px solid #CCC;
    font-family:Verdana, Geneva, sans-serif;
}
#photo {
    margin-bottom:10px;
}
#image {
    margin-top:5px;
}
#Displaytitle {
    font-size:14px;
    color:#333;
    margin-top:5px;
}
</style>
</head>

<body>
<div id="header"><a href="http://www.papermashup.com/"><img src="../images/logo.png" width="348" height="63" border="0" /></a></div>
<div id="container">
  <h3>jQuery Form Preview Demo</h3>
 

 
 
  <div class="left">
    <form>
      Choose a site<br/>
      <select name="pic" class="click" id="photo">
        <option value="images/photo1.png">Tweet.me.it</option>

        <option value="images/photo2.png">Dotdashcreate.com</option>
        <option value="images/photo3.png">Papermashup.com</option>
      </select>
      <br/>
      Add a Tagline
      <textarea id="title" class="click" name="title" cols="40" rows="4">This is your default advert text. </textarea>
    </form>
  </div>

  <div class="right">
    <noscript>
    This is where you could put a link for users who have JavaScript disabled to see a preview :)
</noscript>
    <div id="preview"> This is how your advert will look
      <div id="image"></div>
      <div id="Displaytitle"></div>
    </div>
  </div>
  <div class="clear"></div>

</div>
<div id="footer"><a href="http://www.papermashup.com">papermashup.com</a> | <a href="http://papermashup.com/create-a-dynamic-form-preview/">Back to tutorial</a></div>
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-7025232-1");
pageTracker._trackPageview();
} catch(err) {}</script>
</body>
</html>

 

 

Thanks

Rajesh

rahul's picture

Hi Rajesh, well that was a way lot of code for a very simple situation.

Did you had a look at Drupal Form API reference:
http://api.drupal.org/api/drupal/developer--topics--forms_api_reference....

 It contains everything you need for your purpose. If you have been unable to find out, please check out the #attributes section:
http://api.drupal.org/api/drupal/developer--topics--forms_api_reference....

That would enable you to add arbitrary attributes (including javascript handlers) to your form fields.

I might truncate some of your non-related code above to keep the sanity of the page.

Hello Rahul,

I am trying to add validation my new user registration form.

I had made one module for that.

Now here is my code.

I want to check if the user had only entered the numbers in Phone No text field and Mobile No text field.

My module name is form_intro.

Can you give me suggestion, what i had done wrong?

 

<?php
function form_intro_form_alter($form_id,&$form){
   
if($form_id == 'user_register' || $form_id == 'user_edit'){
        $form
['Personal Information']['profile_pno']['#validate'] = array('form_intro_pno_validate' => array());   //profile_pno is for Phone No.
        $form
['Personal Information']['profile_mno']['#validate'] = array('form_intro_mno_validate' => array());   //profile_mno is for Mobile No.
   
}
}


function form_intro_pno_validate($element){
   
if(!is_numeric($element['#value'])){
        form_set_error
('profile_pno' , t('Please Enter Only Number in Phone no'));
   
}
}

function form_intro_mno_validate($element){
   
if(!is_numeric($element['#value'])){
        form_set_error
('profile_mno' , t('Please Enter Only Number in Mobile no'));
   
}
}
?>



 
rahul's picture

Hi Nitish, I think you have got it completely wrong. You provide a single validation handler method for a form in which you validate all form fields (instead of a separate validation handler for each form field).

I think the following code should work for you:

function form_intro_form_alter($form_id,&$form){
    if($form_id == 'user_register' || $form_id == 'user_edit'){
        $form['Personal Information']['profile_pno']['#validate'] = array('form_intro_pno_validate' => array());   //profile_pno is for Phone No.
        $form['Personal Information']['profile_mno']['#validate'] = array('form_intro_mno_validate' => array());   //profile_mno is for Mobile No.
    }
}

/**
 * Implementation of hook_validate().
 */
function form_intro_validate($form, &$form_state) {
    if(!is_numeric($form_state['values']['profile_pno'])){
        form_set_error('profile_pno' , t('Please Enter Only Number in Phone no'));
    }

    if(!is_numeric($form_state['values']['profile_mno'])){
        form_set_error('profile_mno' , t('Please Enter Only Number in Mobile no'));
    }
  }
}
I think you are adding your fields inside a FieldSet in hook_form_later. In that case, make sure #tree is set to false to the FieldSet, otherwise the above validation code would need to change.

Hello Rahul,

 

I've come acros your site after searching for a whole day for a fix for my problem.

[code]/**

* Validate the festival_tickets_multiform form.

*/

function festival_tickets_multiform_validate($form, &$form_state){

switch($form_state['storage']['step']){

case 1:

$tickets = $form_state['values']['tickets'];

if(!is_numeric($tickets)){

form_set_value($form['tickets'],'1',$form_state);

form_set_error('tickets',t('Please enter a number.'));

$form_state['rebuild'] = TRUE;

}[/code]

[code]$form['tickets'] = array(

'#type' => 'textfield',

'#title' => t('Amount'),

'#default_value' => isset($default_value['tickets']) ? $default_value['tickets'] : '1',

'#size' => 4,

'#maxlength' => 2,

'#required' => TRUE,

'#attributes' => array('onchange' => 'update_price_total(this.value);'),

);[/code]

But when I enter for example; A into my inputbox and click on submit the A remains in the inputbox but the error is shown. I've tried several methods but somehow drupal only wants to show the submitted content?

rahul's picture

Hi Siskos, I don't think I understand clearly what you are trying to do, but here are my observations to your code:

  1. Shouldn't this:
    switch($form_state['storage']['step']){

    be converted to:
    switch($form_state['values']['storage']['step']){ // If storage is a FieldSet with #tree=>TRUE

    or to:
    switch($form_state['values']['step']){   // If storage is a FieldSet with #tree=>FALSE
  2. Once you set an error on the form:
    form_set_error('tickets',t('Please enter a number.'));
    Drupal would always render the same form with the error field highlighted. I haven;t tried this, but if you want to rebuild the form (I cannot see why from your code) on an invalid input, don't use form_set_error, but simply set the form to rebuild (rebuild=>TRUE).

In case, the above points do not resolve your issue, can you please explain a bit in detail what exactly are you trying to accomplish??

where is to be defined   update_price_total 

rahul's picture

Well that is as out-of-context as it can get. What are you trying to ask Saranyamohan?

hai rahul ,

thanks for ur response. iam trying to  ajax. .. i want a onchange action on the  selectbox..but but i don't know where the on change  function to be defined 

rahul's picture

Hi saranyamohan, please read this:
http://www.rahulsingla.com/chit-chat/2010/07/general-help-and-support

and redirect your question here:
http://ike.co.in/forum 

thank u

Hi Rahul!

     Since i am new to drupal, i have a problem in giving validation for the form fields. here are my two form fields, can u please give me  a code which validates the below form field.

 $form['contact_details_designation'] = array('#type' => 'textfield',
    '#title' => t('Designation'),
    '#default_value' => $node->contact_details_designation,
    '#size' => 25,
    '#attributes' => array('class' => 'stateprogram-contact_details_designation'),
    '#required' => TRUE,
  );

   $form['contact_details_telephone'] = array('#type' => 'textfield',
    '#title' => t('Telephone'),
    '#default_value' => $node->contact_details_telephone,
    '#size' => 25,
    '#attributes' => array('class' => 'stateprogram-contact_details_telephone'),
    '#required' => FALSE,
  );

where i have to check whether

1.  Designation field should be in uppercase
2.  Telephone should be only numbers

rahul's picture

Hi Joe, Please check the methods: take_control_qp_admin_form and take_control_qp_admin_form_validate at the following url:
http://drupalcode.org/viewvc/drupal/contributions/modules/take_control/qp/take_control_qp.module?revision=1.3&view=markup

You can perform any desired custom validation in the hook formid_validate. For any fields that fail validation, use the form_set_error method to indicate the error, which also prevents the submission of the form.

I did not quite understand ... seems missing AJAX part ...

rahul's picture

James, what exactly are you trying to accomplish??

Amazing Post! Thank you very much for sharing this.

Thank you for sharing. I learned a lot from the post and the comments. As for me, when I learned Drupal I used this book: Learning Drupal 6 Module Development. It is really a great book to learn and practise Drupal.

Hello, I want to say that your blog is providing a lot of usefull informations.

I'm trying to do the samething explained in this post, but I'm using Drupal 7.  I'm not able to make it work.

In fact, the values that are changed in the submit handler with the form_set_value function are rendered with the user input value. When a do a print_r() of the form_state['values'], I see that my values was changed with your recursive function !

Any idea ? Thanks in advance !

Regards,
Ben

Hi, I just need to add another information related to my previous concern. The form is submitted from a button with an #ajax callback.

Thanks in advance !

Regards,
Ben

rahul's picture

Hi Benoit, I have not tried this approach with D7 yet. And I am utterly short on time to try this for some days. But what I recall is something changed related to $form_state['rebuild']. Can you please try setting $form_state['#rebuild'] to TRUE? You might also want to try out using drupal_rebuild_form. The important point here is that your form generation method (take_control_settings_form in above code) gets called after you set $form_state['#rebuild'] to TRUE. So, you can put breakpoints and see if that method gets called twice on post back once before and after you set $form_state['#rebuild'] to TRUE on post back.

Finally, can you please try dropping the Ajax callback and try this with a regular postback to ensure that Ajax is not the problem.

rahul's picture

An important point to note is that the values set by form_set_value are NOT changed by the recursive function in the above code. Rather drupal changes the value. The recursive method restores these values to the form when you set the form to rebuild ($form_state['#rebuild'] = TRUE).

rahul's picture

Hi aga, I am not too sure what is your question on StackExchange. Please note that I do not answer questions unrelated to the blog posts, I might remove your further comments in reply to this if they do not relate to the post:
http://www.rahulsingla.com/chit-chat/2010/07/general-help-and-support

$form_state['rebuild'] = TRUE;

before:

form_set_error('...');

works well for updating the user submitted values.

Thanks

Still haven't nailed this. It worked for a short while, but not now... If I set an error with for_set_error, the newly inputted values are not taken into account, even if form_state rebuild is set to TRUE

rahul's picture

Hi natuk, I am not sure of the context of your comments, what you are trying to achieve and what is happening. Can you please be more precise and provide some more details?

Post new comment

The content of this field is kept private and will not be shown publicly.
Type the characters you see in this picture. (verify using audio)
Type the characters you see in the picture above; if you can't read them, submit the form and a new image will be generated. Not case sensitive.

Recent comments