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
Comment #2
joegl commentedComment #3
joegl commentedComment #4
joegl commentedWell, 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:
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":
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.
Comment #5
joegl commentedThis 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.
Comment #6
joegl commentedThis 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
Comment #7
joegl commentedAdding related issues and links:
#2508796: Query string is appended multiple time after each AJAX request
#2504115: AJAX forms should submit to $form['#action'] instead of <current>
#2504709: Prevent _wrapper_format and ajax_form parameters from bleeding through to generated URLs
The $form[#action] issues seems to be the primary problem, but I believe it's still a combination of both.
Comment #8
joegl commentedSetting 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.
Comment #9
joegl commentedComment #10
jasonyarringtonAnyone have a workaround for this in the meantime?
Comment #11
mukila commentedThanks 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.
Comment #12
joegl commentedComment #14
bdlangton commentedmukila's comment #11 also fixed the issue that I was having. Thanks!
Comment #15
birk commentedI'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?
Comment #16
eiriksmJust 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.
Comment #17
joegl commentedI 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:
FormController:
Custom "SendAjaxRequest" Ajax Command:
Custom "SendAjaxRequest" Ajax JavaScript
First form Ajax Callback/Method (using submitForm) for triggering second form build:
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.
Comment #19
nickdickinsonwildeI 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
So in your case, you'd probably need to call
setAttachments()in yourclickMyCheckbox()function.Comment #20
pfrenssenI 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):
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:
And the second form also needs its own route that the AJAX callback can call. From my
module.routing.yml:Comment #21
pfrenssenI am not sure if this is a bug or something that needs to be documented.
Comment #22
yhpatel commentedGood Work on #20. I was having exact same issue. Fixed it.
Comment #23
owenbush commentedI'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.
I have my #ajax callback function like so:
I have my route:
And my ajaxCreationForm function on MyModuleController:
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:
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_ajaxI'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?
Comment #24
geek-merlin> 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.
Comment #25
geek-merlinComment #26
alex andrascu commentedJust here to confirm we've hit this too on a 8.7.7 / Commerce 2.x setup.
Comment #28
pabloveintimilla commentedWork #20, thanks @pfrenssen!
Comment #29
lukasss commentedWork #20
Comment #31
oliversiegel commented#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:
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
$formstatewhich caused it to not work at all.This was more complicated than neccessary, but it finally works! :)
Comment #33
au_dave commentedI'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 :(
Any suggestions welcome
Comment #34
andres.torres commentedAny 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:
Any help is really appreciated. Thanks!
Comment #36
andriy khomych commentedThanks, #20 helped me too.
Comment #39
berliner commentedStill an issue in 9.3, but thanks to the excellent explanation in #20 there is a work-around.
Comment #42
sadashiv commented#20 works, thank you for that.
Thanks,
Sadashiv.