This could be a bug, or it could be working as designed and my methodology is wrong.

Summary:
I have a form with a checkbox. The checkbox has an ajax callback. The ajax callback prepares a separate second form, and opens it in a modal. The problem is the second form's ajax is broken.

Here is a basic setup:

First Form buildForm:

public function buildForm(array $form, FormStateInterface $form_state) {
    $form['my_checkbox'] = array(
      '#type' => 'checkbox',
      '#title' => 'my checkbox',
      '#ajax' => array(
        'callback' => array($this, 'clickMyCheckbox'),
        'event' => 'change',
        'progress' => 'throbber',
      ),
    );
    return $form;
  }

First Form "clickMyCheckbox" AJAX Callback:

public static function clickMyCheckbox(array &$form, FormStateInterface $form_state) {
    $second_form = \Drupal::formBuilder()->getForm('Drupal\my_project\Form\SecondForm');
    $response = new AjaxResponse();
    $response->addCommand(new OpenModalDialogCommand('Second Form', $second_form, array('width' => '600')));
    return $response;
}

Second Form buildForm:

public function buildForm(array $form, FormStateInterface $form_state) {
    $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
    $form['my_actions'] = array('#type' => 'actions');
    $form['my_actions']['cancel'] = array(
      '#type' => 'button',
      '#value' => t('Cancel'),
      '#ajax' => array(
        'callback' => array($this, 'cancelForm'),
        'event' => 'click',
        'progress' => FALSE,
      ),
    );
    $form['my_actions']['submit'] = array(
      '#type' => 'button',
      '#value' => t('Confirm'),
      '#ajax' => array(
        'callback' => array($this, 'submitForm'),
        'event' => 'click',
        'progress' => FALSE,
      ),
    );
    return $form;
  }

Second Form "cancelForm" AJAX Callback:

public function cancelForm(array &$form, FormStateInterface $form_state) {
    $response = new AjaxResponse();
    $response->addCommand(new CloseModalDialogCommand());
    return $response;
  }

In the above example, clicking the checkbox opens the second form in a modal, however, the Cancel button doesn't close the modal; none of the AJAX works.

From what I can tell working with this issue, it seems the second form is built using data from and being wrapped in the first form, instead of being a standalone form. The ajaxTrustedUrl being passed also includes the _wrapper_format=html query, when it should be ajax. If I build the second form on its own, the AJAX works, so I know it has to be due to building the form in the AJAX callback for another form.

Am I going about this the correct way? Is there a way to unset/reset these settings when building the second form? I've found two issues which appear to be somewhat related:

https://www.drupal.org/project/drupal/issues/2504709
https://www.drupal.org/project/drupal/issues/2504115

Comments

joegl created an issue. See original summary.

joegl’s picture

Issue summary: View changes
joegl’s picture

Issue summary: View changes
joegl’s picture

Well, hilariously enough, I opened a practically identical support request a year ago: https://www.drupal.org/project/drupal/issues/2851317

And from my feedback there, believe I found the culprit.

In the RenderElement::preRenderAjaxForm($element) method: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21...

If there is an ajax callback specified, but no AJAX URL is specified, a URL is generated based on the current request and ALL of the query parameters:

if (array_key_exists('callback', $settings) && !isset($settings['url'])) {
      $settings['url'] = Url::fromRoute('<current>');
      // Add all the current query parameters in order to ensure that we build
      // the same form on the AJAX POST requests. For example,
      // \Drupal\user\AccountForm takes query parameters into account in order
      // to hide the password field dynamically.
      $settings['options']['query'] += \Drupal::request()->query->all();
      $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
    }

Because the request in the first form ajax callback has the "_wrapper_format=html" query parameter, the generated URL for the second form also has it.

Near the bottom, the url is set directly on the element through the "ajaxTrustedUrl":

$element['#attached']['drupalSettings']['ajaxTrustedUrl'][$settings['url']] = TRUE;

I'm trying to figure out if there is a way to interrupt this process but I don't think altering the request beforehand is the right solution.

joegl’s picture

This comment seems to be the exact same problem: https://www.drupal.org/project/drupal/issues/2504115#comment-10838564

I've tried adding a custom URL to the #ajax, and while it is an improvement, it's still not triggering the correct form callback.

