1

I'm trying to integrate Selectize plugin within Drupal 8 Form API. The important thing that I want to create 'optgroup' options.

I've used Selectize module and created a custom FieldWidget Field plugin.
Almost everything works, but I got some troubles with validation of my element.
Every time my form element submitted I receive an validation error:

"An illegal choice has been detected. Please contact the site administrator."

I've found the function, which sends me that error - performRequiredValidation().
I've found lines, so I can hack the core and add a case for my widget. But it's not a case for me.

if ($elements['#type'] == 'select') {
  $options = OptGroup::flattenOptions($elements['#options']);
}
else {
  $options = $elements['#options'];
}

So the only way I see - I have to avoid this validation somehow.
Plase help. Any suggestions would be appreciated.

Here is a simplified code of my widget:

namespace Drupal\my_fields\Plugin\Field\FieldWidget;

use Drupal;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget;

/**
 * Plugin implementation of the 'my_selectize_widget' widget.
 *
 * @FieldWidget(
 *   id = "my_selectize_widget",
 *   label = @Translation("My Selectize Widget"),
 *   module = "my_fields",
 *   field_types = {
 *     "entity_reference"
 *   },
 *   multiple_values = TRUE
 * )
 */
class MySelectizeWidget extends OptionsSelectWidget {

  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $element = parent::formElement($items, $delta, $element, $form, $form_state);

    // Get our types.
    $grouped_types = _getTypesGrouped();

    // Override select form element.
    $element['#settings'] = [
      'plugins' => ['optgroup_columns'],
    ];
    $element['#type'] = 'selectize';
    $element['#options'] = $grouped_types;

    return $element;
  }

  /**
   * Get types grouped (options).
   */
  protected function _getTypesGrouped() {
    /**
     * // For example:
     * return [
     *   '1' => $this->t('One'),
     *   '2' => [
     *     '2.1' => $this->t('Two point one'),
     *     '2.2' => $this->t('Two point two'),
     *   ],
     *   '3' => $this->t('Three'),
     * ];
     */
  }

}
3
  • If you are getting that error, it means the code is doing something wrong. You should fix the code you are using, not Drupal core.
    – avpaderno
    Commented Aug 5, 2016 at 16:01
  • You cannot change the question, if it invalidates the given answers. Also, I need flatten my options somewhere is not clear. Your code is not doing that; if it needs to do it, it is just a method call.
    – avpaderno
    Commented Aug 6, 2016 at 5:34
  • You need to ask a new question, possibly after debugging your code. Show the code without any typo, and possibly show more than the code you shown here.
    – avpaderno
    Commented Aug 6, 2016 at 5:38

2 Answers 2

1

That error is caused from the following code.

  if (isset($elements['#options']) && isset($elements['#value'])) {
    if ($elements['#type'] == 'select') {
      $options = OptGroup::flattenOptions($elements['#options']);
    }
    else {
      $options = $elements['#options'];
    }
    if (is_array($elements['#value'])) {
      $value = in_array($elements['#type'], array('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.', array('%choice' => $v, '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']));
        }
      }
    }
    // Non-multiple select fields always have a value in HTML. If the user
    // does not change the form, it will be the value of the first option.
    // Because of this, form validation for the field will almost always
    // pass, even if the user did not select anything. To work around this
    // browser behavior, required select fields without a #default_value
    // get an additional, first empty option. In case the submitted value
    // is identical to the empty option's value, we reset the element's
    // value to NULL to trigger the regular #required handling below.
    // @see \Drupal\Core\Render\Element\Select::processSelect()
    elseif ($elements['#type'] == 'select' && !$elements['#multiple'] && $elements['#required'] && !isset($elements['#default_value']) && $elements['#value'] === $elements['#empty_value']) {
      $elements['#value'] = NULL;
      $form_state->setValueForElement($elements, NULL);
    }
    elseif (!isset($options[$elements['#value']])) {
      $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.', array('%choice' => $elements['#value'], '%name' => empty($elements['#title']) ? $elements['#parents'][0] : $elements['#title']));
    }
  }

It means that the value set for $elements['#value'] (which could also be a value submitted from the users) is not contained in $elements['#options']. In fact, your code contains the following code.

$grouped_types = _getTypesGrouped();

// Override select form element.
$element['#settings'] = [
  'plugins' => ['optgroup_columns'],
];
$element['#type'] = 'selectize';
$element['#options'] = $grouped_types;

Since there isn't a _getTypesGrouped() function, you are essentially setting $element['#options'] to NULL, which means you are removing all the possible values the form field for that widget can have.
The correct code you want to use is the following one.

$grouped_types = $this->_getTypesGrouped();

// Override select form element.
$element['#settings'] = [
  'plugins' => ['optgroup_columns'],
];
$element['#type'] = 'selectize';
$element['#options'] = $grouped_types;

As for avoiding Drupal runs FormValidator::performRequiredValidation(), that is a bad idea. The purpose of that method is avoiding users submit a forget form; removing it means to mine the site security.

As side note, protected method don't need an underscore at the beginning of their name, as per Drupal coding standards.

2
  • Everything that you wrote is correct, thanks, but it doesn't solve the problem. Please see UPD section from my question. Is there another (the right) way to accomplish that goal?
    – graceman9
    Commented Aug 5, 2016 at 20:44
  • Still, the answer is correct: The error is returned because a form element contains a #value that is different from the values allowed for that form element (i.e. its #options values). It can also happen if the form token has been changed, but since you are not showing code that changes it, the cause can only be the first one.
    – avpaderno
    Commented Aug 6, 2016 at 5:41
0

For those which create a custom element based on the Select one and want to have OptGroup work, here's the trick:

class MyElement extends Select {

  #[...]

  public static function processSelect(&$element, FormStateInterface $form_state, &$complete_form) {
    $element = parent::processSelect($element, $form_state, $complete_form);
    $element['#type'] = 'select'; // trick to have OptGroup in our element

    #[...]
  }
}

Now this will work as expected in FormValidator::performRequiredValidation():

if ($elements['#type'] == 'select') {
  $options = OptGroup::flattenOptions($elements['#options']);
}
else {
  $options = $elements['#options'];
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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