Commit d1ae9b77 authored by catch's avatar catch

Issue #2651102 by mikeker, amateescu, gaurav.pahuja, Sagar Ramgade, dawehner,...

Issue #2651102 by mikeker, amateescu, gaurav.pahuja, Sagar Ramgade, dawehner, akalata, catch: Using checkboxes for exposed filters results in zero rows displaying
parent bbfb4178
......@@ -122,4 +122,40 @@ public static function valueCallback(&$element, $input, FormStateInterface $form
}
}
/**
* 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));
}
}
......@@ -5,6 +5,7 @@
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\Checkboxes;
use Drupal\Core\Url;
use Drupal\views\ExposedFormCache;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -146,26 +147,46 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Form input keys that will not be included in $view->exposed_raw_data.
$exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset');
$values = $form_state->getValues();
foreach (array('field', 'filter') as $type) {
/** @var \Drupal\views\Plugin\views\ViewsHandlerInterface[] $handlers */
$handlers = &$form_state->get('view')->$type;
foreach ($handlers as $key => $info) {
$handlers[$key]->submitExposed($form, $form_state);
if ($handlers[$key]->acceptExposedInput($values)) {
$handlers[$key]->submitExposed($form, $form_state);
}
else {
// The input from the form did not validate, exclude it from the
// stored raw data.
$exclude[] = $key;
}
}
}
$view = $form_state->get('view');
$view->exposed_data = $form_state->getValues();
$view->exposed_data = $values;
$view->exposed_raw_input = [];
$exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset');
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */
/** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */
$exposed_form_plugin = $view->display_handler->getPlugin('exposed_form');
$exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude);
foreach ($form_state->getValues() as $key => $value) {
if (!in_array($key, $exclude)) {
$view->exposed_raw_input[$key] = $value;
foreach ($values as $key => $value) {
if (!empty($key) && !in_array($key, $exclude)) {
if (is_array($value)) {
// Handle checkboxes, we only want to include the checked options.
// @todo: revisit the need for this when
// https://www.drupal.org/node/342316 is resolved.
$checked = Checkboxes::getCheckedCheckboxes($value);
foreach ($checked as $option_id) {
$view->exposed_raw_input[$option_id] = $value[$option_id];
}
}
else {
$view->exposed_raw_input[$key] = $value;
}
}
}
}
......
......@@ -7,6 +7,7 @@
use Drupal\Core\Form\FormHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Checkboxes;
use Drupal\user\RoleInterface;
use Drupal\views\Plugin\views\HandlerBase;
use Drupal\Component\Utility\Html;
......@@ -1332,15 +1333,20 @@ public function storeGroupInput($input, $status) {
}
/**
* Check to see if input from the exposed filters should change
* the behavior of this filter.
* Determines if the input from a filter should change the generated query.
*
* @param array $input
* The exposed data for this view.
*
* @return bool
* TRUE if the input for this filter should be included in the view query.
* FALSE otherwise.
*/
public function acceptExposedInput($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}
if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}
......@@ -1358,6 +1364,12 @@ public function acceptExposedInput($input) {
if ($value == 'All' || $value === array()) {
return FALSE;
}
// If checkboxes are used to render this filter, do not include the
// filter if no options are checked.
if (is_array($value) && Checkboxes::detectEmptyCheckboxes($value)) {
return FALSE;
}
}
if (!empty($this->alwaysMultiple) && $value === '') {
......
......@@ -282,15 +282,17 @@ public function reduceValueOptions($input = NULL) {
return $options;
}
/**
* {@inheritdoc}
*/
public function acceptExposedInput($input) {
// A very special override because the All state for this type of
// filter could have a default:
if (empty($this->options['exposed'])) {
return TRUE;
}
// If this is non-multiple and non-required, then this filter will
// participate, but using the default settings, *if* 'limit is true.
// The "All" state for this type of filter could have a default value. If
// this is a non-multiple and non-required option, then this filter will
// participate by using the default settings *if* 'limit' is true.
if (empty($this->options['expose']['multiple']) && empty($this->options['expose']['required']) && !empty($this->options['expose']['limit'])) {
$identifier = $this->options['expose']['identifier'];
if ($input[$identifier] == 'All') {
......
......@@ -206,6 +206,48 @@ public function testExposedFormRender() {
$this->assertEqual(count($result), 1, 'Filter description was found.');
}
/**
* Tests overriding the default render option with checkboxes.
*/
public function testExposedFormRenderCheckboxes() {
// Make sure we have at least two options for node type.
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalCreateNode(['type' => 'page']);
// Use a test theme to convert multi-select elements into checkboxes.
\Drupal::service('theme_handler')->install(array('views_test_checkboxes_theme'));
$this->config('system.theme')
->set('default', 'views_test_checkboxes_theme')
->save();
// Set the "type" filter to multi-select.
$view = Views::getView('test_exposed_form_buttons');
$filter = $view->getHandler('page_1', 'filter', 'type');
$filter['expose']['multiple'] = TRUE;
$view->setHandler('page_1', 'filter', 'type', $filter);
// Only display 5 items per page so we can test that paging works.
$display = &$view->storage->getDisplay('default');
$display['display_options']['pager']['options']['items_per_page'] = 5;
$view->save();
$this->drupalGet('test_exposed_form_buttons');
$actual = $this->xpath('//form//input[@type="checkbox" and @name="type[article]"]');
$this->assertEqual(count($actual), 1, 'Article option renders as a checkbox.');
$actual = $this->xpath('//form//input[@type="checkbox" and @name="type[page]"]');
$this->assertEqual(count($actual), 1, 'Page option renders as a checkbox');
// Ensure that all results are displayed.
$rows = $this->xpath("//div[contains(@class, 'views-row')]");
$this->assertEqual(count($rows), 5, '5 rows are displayed by default on the first page when no options are checked.');
$this->clickLink('Page 2');
$rows = $this->xpath("//div[contains(@class, 'views-row')]");
$this->assertEqual(count($rows), 1, '1 row is displayed by default on the second page when no options are checked.');
$this->assertNoText('An illegal choice has been detected. Please contact the site administrator.');
}
/**
* Tests the exposed block functionality.
*/
......
name: Views test checkboxes theme
type: theme
description: Theme for testing Views rendering of checkboxes.
version: VERSION
core: 8.x
<?php
/**
* @file
* Changes an exposed "type" filter from a multi-select to checkboxes.
*/
use Drupal\Core\Form\FormStateInterface;
/**
* Changes an exposed "type" filter from a multi-select to checkboxes.
*/
function views_test_checkboxes_theme_form_views_exposed_form_alter(&$form, FormStateInterface $form_state) {
$form['type']['#type'] = 'checkboxes';
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment