Use the checkboxes dynamic component when you need to help users select one or more options from a list of options.
Do not use the checkboxes dynamic component when you need to help users:
In these cases, use (non-dynamic) checkboxes instead.
The checkboxes dynamic component is an adapted version of the checkboxes component that can handle various numbers of checkboxes, both short lists of checkboxes as well as average-length lists as long lists of checkboxes.
Depending on the number of checkboxes, the checkboxes dynamic component presents the checkboxes in a different way:
When there are more than 20 checkboxes, what would normally be a long list of checkboxes, is replaced by showing only the first 3 checkboxes with a “Show more” button that opens a modal where the list of checkboxes is placed instead.
The behavior is as follows:
When the user selects one or more of the first 3 checkboxes that are already visible outside the modal, the corresponding checkboxes inside the modal are also selected.
When one or more checkboxes are selected, inside or outside the modal, the user can see this both:
This display uses the accordion JS and @digipolis-gent/modal.
We refer you to the accordion and
modal components for a detailed account on how to use these libraries.
Create new accordions for each element with className .checkbox-accordion
.
new Accordion(element);
Create new modals for each element with className .modal:not(.has-custom-binding)
,
if you haven’t done so already.
new Modal(element);
On top of that, create a new CheckboxFilterDynamic instances
for each element with className checkbox-filter-dynamic
.
Don’t forget to add your translation for the hiddenTagText.
new CheckboxFilterDynamic(element, {hiddenTagText: 'Remove tag'})
The checkboxes shown as preview are copies!
You link them to their original checkbox by providing a data-original
attribute
with the ID of the original checkbox.
Do not give them a name if you don’t want them to show up in your FormData!
Option | Type | Default | Description |
---|---|---|---|
filterField |
String |
.checkbox-filter__filter |
QuerySelector for the filter input field. |
modalPreview |
String |
.modal-preview |
QuerySelector for the wrapper containing modal preview checkboxes. |
accordionPreview |
String |
.accordion-preview |
QuerySelector for the wrapper containing accordion preview checkboxes. |
accordionBtn |
String |
button.accordion--button |
QuerySelector for the button that toggles the accordion. |
previewCheckboxes |
String |
div.checkbox.preview |
QuerySelector for the preview checkboxes. |
checkboxes |
String |
div.checkbox:not(.preview) |
QuerySelector for the checkboxes. |
selectedContainer |
String |
.checkbox-filter__selected |
QuerySelector for the container holding the selected filter tags. |
checkboxContainers |
String |
.checkbox-filter__checkboxes |
QuerySelector for the container holding the checkboxes. |
openBtn |
String |
.checkbox-filter__open |
QuerySelector for the button used to open the modal. |
submitBtn |
String |
.checkbox-filter__submit |
QuerySelector for the button used to confirm the selection and close the modal. |
closeBtns |
String |
.checkbox-filter__close |
QuerySelector for a list of buttons used to close the modal. |
resultSpan |
String |
.checkbox-filter__result |
QuerySelector for the container to display the number of search results. |
makeTags |
Boolean |
true |
Prevent creation of tags, in case you have your own implementation. |
hiddenTagText |
String |
Remove tag |
Text used behind the remove-tag button, insert your translation here. |
onRemoveTag |
function |
function(checkbox, tag){} |
A hook to append your own logic after a tag has been removed. |
{% if options|length < 6 %}
{% include '@checkboxes' %}
{% else %}
<fieldset class="form-item {{ modifier }} checkbox-filter-dynamic">
<legend>
<span class="legend-title">{{ label }}</span>
{% if label_optional %}
<span class="label-optional">({{ label_optional }})</span>
{% endif %}
</legend>
<div class="form-item">
{% if field_description %}
{% include '@field-message' with {
field_message: field_description,
modifier: null
} %}
{% endif %}
<div class="form-columns">
<div class="form-item-column">
{% if options | length < 21 %}
<div class="accordion-preview">
{% for option in options|slice(0, 3) %}
{% include '@input' with {
id: "input-" ~ id ~ "-" ~ modifier ~ "-" ~ option.id,
type: 'checkbox',
name: option.name,
label: option.label,
modifier: modifier
} %}
{% endfor %}
</div>
<div class="checkbox-accordion">
<div class="accordion--content" aria-hidden="true"
hidden="hidden" id="{{ "hidden-options-" ~ id ~ "-" ~ modifier }}">
{% for option in options|slice(3, 21) %}
{% include '@input' with {
id: "input-" ~ id ~ "-" ~ modifier ~ "-" ~ option.id,
type: 'checkbox',
name: option.name,
label: option.label,
modifier: modifier
} %}
{% endfor %}
</div>
<button type="button"
class="accordion--button button button-secondary button-small icon-left"
aria-expanded="false"
aria-controls="{{ "hidden-options-" ~ id ~ "-" ~ modifier }}">
Show more
</button>
</div>
{% else %}
<div class="modal-preview">
{% for option in options|slice(0, 3) %}
{% include '@input' with {
id: "input-" ~ id ~ "-" ~ modifier ~ "-" ~ option.id ~ "-preview",
type: 'checkbox',
name: null,
attributes: 'data-original="' ~ "input-" ~ id ~ "-" ~ modifier ~ "-" ~ option.id ~ '"',
label: option.label,
modifier: modifier,
modifierWrapper: "preview",
value: option.value
} %}
{% endfor %}
</div>
{% set modalTitle = label %}
{% if label_optional %}
{% set modalTitle = modalTitle ~ '<span class="label-optional">(' ~ label_optional ~ ')</span>' %}
{% endif %}
{% set modalContent %}
<div class="form-item checkbox-filter__filter__search-wrapper">
<input type="search" id="checkboxes__filter_id_{{ modifier }}"
class="checkbox-filter__filter"
aria-label="Filter the list below">
<div aria-live="polite" aria-atomic="true"
class="checkbox-filter__result-wrapper">
<span class="checkbox-filter__result">#</span> filter(s) found
</div>
</div>
{% include '@tag-list' with { modifier: 'checkbox-filter__selected' } %}
{% include '@checkboxes' %}
{% endset %}
{% set modalActions %}
<button type="button"
class="button button-primary checkbox-filter__submit modal-close"
data-target="{{ 'modal' ~ modifier }}">Confirm selection
</button>
{% endset %}
{% include "@modal" with {
id: 'modal' ~ modifier,
title: modalTitle,
title_heading_level: 'h2',
classes: 'checkbox-filter__modal',
cancelClasses: 'checkbox-filter__close',
modifier: 'fixed-height',
content: modalContent,
actions: modalActions,
hasFocusables: true
} %}
<button type="button"
class="button button-secondary button-small icon-left icon-search checkbox-filter__open"
aria-controls="{{ 'modal' ~ modifier }}"
aria-expanded="false">
Show more
</button>
{% endif %}
</div>
{% apply spaceless %}
<div class="form-item-column">
{% if modifier == 'error' %}
{% include '@field-message' with {
"modifier": "error"
} %}
{% endif %}
{% if modifier == 'success' %}
{% include '@field-message' with {
"modifier": "success"
} %}
{% endif %}
</div>
{% endapply %}
</div>
</div>
</fieldset>
{% endif %}
<fieldset class="form-item ">
<legend>
<span class="legend-title">Checkboxes</span>
</legend>
<div class="form-item">
<div class="field-message " id="default-description">
Optional field description.<br> --- <br> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium consectetur eveniet illo porro quis sint.
<div class="accolade "></div>
</div>
<div class="form-columns">
<div class="form-item-column">
<div class="checkbox">
<input type="checkbox" id="input-default--checkbox-1-default" name="checkboxes-dynamic" value="1" class="checkbox" />
<label for="input-default--checkbox-1-default">Checkbox option 1</label>
</div>
<div class="checkbox">
<input type="checkbox" id="input-default--checkbox-0-default" name="checkboxes-dynamic" class="checkbox" />
<label for="input-default--checkbox-0-default">Checkbox option 0</label>
</div>
</div>
<div class="form-item-column"></div>
</div>
</div>
</fieldset>
{
"label": "Checkboxes",
"id": "default",
"field_description": "Optional field description.<br> --- <br> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium consectetur eveniet illo porro quis sint.",
"field_message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec laoreet, urna sit amet convallis rhoncus, felis ex.",
"description": "Description checkboxes.",
"options": [
{
"label": "Checkbox option 1",
"id": "checkbox-1-default",
"value": 1,
"name": "checkboxes-dynamic"
},
{
"label": "Checkbox option 0",
"id": "checkbox-0-default",
"value": 0,
"name": "checkboxes-dynamic"
}
]
}
.checkbox-filter-dynamic {
.modal-preview,
.accordion-preview,
.checkbox-accordion {
.checkbox:last-child {
margin-bottom: .5rem;
}
}
.checkbox-accordion {
.accordion--content {
// Adjust for checkbox focus outline.
margin: -4px -4px 0;
padding: 4px 4px 0;
transition: max-height .5s ease-in-out;
overflow: hidden;
}
.accordion--button {
&[aria-expanded=false] {
@include icon(chevron-down);
}
&[aria-expanded=true] {
@include icon(chevron-up);
}
}
}
}
.checkbox-filter__modal {
.tag-list-wrapper {
min-height: 2.2rem;
margin-bottom: 1.2rem;
}
.tag-list {
.tag {
margin: 0;
}
}
}
'use strict';
(function () {
if (Accordion) { // eslint-disable-line no-undef
const selected = document.querySelectorAll('.checkbox-accordion');
for (let i = selected.length; i--;) {
new Accordion(selected[i]); // eslint-disable-line no-undef
}
}
if (CheckboxFilterDynamic) { // eslint-disable-line no-undef
const selected = document.querySelectorAll('.checkbox-filter-dynamic');
for (let i = selected.length; i--;) {
new CheckboxFilterDynamic(selected[i], {hiddenTagText: 'Remove tag'}); // eslint-disable-line no-undef
}
}
})();
'use strict';
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
}
else {
if (typeof exports === 'object') {
module.exports = factory();
}
else {
root.CheckboxFilterDynamic = factory();
}
}
})(this || window, function () {
return function (elem, options) {
options = options || {};
options.filterField = options.filterField || '.checkbox-filter__filter';
options.modalPreview = options.modalPreview || '.modal-preview';
options.accordionPreview = options.accordionPreview || '.accordion-preview';
options.accordionBtn = options.accordionBtn || 'button.accordion--button';
options.previewCheckboxes = options.previewCheckboxes || 'div.checkbox.preview';
options.checkboxes = options.checkboxes || 'div.checkbox:not(.preview)';
options.selectedContainer = options.selectedContainer || '.checkbox-filter__selected';
options.checkboxContainers = options.checkboxContainers || '.checkbox-filter__checkboxes';
options.openBtn = options.openBtn || '.checkbox-filter__open';
options.submitBtn = options.submitBtn || '.checkbox-filter__submit';
options.closeBtns = options.closeBtns || '.checkbox-filter__close';
options.resultSpan = options.resultSpan || '.checkbox-filter__result';
options.makeTags = options.makeTags !== false;
options.hiddenTagText = options.hiddenTagText || 'remove tag';
options.onRemoveTag = options.onRemoveTag || function () {};
/**
* Filter input field.
* @type {HTMLInputElement}
*/
const filterField = elem.querySelector(options.filterField);
/**
* Container for the modal preview checkboxes.
* @type {HTMLElement}
*/
const modalPreview = elem.querySelector(options.modalPreview);
/**
* Container for the accordion preview checkboxes.
* @type {HTMLElement}
*/
const accordionPreview = elem.querySelector(options.accordionPreview);
/**
* Button to toggle the accordion.
* @type {HTMLElement}
*/
const accordionBtn = elem.querySelector(options.accordionBtn);
/**
* List of preview checkboxes, these exist outside of the modal.
* @type {NodeList|Array}
*/
const previewCheckboxes = elem.querySelectorAll(options.previewCheckboxes) || [];
/**
* List of checkboxwrappers, each containing a checkbox and a label.
* @type {NodeList|Array}
*/
const checkboxes = elem.querySelectorAll(options.checkboxes) || [];
/**
* Container to display the selected items.
* @type {HTMLElement}
*/
const selectedContainer = elem.querySelector(options.selectedContainer);
/**
* Container for the checkboxes.
* @type {HTMLElement}
*/
const checkboxContainers = elem.querySelectorAll(options.checkboxContainers);
/**
* Button to trigger opening the modal.
* @type {HTMLElement}
*/
const openBtn = elem.querySelector(options.openBtn);
/**
* Button to confirm the selection and close the modal.
* @type {Element}
*/
const submitBtn = elem.querySelector(options.submitBtn);
/**
* A list of elements to trigger closing the modal.
* At least one must have the button role.
* @type {NodeList}
*/
const closeBtns = elem.querySelectorAll(options.closeBtns);
/**
* Container to display the number of search results.
* @type {HTMLElement}
*/
const resultSpan = elem.querySelector(options.resultSpan);
/**
* Store the checked checkboxes prior to making changes.
* @type {Array}
*/
let selectedFilters = [];
/**
* Filter the displayed checkboxes.
* @param {boolean} clear Clear the filtervalue if true.
*/
const filter = clear => {
if (!filterField) {
return;
}
if (clear) {
filterField.value = '';
}
let count = 0;
[].slice.call(checkboxContainers).forEach(container => {
container.style.display = 'none';
});
checkboxLoop(({checkboxWrapper, checkbox, label}) => {
if (
!label ||
label.textContent
.toUpperCase()
.indexOf(filterField.value.toUpperCase()) === -1
) {
checkboxWrapper.setAttribute('hidden', 'true');
checkbox.setAttribute('hidden', 'true');
}
else {
checkboxWrapper.removeAttribute('hidden');
checkbox.removeAttribute('hidden');
count++;
}
});
[].slice.call(checkboxContainers).forEach(container => {
let displayedCount = container.querySelectorAll(`${options.checkboxes}:not([hidden])`).length;
if (displayedCount) {
container.style.display = '';
}
});
if (resultSpan) {
resultSpan.textContent = '' + count;
}
};
/**
* Make a tag.
* @param {HTMLInputElement} checkbox Input type checkbox.
* @param {Element} label Label for the input type checkbox.
* @return {Element} A gent styleguide tag component.
*/
const makeTag = (checkbox, label) => {
const li = document.createElement('li');
const tag = document.createElement('span');
tag.className = 'tag filter';
tag.textContent = label.textContent;
tag.setAttribute('data-value', checkbox.value);
const button = document.createElement('button');
button.type = 'button';
button.innerHTML = `<span class="visually-hidden">${options.hiddenTagText} ${label.textContent}</span>`;
button.addEventListener('click', () => checkbox.click());
tag.appendChild(button);
li.appendChild(tag);
return li;
};
/**
* Remove a tag from the selectedContainer.
* @param {HTMLInputElement} checkbox Input type checkbox.
*/
const removeTag = checkbox => {
if (options.makeTags && selectedContainer) {
const tag = selectedContainer.querySelector('.filter[data-value="' + checkbox.value + '"]');
if (tag) {
selectedContainer.removeChild(tag.parentElement);
options.onRemoveTag(checkbox, tag);
}
}
};
const addTag = (checkbox, label) => {
if (options.makeTags && selectedContainer) {
selectedContainer.appendChild(makeTag(checkbox, label));
}
};
/**
* Loop over all checkboxes and execute a callback for each iteration.
* @param {function} next The callback function.
*/
const checkboxLoop = next => {
for (let i = 0; i < checkboxes.length; i++) {
let checkboxWrapper = checkboxes[i];
let checkbox = checkboxWrapper.querySelector('input[type=checkbox]');
let label = checkboxWrapper.querySelector('label');
// Sometimes the input element isn't rendered at this point.
if (checkbox && label) {
next({checkboxWrapper, checkbox, label});
}
}
};
/**
* Reset the component to it's stored value.
*/
const reset = () => {
checkboxLoop(({checkbox}) => {
const isSelected = selectedFilters.indexOf(checkbox) !== -1;
if ((isSelected && !checkbox.checked) || (!isSelected && checkbox.checked)) {
checkbox.click();
}
});
};
/**
* Initialise the component.
*/
const init = () => {
checkboxLoop(({checkbox, label}) => {
if (checkbox.checked && !checkbox.indeterminate) {
addTag(checkbox, label);
}
});
filter(true);
};
/**
* Bind two checkboxes
* @param {HTMLInputElement} a A checkbox.
* @param {HTMLInputElement} b Another checkbox.
*/
const bindState = (a, b) => {
if (a.checked !== b.checked) {
a.click();
}
};
/**
* Bind the preview checkboxes to their original counterpart in the modal.
*/
const addPreviewCheckboxesEvent = () => {
if (!previewCheckboxes.length) {
return;
}
for (let i = previewCheckboxes.length; i--;) {
const checkboxWrapper = previewCheckboxes[i];
const checkbox = checkboxWrapper.querySelector('input[type=checkbox]');
const original = elem.querySelector('#' + checkbox.getAttribute('data-original'));
if (checkbox) {
checkbox.addEventListener('change', () => bindState(original, checkbox));
original.addEventListener('change', () => bindState(checkbox, original));
}
}
};
const addModalEvents = () => {
// Enable opening the modal.
if (openBtn) {
openBtn.addEventListener('click', () => {
selectedFilters = [];
checkboxLoop(({checkbox}) => {
if (checkbox.checked) {
selectedFilters.push(checkbox);
}
});
document.addEventListener('keydown', handleKeyboardInput);
});
}
// Add close events to all closeBtns.
if (closeBtns) {
for (let i = closeBtns.length; i--;) {
closeBtns[i].addEventListener('click', () => {
reset();
document.removeEventListener('keydown', handleKeyboardInput);
});
}
}
// Update selectedFilters and close.
if (submitBtn) {
submitBtn.addEventListener('click', () => {
document.removeEventListener('keydown', handleKeyboardInput);
});
}
// Make sure the filter method is not repeated while typing.
if (filterField) {
let filterTimeOut = null;
filterField.addEventListener('input', () => {
if (filterTimeOut) {
clearTimeout(filterTimeOut);
}
filterTimeOut = setTimeout(filter, 200);
});
}
};
const createPreviewClone = (checkboxWrapper, checkbox) => {
if (!modalPreview) {
return;
}
if (modalPreview.querySelector('#' + checkbox.id + '-preview')) {
return;
}
const wrapperClone = checkboxWrapper.cloneNode(true);
const checkboxClone = wrapperClone.querySelector('input');
const labelClone = wrapperClone.querySelector('label');
checkboxClone.name = null;
checkboxClone.id += '-preview';
labelClone.for += '-preview';
checkbox.addEventListener('change', () => bindState(checkboxClone, checkbox));
checkboxClone.addEventListener('change', () => bindState(checkbox, checkboxClone));
modalPreview.appendChild(wrapperClone);
};
const addAccordionEvent = () => {
if (!accordionBtn || !accordionPreview) {
return;
}
accordionBtn.addEventListener('click', () =>
checkboxLoop(({checkboxWrapper, checkbox}) => {
if (checkbox.checked && !accordionPreview.contains(checkboxWrapper)) {
accordionPreview.appendChild(checkboxWrapper);
}
})
);
};
/**
* Add all events.
*/
const addEvents = () => {
addModalEvents();
addPreviewCheckboxesEvent();
addAccordionEvent();
// Add events for all checkboxes.
checkboxLoop(({checkboxWrapper, checkbox, label}) => {
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
addTag(checkbox, label);
createPreviewClone(checkboxWrapper, checkbox);
}
else {
removeTag(checkbox);
}
});
});
};
/**
* Handle keyboard input
* @param {object} e event
*/
const handleKeyboardInput = e => {
let keyCode = e.keyCode || e.which;
if (keyCode === 27) {
e.preventDefault();
reset();
}
};
init();
addEvents();
return {};
};
});