DEV Community

Cover image for Select Component with RiotJS (Material Design)
Steeve
Steeve

Posted on • Edited on

Select Component with RiotJS (Material Design)

This article covers creating a Riot Select 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/

A Select provides a menu of options: The goal is to create a Select component with BeerCSS design and listen to change events.

Select Elements made with BeerCSS

Select Component Base

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

Write the following HTML code (found on the BeerCSS documentation) in ./components/c-select.riot:

<c-select>
    <div class="field suffix 
                {props?.label ? ' label' : null }
                {props?.error ? ' invalid' : null }
            ">
        <select>
            <option if={ !props?.loading }  each={option in props.options} value={ option } selected={ option === props.value ? true : null }>{ option }</option>
        </select>
        <label if={ props?.label }>{ props.label }</label>
        <i if={ !props?.loading && !props?.img  && !props?.icon} >arrow_drop_down</i>
        <i if={ !props?.loading && !props?.img  && props?.icon} >{ props.icon }</i>
        <img if={ !props?.loading && props?.img } class="circle" src={ props.img }>
        <progress if={ props?.loading } class="circle"></progress>
        <span class="helper" if={ props?.helper && !props?.error }>{ props.helper }</span>
        <span class="error" if={ props?.error }>{ props.error }</span>
    </div>
</c-select>
Enter fullscreen mode Exit fullscreen mode

Let's break down the code:

  1. The <c-select> and </c-select> define 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.
  2. The list of options passed as attributes is an Array of String, for instance ["red", "green", "blue" ], and it is looped thanks to the each Riot attribute as following: each={option in props.options}. A new <option> tag is created for each element of the list.
  3. For each element of the list, two important attributes are defined on the <option> tag:
    • The value attribute gets the list element value, either red, green or blue. The change event will emit the value if the option is selected.
    • The selected attribute defines whether the props. value is equal to the option's value.
  4. The component has a loading state: if the props?.loading exists, option tags, icon and image are hidden with if={ !props?.loading }; finally, it will display a loading icon.
  5. The select has a default icon arrow_drop_down, and it is possible to overwrite it with another Google Material Icon by passing the props icon. The icon is displayed only if there is no image, and no loading state.
  6. Instead of an icon for the select element, an image with the attribute img can be included. The image is not printed if the loading property is activated.
  7. To help the user, a helper property can be added; it will show a message below the select element. If the props?. helper exists, the helper class is added to the component.
  8. If an error occurs, it is possible to display an error message below the select input thanks to the error attribute. If the props.error exists, the error class is added to the component.

Finally, load and instantiate the c-select.riot in a front page named index.riot:

<index-riot>
    <div style="width:600px;padding:20px;">
        <h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
        <c-select label="Color" options={ state.options } value={ state.value } onchange={ changed } />
        <c-select img="./favicon.png" helper={ "Color selected: " + state.value } label="Color" options={ state.options } value={ state.value } onchange={ changed }/>
    </div>
    <script>
        import cSelect from "./components/c-select.riot"

        export default {
            components: {
                cSelect
            },
            state: {
                options: [
                    'red',
                    'green',
                    'blue',
                    'purple'
                ],
                value: 'green'
            },
            changed (ev) {
                this.update({ value: ev.target.value })
            }
        }
    </script>
</index-riot>
Enter fullscreen mode Exit fullscreen mode

Code details:

  1. Components are imported with import cSelect from "./components/c-select.riot"; then loaded in the components:{} Riot object.
  2. For testing, the component cSelect is instantiated two times with <c-select/> on the HTML. The second select takes an image with the img attribute.
  3. The select component takes the list of options in the option attribute with options={ state.options }.
  4. The selected value is stored in the State Riot object state: { value: 'green' }. The default value selected is green.
  5. If an option is selected, the change events is fired, and the changed function is executed to update the state.value.
  6. To update a component state, the this.update() Riot function must be used. In our case the state.value gets the event value, such as: this.update({ value: ev.target.value }).

Here is the generated HTML:
Two select components made with RiotJS with a list of strings as option

Select Component Advanced

A production front-end often gets a list of objects coming from a Database/API, including:

  • An ID as a number or UUID as a String for the option's value.
  • A different String for the option's label.

As an example, let's provide a list of cities to the select component:

export default {
  state: {
    list: [
       { id: 0, city: "Paris" },
       { id: 1, city: "London"},
     { id: 2, city: "Berlin"},
     { id: 3, city: "New York"}
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The current Select component takes only a list of Strings, and changes must be made to support a list of Objects. Let's add the following HTML to the c-select.riot component:

<option each={option in props.options} value={ option[props.itemValue] }  key={ option[props.itemValue] } selected={ option[props.itemValue] === props.value ? true : null } if={ props.itemValue && !props?.loading }>{ option[props?.itemLabel || props?.itemValue] }</option>
Enter fullscreen mode Exit fullscreen mode

Details of the code:

  • To loop through all objects, the each Riot attribute is used each={option in props.options}.
  • To assign a value based on an object, the property item-value (props.itemValue) defines the key assigned to the value attribute. To decompose the expression value={ option[props.itemValue] }:
    • The option is an item from the list
    • Passing the key props.itemValue under square brackets returns the object's value.
  • To assign a label based on an object, the property item-label (props?.itemLabel) defines the key assigned to the option's label. If the itemLabel does not exist, it takes the itemValue as a label by default.
  • Notice the key={ option[props.itemValue] }: Adding the key Riot attribute to the looped tags provides a more precise strategy for tracking the item’s position. If the lists are immutable, this will greatly improve the loop performance.
  • Finally, the option is printed only if the itemValue property exists and the component is not loading.
  • Bonus: a Google Material Icon is passed named "explore".

Let's change the index.riot and provide a list of objects to the Select component:

<index-riot>
    <div style="width:600px;padding:20px;">
        <h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
        <c-select icon="explore" helper={ "City selected: " + state.city } label="Cities" options={ state.list } value={ state.city } item-value="id" item-label="city" placeholder="Select a city" onchange={ changed }/>
    </div>
    <script>
        import cSelect from "./components/c-select.riot"

        export default {
            components: {
                cSelect
            },
            state: {
                list: [
                    { id: 0, city: "Paris" },
                    { id: 1, city: "London"},
                    { id: 2, city: "Berlin"},
                    { id: 3, city: "New York"}
                ],
                city: 3
            },
            changed (ev) {
                this.update({ city: parseInt(ev.target.value)})
            }
        }
    </script>
</index-riot>

Enter fullscreen mode Exit fullscreen mode

Code breakdown:

  1. The list of cities is passed to the Select component with options={ state.list }.
  2. The item-value="id" attribute defines the key of the object used for the option's value.
  3. The item-label="city" attribute defines the key of the object used for the option's label.
  4. The state.city value is updated when a change event is fired: it will execute the changed function. parseInt is required because the event value is a string.

The generated HTML:
Select component made with RiotJS with a list of object for options

Select Component Testing

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

Conclusion

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

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

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

Have a great day! Cheers 🍻

Top comments (0)