The CheckboxGroup is a Custom Element which progressively enhances a semantic group of checkbox input elements, providing the group with convenient features like seameless form validation and easier value management.
npm install @itenthusiasm/custom-elements
<!-- HTML -->
<form>
<checkbox-group id="toppings" name="toppings" min="2" max="4">
<fieldset>
<label for="toppings">Toppings</label>
<div>
<input id="pepperoni" type="checkbox" value="pepperoni" checked />
<label for="pepperoni">Pepperoni</label>
<input id="sausage" type="checkbox" value="sausage" />
<label for="sausage">Sausage</label>
<input id="onions" type="checkbox" value="onions" />
<label for="onions">Onions</label>
<input id="peppers" type="checkbox" value="peppers" />
<label for="peppers">Peppers</label>
<input id="olives" type="checkbox" value="olives" />
<label for="olives">Olives</label>
</div>
</fieldset>
</checkbox-group>
</form>NOTE: Only the
<fieldset>element is allowed to be a direct descendant of a<checkbox-group>.
/* JavaScript */
import { CheckboxGroup } from "@itenthusiasm/custom-elements";
// or import { CheckboxGroup } from "@itenthusiasm/custom-elements/CheckboxGroup";
customElements.define("checkbox-group", CheckboxGroup);
// Retrieve some info about the `checkbox-group`
const form = document.querySelector("form");
const formData = new FormData(form);
console.log(formData.getAll("toppings")); // ["pepperoni"]
const checkboxGroup = document.querySelector("checkbox-group");
console.log(checkboxGroup.form === form); // true
console.log(checkboxGroup.value); // ["pepperoni"]
// Right now, the group is invalid because the `min` attribute dictates that at least 2 items must be selected
console.log(checkboxGroup.validity.valid); // false
console.log(checkboxGroup.validity.rangeUnderflow); // trueThe example HTML displayed at the beginning of this document is fully functional, but it only works when JavaScript is enabled for your application. To ensure that your checkbox group will work even when JavaScript is disabled, you should render your checkbox group the "old fashioned way".
<form>
<checkbox-group min="2" max="4">
<fieldset>
<legend>Toppings</legend>
<div>
<input id="pepperoni" name="toppings" type="checkbox" value="pepperoni" checked />
<label for="pepperoni">Pepperoni</label>
<input id="sausage" name="toppings" type="checkbox" value="sausage" />
<label for="sausage">Sausage</label>
<input id="onions" name="toppings" type="checkbox" value="onions" />
<label for="onions">Onions</label>
<input id="peppers" name="toppings" type="checkbox" value="peppers" />
<label for="peppers">Peppers</label>
<input id="olives" name="toppings" type="checkbox" value="olives" />
<label for="olives">Olives</label>
</div>
</fieldset>
</checkbox-group>
</form>The above HTML will work even if JavaScript is disabled. When the user submits the form provided above, an array of values containing every item which the user selected will be sent to the server (even when JS is disabled).
Remember that client-side form validation for checkbox groups is a feature provided by the CheckboxGroup Web Component (meaning that JavaScript must be enabled). So you'll need server-side validation in place to enforce that the minimum/maximum number of items is selected to cover the case where a user has JS disabled. (That said, you should always write server-side validation anyway to ensure the security and integrity of your web applications.)
When the CheckboxGroup Custom Element is registered and mounted to the DOM, it transforms the HTML above into the following:
<form>
<checkbox-group id="RANDOMLY_GENERATED_ID" name="toppings" min="2" max="4">
<fieldset role="none">
<label for="RANDOMLY_GENERATED_ID">Toppings</label>
<div>
<input id="pepperoni" type="checkbox" value="pepperoni" form="" checked />
<label for="pepperoni">Pepperoni</label>
<input id="sausage" type="checkbox" value="sausage" form="" />
<label for="sausage">Sausage</label>
<input id="onions" type="checkbox" value="onions" form="" />
<label for="onions">Onions</label>
<input id="peppers" type="checkbox" value="peppers" form="" />
<label for="peppers">Peppers</label>
<input id="olives" type="checkbox" value="olives" form="" />
<label for="olives">Olives</label>
</div>
</fieldset>
</checkbox-group>
</form>As you can see from the transformed markup above, the CheckboxGroup does a few things. It...
- Replaces the
<legend>element with a<label>element.- The
CheckboxGroupcan only be labeled by a<label>element, not a<legend>element. This is the reason for replacing the<legend>, and it is the reason for the randomly-generatedidas well. If you provide your ownidto the<checkbox-group>element, then the component will use that ID instead of randomly generating its own. - If no
<legend>is found on mount, then the component will not try to create an accessible label for itself.
- The
- Transfers the
nameattribute (if it exists) from thecheckboxes to itself.- Since the
<checkbox-group>itself is a form control, and since it represents the whole group ofcheckboxes, it is the only form control in the group that needs thenameattribute.
- Since the
- Dissociates the
checkboxes from the wrapping<form>.- This is done by applying the empty
[form=""]attribute to all thecheckboxes, and it is done to avoid running into edge cases where events or form data could be processed in a duplicated fashion. Again, since the<checkbox-group>is a form control representing the whole group ofcheckboxes, it is the only form control that needs to be associated with the<form>.
- This is done by applying the empty
- Applies the
nonerole to the<fieldset>element.<fieldset>s typically have the accessiblegrouprole. The<checkbox-group>element also has this role, so[role="none"]is applied to the<fieldset>to avoid duplicating accessibility information.
Although it wasn't shown in the example above, the <checkbox-group> does one last thing on mount: It transfers all of the <fieldset>'s attributes to itself. This can be useful, for example, for transferring important accessibility attributes (like aria-describedby) from the <fieldset> to the <checkbox-group> after your application's JavaScript has loaded.
Note: No matter what role the
<fieldset>has on mount, the<checkbox-group>will always end up with agrouprole after mounting, and the<fieldset>will always end up with anonerole.
Some JS frameworks (such as React) may get confused if you remove elements from the DOM with tools other than what the framework provides. This makes the <legend> replacement feature of the <checkbox-group> component problematic. To preserve this progressive enhancement behavior without breaking the expectations of your framework, you can write a component that performs the <legend> replacement itself. Here is an example in React:
import { useState, useEffect } from "react";
import type {} from "@itenthusiasm/custom-elements/types/react";
// Or import type {} from "@itenthusiasm/custom-elements/CheckboxGroup/types/react";
interface CheckboxGroupProps extends React.ComponentProps<"checkbox-group"> {
label?: string;
}
export default function CheckboxGroup({ label, children, ...rest }: CheckboxGroupProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return (
<checkbox-group {...(mounted ? rest : undefined)} manual>
<fieldset {...(mounted ? undefined : (rest as React.ComponentProps<"fieldset">))}>
{!!label && (mounted ? <label htmlFor={rest.id}>{label}</label> : <legend>{label}</legend>)}
{children}
</fieldset>
</checkbox-group>
);
}Notice the manual attribute that was applied to the <checkbox-group>. This tells the component not to remove or add any children on its own, giving developers the freedom to make such modifications themselves. Note that the CheckboxGroup will still perform other automated actions unrelated to DOM insertion/removal in this mode, such as altering the name and form attributes of every checkbox inserted into it. These other actions are all safe for JS frameworks.
If your application is a SPA, then you don't need to be concerned about progressive enhancement, and you can just structure your code in a manner similar to what was shown at the beginning of this document.
Many JS frameworks, such as Svelte and React, often define their own "Element Namespaces". Because of this, most frameworks are not able (on their own) to recognize the correct attributes, properties, and event listeners that belong to the Custom Elements which you use. Thankfully, our library ships with TypeScript types that tell the various JS Frameworks about the existence and shape of our Custom Elements. To define all of our library's Custom Elements within a Framework's "Element Namespace", simply import the appropriate type definition file:
import type {} from "@itenthusiasm/custom-elements/types/react";
// For Svelte: import type {} from "@itenthusiasm/custom-elements/types/svelte";
// For Vue: import type {} from "@itenthusiasm/custom-elements/types/vue";
// etc. ...If you only intend to use some of the Custom Elements provided by this library, then you should only import the types for those components.
// Define ONLY the `CheckboxGroup` component's types in the framework's "Element Namespace"
import type {} from "@itenthusiasm/custom-elements/CheckboxGroup/types/react";
// For Svelte: import type {} from "@itenthusiasm/custom-elements/CheckboxGroup/types/svelte";
// For Vue: import type {} from "@itenthusiasm/custom-elements/CheckboxGroup/types/vue";
// etc. ...The component also ships with types that enhance the native DOM methods if you need them:
import type {} from "@itenthusiasm/custom-elements/types/dom";
// Or more specifically, import type {} from "@itenthusiasm/custom-elements/CheckboxGroup/types/dom";
// This variable will be properly typed by TypeScript now, instead of just being a general `Element` type.
const checkboxGroup = document.querySelector("checkbox-group");Because the <checkbox-group> is simply a wrapper around the <fieldset> element, it does not need any custom CSS. Therefore, the library does not ship any custom CSS for this component. You can style the <checkbox-group>, <fieldset>, and other elements as usual.
The CheckboxGroup Custom Element enforces the following restrictions:
- Only the
<fieldset>element is allowed to be a direct descendant of a<checkbox-group>. - Checkboxes (
<input type="checkbox">) are the only kind of form controls allowed within the<checkbox-group>. Other kinds of form controls are forbidden. However, all non-form-control elements are permitted within the<checkbox-group>(as demonstrated in the examples above). - Nesting
<checkbox-group>s is not supported.
This section describes the attributes, properties, and events associated with the CheckboxGroup Custom Element.
As a Custom Element, the CheckboxGroup supports all of the global attributes. The attributes which are specific to the CheckboxGroup are as follows:
-
name -
Same as the
nameattribute seen on native form controls like the<input>element. Ensures that the element's value will be submitted to the server under the specifiednameon form submission. The name can also be used to access the element via theHTMLFormElement.elementsproperty of the owning form.This attribute is reflected by the
CheckboxGroup.nameproperty. -
disabled -
Same as the
disabledboolean attribute seen on native form controls like the<input>element. When this attribute is present, the entireCheckboxGroupcomponent (including the checkboxes that it owns) will become non-interactive, and its value will not be sent to the server.This attribute is reflected by the
CheckboxGroup.disabledproperty. -
required -
Same as the
requiredboolean attribute seen on native form controls like the<input>element. Indicates that the element's owning form cannot be submitted if its value is an empty array (i.e., if no checkboxes are selected).NOTE: In general, you should always prefer the
minattribute to therequiredattribute. This is because therequiredattribute is effectively the same as setting[min="1"]. Theminattribute is more succinct and vastly more flexible.This attribute is reflected by the
CheckboxGroup.requiredproperty. -
valuemissingerror -
Determines the
validationMessagethat the element will display if it has aValidityState.valueMissingerror. The element enters this error state if its value is an empty array when it has therequiredattribute. If the element'srequiredconstraint is broken when its owning form is submitted, then form submission will fail, and thevaluemissingerrorwill be shown to the user in an error message bubble.To learn more about client-side form validation, see MDN's Client-side Form Validation tutorial. If you're looking for a robust form validation library that works with Custom Elements, try the
FormValidityObserver.This attribute is reflected by the
CheckboxGroup.valueMissingErrorproperty. -
min -
Same as the
minattribute found on the native<input>element. Indicates that the element's owning form cannot be submitted until the minimum required number of items are selected. Note that the constraint is only applied if the attribute is a valid number.This attribute is reflected by the
CheckboxGroup.minproperty. -
rangeunderflowerror -
Determines the
validationMessagethat the element will display if it has aValidityState.rangeUnderflowerror. The element enters this error state if the user has selected a number of items less than the value specified by theminattribute. If the element'sminconstraint is broken when its owning form is submitted, then form submission will fail, and therangeunderflowerrorwill be shown to the user in an error message bubble.NOTE: If the element's
minandrequiredconstraints are both broken, then only therangeunderflowerrorwill be shown. This is to minimize the number of steps the user will need to take to fix the form control's value.This attribute is reflected by the
CheckboxGroup.rangeUnderflowErrorproperty. -
max -
Same as the
maxattribute found on the native<input>element. Indicates that the element's owning form cannot be submitted until the number of selected items is less than or equal to the maximum allowed amount. Note that the constraint is only applied if the attribute is a valid number.NOTE: You should always ensure that the
maxattribute is greater than or equal to theminattribute.This attribute is reflected by the
CheckboxGroup.maxproperty. -
rangeoverflowerror -
Determines the
validationMessagethat the element will display if it has aValidityState.rangeOverflowerror. The element enters this error state if the user has selected a number of items greater than the value specified by themaxattribute. If the element'smaxconstraint is broken when its owning form is submitted, then form submission will fail, and therangeoverflowerrorwill be shown to the user in an error message bubble.This attribute is reflected by the
CheckboxGroup.rangeOverflowErrorproperty. -
manual -
A boolean attribute indicating that the
CheckboxGroupshould not replace its<fieldset>'s accessible label (<legend>) with its own (<label>) when mounted. You should only enable this property if you intend to perform the label replacement yourself.Note that the other automated actions unrelated to DOM insertion/removal that are described in the Progressive Enhancement section will still occur even when this mode is turned on. (This is for accessibility purposes.)
This attribute is reflected by the
CheckboxGroup.manualproperty.
As a Custom Element, the CheckboxGroup inherits all of the methods and properties of the HTMLElement interface. The properties which are specific to the CheckboxGroup are as follows:
-
value -
Sets or retrieves the value of the element. Type is
string[].When this property is set, every checkbox with a
valuecorresponding to an item in the array will be checked; all other checkboxes will be unchecked. If an item in the provided array has no corresponding checkbox, then it will be deleted. -
name -
Same as the
nameproperty found on native form controls like the<input>element: It reflects the value of thenameattribute. Type isstring. -
disabled -
Same as the boolean
disabledproperty found on native form controls like the<input>element: It reflects the value of thedisabledattribute. -
required -
Same as the boolean
requiredproperty found on native form controls like the<input>element: It reflects the value of therequiredattribute. -
valueMissingError - Reflects the
valuemissingerrorattribute. Type isstring. -
min -
Same as the
minproperty found on the native<input>element: It reflects the value of theminattribute. Type isstring. -
rangeUnderflowError - Reflects the
rangeunderflowerrorattribute. Type isstring. -
max -
Same as the
maxproperty found on the native<input>element: It reflects the value of themaxattribute. Type isstring. -
rangeOverflowError - Reflects the
rangeoverflowerrorattribute. Type isstring. -
manual - A
booleanproperty which reflects the value of themanualattribute. -
fieldset -
Returns the
HTMLFieldSetElementwrapped by theCheckboxGroup. Useful for iterating over all of the group's checkboxes programmatically. -
labels -
Same as the read-only
labelsproperty found on native form controls like the<input>element: Returns aNodeListof all the<label>elements associated with theCheckboxGroup. -
form -
Same as the read-only
formproperty found on native form controls like the<input>element: Returns theHTMLFormElementwith which theCheckboxGroupis associated. Returnsnullif no<form>owns the element. -
validity -
Same as the read-only
validityproperty found on native form controls like the<input>element: Returns the element'sValidityStateobject. -
validationMessage -
Same as the read-only
validationMessageproperty found on native form controls like the<input>element: Returns the message that will be displayed to users when theCheckboxGroupfails constraint validation. Type isstring. To learn more about client-side form validation and when the user is shown an error message bubble, see MDN's Client-side Form Validation tutorial. -
willValidate -
Same as the read-only boolean
willValidateproperty found on native form controls like the<input>element: Returnsfalseif the element will skip constraint validation. See MDN's Documentation for more info on howwillValidateis determined for form controls.
In addition to the methods that exist on the HTMLElement interface, the CheckboxGroup defines the instance methods listed below.
-
checkValidity()Signature:() => boolean -
Same as the
checkValidity()method found on native form controls like the<input>element: Returnstrueif the element passes constraint validation. Otherwise, returnsfalse. If validation fails, aninvalidevent will be fired on the element, but the element will not display an error message bubble. -
reportValidity()Signature:() => boolean -
Same as the
reportValidity()method found on native form controls like the<input>element: Behaves the same as thecheckValidity()method. However, it will display an error message bubble if the element fails constraint validation, as long as theinvalidevent has not been canceled. -
setCustomValidity()Signature:(error: string) => void -
Same as the
setCustomValidity()method found on native form controls like the<input>element: Marks the element as invalid by giving it a custom error message. This error state/message takes precedence over all other error states/messages (such as therequirederror state/message and all the other ones). Note that any form control which has a custom error message will always fail constraint validation. (This is also true for native form controls.) To remove the error, call this method with an empty string.
As a Custom Element, the CheckboxGroup supports all of the events for the HTMLElement, Element and Node interfaces. Additionally, it supports the events below. You can listen for them by using the addEventListener() method.
-
input -
Fires whenever the user toggles the state of a checkbox in a
CheckboxGroupwith their Mouse or Keyboard.Just like the native event for
<input type="checkbox">elements, this event is not cancelable, and its type isEvent. Note: Although this event is not cancelable, it is possible to prevent a user's attempt at toggling a checkbox from succeeding by canceling the correspondingclickevent.Note: The
CheckboxGroup"absorbs" theinputandchangeevents dispatched by its checkboxes. Thus, if you want to listen for either of those events, you'll need to listen for them on theCheckboxGroup, not on the checkboxes. -
change -
Identical to the
inputevent, but fired after theinputevent.
You've learned everything that you need to know about the CheckboxGroup component. Now, it's time for you to try it out in one of your own applications! We've provided a StackBlitz Demo to help you get more familiar with the component as well if needed.