Score:2

buildForm: how differentiate between page reload and ajax callback?

br flag

I'm building a custom form in Drupal 9, which has some ajax callbacks.

During the buildForm I need to load some extra data via a rest call to an external service, which then I put inside a private_tempstore variable.

I'd like to avoid to call the rest end-point during the ajax callbacks, and rely on the stored variable.

However, I can't find a way to differentiate between the "load page" case and the "ajax case". Is it possible?

I've found this answer that seems to generally work:

// Example for brevity only, inject the request_stack service and call 
// getCurrentRequest() on it to get the request object if possible.
$request = \Drupal::request();
$is_ajax = $request->isXmlHttpRequest();

But I'd like to know if there's some helper\solution using the form API.

ru flag
IMHO no form logic should ever be based on Page load vs. Ajax. Given core's BigPipe might load anything by Ajax (or not) depending on block placement, environment, config, etc..., this seems like a guaranteed road to fiasco.
Giuseppe avatar
br flag
@Hudri that's a big consideration I didn't realize, thank you. However, is there another way to solve this question? I mean, I'd like to avoid to do an external rest call to every ajax callback of the form.
ru flag
TBH I don't quite understand the reason for the question. `buildForm` has access to `FormStateInterface $form_state`, what's wrong with `$form_state`?
Giuseppe avatar
br flag
@Hudri I'll try to rephrase: during the first run of the `buildForm` the data I need is get from the rest endpoint, it is not inside the `$form_state`. I then store that data inside the `tempstore` (but I could also use `$form_state->setTemporaryValue()`. During the following ajax callbacks the data could be thus accessed via `$form_state`, but I obviously need to differentiate between the first case and the following ones.
ru flag
I believe you don't see the wood for all the trees :-) Pseudo-Code `function buildForm() { if (!$form_state->get('some_helper_var') { $tempStore = load_external_stuff(); $form_state->set('some_helper_var', TRUE); } $form['field_foo']['default_value'] = $form_state->get('field_foo') ?? $tempStore->get('foo')); }`
Giuseppe avatar
br flag
@Hudri yeah, I couldn't see that solution :facepalm: However now that I'm trying it the `buildForm` is called twice during the ajax callback - at least while debugging. The first time the form state values are empty, so the "external stuff" is still loaded every time, so it isn't really working :-(
Score:5
cn flag

Since Drupal 8 the form object instantiated with buildForm() is not preserved between the request rendering the form and the first Ajax request. So be prepared that buildForm() is called again and has to produce the exact same result. When you get data from $form_state this is not the data you expect from the first buildForm() because this is never cached. Adding to the complexity the rendered result of the first build is cached, so what you store in the first build elsewhere, for example in tempStore, might be outdated in the Ajax request. The only data which works as expected are form values, which can be hidden if you want to have them in the submitted form and not visible to the user.

TLDR: buildForm() is called more often than you think and you shouldn't put code in it which is expensive to run. Refactor the external API call to a service, with proper caching, so that it doesn't matter how often it is called. Invalidate the service cache the same way as the rendered form, so that neither of them has outdated data.

Giuseppe avatar
br flag
1. thank you for the explanation, although it is not really clear to me. E.g. I don't get how data stored in tempStore can be outdated, if $form_state is not cached, and what relationship the cached rendered result have with that. Is there a more detailed documentation on how this works to get a better idea?
Giuseppe avatar
br flag
2. "Invalidate the service cache the same way as the rendered form, so that neither of them has outdated data." How can do that? What should be the "trigger" to invalidate the service cache? I mean, I'd like to do that at page reload, should I use a `KernelEvents::REQUEST` looking for that specific route? And for the form cache, does that mean that I should simply make the form not cacheable at all?
4uk4 avatar
cn flag
1. The main problem is you are trying to use buildform() out of scope. With the same input it has to produce the same result as often as Drupal is calling this method. Only if $form_state contains submitted and processed form values or a triggering element you can rebuild a different $form and store data.
4uk4 avatar
cn flag
2. Add a cache bin to the service and cache with the same cache metadata you have attached to the rendered $form.
4uk4 avatar
cn flag
If it's critical that you use the same version of the external API data you could add a version string as hidden form element.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.