joegl’s picture

This also appears to be related: https://www.drupal.org/project/drupal/issues/2504709

It appears when you render a Drupal render array from within a form AJAX callback, it will inherit all the query parameters, ajax wrappers, etc., of that AJAX callback.

So in practice, I'm beginning to think I should be using AJAX Form Callbacks to modify the current form ONLY, and not to makes any actions outside of the form (e.g., render a new form in a modal, rebuild a separate element on the same page, etc.,).

My theory is, if I want to make actions outside of the form in the callback I should:

A) Set a variable in the $form_state for the AJAX Callback
B) Trigger a form rebuild in the AJAX Callback
C) In the buildForm for the form, check $form_state for the variable I set
D) If the variable is set, build the AjaxResponse object I wanted to send from the callback here instead and send it

I will try it and report back

joegl’s picture

Setting the $form['action'] to the request creates the problem of pushing AJAX callbacks for forms built within an AJAX callback to the current request. This wouldn't be an issue, if the callback for the request wasn't issued, however, while the AjaxResponse never comes back for the bogus request, the callback code is still being ran. If we could just prevent this happening, we'd be hunky dory.

joegl’s picture

Title: Forms built in a different form's AJAX callback wrapped wrong, AJAX broken » Ajax is broken in forms built within an Ajax callback
Version: 8.2.x-dev » 8.3.x-dev
jasonyarrington’s picture

Anyone have a workaround for this in the meantime?

mukila’s picture

Thanks Joegl for your summary and reporting the issue.

Faced the similar issue. I overwrite the ajax settings url and query parameters in buildform(). Now it is working fine.

 $form['actions']['next'] = [
   '#type' => 'submit',
   '#value' => $this->t('Submit'),
   '#ajax' => [
     'callback' => '::ajaxSubmitForm',
     'event' => 'click',
     'url' => Url::fromRoute('custom.route_name'),
     'options' => ['query' => ['ajax_form' => 1]],
    ],
  ];
joegl’s picture

Version: 8.3.x-dev » 8.6.x-dev
Priority: Normal » Major

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

bdlangton’s picture

mukila's comment #11 also fixed the issue that I was having. Thanks!

birk’s picture

I've tried the workaround supplied by #11, it seems to change the URL, so it no longer sends double _wrapper_format, but it still doesn't run the callback. I simply get the rendered page I put into the URL (which in my case is the current), which results in an AjaxError, because it's not able to parse the callback.

Anyone else encountered this, and found a fix?

eiriksm’s picture

Just encountered this today, and while changing the URL fixes the URL, it will not run the callback unless that URL is actually a URL where you have your form.

So what solved it for me was creating a route for the form, and changing the URL to that URL.

joegl’s picture

I thought about this a little more the other day and came up with a quick workaround. The problem with building a form in an AJAX callback for another form (for example a SubmitForm() method which returns an AjaxResponse with a different form built inside) is currently due to this issue: #2504115: AJAX forms should submit to $form['#action'] instead of <current> -- the "current" request route is still the route the "current" (first) form was built in, so any AJAX inside the second form will be submitted through the route/request used to build the first form. Essentially, any form with AJAX in it needs to built through its own route.

What I have done to get around this is:

1. Created a route for each form. The route leads to a controller which builds the form and returns it in a modal in an AjaxResponse (I want to make this more dynamic in the future, and hopefully use one route for all forms -- I think this is what the Views module does)
2. Inside the first form's SubmitForm method, instead of building the second form and returning it, I've created a custom response which takes the route for the second form instead and triggers it via JavaScript (ajax) request.

The chaining is not ideal, but it is working for me now (the second form is correctly built with its own URL/route instead of the previous forms). I believe this is overkill, but I don't know what else to do. Code examples are below:

Route:

# open second form in modal
my_module.openSecondForm:
  path: '/ajax/openSecondForm'
  defaults:
    _controller: 'Drupal\my_module\Controller\FormController::openSecondForm'
    _title: 'Second Form'

FormController:

/**
* @file
* Contains \Drupal\my_module\Controller\FormController.
*/
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\my_module\Form\SecondForm;


class FormController extends ControllerBase {

