Changing Submit Input Elements Into Buttons In Drupal 7

16th November 2012 - 6 minutes read time

I spent what seemed like an eternity today trying to figure out something in a form I was creating on a Drupal site. I was building a multi step form with previous and next buttons, both of which were submit elements like this.

$form['next'] = array(
  '#type' => 'submit',
  '#name' => 'next',
  '#submit' => array(
    'mymodule_next_submit'
  ),
  '#value' => 'Next Step',
);

What I needed to do was override these and make them button elements instead of input elements. This involved creating a theme function in my template that looked like this. You could also do this in your module by adding theme overrides using hook_theme_registry_alter().

/**
 * Override of theme_button().
 *
 * Render the button element as a button and the submit element as an input element.
 */
function mytheme_button($variables) {
  $element = $variables['element'];
  $element['#attributes']['type'] = 'submit';
  
  element_set_attributes($element, array('id', 'name', 'value'));  
  
  $element['#attributes']['class'][] = 'form-' . $element['#button_type'];
  if (!empty($element['#attributes']['disabled'])) {
    $element['#attributes']['class'][] = 'form-button-disabled';
  }

  if (isset($element['#buttontype']) && $element['#buttontype'] == 'button') {
    $value = $element['#value'];
    unset($element['#attributes']['value']);
    return '<button' . drupal_attributes($element['#attributes']) . '>' . $value . '</button>';
  }
  else {
    return '<input' . drupal_attributes($element['#attributes']) . ' />';
  }
}

I then changed the previous and next button definitions to look like the following. The elements were still submit form elements, but they were rendered as buttons by using a #buttontype custom property.

$form['prev'] = array(
  '#type' => 'submit',
  '#name' => 'prev',
  '#validate' => array(
    'mymodule_previous_validate'
  ),
  '#submit' => array(
    'mymodule_previous_submit'
  ),
  '#buttontype' => 'button',
  '#value' => '<i class="icon-chevron-left"></i> ' . t('Previous Step'),
);

$form['next'] = array(
  '#type' => 'submit',
  '#name' => 'next',
  '#submit' => array(
    'mymodule_next_submit'
  ),
  '#buttontype' => 'button',
  '#value' => t('Next Step') . ' <i class="icon-chevron-right"></i>',
);

All of this generated the following output on the form.

<button class="form-submit" type="submit" id="edit-prev" name="prev"><i class="icon-chevron-left"></i> Previous Step</button>
<button class="pull-right form-submit" type="submit" id="edit-next" name="next">Next Step <i class="icon-chevron-right"></i></button>

This all worked fine on forms that contained a single button element (like the first form in the multi step process) but it all broke down if another button or submit element was present on the form. It seemed as if when the form was submitted that it either did nothing or went back a page. None of the correct validation or submission functions were being run either, so I had to delve deeper.

After lots of digging around and following the form code execution I managed to figure out that Drupal wasn't able to see what button had been clicked on and so assumed that the first button defined in the form was the correct element. This tied in with the behaviour I was seeing as the previous button was being executed first, even if the next button was being clicked on. Some pages of the form had "Add new item" AJAX buttons on them that were being picked up first by the form processing, meaning that none of the correct functions were being processed.

The code that is causing this is in the form_builder() function. The triggering_element item in the $form_state array is forced to be the first button that is found on the form (if it hasn't been set) before the form then processes the validation and submit functions.

function form_builder($form_id, &$element, &$form_state) {

  // ... snip
  
    if (isset($element['#type']) && $element['#type'] == 'form') {
    
    // ... snip
    
    if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) {
      $form_state['triggering_element'] = $form_state['buttons'][0];
    }

    // If the triggering element specifies "button-level" validation and submit
    // handlers to run instead of the default form-level ones, then add those to
    // the form state.
    foreach (array('validate', 'submit') as $type) {
      if (isset($form_state['triggering_element']['#' . $type])) {
        $form_state[$type . '_handlers'] = $form_state['triggering_element']['#' . $type];
      }
    }
    
    // ... snip

   }

  // ... snip
}

I figured out that I could get the code to execute in the way I wanted by using the #after_build property of the form. This is run as the form is processed and allows me to add the correct setting to the $form_state array before it hits the code above.

$form['#after_build'][] = 'mymodule_force_triggering_element';

The function in the #after_build element is used to figure out which button has been clicked on. The correct button was present in the input element in the $form_state array so a simple bit of logic allowed me to add the correct triggering_element for the form state.

function mymodule_force_triggering_element($form, &$form_state) {
  if (isset($form_state['input']['next'])) {
    $form_state['triggering_element'] = $form['next'];
  } elseif (isset($form_state['input']['prev'])) {
    $form_state['triggering_element'] = $form['prev'];
  }
  return $form;
}

After this, the form runs as normal with the correct buttons running the correct behaviours. I hope this helps out other Drupal devs who have had the same issue.

Comments

Permalink
Just wanted to say thank you for this. I was wrestling with this exact problem (in order to use an icon, just like you) but for the search box submit button. Your code worked great - thanks!

Chip Cullen (Thu, 04/04/2013 - 15:15)

Permalink
I'm also trying to change the search box, but this clearly does far more. How can I pare this down for just the search box?

Mike (Tue, 07/29/2014 - 01:56)

Permalink
I managed to change the input element like so... $form['actions']['submit'] = array( '#markup' => '

Mike (Tue, 07/29/2014 - 02:11)

Permalink
Are you kidding me?! I have been struggling with this for a long time and designed around this Drupal 7 submit button "feature". Now I can take control of forms with multiple submit buttons like I have wanted. THANK YOU!!! Internet FTW.

Jim (Wed, 07/15/2015 - 17:26)

Permalink
Thank you for posting this. The form I inherited had a number of other issues with it, but this post helped shine the light on what was really happening and get it straightened out. I only wish I'd seen it a few hours earlier. :)

Chris (Tue, 10/20/2015 - 06:00)

Permalink
Thanks for this, really useful. In the end I went with a different solution:function my_form() { $form['submit'] = ['#type' => 'submit', '#value' => 'Go', '#prefix' => '', '#ajax' => ... etc etc. ]; // ... return $form; }Then in CSS:span.icon { background-image:url(path/to/icon); position:absolute; margin-left:1em; margin-top:0.5em; pointer-events:none; } input[type="submit"] { border:solid 1px black; padding:0.5em 1em 0.5em 3em; }This basically just sticks an icon over the top of the submit button. pointer-events is there to make clicks go through it to the button below. It's a hack, but it's a small hack :-)

Rich (Fri, 09/16/2016 - 10:23)

Permalink

Why in your hook are you checking $form_state['input']['next'] instead of $form_state['values']['next']?  I am using Drupal 7.73 and for my form, my buttons never show up in $form_state['input'].  This works for me but not what you originally posted.

Raul Rodriguez (Fri, 01/29/2021 - 23:10)

Permalink

@Raul The main difference is that $form_state['input'] is usually used during an ajax callback, whereas $form_state['values'] is used for normal form submits. That might be what I has originally been trying to do here. It has been 8 years since I wrote this post so I'm not sure.

I'm glad you found the right solution tough!

Add new comment

The content of this field is kept private and will not be shown publicly.