Form submits don't work properly when page is translated by browser

Created on 24 June 2021, almost 3 years ago
Updated 2 April 2024, 3 months ago

Problem/Motivation

On our website, the button in the cart page for checkout didn't work if the page was translated by the browser to a different language.
We realized that this happens with every form that has multiple submit actions. Or any submit button in form which has own #submit functions

Steps to reproduce

  1. Install drupal with standard installation profile
  2. Go to /admin/content
  3. Fill title filter with any value and click "Filter"
  4. Translate page into a different language using in-browser translation (In my case Chromium) that changes text in buttons
  5. Click translated reset button

Filter will not reset, since "Filter" was triggered.

Steps to reproduce using Drupal Commerce

  1. Install drupal with standard installation profile
  2. Install drupal commerce and enable commerce cart and commerce checkout modules (this should enable all required modules)
  3. Create new product and add it to cart
  4. Using browser, translate website to any other language that changes value of โ€œCheckoutโ€ button
  5. Click the now translated checkout button and page will reload, since update cart will be triggered

This should also happen by clicking the Remove button on the order item.

Cause

This happens because the value attribute of the submit button changes and form API canโ€™t recognize the value and it defaults to default submit handlers.

๐Ÿ› Bug report
Status

Active

Version

11.0 ๐Ÿ”ฅ

Component
Formย  โ†’

Last updated about 2 hours ago

Created by

๐Ÿ‡ธ๐Ÿ‡ฐSlovakia erik_petra

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.

  • This is a nasty bug, as sites will have carts that can't proceed to checkout if they are translating the site.

  • If anyone encounters this, you can stop the bleeding momentarilly by setting notranslate on the class for each element.

    You can add this in a custom module in a hook_form_alter.

      if (strpos($form_id, 'views_form_commerce_cart_form_') === 0) {
        // Google Translate fix: Add notranslate class to all submit buttons on
        // Commerce cart form to prevent Google Translate from changing submit value,
        // which makes Drupal form no longer know what button was clicked because the action changed.
        $form['actions']['submit']['#attributes']['class'][] = 'notranslate';
        $form['actions']['checkout']['#attributes']['class'][] = 'notranslate';
        $form['actions']['empty_cart']['#attributes']['class'][] = 'notranslate';
        // Also for the remove buttons.
        foreach (Element::children($form['remove_button']) as $key) {
          $form['remove_button'][$key]['#attributes']['class'][] = 'notranslate';
        }
      }

    This is a poor solution, but at least customers can checkout...

  • ๐Ÿ‡ฏ๐Ÿ‡ตJapan magaki

    The cause seems that the triggered element refers to a different than the actually pressed element.

    A #submit callbacks will be set to $form_state in the FormBuilder::doBuildForm(), line:1111-1113.

    if (isset($triggering_element['#submit'])) {
      $form_state->setSubmitHandlers($triggering_element['#submit']);
    }
    

    This $triggering_element is the value got from $form_state->getTriggeringElement();, and the triggered element will be set for form_state in the FormBuilder::handleInputElement(), line:1300-1305.

    $buttons = $form_state->getButtons();
    $buttons[] = $element;
    $form_state->setButtons($buttons);
    if ($this->buttonWasClicked($element, $form_state)) {
      $form_state->setTriggeringElement($element);
    }
    

    A triggered buttons is determined using the FormBuilder::buttonWasClicked().
    The function determines whether the element's #value value exists in the input of $form_state.
    However, if a button that is translated by the browser is pressed, input has the translated value, so buttonWasClicked returns FALSE.

    protected function buttonWasClicked($element, FormStateInterface &$form_state) {
      // First detect normal 'vanilla' button clicks. Traditionally, all standard
      // buttons on a form share the same name (usually 'op'), and the specific
      // return value is used to determine which was clicked. This ONLY works as
      // long as $form['#name'] puts the value at the top level of the tree of
      // \Drupal::request()->request data.
      $input = $form_state->getUserInput();
      // The input value attribute is treated as CDATA by browsers. This means
      // that they replace character entities with characters. Therefore, we need
      // to decode the value in $element['#value']. For more details see
      // http://www.w3.org/TR/html401/types.html#type-cdata.
      if (isset($input[$element['#name']]) && $input[$element['#name']] == Html::decodeEntities($element['#value'])) {
        return TRUE;
      }
      // When image buttons are clicked, browsers do NOT pass the form element
      // value in \Drupal::request()->Request. Instead they pass an integer
      // representing the coordinates of the click on the button image. This means
      // that image buttons MUST have unique $form['#name'] values, but the
      // details of their \Drupal::request()->request data should be ignored.
      elseif (!empty($element['#has_garbage_value']) && isset($element['#value']) && $element['#value'] !== '') {
        return TRUE;
      }
      return FALSE;
    }
    

    If the button was not found in the above process, the first button among the elements registered as buttons will be set as the triggered element in the FormBuilder::doBuildForm().

    $buttons = $form_state->getButtons();
    if (!$form_state->isProgrammed() && !$form_state->getTriggeringElement() && !empty($buttons)) {
      $form_state->setTriggeringElement($buttons[0]);
    }
    

    As a result, when a translated button is pressed,

    - if there are one or more buttons, it will refer to the first button and set in $form_state, and the #submit will be executed.
    - if there is no button, nothing happens.

    As a solution, since #element[#value] will be rewritten by the browser, it may be possible to solve the problem by defining any identifiable values and using them to determine the triggered element.

    I checked the work using the following Form class.
    Normally, pressing buttons 1, 2, and 3 each show a different message.
    However, pressing buttons 2, and 3 will show the message for button 1 when the element has been translated by the browser.

    <?php
    
    namespace Drupal\sandbox\Form;
    
    use Drupal\Core\Form\FormBase;
    use Drupal\Core\Form\FormStateInterface;
    
    class SandboxForm extends FormBase {
    
      public function getFormId(): string {
        return 'sandbox_form';
      }
    
      public function buildForm(array $form, FormStateInterface $form_state): array {
        $form['button1'] = [
          '#type' => 'submit',
          '#value' => 'This is button 1',
          '#submit' => [
            '::callback1',
          ],
        ];
    
        $form['button2'] = [
          '#type' => 'submit',
          '#value' => 'This is button 2',
          '#submit' => [
            '::callback2',
          ],
        ];
    
        $form['button3'] = [
          '#type' => 'submit',
          '#value' => 'This is button 3',
          '#submit' => [
            '::callback3',
          ],
        ];
    
        return $form;
      }
    
      public function submitForm(array &$form, FormStateInterface $form_state): array {
        return $form;
      }
    
      public function callback1(array $form, FormStateInterface $form_state): void {
        \Drupal::service('messenger')->addStatus('Button1 was pressed (callback1)');
      }
    
      public function callback2(array $form, FormStateInterface $form_state): void {
        \Drupal::service('messenger')->addStatus('Button2 was pressed (callback2)');
      }
    
      public function callback3(array $form, FormStateInterface $form_state): void {
        \Drupal::service('messenger')->addStatus('Button3 was pressed (callback3)');
      }
    
    }
    
Production build 0.69.0 2024