Switch Component with RiotJS (Material Design)
Tags: riotjs, javascript, beginners, frontend
Published: 2024-03-26 12:01:06

This article covers creating a Riot Switch 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](https://dev.to/steeve/riotjs-material-design-how-to-setup-beercss-269c).

> These articles form a series focusing on [RiotJS](https://riot.js.org/) paired with [BeerCSS](https://www.beercss.com/), 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 Switch states exist: checked, unchecked, disabled, and mixed (see the following screenshot). The goal is to create a switch Riot component with BeerCSS design and listen to change events.

![Screenshot of Switch elements made with BeerCSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/humkgjpzwd4djouql9yo.png)

## Switch Component Base

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

Into `./components/c-switch.riot`, write the following HTML (found on the BeerCSS documentation):
```html
<c-switch>
    <label class="
            switch
            { props?.icon ? 'icon' : ''}
        ">
        <input type="checkbox" value={ props?.value ? true : false  } checked={ props?.value } disabled={ props?.disabled }>
        <span>
            <i if={ props?.icon }>{ props.icon }</i>
        </span>
    </label>
</c-switch>
```
Let's break down the code:
* The `<c-switch>` and `</c-switch>` 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**.
* Behind the switch, it uses an input tag as a **checkbox**, and the **value** and **checked** are two different attributes; the component **unifies** the input and checked values.
* The element is disabled if the `props.disabled` attribute exists and the value is `true`.
* A custom icon can be displayed on the switch; the `props.icon` HTML attribute must exist and it will add a class `icon` and a tag `<i>icon_name</i>`.

Finally, instantiate the **c-switch.riot** into a front page **index.riot**:
```html
<index-riot>
    <div style="width:600px;padding:20px;">
        <h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
        <c-switch  onclick={ clicked } value={ state.value } /><br>
        <c-switch icon="wifi" value={ true } /><br>
        <c-switch icon="bluetooth" disabled={ true } /><br>
        <c-switch icon="dark_mode" disabled={ true } value={ true} /><br>
    </div>
    <script>
        import cSwitch from "./components/c-switch.riot";

        export default {
            components: {
                cSwitch
            },
            state: {
                value: true
            },
            clicked (ev) {
                if (ev.target.tagName === "INPUT") {
                    this.update({ value: !this.state.value })
                }
            }
        }
    </script>
</index-riot>
```
Code break-down:
1. The component is imported with `import cSwitch from "./components/c-switch.riot";` then loaded into the **components:{}** Riot object.
2. The component is instantiated with `<c-switch />` on the HTML. Add the attribute "icon" to display a [Google Material Icon](https://fonts.google.com/icons), such as `<c-switch icon="home" />`
3. The state of the switch is stored in the state Riot object `state: { value: true }`. **True** is the default value.
4. To listen to a click or change event, the attribute `onclick={}` or `onchange={}` must be bound to a local function. In our case, it is firing the `clicked` function.
5. On click, the **state.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 === "INPUT")` accepts only one event.

Screenshot of the generated HTML:


![Four switch components made with RiotJS in various states](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i97n8dwmln4xvbqsco00.png)

## Fix the Switch 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-switch>` tag and the child switch 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:
* the `change` event emits **true** if the input is checked.
* the `change` event emits **false** if the input is unchecked.

The **c-switch.riot** updated:
```html
<c-switch>
    <label 
        class="
            switch
            { props?.icon ? 'icon' : ''}
        " 
        onclick={ clicked }
    >
        <input type="checkbox" value={ props?.value ? true : false  } checked={ props?.value } disabled={ props?.disabled }>
        <span>
            <i if={ props?.icon }>{ props.icon }</i>
        </span>
    </label>
    <script>
        export default {
            clicked (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-switch>
```
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 switch input takes its opposite.
* The **click** and **change** events are re-emitted thanks to the `dispatchEvent`

The update of the **state.value**on the parent component **index.riot** can be simplified:
```html
<index-riot>
    <div style="width:600px;padding:20px;">
        <h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
        <c-switch  changed={ changed } value={ state.value } /><br>
    </div>
    <script>
        import cSwitch from "./components/c-switch.riot";

        export default {
            components: {
                cSwitch
            },
            state: {
                value: true
            },
            changed (ev) {
                this.update({ value: ev.target.value })
            }
        }
    </script>
</index-riot>
```
Now the **state.value** takes the value from the <u>change Event</u>, and the Event value always mirrors the current state of the switch. Most of all: the click event is fired only one time.

**Tips to simplify even more:** It is not required to create a "changed" function, one line is enough to update the value:
```html
<c-switch  changed={ (ev) => update({ value: ev.target.value }) } value={ state.value } /><br>
```

## Switch Component Testing

It exists two methods for testing the Switch component, and it is covered in two different articles:
* [Test with Vitest and Riot-SSR in a Node Environment](https://dev.to/steeve/riot-component-unit-test-with-vitest-node-env-hk0)
* [Test with Vitest in a JsDom Environment](https://dev.to/steeve/riot-component-unit-test-with-vitest-jsdom-env-182l)

## Conclusion

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

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

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

Have a great day! Cheers 🍻