Search Component with RiotJS (Material Design)
Tags: riotjs, javascript, beginners, frontend
Published: 2024-04-03 13:19:15

This article covers creating a Riot Search component, using the Material Design CSS BeerCSS, and executing an action on input and select events.

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/

**Search lets people enter a keyword or phrase to get relevant information:** Users input a query into the search bar (1) and then see related results (2).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2vf9bvqljl62bi42hdo5.png)


## Search Component Base

The goal is to create a Riot app with a Search bar, display search results, and execute an action when a result is selected.

![GIF of a search component made with RiotJS and BeerCSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xed425ilr9fkxdfgxqat.gif)

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

Write the following code in `./components/c-search.riot`. The HTML comes from the [BeerCSS documentation](https://www.beercss.com/#search) and I added [RiotJS syntax](https://riot.js.org/documentation/#syntax) for the logic:
```html
<c-search>
    <div class="field prefix
        { props?.outlined ? " border" : ''}
        { props?.round ? " round" : ''}
        { props?.fill ? " fill" : ''}
        { props?.small ? " small" : ''}
        { props?.medium ? " medium" : ''}
        { props?.large ? " large" : ''}
        { props?.extra ? " extra" : ''}
        { props?.error ? " invalid" : '' }
        ">
        <progress if={ props?.loading } class="circle"></progress>
        <i if={ !props?.loading } class="front">{ props?.iconSearch ?  props.iconSearch : "search"}</i>
        <input type="text" value={ props?.value } placeholder={ props?.placeholder } />
        <menu class="{ props?.max ? "max" : "min" }">
            <div class="field prefix suffix no-margin fixed         
                            { props?.outlined ? " border" : ''}
                            { props?.round ? " round" : ''}
                            { props?.fill ? " fill" : ''}
                            { props?.small ? " small" : ''}
                            { props?.medium ? " medium" : ''}
                            { props?.large ? " large" : ''}
                            { props?.extra ? " extra" : ''}
                            { props?.error ? " invalid" : '' }
                        ">
                <i class="front">{ props?.iconBack ?  props.iconBack : "arrow_back"}</i>
                <input type="text" value={ props?.value } placeholder={ props?.placeholder } />
                <i onclick={ clear } class="front">{ props?.iconClose ?  props.iconClose : "close"}</i>
            </div>
            <a class="row" each={res in props?.results} onclick={ (ev) => select(ev, res) }>
                <i>{ res?.icon ?? props?.iconResult ?? 'history' }</i>
                <div>{ res?.label ?? res?.name ?? res }</div>
            </a>
        </menu>
    </div>
    <script>
        export default {
            select(e, result) {
                e.preventDefault();
                e.stopPropagation();
                this.root.value = result;
                this.root.dispatchEvent(new Event('select'));
            },
            clear (e) {
                e.preventDefault();
                e.stopPropagation();
                this.root.dispatchEvent(new Event('clear'));
            }
        }
    </script>
</c-search>
```
> Source Code: https://github.com/steevepay/riot-beercss/blob/main/components/c-search.riot

Let's break down the code:
1. The `<c-search>` and `</c-search>` define a custom root tag, with the same name as the file. You must write it; otherwise, it may create unexpected results. Using the `<div></div>` as a root tag or redefining native HTML tags is a bad practice, starting with `c-` is a good convention.
2. The search has two states:
   * **Default state:** Without interactions, the main search bar is a persistent and prominent search field at the top of the screen: the first `<input>` HTML element is printed.
   * **Focus State:** On focus, the search bar expands into a search view, displaying historical search suggestions: The second `<input>` HTML element is printed inside a menu Element `<menu>` followed by a list of results.
3. The list of results is printed thanks to a [Each Riot expression](): `<a class="row" each={res in props?.results}></a>`. The result is printing an icon and the name. The result can be an Array of objects or an Array of strings; then the name is printed thanks to the condition: `{ res?.label ?? res?.name ?? res }`. It takes the `label` property first, then `name`; if nothing, it shows the `res` as a String.
4. If a click appends on one of the results, the function `select` is executed: A custom event **select** is emitted to notify the higher components with the selected result as a value.
5. If a clicks append on the close icon, the function `reset` is executed: A custom event **clear** is emitted to notify the parent component with no value.
6. The default search icon can be replaced with a [Google Font Icon](https://fonts.google.com/icons): Provide the attribute `icon`, accessible on the component with `props.icon`.
7. The Search input can have different styles, and it must be provided as an HTML attribute, for instance making the input rounded: `{ props?.round ? " round" : ''}` if the `props.round` exists, the `round` class is applied on the two fields.
8. To show the search result in fullscreen, the property `props.max` must exist and be true, finally the class `max` is applied on the `<menu>`.

Finally, load and instantiate the **c-search.riot** in a front page named **index.riot**:
```html
<index-riot>
    <div style="width:600px;padding:20px;">
        <c-search placeholder="Fruit name..." value={ state.value } onchange={ changed } onkeyup={ changed } results={ state.results } onselect={ selected } onclear={ reset } outlined="true" large="true" round="true" fill="true" />
    </div>
    <script>
        import cSearch from "../components/c-search.riot"
        import fruits from "./data/fruits.js"

        const _defaultState = {
            value: "",
            results: ["Mango", "Strawberries", "Bananas"]
        }

        export default {
            components: {
                cSearch
            },
            state: _defaultState,
            changed (ev) {
                this.update({ 
                    value   : ev.target.value,
                    results : fruits.filter((el) => el.toLowerCase().includes(ev.target.value?.toLowerCase()) === true) 
                })
            },
            selected (ev) {
                this.update({ value: ev.target.value })
            },
            reset () {
                this.update(_defaultState)
            }
        }
    </script>
</index-riot>
```
> Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/index.search.riot

Code details:
1. The component is imported with `import cSearch from "./components/c-search.riot";` then loaded in the **components:{}** Riot object.
2. The `search` component is instantiated with `<c-search />` on the HTML.
3. The state of the Search is stored in the **state:{}** Riot object under the `state.value` String property. The property is passed as an attribute, such as: `<c-search value={ state.value } />`. Inside the **state**, the result is initialised with default values to act as a "historical search". The default value of **state.results** can be initialised from an API.
4. If the search input changes, two events **change** and **keyup** are fired: the function **changed** is executed to update **state.value**, and to refresh the list of results.
5. The **state.results** list takes a filtered list of fruits; however, in a production application, the result can be computed from a server/DB and returned from an HTTP API request.
6. The search result is passed has attribute of the component with `results={ state.results }`.
7. **If a click append on one list element:** the **select** event is fired, and the function `selected` is executed to set the `state.value` to the selected element. <u>In a production front-end application, you can make an API call, load a specific page, or execute any action</u>.
8. If the close icon is clicked, the event `clear` is emitted, and the function `reset` is executed to clear the input and the list of results with the default values.

## Search Component Testing

It exists two methods for testing the Search 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 made a Search Riot Component using Material Design elements with BeerCSS.

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

Have a great day! Cheers 🍻