Search Component with RiotJS (Material Design)

Steeve - Apr 3 - - Dev Community

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.

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/

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

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

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 and I added RiotJS syntax for the logic:

<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>
Enter fullscreen mode Exit fullscreen mode

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: 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:

<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>
Enter fullscreen mode Exit fullscreen mode

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. In a production front-end application, you can make an API call, load a specific page, or execute any action.
  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:

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 🍻

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