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 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>
Let's break down the code:
- 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 startingc-
is a good naming. - 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. - 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
orblue
. Thechange
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.
- The value attribute gets the list element value, either
- The component has a loading state: if the
props?.loading
exists, option tags, icon and image are hidden withif={ !props?.loading }
; finally, it will display a loading icon. - 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. - 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. - To help the user, a
helper
property can be added; it will show a message below the select element. If theprops?. helper
exists, thehelper
class is added to the component. - If an error occurs, it is possible to display an error message below the select input thanks to the
error
attribute. If theprops.error
exists, theerror
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>
Code details:
- Components are imported with
import cSelect from "./components/c-select.riot";
then loaded in the components:{} Riot object. - For testing, the component cSelect is instantiated two times with
<c-select/>
on the HTML. The second select takes an image with theimg
attribute. - The select component takes the list of options in the option attribute with options={ state.options }.
- The selected value is stored in the State Riot object state: { value: 'green' }. The default value selected is green.
- If an option is selected, the
change
events is fired, and thechanged
function is executed to update thestate.value
. - To update a component state, the
this.update()
Riot function must be used. In our case thestate.value
gets the event value, such as:this.update({ value: ev.target.value })
.
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"}
]
}
}
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>
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 thevalue
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.
- The
- To assign a label based on an object, the property
item-label (props?.itemLabel)
defines the key assigned to the option's label. If theitemLabel
does not exist, it takes theitemValue
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>
Code breakdown:
- The list of cities is passed to the Select component with
options={ state.list }
. - The
item-value="id"
attribute defines the key of the object used for the option's value. - The
item-label="city"
attribute defines the key of the object used for the option's label. - The
state.city
value is updated when a change event is fired: it will execute thechanged
function.parseInt
is required because the event value is a string.
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 🍻