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.
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>
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 startingc-
is a good naming. - To enable the
checked
attribute, theprops.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
, thedisabled
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">
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'>
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>
Code break-down:
- The component is imported with
import cCheckbox from "./components/c-checkbox.riot";
then loaded into the components:{} Riot object. - On the HTML, the Button component is instantiated with
<c-checkbox onclick={ clicked } />
- The state of the checkbox is stored in state Riot object
state: { value: false }
. False is the default value. - The click event is watched: when the event click is fired, the function clicked is executed.
- On click, the value is updated to its opposite with
this.update({ value: !this.state.value })
- 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 toev.stopPropagation();
andev.preventDefault();
.
Screenshot of the generated HTML:
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>
Code breakdown:
- If a click occurs on the
<label>
, the click event is not propagated and cancelled, thanks toe.preventDefault();
ande.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>
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 }/>
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 🍻