Input Component with RiotJS (Material Design)

Steeve - Mar 20 - - Dev Community

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.

Screenshot of the BeerCSS input documentation

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

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

Code break-down:

  1. The component is imported with import cInput from "../components/c-input.riot";, then loaded into the components Riot object.
  2. On the HTML, the Input component is instantiate with <c-input value={ state.firstname } onchange={ updateValue } onkeydown={ updateValue }></c-input>
  3. 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.
  4. 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>
Enter fullscreen mode Exit fullscreen mode

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

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

The result:

Riot input component with a helper as attribute
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>
Enter fullscreen mode Exit fullscreen mode

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 the helper because printing the error under the input is more important than an information props.helper.

Riot Input Component with an error state

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

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

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" : ''}
Enter fullscreen mode Exit fullscreen mode

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

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

Here is the result:
Image description

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 🍻

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