  /**
   * Open the seocnd form in a modal
   */
  public static function openSecondForm() {
    $response = new AjaxResponse();
    $second_form = new SecondForm();
    $second_form = \Drupal::formBuilder()->getForm($second_form);
    $modal_options = array(
      'width' => '800',
    );

    // open new modal
    $response->addCommand(new OpenModalDialogCommand("Second Form", $second_form, $modal_options));

    return $response;
  }

}

Custom "SendAjaxRequest" Ajax Command:

namespace Drupal\my_module\Ajax;
use Drupal\Core\Ajax\CommandInterface;
use Drupal\Core\Url;


/**
 * Provides an AJAX command for using ajax to send another url request
 *
 */
class SendAjaxRequest implements CommandInterface {

  /**
   * A url
   *
   * @url
   */
  protected $url;

  /**
   * Construct this form
   */
  public function __construct(Url $url) {
    $this->url = $url;
  }

  /**
   * {@inheritdoc}
   */
  public function render() {
    return [
      'command' => 'sendAjaxRequest',
      'url' => $this->url->toString(),
    ];
  }
}

Custom "SendAjaxRequest" Ajax JavaScript

/**
 * @file
 * Handles custom AJAX commands
 */

(function ($, Drupal, drupalSettings) {

  'use strict';

  Drupal.AjaxCommands.prototype.sendAjaxRequest = function (ajax, response, status) {
      // build the ajax request
      var ajaxSettings = {
        url: response.url,
      };
      var ajax_request = Drupal.ajax(ajaxSettings);
      // execute the request
      ajax_request.execute();
    };

})(jQuery, Drupal, drupalSettings);

