Score:2

How to access validated values after AJAX callback when fields are nested in a container?

in flag

I have a custom form where I have a search field and button. After the users clicks the button, I load some items based on what they've entered into the search field.

The problem that I'm having is that when my form is being rebuilt after the AJAX callback is fired, there are no values in $form_state->values so I can't access them with $form_state->getValue(). However, I do see the input in $form_state->input. When I view the documentation for $form_state->getUserInput(), it states:

These are raw and unvalidated, so should not be used without a thorough understanding of security implications. In almost all cases, code should use self::getValues() and self::getValue() exclusively.

That makes sense to me, and I'd love to, but the values aren't there. However, I did notice that if I remove the container from my example below, so that all of my fields are directly at the root of the form array, the values are in $form_state->values in ::buildSearchResults() after AJAX rebuild. Why is that the case? What am I missing here?

My code:

public function buildForm(array $form, FormStateInterface $form_state) {
  // Note that I see the same issue even when setting #tree to FALSE.
  $form['container'] = [
    '#type' => 'container',
    '#tree' => TRUE,
  ];

  $form['container']['search_field'] = [
    '#type' => 'textfield',
    '#title' => $this->t('Search field'),
  ];

  $form['container']['ajax_button'] = [
    '#type' => 'button',
    '#value' => t('Search'),
    '#name' => 'ajax_button',
    '#limit_validation_errors' => [],
    '#ajax' => [
      'callback' => [static::class, 'myCallbackFunction'],
      'wrapper' => 'search_results',
      'event' => 'click',
    ],
  ];

  $form['container']['search_results'] = [
    '#type' => 'container',
    '#attributes' => [
      'id' => 'search_results',
    ],
    'items' => $this->buildSearchResults($form_state),
  ];

  return $form;
}

public static function myCallbackFunction(array &$form, FormStateInterface $form_state): array {
  // I think I need this here?
  $form_state->setRebuild();
  return $form['container']['search_results'];
}

public function buildSearchResults(FormStateInterface $form_state) {
  // This is always empty at this stage. :(
  // In fact, the values array is totally empty. 
  // Though I can see the values in $form_state->input.
  // And, as I stated earlier, if I drop the container from my
  // form and put all fields at the root of the form, the values
  // property is not empty and I can access my field values there.
  $search_text = $form_state->getValue(['container', 'search_field'], '');

  return views_embed_view('view_id', 'display_id', $search_text);
}

Update 1: @4uk4's answer inspired some more testing. It seems that even if I remove the AJAX functionality so that the button triggers my form's submitForm() method, the values are still missing from the $form_state->values array. After retesting this example code, I can no longer reproduce my issue at all.

Update 2: I believe the behavior @4uk4 was describing was causing me to see some false positives here when testing the example code. When I tested with my real code, I'd see my issue always, on first AJAX rebuild, with no triggering element set and on all subsequent rebuilds. However, I think I was only ever testing this example code with to the first rebuild, the rebuild with no triggering element and no values were processed - I'd see no values in $form_state->values (what I was expecting, given my tests with my actual code), but if I had ever tested long enough for the second rebuild, the actual AJAX submission, I'd have seen the field values in $form_state->values.

Score:1
cn flag

In the first Ajax request fired, the form is rebuilt with empty values and should return exactly the same form build as the original build. The only difference is that the raw input data is present, but in normal cases ignore that and rebuild exactly the same form. This form build is needed to submit the input data and fill values and triggering element. Only after that, the Ajax rebuild is triggered. To be sure you have the official Ajax rebuild check the triggering element in $form_state.

The second Ajax request runs more as expected, the form object is retrieved from the cache and the form build function is no longer needed to reconstruct the previous form build.

See this change record https://www.drupal.org/node/2501435

You don't need $form_state->setRebuild() in an Ajax callback, Ajax forms are rebuilt automatically. This only makes sense in a #submit callback to have a fallback in case Ajax is disabled and you still want to show the rebuilt form.

sonfd avatar
in flag
Thank you for your response. I'm definitely seeing the double rebuild after the first AJAX submission. On the first rebuild, it's as you say, the triggering_element **is not** set and I see nothing in `$form_state->values`. However, on that second rebuild, and all subsequent AJAX submissions, I do see *some* data in `$form_state->values`, but it's only that of the button that triggered the AJAX, e.g. `$form['container']['ajax_button']` from my example; there's still no data from any other fields.
sonfd avatar
in flag
And just to reiterate a point from my original post because I think it's important, if I don't add the form elements to a container, if I just add them directly to the root of the form, e.g. `$form['search_field']` rather than `$form['container']['search_field']`, **I do see** those values in `$form_state->values` during rebuilds.
sonfd avatar
in flag
Updating my post with some new information, it seems unrelated to AJAX altogether.
4uk4 avatar
cn flag
Yes, the logic of form rebuilding is the same, no matter whether you request a rebuild through ajax or a submit method.
Score:1
in flag

Ultimately, my issue was caused by my use of #limit_validation_errors on my button (something that I had very unfortunately omitted from my original question's example code).

My example code in the original post is just a small portion of a larger form. My goal was to avoid validating the whole form when a user is trying to AJAX load some additional form content in one section.

After removing the #limit_validation_errors key from my button altogether, I started to see the values in $form_state->values as expected per @4uk4's answer. But this was forcing me to fill all the unrelated, but ultimately required for final form submission, fields before loading in the new options based on input. To fix this, I referenced How to correctly use #limit_validation_errors element of Form API in Drupal 7 and updated to limit validation errors to only my elements rather than removing validation from the form as a whole.

$form['container']['ajax_button'] = [
  '#type' => 'button',
  '#value' => t('Search'),
  '#name' => 'ajax_button',
  // I reference my container here because I want to validate all fields
  // within my container.
  '#limit_validation_errors' => [['container']],
  // Alternate approach where to validate fields individually.
  // '#limit_validation_errors' => [['container', 'search_field'], ['container', 'some_other_field']],
  '#ajax' => [
    'callback' => [static::class, 'myCallbackFunction'],
    'wrapper' => 'search_results',
    'event' => 'click',
  ],
];

As an aside, this feels like a pretty terrible name for this key. With its current name, it sounds like it's limiting only the errors, not limiting validation entirely. Had this key been named `#limit_validation', I might not have spent half a day on this issue. :(

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.