Split from #811542-247: Regression: Required radios throw illegal choice error when none selected since it wasn't working for older versions of Drupal 7

I was able to break it with tableselect, apply attached patch after applying #244, enable the form_test module, navigate to /form_test/tableselect/multiple-false and press submit: "An illegal choice has been detected. Please contact the site administrator."

The following code will throw "An illegal choice has been detected. Please contact the site administrator." after submitting

  $form['tableselect'] += array(
     '#type' => 'tableselect',
     '#header' => $header,
     '#options' => $options,
     '#multiple' => FALSE,
     '#required' => TRUE,
     '#empty' => t('Empty text.'),
   );

Attaching the patches made @effulgentsia

Comments

pol’s picture

Issue summary: View changes

Got the same problem in a form, I'm still looking for a solution.

bogdan.racz’s picture

Can you please provide the steps to reproduce it?

pol’s picture

Hey, I was about to reply to this thread from home tonight.

Actually I fixed it, I was using the wrong array, see line 165.

Sorry for the noise.

cilefen’s picture

Status: Active » Closed (works as designed)
alfaguru’s picture

I don't believe this is fixed. (Drupal 8.6.17)

In the function performRequiredValidation() of \Drupal\Core\Form\FormValidator, the validation of an element with options does not allow for the case of a non-multiple (ie not array) value for anything other than a select type.

For a required non-multiple tableselect, if nothing is selected, the code _always_ reports 'illegal choice' rather than field being required, via the condition at line 366.

The code linked at https://www.drupal.org/project/drupal/issues/1699644#comment-10608676 does not apply as #multiple is TRUE in that case.

The performRequiredValidation() function is not well factored. In an ideal world this sort of thing would be handled by the element class rather than a generic function, because it makes addressing this issue harder than it need be and I have not had time to find either a patch or workaround. Neither the form or element validation is called ahead of this code else I'd use that to work around the issue.

The form I am working on is for an admin tool so it's less critical otherwise I'd be willing to investigate further.

alfaguru’s picture

Status: Closed (works as designed) » Active

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

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Branches prior to 8.8.x are not supported, and Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should 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.

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

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

cosmicdreams’s picture

Found this issue today because I have created a form that has a tableselect element that I load up with custom data.
I then have both a submit button and an additional button I use to gather more data. When I click that second, non-default button I always run into the "An illegal choice has been detected" error.

After following the logic some it looks like my element is initializing it's #value to be and array that looks like [0 => ""], that causes the logic in FormValidator::performRequiredValidation trigger an error on line 348 where it reads

if (is_array($elements['#value'])) {
  $value = in_array($elements['#type'], ['checkboxes', 'tableselect']) ? array_keys($elements['#value']) : $elements['#value'];  
  foreach ($value as $v) {
    if (!isset($options[$v])) {
      $form_state->setError($elements, $this->t('An illegal choice has been detected. Please contact the site administrator.'));
      $this->logger->error('Illegal choice %choice in %name element.', ['%choice' => $v, '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']]);
    }
  }
}

Perhaps the problem is how this value is initialized. If there's no value to set, then the element wouldn't get the '#needs_validation' key and then the performRequiredValidation logic would not be triggered. I'm going to explore the reasoning why tableselect is getting a value to see if there is a way to avoid this trap.

cosmicdreams’s picture

The problem could be Tableselect's valueCallback, which reads:

 /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
    // If $element['#multiple'] == FALSE, then radio buttons are displayed and
    // the default value handling is used.
    if (isset($element['#multiple']) && $element['#multiple']) {
      // Checkboxes are being displayed with the default value coming from the
      // keys of the #default_value property. This differs from the checkboxes
      // element which uses the array values.
      if ($input === FALSE) {
        $value = [];
        $element += ['#default_value' => []];
        foreach ($element['#default_value'] as $key => $flag) {
          if ($flag) {
            $value[$key] = $key;
          }
        }
        return $value;
      }
      else {
        return is_array($input) ? array_combine($input, $input) : [];
      }
    }
  }

The key line is return is_array($input) ? array_combine($input, $input) : [];. In this case $input = null. and therefore what is returned is an empty array. This leads the tableselect element to include the needs_validation key and that then triggers the performRequiredValidation logic that leads to our error.

I'm searching for history around this callback to understand why valueCallback's empty case must always be an empty array. I think we're finding edge cases where having the value be an empty array leads to problems.

cosmicdreams’s picture

The trail runs cold around 2014 where on 8/28 the tableselect was converted to an object for Drupal 8. From that time on (and likely before), the element has always returned an empty array and therefore may have triggered the needs_validation key when a button that isn't the submit button is submitted.

Next Steps:
* Explore if it is worthwhile to override the TableSelect element so I can use my own valueCallback, or do that in some way.
* Does setting a default value sidestep the issue? Doesn't seem like it should but I haven't tried that yet.
* What do other valueCallbacks do?

cosmicdreams’s picture

I haven't found a fix but I have found a workaround.

If you are creating a Form object, in your buildForm(array $form, FormStateInterface $form_state): add a $form_state->setProgrammed();

This establishes that your form isn't a default form, it was programmed by you. I believe this value is explicitly set when you invoke the submitForm method but when you programmatically include a tableselect render element you need it when the form rebuilds too.

There's likely a better way to do this, but I thought I'd share my "fix" if this sparks an idea in someone else. Maybe you can find the full fix.

NOTE:
If your form intends to kick off a batch process you'll need to set the form to "not programmed" $form_state->setProgrammed(FALSE); if you want to see the batch process ui.

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

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

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should 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.

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

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should 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: 9.5.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. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

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.