First form Ajax Callback/Method (using submitForm) for triggering second form build:

  /**
   * {@inheritdoc}.
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // build a response
    $response = new AjaxResponse();

    // create new request for second form
    $open_second_form_url = Url::fromRoute('timber_adopt.openSecondForm');
    $response->addCommand(new SendAjaxRequest($open_second_form_url));

    return $response;
  }

It's extremely ugly and opens up some pretty gross potential problems. But it has helped me for now chain forms together via AJAX without any issues.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

nickdickinsonwilde’s picture

Category: Bug report » Support request
Status: Active » Needs review

I strongly believe this can be simply fixed - my path to fixing it personally was taking a look at core\lib\Drupal\Core\Render\MainContent\AjaxRenderer.php - specifically line 63.
I just added

$response->setAttachments($rendered_content['#attached']);

So in your case, you'd probably need to call setAttachments() in your clickMyCheckbox() function.

pfrenssen’s picture

Category: Support request » Bug report
Status: Needs review » Active

I also saw #2504115: AJAX forms should submit to $form['#action'] instead of <current> and there were some more recent comments made there that hint on how to solve it. Thanks to those I have managed to make it work without having to use a custom Ajax command.

My use case is that I should show a popup after a user joins a group to invite the user to subscribe to the group's newsletter. This is very similar to the use case in the issue summary. I also need to open a second form in the ajax callback of the submit button of the first form.

My first form (I am leaving out the non-relevant things like validate and submit handlers, documentation etc):

<?php

declare(strict_types = 1);

namespace Drupal\collection\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;

class JoinGroupForm extends FormBase {

  protected $formBuilder;

  public function __construct(FormBuilderInterface $form_builder) {
    $this->formBuilder = $form_builder;
  }

  public static function create(ContainerInterface $container): JoinGroupForm {
    return new static(
      $container->get('form_builder')
    );
  }

  public function getFormId(): string {
    return 'join_group_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state): array {
    $membership = $this->getGroupMembership();
    if (empty($membership)) {
      $form['join'] = [
        '#ajax' => [
          'callback' => '::showSubscribeDialog',
        ],
        '#type' => 'submit',
        '#value' => $this->t('Join this group'),
      ];
    }
    else {
      $form['leave'] = [
        '#type' => 'link',
        '#title' => $this->t('Leave this group'),
        '#url' => Url::fromRoute('group.leave_form'),
      ];
    }

    return $form;
  }

  public function showSubscribeDialog(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();

    // Output messages in the page.
    $messages = ['#type' => 'status_messages'];
    $response->addCommand(new PrependCommand('.section--content-top', $messages));

    // If the form submitted successfully, make an offer the user cannot refuse.
    if (!$form_state->getErrors()) {
      // Rebuild the form and replace it in the page, so that the "Join this
      // "group" button will be replaced with the "Leave this group" link.
      $rebuilt_form = $this->formBuilder->rebuildForm('join_group_form', $form_state, $form);
      $response->addCommand(new ReplaceCommand('#join-group-form', $rebuilt_form));

      // Open the second form in a modal dialog.
      $title = $this->t('Welcome to the group!');

      $modal_form = $this->formBuilder->getForm('\Drupal\collection\Form\SubscribeToGroupForm');
      $modal_form['#attached']['library'][] = 'core/drupal.dialog.ajax';

      $response->addCommand(new OpenModalDialogCommand($title, $modal_form, ['width' => '500']));
    }
    return $response;
  }

}

The second form, this includes the key to make it work, we need to specify the AJAX URL to use, and manually specify that the AJAX_FORM_REQUEST needs to be passed in the query arguments:

<?php

declare(strict_types = 1);

namespace Drupal\collection\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;

class SubscribeToGroupForm extends FormBase {

  public function getFormId(): string {
    return 'subscribe_to_group_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state): array {
    $form['description'] = [
      '#type' => 'html_tag',
      '#tag' => 'p',
      '#value' => $this->t('Do you want to receive our weekly newsletter?'),
    ];
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['actions']['cancel'] = [
      '#type' => 'submit',
      '#value' => $this->t('No thanks'),
      '#attributes' => [
        'class' => ['dialog-cancel'],
      ],
    ];

    // The trick to making it work lies here. We need to specify the callback,
    // the URL to the form route, and set AJAX_FORM_REQUEST in the query.
    $form['actions']['confirm'] = [
      '#type' => 'submit',
      '#value' => $this->t('Subscribe'),
      '#ajax' => [
        'callback' => [static::class, 'confirmSubscription'],
        'url' => Url::fromRoute('collection.subscribe_to_group_form'),
        'options' => [
          'query' => [
            FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
          ],
        ],
      ],
    ];

    return $form;
  }

  public function confirmSubscription(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();

    // Output messages in the page.
    $messages = ['#type' => 'status_messages'];
    $response->addCommand(new PrependCommand('.section--content-top', $messages));

    if (!$form_state->getErrors()) {
      $response->addCommand(new CloseModalDialogCommand());
    }
    return $response;
  }

}

And the second form also needs its own route that the AJAX callback can call. From my module.routing.yml:

collection.subscribe_to_group_form:
  path: '/ajax/group/subscribe'
  defaults:
    _form: \Drupal\collection\Form\SubscribeToGroupForm
pfrenssen’s picture

I am not sure if this is a bug or something that needs to be documented.

yhpatel’s picture

Good Work on #20. I was having exact same issue. Fixed it.

owenbush’s picture

I've tried a number of the suggestions herein but still can't seem to get AJAX on a form loaded in a modal to work.

A couple of things are a little different in my setup to these, however.
Firstly, I'm using a hook_form_alter to add an extra button to an entity form to open up a modal.
Secondly, the modal itself will have an entity form, not just a regular form.

/**
 * Implements hook_form_alter().
 */
function mymodule_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Do some stuff here... 
  $form['add_field'] = [
    '#type' => 'submit',
    '#value' => t('Add Field'),
    '#attributes' => [
      'class' => [
        'use-ajax',
      ],
    ],
    '#ajax' => [
      'callback' => 'mymodule_entity_form_add',
      'event' => 'click',
      'url' => Url::fromRoute('mymodule.creation_form', [
        'entity_type' => $entity->getEntityTypeId(),
        'entity_bundle' => $entity->bundle(),
      ]),
      'options' => [
        'query' => [
          FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
        ],
      ],
    ],
  ];
}

I have my #ajax callback function like so:

/**
 * AJAX Form Callback: My Module Entity Form.
 */
function mymodule_entity_form_add(&$form, FormStateInterface $form_state) {
  // Do some stuff...
  $response = new AjaxResponse();
  $modal_form = \Drupal::service('entity.form_builder')->getForm($entity, 'add');
  $response->addCommand(new OpenModalDialogCommand('Add Field', $modal_form, ['width' => '800']));
  return $response;
}

I have my route:

