This article covers how to create an Riot input 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/
BeerCSS provides many input states, such as "error", "icons", or "loading", and it supports different stylings, such as "round", "small", and "medium". The goal is to create one Input component that dynamically supports all stylings and listens to value input.
Base input component
First, create a new file named c-input.riot
under the components
folder. The c-
stands for "component", a useful naming convention and a good practice.
Into ./components/c-input.riot
, write the minimum HTML and Javascript code for an input:
<c-input>
<div class="field border">
<input type="text" value="{ props?.value }">
</div>
</c-input>
Let's break down the code:
- The
<c-input>
and</c-input>
defined a custom root tag, with the same name as the file. You must write it; otherwise, it may create unexpected results. Using only<input>
or redefining native HTML tags is a bad practice, so startingc-
is a good naming. - The child
div
tag is copied from BeerCSS input examples. - To inject a custom value into the input, you must use value="{ props?.value }": The value is passed as
props
: With props we can pass data to the components via custom attributes to the component tag.
Then we can load and instantiate the c-input.riot
, into a front page:
<index-riot>
<div style="width:500px;padding:20px;">
<h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
<c-input value={ state.firstname } onchange={ updateValue } onkeydown={ updateValue }></c-input>
</div>
<script>
import cInput from "../components/c-input.riot";
export default {
components: {
cInput
},
state: {
firstname: 'Default name'
},
updateValue (ev) {
this.update({
firstname: ev.target.value
})
}
}
</script>
</index-riot>
Code break-down:
- The component is imported with
import cInput from "../components/c-input.riot";
, then loaded into thecomponents
Riot object. - On the HTML, the Input component is instantiate with
<c-input value={ state.firstname } onchange={ updateValue } onkeydown={ updateValue }></c-input>
- Three attributes are passed
- value: it will be the input value
-
onchange and onkeydown: These are events emitted if the input value changes or a key is pressed. When an event is fired, it executes a function, in our case
updateValue
.
- The default value of the
firstname
is "Default name".
To store the input value, a state named firstname
is required, and it must be updated thanks to the function named updateValue
. If the input value changes, the firstname
will take the new input value, and then propagate it into props
.
Writing a dedicated function to update state.firstname
is not required, it can be simplified into one line:
<c-input value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeydown={ (ev) => update({ firstname: ev.target.value }) }></c-input>
Add input component styles
An input can print multiple styles: normal, helper, errors, icons, loading, or all combined.
Let's add the helper
<c-input>
<div class="field border">
<input type="text" value={ props?.value ?? '' }>
<span class="helper" if={ props?.helper }>{ props.helper }</span>
</div>
</c-input>
The helper HTML is coming from BeerCSS, then I just added two elements:
-
{ props.helper }
It will print the helper value through props (HTML attribute). -
if={ props?.helper }
condition to display or hide the helper if a value exists
Let's try the helper by defining the attribute "helper" with a string, such as:
<c-input helper="This is a helper" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeydown={ (ev) => update({ firstname: ev.target.value }) }></c-input>
The result:
If the helper attribute is empty, it will hide the helper HTML tags.
The expression is the same for a label and error state:
<c-input>
<div class="
field border
{ props?.label ? ' label' : '' }
">
<input type="text" value={ props?.value }>
<label if={ props?.label }>{ props.label }</label>
<span class="helper" if={ props?.helper && !props?.error }>{ props.helper }</span>
<span class="error" if={ props?.error }>{ props.error }</span>
</div>
</c-input>
Code Breakdown:
- Riot support writing conditions into attributes values, in our case we are adding the class label if the
props.label
exists. - The label tag and value is displayed if the
props.label
exists. - The error is printed only if the
props.error
exists. It hides thehelper
because printing the error under the input is more important than an informationprops.helper
.
We can follow the same logic to support the loading state, and icons as prefix (props.input) and suffix (props.inputend):
<c-input>
<div class="
field border
{ props?.error ? " invalid" : '' }
{ props?.label ? " label" : '' }
{ props?.icon || props?.loading ? " prefix" : '' }
{ props?.iconend || props?.loadingend ? " suffix" : '' }
">
<progress if={ props?.loading } class="circle"></progress>
<i if={ props?.icon && !props?.loading }>{ props?.icon }</i>
<input type="text" value={ props?.value }>
<label if={ props?.label }>{ props.label }</label>
<i if={ props?.iconend && !props?.loadingend }>{ props?.iconend }</i>
<progress if={ props?.loadingend } 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-input>
Many front-end applications need different input types, such as numbers, passwords, and more. Let's create a props to support all input types:
<input type={props?.type ?? 'text'} value={ props?.value }>
The default input type is text, and it can also support: number, password, file, color, date, time.
The component is almost done; let's support multiple sizes and shapes (like round, fill, small, etc..). We have to add class conditionally to the input:
{ props?.round ? " round" : ''}
{ props?.fill ? " fill" : ''}
{ props?.small ? " small" : ''}
{ props?.medium ? " medium" : ''}
{ props?.large ? " large" : ''}
{ props?.extra ? " extra" : ''}
Here is the final Input component code:
<c-input>
<div class="
field border
{ props?.round ? " round" : ''}
{ props?.fill ? " fill" : ''}
{ props?.small ? " small" : ''}
{ props?.medium ? " medium" : ''}
{ props?.large ? " large" : ''}
{ props?.extra ? " extra" : ''}
{ props?.error ? " invalid" : '' }
{ props?.label ? " label" : '' }
{ props?.icon || props?.loading ? " prefix" : '' }
{ props?.iconend || props?.loadingend ? " suffix" : '' }
">
<progress if={ props?.loading } class="circle"></progress>
<i if={ props?.icon && !props?.loading }>{ props?.icon }</i>
<input type={props?.type ?? 'text'} value={ props?.value }>
<label if={ props?.label }>{ props.label }</label>
<i if={ props?.iconend && !props?.loadingend }>{ props?.iconend }</i>
<progress if={ props?.loadingend } 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-input>
Finally we can instantiate on the index.riot
a couple of <c-inputs>
to try all attributes:
<c-input label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input helper="This is a helper" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input error="Something is wrong" helper="This is a helper" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input icon="edit" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input iconend="edit" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input icon="search" iconend="edit" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input loading="true" icon="edit" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input loadingend="true" iconend="edit" label="Firstname" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
<c-input fill="true" round="true" small="true" label="Test" value={ state.firstname } onchange={ (ev) => update({ firstname: ev.target.value }) } onkeyup={ (ev) => update({ firstname: ev.target.value }) }></c-input>
Find the source code on the following link:
https://github.com/steevepay/riot-beercss
Input Testing
It exist two methods for testing the Input component, and it is covered in two different articles:
Conclusion
Voilà 🎉 We created an Input Riot Component using Material Design elements with BeerCSS.
Feel free to comment if you have questions or need help about RiotJS.
Have a great day! Cheers 🍻