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).
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.
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>
Source Code: https://github.com/steevepay/riot-beercss/blob/main/components/c-search.riot
Let's break down the code:
- 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 withc-
is a good convention. - 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.
-
Default state: Without interactions, the main search bar is a persistent and prominent search field at the top of the screen: the first
- 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 thelabel
property first, thenname
; if nothing, it shows theres
as a String. - 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. - 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. - The default search icon can be replaced with a Google Font Icon: Provide the attribute
icon
, accessible on the component withprops.icon
. - 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 theprops.round
exists, theround
class is applied on the two fields. - To show the search result in fullscreen, the property
props.max
must exist and be true, finally the classmax
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>
Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/index.search.riot
Code details:
- The component is imported with
import cSearch from "./components/c-search.riot";
then loaded in the components:{} Riot object. - The
search
component is instantiated with<c-search />
on the HTML. - 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. - 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.
- 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.
- The search result is passed has attribute of the component with
results={ state.results }
. -
If a click append on one list element: the select event is fired, and the function
selected
is executed to set thestate.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. - If the close icon is clicked, the event
clear
is emitted, and the functionreset
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 🍻