# AJAX Field creation form
mymodule.creation_form:
  path: '/ajax/mymodule/creation/{entity_type}/{entity_bundle}'
  defaults:
    _title: 'Create Field'
    _controller: '\Drupal\mymodule\Controller\MyModuleController::ajaxCreationForm'
  requirements:
    _permission: 'administer mymodule'

And my ajaxCreationForm function on MyModuleController:

namespace Drupal\mymodule\Controller;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFormBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * The MyModuleController class.
 */
class MyModuleController extends ControllerBase implements ContainerInjectionInterface {

  /**
   * The form builder.
   *
   * @var \Drupal\Core\Entity\EntityFormBuilder
   */
  protected $formBuilder;

  /**
   * Constructs a MyModuleController object.
   *
   * @param \Drupal\Core\Entity\EntityFormBuilder $formBuilder
   *   The form builder.
   */
  public function __construct(EntityFormBuilder $formBuilder) {
    $this->formBuilder = $formBuilder;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity.form_builder')
    );
  }

 /**
   * Gets the creation form in a modal.
   * 
   * @param string $entity_type
   *   The entity type.
   * @param string $entity_bundle
   *   The entity bundle.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   Returns an ajax response.
   */
  public function ajaxCreationForm($entity_type = NULL , $entity_bundle = NULL) {
    // Do some stuff...

    $response = new AjaxResponse();
    $modal_form = $this->formBuilder->getForm($entity, 'add');
    $response->addCommand(new OpenModalDialogCommand('Add Field', $modal_form, ['width' => '800']));
    return $response;
  }

And the entity form that is loaded into the modal has AJAX to detect changes to the value of several select elements, and will populate other selects based on the response from the AJAX. Like so:

/**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);
    // Do some stuff...
    $form['source']['entity_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Entity Type'),
      '#options' => $entity_types,
      '#required' => TRUE,
      '#default_value' => $field->entityType(),
      '#ajax' => [
        'callback' => [$this, 'updateFieldOptions'],
        'event' => 'change',
        'options' => ['query' => ['ajax_form' => 1]],
        'progress' => [
          'type' => 'throbber',
          'message' => $this->t('Fetching options...'),
        ],
      ],
    ];

    $form['source']['entity_bundle'] = [
      '#type' => 'select',
      '#title' => $this->t('Entity Bundle'),
      '#options' => $entity_bundles,
      '#required' => TRUE,
      '#default_value' => $field->entityBundle(),
      '#ajax' => [
        'callback' => [$this, 'updateFieldOptions'],
        'event' => 'change',
        'options' => ['query' => ['ajax_form' => 1]],
        'progress' => [
          'type' => 'throbber',
          'message' => $this->t('Fetching options...'),
        ],
      ],
      '#states' => [
        'visible' => [
          'select[name="source_entity_type"]' => ['!value' => ''],
        ],
      ],
    ];
 }

 /**
   * AJAX Callback: Update Field Options.
   */
  public function updateFieldOptions(array &$form, FormStateInterface $form_state) {
    $response = new AjaxResponse;
    $response->addCommand(new ReplaceCommand('#field-add-form--wrapper', $form));
    return $response;
  }

This works absolutely fine when the entity form is not displayed in a modal. But when it is, this is the sort of request I see:

https://mysite.ddev.site:8989/ajax/mymodule/creation/node/page?ajax_form=1&_wrapper_format=drupal_ajax&_wrapper_format=drupal_ajax

I've read a bunch of other related issues regarding the bleeding of the _wrapper_format into links within a modal, and I've tried the workarounds I've seen in this thread, but so far no dice.

Any ideas?

geek-merlin’s picture

> I am not sure if this is a bug or something that needs to be documented.

I'd say every blind copying of query parameters to links (actually there are quite some in the source) is a bug in itself. Views does it because it has no simple way to query a whitelist of "its" query parameters.

geek-merlin’s picture

alex andrascu’s picture

Just here to confirm we've hit this too on a 8.7.7 / Commerce 2.x setup.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

pabloveintimilla’s picture

Work #20, thanks @pfrenssen!

lukasss’s picture

Work #20

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

oliversiegel’s picture

#20 worked, thanks, but I had 2 things I had to consier:

