Skip to content

Latest commit

 

History

History

CheckboxGroup

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

The CheckboxGroup Element

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.

Install

npm install @itenthusiasm/custom-elements

Quickstart

<!-- 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); // true

Progressive Enhancement

The 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...

  1. Replaces the <legend> element with a <label> element.
    • The CheckboxGroup can 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-generated id as well. If you provide your own id to 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.
  2. Transfers the name attribute (if it exists) from the checkboxes to itself.
    • Since the <checkbox-group> itself is a form control, and since it represents the whole group of checkboxes, it is the only form control in the group that needs the name attribute.
  3. Dissociates the checkboxes from the wrapping <form>.
    • This is done by applying the empty [form=""] attribute to all the checkboxes, 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 of checkboxes, it is the only form control that needs to be associated with the <form>.
  4. Applies the none role to the <fieldset> element.
    • <fieldset>s typically have the accessible group role. 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 a group role after mounting, and the <fieldset> will always end up with a none role.

Progressive Enhancement in JS Frameworks

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.

TypeScript Usage in JS Frameworks

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");

CSS Styles

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.

Restrictions

The CheckboxGroup Custom Element enforces the following restrictions:

  1. Only the <fieldset> element is allowed to be a direct descendant of a <checkbox-group>.
  2. 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).
  3. Nesting <checkbox-group>s is not supported.

API

This section describes the attributes, properties, and events associated with the CheckboxGroup Custom Element.

Attributes

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 name attribute seen on native form controls like the <input> element. Ensures that the element's value will be submitted to the server under the specified name on form submission. The name can also be used to access the element via the HTMLFormElement.elements property of the owning form.

This attribute is reflected by the CheckboxGroup.name property.

disabled

Same as the disabled boolean attribute seen on native form controls like the <input> element. When this attribute is present, the entire CheckboxGroup component (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.disabled property.

required

Same as the required boolean 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 min attribute to the required attribute. This is because the required attribute is effectively the same as setting [min="1"]. The min attribute is more succinct and vastly more flexible.

This attribute is reflected by the CheckboxGroup.required property.

valuemissingerror

Determines the validationMessage that the element will display if it has a ValidityState.valueMissing error. The element enters this error state if its value is an empty array when it has the required attribute. If the element's required constraint is broken when its owning form is submitted, then form submission will fail, and the valuemissingerror will 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.valueMissingError property.

min

Same as the min attribute 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.min property.

rangeunderflowerror

Determines the validationMessage that the element will display if it has a ValidityState.rangeUnderflow error. The element enters this error state if the user has selected a number of items less than the value specified by the min attribute. If the element's min constraint is broken when its owning form is submitted, then form submission will fail, and the rangeunderflowerror will be shown to the user in an error message bubble.

NOTE: If the element's min and required constraints are both broken, then only the rangeunderflowerror will 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.rangeUnderflowError property.

max

Same as the max attribute 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 max attribute is greater than or equal to the min attribute.

This attribute is reflected by the CheckboxGroup.max property.

rangeoverflowerror

Determines the validationMessage that the element will display if it has a ValidityState.rangeOverflow error. The element enters this error state if the user has selected a number of items greater than the value specified by the max attribute. If the element's max constraint is broken when its owning form is submitted, then form submission will fail, and the rangeoverflowerror will be shown to the user in an error message bubble.

This attribute is reflected by the CheckboxGroup.rangeOverflowError property.

manual

A boolean attribute indicating that the CheckboxGroup should 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.manual property.

Properties

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 value corresponding 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 name property found on native form controls like the <input> element: It reflects the value of the name attribute. Type is string.
disabled
Same as the boolean disabled property found on native form controls like the <input> element: It reflects the value of the disabled attribute.
required
Same as the boolean required property found on native form controls like the <input> element: It reflects the value of the required attribute.
valueMissingError
Reflects the valuemissingerror attribute. Type is string.
min
Same as the min property found on the native <input> element: It reflects the value of the min attribute. Type is string.
rangeUnderflowError
Reflects the rangeunderflowerror attribute. Type is string.
max
Same as the max property found on the native <input> element: It reflects the value of the max attribute. Type is string.
rangeOverflowError
Reflects the rangeoverflowerror attribute. Type is string.
manual
A boolean property which reflects the value of the manual attribute.
fieldset

Returns the HTMLFieldSetElement wrapped by the CheckboxGroup. Useful for iterating over all of the group's checkboxes programmatically.

labels
Same as the read-only labels property found on native form controls like the <input> element: Returns a NodeList of all the <label> elements associated with the CheckboxGroup.
form
Same as the read-only form property found on native form controls like the <input> element: Returns the HTMLFormElement with which the CheckboxGroup is associated. Returns null if no <form> owns the element.
validity
Same as the read-only validity property found on native form controls like the <input> element: Returns the element's ValidityState object.
validationMessage
Same as the read-only validationMessage property found on native form controls like the <input> element: Returns the message that will be displayed to users when the CheckboxGroup fails constraint validation. Type is string. 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 willValidate property found on native form controls like the <input> element: Returns false if the element will skip constraint validation. See MDN's Documentation for more info on how willValidate is determined for form controls.

Methods

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: Returns true if the element passes constraint validation. Otherwise, returns false. If validation fails, an invalid event 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 the checkValidity() method. However, it will display an error message bubble if the element fails constraint validation, as long as the invalid event 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 the required error 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.

Events

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 CheckboxGroup with their Mouse or Keyboard.

Just like the native event for <input type="checkbox"> elements, this event is not cancelable, and its type is Event. 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 corresponding click event.

Note: The CheckboxGroup "absorbs" the input and change events dispatched by its checkboxes. Thus, if you want to listen for either of those events, you'll need to listen for them on the CheckboxGroup, not on the checkboxes.

change

Identical to the input event, but fired after the input event.

What's Next?

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.