'checkboxes', * '#options' => array('SAT' => $this->t('SAT'), 'ACT' => $this->t('ACT')), * '#title' => $this->t('What standardized tests did you take?'), * ... * ); * @endcode * * @see \Drupal\Core\Render\Element\Radios * @see \Drupal\Core\Render\Element\Checkbox * * @FormElement("checkboxes") */ class Checkboxes extends FormElement { use CompositeFormElementTrait; /** * {@inheritdoc} */ public function getInfo() { $class = get_class($this); return [ '#input' => TRUE, '#process' => [ [$class, 'processCheckboxes'], ], '#pre_render' => [ [$class, 'preRenderCompositeFormElement'], ], '#theme_wrappers' => ['checkboxes'], ]; } /** * Processes a checkboxes form element. */ public static function processCheckboxes(&$element, FormStateInterface $form_state, &$complete_form) { $value = is_array($element['#value']) ? $element['#value'] : []; $element['#tree'] = TRUE; if (count($element['#options']) > 0) { if (!isset($element['#default_value']) || $element['#default_value'] == 0) { $element['#default_value'] = []; } $weight = 0; foreach ($element['#options'] as $key => $choice) { // Integer 0 is not a valid #return_value, so use '0' instead. // @see \Drupal\Core\Render\Element\Checkbox::valueCallback(). // @todo For Drupal 8, cast all integer keys to strings for consistency // with \Drupal\Core\Render\Element\Radios::processRadios(). if ($key === 0) { $key = '0'; } // Maintain order of options as defined in #options, in case the element // defines custom option sub-elements, but does not define all option // sub-elements. $weight += 0.001; $element += [$key => []]; $element[$key] += [ '#type' => 'checkbox', '#title' => $choice, '#return_value' => $key, '#default_value' => isset($value[$key]) ? $key : NULL, '#attributes' => $element['#attributes'], '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, // Errors should only be shown on the parent checkboxes element. '#error_no_message' => TRUE, '#weight' => $weight, ]; } } return $element; } /** * {@inheritdoc} */ public static function valueCallback(&$element, $input, FormStateInterface $form_state) { if ($input === FALSE) { $value = []; $element += ['#default_value' => []]; foreach ($element['#default_value'] as $key) { $value[$key] = $key; } return $value; } elseif (is_array($input)) { // Programmatic form submissions use NULL to indicate that a checkbox // should be unchecked. We therefore remove all NULL elements from the // array before constructing the return value, to simulate the behavior // of web browsers (which do not send unchecked checkboxes to the server // at all). This will not affect non-programmatic form submissions, since // all values in \Drupal::request()->request are strings. // @see \Drupal\Core\Form\FormBuilderInterface::submitForm() foreach ($input as $key => $value) { if (!isset($value)) { unset($input[$key]); } } return array_combine($input, $input); } else { return []; } } /** * Determines which checkboxes were checked when a form is submitted. * * @param array $input * An array returned by the FormAPI for a set of checkboxes. * * @return array * An array of keys that were checked. */ public static function getCheckedCheckboxes(array $input) { // Browsers do not include unchecked options in a form submission. The // FormAPI tries to normalize this to keep checkboxes consistent with other // form elements. Checkboxes show up as an array in the form of option_id => // option_id|0, where integer 0 is an unchecked option. // // @see \Drupal\Core\Render\Element\Checkboxes::valueCallback() // @see https://www.w3.org/TR/html401/interact/forms.html#checkbox $checked = array_filter($input, function($value) { return $value !== 0; }); return array_keys($checked); } /** * Determines if all checkboxes in a set are unchecked. * * @param array $input * An array returned by the FormAPI for a set of checkboxes. * * @return bool * TRUE if all options are unchecked. FALSE otherwise. */ public static function detectEmptyCheckboxes(array $input) { return empty(static::getCheckedCheckboxes($input)); } }