0

I've altered a node\edit form where there are two taxonomy refence fields with a select widget.

I've added a custom ajax callback on the first field, in order to populate the options of the second one only with a subset of values.

This works fine, the problem is the presence of another ajax field - e.g. a image field.

If the user set the "select fields" first and then try to add an image, the ajax of the image field always returns "1 error has been found" on the second select field. Debugging that part kinda makes sense, as the form_state values only contains at that point the values of "image field" and thus the 2nd select field is left only with "_none" value.

With this patch the problem is somewhat better, as the ajax on the image fields works. However if the user populate the select first and the image field after, the form submit validation returns again the "1 error has been found" on the second select field, with that field populated only by "_none" value, while the first select field still contains the correct selected value.

I don't know if this is a known bug of D9, if there are other relative patches I couldn't find, or if I'm doing something wrong in my custom code - attached below.

Thanks for any help.

<?php

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_node_my_node_form_alter(&$form, $form_state, $form_id)
{
    my_module_node_custom_form_alter($form, $form_state);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_node_my_node_edit_form_alter(&$form, $form_state, $form_id)
{
    my_module_node_custom_form_alter($form, $form_state);
}

function my_module_node_custom_form_alter(&$form, $form_state)
{
     $form['my_first_field']['widget']['#ajax'] = [
        'callback' => 'my_module_my_first_field_callback',
        'wrapper' => 'my-second-field-wrapper',
        'event' => 'change',
        'progress' => [
            'type' => 'throbber',
        ],
     ];
     $form['my_first_field']['#limit_validation_errors'] = [];

     $selected_value = !empty($form_state->getValue('my_first_field')) ? $form_state->getValue('my_first_field')[0]['target_id'] : null;
     if(empty($selected_value)) {
         if(!empty($form["my_first_field"]["widget"]["#default_value"])) {
             $selected_value = $form["my_first_field"]["widget"]["#default_value"][0];
         }
     }

    my_module_set_my_second_field($form, $selected_value);
}

function my_module_set_my_second_field(&$form, $selected_value)
{
     $form['my_second_field']['#limit_validation_errors'] = [];
     $options = my_module_get_my_second_field_options($selected_value);

     $form['my_second_field']['#prefix']='<div id="my-second-field-wrapper">';
     $form['my_second_field']['#suffix']='</div>';
     $form['my_second_field']['widget']['#options'] = $options;
     $form['my_second_field']['#limit_validation_errors'] = [];
}

function my_module_get_my_second_field_options($selected_value, $vid = 'my_vid')
{
    if(empty($selected_value)) {
        return ['_none' => t("- Select 'my first field' first -")];
    }

    $options = ['_none' => t("- Select a value -")];

    $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid, 0, null, true);
    foreach($terms as $term) {
        if($term->field_to_check->target_id == $selected_value) {
            $options[$term->id()] = $term->getName();
        }
    }

    return $options;
}

function my_module_my_first_field_callback(array &$form, \Drupal\Core\Form\FormStateInterface $form_state)
{
    return $form['my_second_field'];
}

edit:

as I had to deliver something usable, as workaround I've set up a check on the triggering_element: it does the logic if that is empty (case of page load\submit) or has the name of "my_field", otherwise skip it. Not probably the correct or most elegant way to do it, but it seems to work so for now I'm doing this way. Kudos to anyone who know and explains how to do it better.

Updated relative code:

function my_module_node_custom_form_alter(&$form, $form_state)
{
     $form['my_first_field']['widget']['#ajax'] = [
        'callback' => 'my_module_my_first_field_callback',
        'wrapper' => 'my-second-field-wrapper',
        'event' => 'change',
        'progress' => [
            'type' => 'throbber',
        ],
     ];

     if( !isset($triggering_element) || (isset($triggering_element['#field_name']) && $triggering_element['#field_name'] === 'my_first_field')) {
      $selected_value = !empty($form_state->getValue('my_first_field')) ? $form_state->getValue('my_first_field')[0]['target_id'] : null;
      if(empty($selected_value)) {
          if(!empty($form["my_first_field"]["widget"]["#default_value"])) {
              $selected_value = $form["my_first_field"]["widget"]["#default_value"][0];
          }
      }

      my_module_set_my_second_field($form, $selected_value);
    }
}
2
  • A few tips: try adding the ['#limit_validation_errors'] to the [''widget'] instead. Use Xdebug to see what causes an error in form validation.
    – milkovsky
    Jan 13, 2021 at 12:21
  • @milkovsky thank you for the answer, limit_validation_errors attached to the widget didn't solve the problem. I tried to skim a little in the validation process but I had no time to understand\find the eventual problem here. I resolved for now with a (dirty?) workaround, I've edited the question with the relative code.
    – Giuseppe
    Jan 15, 2021 at 16:59

1 Answer 1

0

I had to work again on this part of my site. This time to check a dependant field inside a paragraph, so I had the same problem - empty form_state values - in a more complicated situation.

In the end what worked was to switch to form_state->getUserInput, which seems to always contain the updated data. Please note, as stated by the docs, this array insn't sanitized nor validated so it could be vulnerable to user injection - however my case was safe enough, as only integer are allowed values.

Here the code I'm using to get the 1st select value, from node and paragraph alter hook:

  $user_input = $form_state->getUserInput();

  //check to avoid user injection, my case was a taxonomy term select, so allowed values are integer
  $my_field_value = isset($user_input["my_field"]) ? intval($user_input["my_field"]) : NULL;

  // in case of edit node, when loading the form user input is empty -> get the initial value 
  if (empty($my_field_value) && !empty($form["my_field"]["widget"]["#default_value"])) {
    $my_field_value = $form["my_field"]["widget"]["#default_value"][0];
  }

  if(empty($my_field_value)) {
    $my_field_value = '_none';
  }

  return $my_field_value;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.