It says to add the route that directs to the form. However, when I do as described, I got a 403 error on my ajax call and a access denied page from Drupal upon visiting the path specified in the route (/ajax/group/subscribe)

But I fixed that by adding the following lines to my routing.yml file:

  requirements: 
    _permission: 'access content'

Also, the final ajax call on the ajax generated modal form that I've tried calling (but it failed, which lead me here) needs to be a static function. So that would be the "confirmSubscription" method in the example provided.

public static function confirmSubscription(array &$form, FormStateInterface $form_state)

Finally, the parameters in the build method of the form that points to this route have to spelled exactly right:
public function buildForm(array $form, FormStateInterface $form_state)
I had $formstate which caused it to not work at all.

This was more complicated than neccessary, but it finally works! :)

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

au_dave’s picture

I've run into a similar problem using drupal_commerce

I want users to be able to update their cart via ajax, so things like item quantity etc... The commerce cart is a view with form fields.

It works the first time 'update cart' is used. Not the second :(

function mymodule_module_form_alter(&$form, $form_state, $form_id) {
  if (isset($form_state->getBuildInfo()['base_form_id'])
    && ($form_state->getBuildInfo()['base_form_id'] == 'views_form_commerce_cart_form_default' || $form_state->getBuildInfo()['base_form_id'] == 'views_form_commerce_cart_form_block_1' )
    && $form['actions']['submit']['#update_cart'] == TRUE)
  {
    $form['actions']['submit']['#ajax'] = [
      'callback' => 'mymodule_module_ajax_update_cart',
      'url' => Url::fromRoute('commerce_cart.page'),
      'options' => [
        'query' => [
          FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
        ],
      ],
    ];
    $form['actions']['submit']['#ajax']['disable-refocus'] = TRUE;
  }
}
function mymodule_module_ajax_update_cart(array &$form, FormStateInterface $form_state) {
  $order_storage = \Drupal::entityTypeManager()->getStorage('commerce_order');
  /** @var \Drupal\commerce_order\Entity\OrderInterface $order */

  $triggering_element = $form_state->getTriggeringElement();
  $order = $order_storage->load($triggering_element["#order_id"]);

  $view = \Drupal\views\Views::getView('commerce_cart_form');
  $view->setDisplay('block_1');
  $view->setArguments([$order->id()]);

  $view->preview();

  $content = $view->buildRenderable();

  $response = new AjaxResponse();
  $response->addCommand(new ReplaceCommand('.views-element-container > div', $content));
  $response->addAttachments($content['#attached']);

  return $response;

Any suggestions welcome

andres.torres’s picture

Any updates on this? i tried #20, #31 on Drupal 9.1 and still cant get a ajax form submit handler to execute when rendered into a modal after opening the modal through an initial form (form 1) submit. Using simple forms with no special logic on the submit handlers just trying to close the dialog modal on the second form.

Getting this ajax error:

BEGIN OUTPUT from \u0027core\/themes\/classy\/templates\/misc\/status-messages.html.twig\u0027 --\u003E\n\u003Cdiv data-drupal-messages\u003E\n    \u003Cdiv role=\u0022contentinfo\u0022 aria-label=\u0022Error message\u0022 class=\u0022messages messages--error\u0022\u003E\n          \u003Cdiv role=\u0022alert\u0022\u003E\n                  \u003Ch2 class=\u0022visually-hidden\u0022\u003EError message\u003C\/h2\u003E\n                    \u003Cul class=\u0022messages__list\u0022\u003E\n                      \u003Cli class=\u0022messages__item\u0022\u003EThe form has become outdated. Press the back button, copy any unsaved work in the form, and then reload the page.\u003C\/li\u003E\n                      \u003Cli class=\u0022messages__item\u0022\u003EAn unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (512 MB) that this server supports.\u003C\/li\u003E\n                  \u003C\/ul\u003E\n                \u003C\/div\u003E\n      \u003C\/div\u003E\n    \u003C\/div\u003E\n\n\u003C!-- END OUTPUT

Any help is really appreciated. Thanks!

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andriy khomych’s picture

Thanks, #20 helped me too.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

berliner’s picture

Still an issue in 9.3, but thanks to the excellent explanation in #20 there is a work-around.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

sadashiv’s picture

#20 works, thank you for that.

Thanks,
Sadashiv.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.