9

I'm triggering an ajax-enabled button on a form but Drupal's setting $form_state['#triggering_element'] to the wrong element.

The button is a child of a #tree and is identical to other buttons in other tree-ed structures, except for its parents. But I thought this was the point of #tree.

Looking at the drupal code in form.inc it's clear that Drupal only goes on the #name property to identify the triggering element, which is automatically set to 'op' for buttons. This seems to have no idea about trees or the element's parents.

The rather scant and very old docs for tree state:

#parents is used to create the name/ID of the form element itself.

But my button element's names are not being set to anything other than 'op'. I'm creating the buttons in a #process function for the parent element.

Example code

$form['group1'] = [
  '#prefix' => '<div>group1',
  '#suffix' => '</div>',
  '#tree' => TRUE,
  'trig' => [
    '#type' => 'submit',
    '#value' => 'ajax go',
    '#ajax' => [
      'callback' => 'mymod_ajax_callback',
      'wrapper' => 'foo',
      ],
  // Other elements part of group 1...
  ],
$form['group2'] = // The same.

A work-around is to code creating unique names for every one of these elements, but I feel I might be missing a trick/nuance?

4
  • 1
    To the best of my knowledge creating unique names for each element is the correct way to solve this. I wasn't aware it's considered a work-around; have you seen something to suggest that or is it an assumption?
    – Clive
    Apr 8, 2015 at 10:21
  • 1
    just thought that was the point of #tree - to have reusable components (e.g. listing of rows) identified by the context. Also, docs suggest name will be set based on parents, but this is not the case? Apr 8, 2015 at 10:35
  • 1
    It's a quirk at best, a bug at worst. As you've discovered, info about it is difficult to find. Being picky: used to create the name/ID of the form element itself. doesn't explicitly guarantee that there won't be collisions between elements
    – Clive
    Apr 8, 2015 at 12:27
  • 1
    @Clive OK, but it does go on to give the example: "...#parents for baz will be array('foo', 'bar', 'baz') and the name of the element in the HTML will be edit['foo']['bar']['baz']." which is not what I'm seeing either. Apr 8, 2015 at 15:43

5 Answers 5

6

The problem is in form_builder() where it fallbacks to the first button if no button can be found.

A comment from form_builder():

// Therefore, to be as consistent as we
// can be across browsers, if no 'triggering_element' has been identified
// yet, default it to the first button.
if (!$form_state['programmed'] && !isset($form_state['triggering_element']) && !empty($form_state['buttons'])) {
  $form_state['triggering_element'] = $form_state['buttons'][0];
}

More info can be found here: https://www.drupal.org/node/2546700#comment-11164091

There is also a patch for this problem that can be found in the link above.

3

First of all: you should implement all your $form modifications inside the form builder (hook_form or hook_form_alter), not inside ajax callbacks. And my mistake in one form was that when I have added few buttons with one ajax callback but set name html attribute using #attributes['name']. Drupal way in this case is to use '#name' parameter. Only when you set name attribute this way, $form_state['triggering_element'] and $form_state['clicked_button'] will be set to the correct element

3

To get a triggering element you must provide a unique '#name' for each button. Additionally, you can specify parameters to identify the action and record

function example_form($form, &$form_state){
    $form['groups'] = array(
        '#tree' => TRUE,
    );

    foreach(array(5,4,8,10) AS $id){

        $form['groups'][$id] = array(
            'id' => array(
                '#markup' => 'Group '. $id,
            ),
            // Some buttons with different actions for every group
            'btn_clone' => array(
                '#type' => 'submit',
                '#value' => 'Clone',
                // Any unique name for each button
                '#name' => 'clone-'. $id, 
                // Your additional data to identify the action
                '#my_op' => 'clone', 
                '#my_key' => $id,
            ),
            'btn_delene' => array(
                '#type' => 'submit',
                '#value' => 'Delete',
                // 
                '#name' => 'del-'. $id,
                '#my_op' => 'delete', 
                '#my_key' => $id,
            ),
        );  

    }
    return $form;
}

function example_form_submit($form, &$form_state){
    $clicked_element = drupal_array_get_nested_value($form, $form_state['triggering_element']['#parents']);
    drupal_set_message($clicked_element['#my_op'] .' '. $clicked_element['#my_key']);
}
2

Your example code isn't right, it should be:

$form['group1'] = array(
  '#prefix' => '<div>group1',
  '#suffix' => '</div>',
  '#tree' => TRUE,
);

$form['group1']['trig'] = array(
  '#type' => 'submit',
  '#value' => 'ajax go',
  '#ajax' => array(
    'callback' => 'mymod_ajax_callback',
    'wrapper' => 'foo',
  ),
);

You shouldn't need to use $form_state['#triggering_element'] as you should be able to do everything you need in the mymod_ajax_callback function

6
  • 1
    This code is functionally identical to the original, you've just un-nested the trig element (and converted it from >=PHP5.4 to <PHP5.4)
    – Clive
    Apr 8, 2015 at 12:24
  • 2
    That is the correct way Drupal way as per drupal.org/node/48643 & drupal.org/coding-standards#array Apr 8, 2015 at 12:48
  • 3
    Those documents were produced before short array syntax was available in PHP, and now need to to remain as they are for posterity. Drupal is moving to the short array syntax (clone Drupal 8 and have a look). There's also nothing about un-nesting nested arrays in the coding standards (nor should there be, that's developer preference). Your answer can't help to solve the problem, because you haven't functionally changed anything at all. You may as well have copy-pasted the OPs original code, it's not going to make a difference
    – Clive
    Apr 8, 2015 at 12:50
  • 2
    Drupal may be moving to the short syntax but D7 hasn't and unless you're involved with the Drupal Technical Working Group you wouldn't really know, the majority of developers use the published documentation and coding standards. Apr 8, 2015 at 13:04
  • 1
    Yeah I know they do, unfortunately there's a historical "hrmph" when it comes to Drupal developers adopting industry (and not Drupal-) standards. Things are infinitely better in Drupal 8, hence the reference to the sunnier side of the street. What I'm taking issue with is your phrasing. Your example code isn't right, it should be: is not an accurate statement. The code is right. You would just prefer it conformed to different standards than it currently does. The code is fine, the code works.
    – Clive
    Apr 8, 2015 at 13:09
2

When you click on a submit input field, the browser sends the value of the submit button along with the fields. So giving you have a input like this in the HTML:

<input type="submit" value="foo" />

Foo will be posted along with which ever input/select/textarea elements are located in the form.

Drupal uses this to figure out which form element was pushed. The limitation of this is that if you have multiple inputs that have the same value, Drupal can't discern which of them was actually pushed (since it only can look at the data posted) so it will take the same one.

The only way to make Drupal able to figure out which input was clicked is to

  • Give them different names
  • Use ajax with different callback functions.
  • Use some JS + hidden field to post the info and handle this yourself.

As you can see, if you want a solution that doesn't rely in JS, you need to have different values on the submit buttons.

2
  • For Drupal 7, which the original question probably refers to based on the post date, the solution is to implement hook_forms(), and call the form using a form alias. This does not require any of the above solutinos.
    – Jaypan
    Mar 9, 2017 at 15:00
  • @Jaypan The OP has a single form with many buttons in it. So what you write here doesn't make any sense. The possible solutions above will work however.
    – googletorp
    Mar 9, 2017 at 15:20

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.