Checkbox component with RiotJS (Material Design)

Steeve - Mar 24 - - Dev Community

This article covers creating a Riot Checkbox component, using the Material Design CSS BeerCSS. Before starting, make sure you have a base application running, or read my previous article Setup Riot + BeerCSS + Vite.

These articles form a series focusing on RiotJS paired with BeerCSS, designed to guide you through creating components and mastering best practices for building production-ready applications. I assume you have a foundational understanding of Riot; however, feel free to refer to the documentation if needed: https://riot.js.org/documentation/

Four checkbox states exist: unchecked, checked and disabled, and mixed (see the following screenshot). The goal is to create a checkbox Riot component with BeerCSS design and listen to click events.

Screenshot of BeerCSS Checkboxes

Checkbox Component Base

First, create a new file named c-checkbox.riot under the components folder. The c- stands for "component", a useful naming convention and a good practice.

Into ./components/c-checkbox.riot, write the following HTML (found on the BeerCSS documentation):

<c-checkbox>
   <label class="checkbox" onclick={ inputClick } >
        <input type="checkbox" value={ props?.value ? true : false  } checked={ props?.value } disabled={ props?.disabled }>
        <span>{ props.label }</span>
    </label>
</c-checkbox>
Enter fullscreen mode Exit fullscreen mode

Let's break down the code:

  • The <c-checkbox> and </c-checkbox> defined a custom root tag, with the same name as the file. You must write it; otherwise, it may create unexpected results. Using the <label> as a root tag or redefining native HTML tags is a bad practice, so starting c- is a good naming.
  • To enable the checked attribute, the props.value must exist and be true.
  • The default value of a checkbox input is "on" as a String. It's not handy to manipulate in a Javascript application. Using the expression value={ props?.value ? true : false } make sure the checkbox value is always a Boolean, either true or false.
  • On the Web Standards for a checkbox, the value and checked are two different attributes; the component unifies the input and checked values.
  • Thanks to the props.disabled, the disabled attribute is conditionally assigned to the Input tag to disable the checkbox.
  • If the label exists, it is injected into: <span>{props.label}</span>.

W3C states that a boolean property is true if the attribute is present — even if the value is empty or false. As mentioned in the documentation, Riot.js automatically fixes this behaviour: Boolean attributes (checked, selected, etc.) are ignored when the expression value is false:

<input type="checkbox" checked={ null }> becomes <input type="checkbox">
<input type="checkbox" checked={ '' }> becomes <input type="checkbox">
<input type="checkbox" checked={ false }> becomes <input type="checkbox">
Enter fullscreen mode Exit fullscreen mode

In case the expression is true they will be correctly rendered according to the specs:

<input type="checkbox"  checked={ true }> becomes <input type="checkbox" checked='checked'>
<input type="checkbox"  checked={ 1 }> becomes <input type="checkbox"  checked='checked'>
<input type="checkbox"  checked={ 'is-valid' }> becomes <input type="checkbox"  checked='checked'>
Enter fullscreen mode Exit fullscreen mode

Finally, the c-checkbox.riot can be instantiated into a front page index.riot:

<index-riot>
    <div style="width:600px;padding:20px;">
        <h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
        <c-checkbox onclick={ clicked } value={ state.value } label={ state.value }/>
        <c-checkbox onclick={ clicked } label="Disabled" disabled={ true } />
        <c-checkbox onclick={ clicked } label="Disabled" disabled={ true } value="true" /> 
    </div>
    <script>
        import cCheckbox from "./components/c-checkbox.riot";

        export default {
            components: {
                cCheckbox
            },
            state: {
                value: true
            },
            clicked (ev) {
                if (ev.target.tagName === "SPAN") {
                    ev.stopPropagation()
                    ev.preventDefault();
                    this.update({ value: !this.state.value })
                }
            }
        }
    </script>
</index-riot>
Enter fullscreen mode Exit fullscreen mode

Code break-down:

  1. The component is imported with import cCheckbox from "./components/c-checkbox.riot"; then loaded into the components:{} Riot object.
  2. On the HTML, the Button component is instantiated with <c-checkbox onclick={ clicked } />
  3. The state of the checkbox is stored in state Riot object state: { value: false }. False is the default value.
  4. The click event is watched: when the event click is fired, the function clicked is executed.
  5. On click, the value is updated to its opposite with this.update({ value: !this.state.value })
  6. An important issue occurs: the event click is emitted twice! The expression if (ev.target.tagName === "SPAN") is used to accept only one event, then the propagation of the even is stopped thanks to ev.stopPropagation(); and ev.preventDefault();.

Screenshot of the generated HTML:

Four different checkbox components: default with label, disabled, disabled and checked

Fix the checkbox issue: stop the double-click event

As mentioned in the previous section, the click event is fired twice. The issue is that clicking the label triggers a click on both the <c-checkbox> and the child checkbox input <input type="checkbox">.

The solution is to stop the event propagation inside the component, and re-emit the event once. During this moment, I take the opportunity to change the Boolean value to its opposite: the parent HTML will receive a change event with the correct value:

  • If the input is checked, the change event emits true.
  • If the input is unchecked, the change event emits false.

The c-checkbox.riot updated:

<c-checkbox >
    <label class="checkbox" onclick={ inputClick }>
        <input type="checkbox" value={ props?.value ? true : false  } checked={ props?.value } disabled={ props?.disabled }>
        <span>{ props.label }</span>
    </label>
    <script>
        export default {
            inputClick (e) {
                e.preventDefault();
                e.stopPropagation();
                this.root.value = this.props.value === true || this.props.value === "true" ? false : true;
                this.root.dispatchEvent(new Event('click'));
                this.root.dispatchEvent(new Event('change'));
            }
        }
    </script>
</c-checkbox>
Enter fullscreen mode Exit fullscreen mode

Code breakdown:

  • If a click occurs on the <label>, the click event is not propagated and cancelled, thanks to e.preventDefault(); and e.stopPropagation();
  • The value of the checkbox input takes its opposite.
  • The click and change events are re-emitted thanks to the dispatchEvent.

The value update on the parent component index.riot can be simplified:

<index-riot>
    <div style="width:600px;padding:20px;">
        <h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
        <c-checkbox onclick={ clicked } value={ state.value } label={ state.value }/>
    </div>
    <script>
        import cCheckbox from "./components/c-checkbox.riot";

        export default {
            components: {
                cCheckbox
            },
            state: {
                value: false
            },
            clicked (ev) {
                this.update({ value: ev.target.value })
            }
        }
    </script>
</index-riot>
Enter fullscreen mode Exit fullscreen mode

Now the state.value takes the value from the click Event, and the value always mirrors the current state of the checkbox.

Tips to simplify even more: It is not required to create a "clicked" function, one line is enough to update the value:

<c-checkbox onclick={ (ev) => update({ value: ev.target.value }) } value={ state.value } label={ state.value }/>
Enter fullscreen mode Exit fullscreen mode

Checkbox Component Testing

It exists two methods for testing the Checkbox component, and it is covered in two different articles:

Conclusion

Voilà 🎉 We created a Checkbox Riot Component using Material Design elements with BeerCSS.

The source code of the checkbox is available on Github:
https://github.com/steevepay/riot-beercss/blob/main/components/c-checkbox.riot

Feel free to comment if you have questions or need help about RiotJS.

Have a great day! Cheers 🍻

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .