Wrong triggering element on the form submit after an ajax callback call

Created on 14 August 2023, about 1 year ago
Updated 4 September 2024, 4 days ago

Problem/Motivation

I've got a bug with receiving a wrong triggering element on the form submit action, after processing an AJAX callback on the same form.

I have two submit buttons "check" and "send", the first "check" button has an ajax callback, that shows the second "send" button.

And after clicking on the "check" button, and then - on the "send" button, I'm still receiving the "check" triggering element, instead of the "send".

Steps to reproduce

1. Create a form like this:


namespace Drupal\my_module\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class ExampleForm extends FormBase {

  public function getFormId() {
    return 'my_module_example';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['message'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Message'),
    ];
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['actions']['check'] = [
      '#type' => 'submit',
      '#value' => $this->t('Check'),
      '#ajax' => [
        'callback' => '::ajaxSubmitForm',
      ],
    ];
    $form['actions']['send'] = [
      '#type' => 'submit',
      '#value' => $this->t('Send'),
      '#access' => FALSE,
    ];
    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $triggeredElement = $form_state->getTriggeringElement()['#parents'][0];
    $this->messenger()->addMessage("submitForm: Sent successfully. Triggered element: $triggeredElement");
    return $form;
  }

  public function ajaxSubmitForm(array &$form, FormStateInterface $form_state) {
    $triggeredElement = $form_state->getTriggeringElement()['#parents'][0];
    $response = new AjaxResponse();
    $form['actions']['send']['#access'] = TRUE;
    $form['actions']['send']['#value'] = $this->t('Send (checked)');
    $form['actions']['check']['#access'] = FALSE;
    $response->addCommand(new ReplaceCommand('[data-drupal-selector=edit-actions]', $form['actions']));
    $response->addCommand(new MessageCommand("ajaxSubmitForm: Checked successfully. Triggered element: $triggeredElement"));
    return $response;
  }

}

2. Open the form, click on the "Check" button.
The AJAX callback will be performed and you will see a message: "ajaxSubmitForm: Checked successfully. Triggered element: check".
And the button "Check" will be replaced to "Submit"
All is okay.

3. Click on the "Send" button.
You will see a message: "submitForm: Sent successfully. Triggered element: check"

But this is wrong! The clicked button is "send", not the "check"!

Could you please reproduce this? And if this a correct behavior, could you please describe how to fix this problem to get the right "send" triggering element in the third step?

Proposed resolution

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

πŸ› Bug report
Status

Active

Version

11.0 πŸ”₯

Component
FormΒ  β†’

Last updated less than a minute ago

Created by

πŸ‡¦πŸ‡²Armenia Murz Yerevan, Armenia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • Issue created by @Murz
  • πŸ‡¦πŸ‡²Armenia Murz Yerevan, Armenia
  • πŸ‡¨πŸ‡¦Canada alberto56

    @Murz thanks for posting this. I was going around in circles, and your post set me in the right direction. In my case as well, the triggering element seems wrong.

    I would expect:

    $form_state->getTriggeringElement()['#name'];
    

    to be the same as:

    $form_state->getUserInput()['_triggering_element_name'];
    

    which it is in most cases, but in some cases the former gives me, for some reason, the first triggering element in the page whether or not that's the one I clicked.

    $form_state->getUserInput()['_triggering_element_name'];
    

    always seems right.

    There is a lot of custom code in my site, so for now I cannot confirm that I'm not doing something which is causing this (although I cannot find anything).

    For me, a smelly workaround is the best way forward for now. Thanks!

  • πŸ‡¨πŸ‡­Switzerland audacus

    I had a similar issue building an AJAX form with multiple items.
    Each item has its own remove button. When clicking the remove button e.g. of the first item, the triggering element would still be the remove button of the last item.

    I tried to track the issue down and found following checks in \Drupal\Core\Form\FormBuilder::elementTriggeredScriptedSubmission where it will return whether the given element is triggering the submission:

    protected function elementTriggeredScriptedSubmission($element, FormStateInterface &$form_state) {
      $input = $form_state->getUserInput();
      if (!empty($input['_triggering_element_name']) && $element['#name'] == $input['_triggering_element_name']) {
        if (empty($input['_triggering_element_value']) || $input['_triggering_element_value'] == $element['#value']) {
          // If the `#name` and the `#value` match the element in the user input, it is the triggering element...
          return TRUE;
        }
      }
      return FALSE;
    }
    

    If you have multiple elements with the same #name and the same #value, the triggering item will be the last item for which the above checks run through.

    <!--break-->

    I had something like the following:

    for ($i; $i < $num_items; $i++) {
      $form['wrapper']['items'][$i]['actions']['remove'] = [
        '#type' => 'submit',
        '#value' => $this->t('Remove item'),
        '#submit' => ['::removeItem'],
        '#ajax' => [
          'callback' => '::ajaxCallback',
        ],
      ];
    }
    

    Like this, every remove button had the same #name and same #value, and therefore the triggering element always was the remove button of the last item.

    I could solve this issue by adding a unique name or value for each remove button:

    for ($i; $i < $num_items; $i++) {
      $form['wrapper']['items'][$i]['actions']['remove'] = [
        '#type' => 'submit',
        // Item specific name.
        '#name' => 'remove-item-' . $i,
        // Item specific value.
        '#value' => $this->t('Remove item @item', ['@item' => $i]),
        '#submit' => ['::removeItem'],
        '#ajax' => [
          'callback' => '::ajaxCallback',
        ],
      ];
    }
    
Production build 0.71.5 2024