This article covers creating a Riot Table 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 Table is a complex and rich element with many styles and actions. The article aims to create a Table component with BeerCSS design, displaying a list of objects. Then, customise cells with special elements (chips, checkboxes, and more).
Table Component Base
First, create a new file named c-table.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-table.riot
:
<c-table>
<table class="
{ props?.leftAlign ? 'left-align ' : null }
{ props?.centerAlign ? 'center-align ' : null }
{ props?.rightAlign ? 'right-align ' : null }
{ props?.stripes ? 'stripes ' : null }
{ props?.outlined ? 'border ' : null }
{ props?.scroll ? 'scroll' : null }"
>
<thead class="{props?.scroll ? ' fixed' : null }">
<tr>
<th each={ col in Object.keys(props?.items?.[0])}>{ col }</th>
</tr>
</thead>
<tbody>
<tr each={ item in props?.items}>
<td if={ item } each={ val in Object.values(item) }>
{ val }
</td>
</tr>
</tbody>
<tfoot if={ props?.footer === true } class="{props?.scroll ? ' fixed' : null }">
<tr>
<th each={ header in Object.keys(props?.items?.[0])}>{ header }</th>
</tr>
</tfoot>
</table>
<style>
thead, tfoot > tr > th::first-letter {
text-transform:capitalize;
}
</style>
</c-table>
Let's break down the code:
- The
<c-table>
and</c-table>
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 component can receive a list of object
items
as an attribute name, and can be used in the component with props?.items - To create cells, the each Riot expression is used to loop through items:
- A first loop is used for each table row tag with
<tr each={ item in props?.items}>
. Theitem
is one object of the list. - A second child loop is used for each table cell with
<td if={ item } each={ val in Object.values(item) }>{ val }</td>
. The expression Object.values() returns an array of a given object's values.
- A first loop is used for each table row tag with
- For the table header, it uses
key
names of the first item of the listprops.items
: The functionObject.keys
is used to return an array of all object's keys of the given item, such as<th each={ col in Object.keys(props?.items?.[0])}>{ col }</th>
. - BeerCSS allows the creation of a footer, which is the same logic as the header: access the first item of the list, get a list of keys with
Object.keys
and create a loop with the each Riot expression. The footer is displayed conditionally if theprops.footer
property exists. - The first character of each header is capitalised thanks to the CSS expression: th::first-letter { text-transform:capitalize; }
- Finally, BeerCSS provide useful classes to change the table style, for instance, adding stripes or borders or changing the alignment. Classes are injected conditionally with Riot expression:
{ props?.stripes ? 'stripes ' : null }
meaning if the props.stripes property exists, the classstripes
is printed.
Finally, load and instantiate the c-table.riot in a front page named index.riot:
<index-riot>
<div style="width:800px;padding:20px;">
<h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
<c-table items={ state.animals } />
</div>
<script>
import cTable from "./components/c-table-full.riot"
export default {
components: {
cTable
},
state: {
animals: [
{
id: 1,
name: 'African Elephant',
species: 'Loxodonta africana',
diet: 'Herbivore',
habitat: 'Savanna, Forests',
enabled: false
},
{
id: 2,
name: 'Lion',
species: 'Panthera leo',
diet: 'Carnivore',
habitat: 'Savanna, Grassland',
enabled: true
},
{
id: 3,
name: 'Hippopotamus',
species: 'Hippopotamus amphibius',
diet: 'Herbivore',
habitat: 'Riverbanks, Lakes',
enabled: false
}
]
}
}
</script>
</index-riot>
Code details:
- Components are imported with
import cTable from "./components/c-table.riot";
then loaded in the components:{} Riot object. - The table is instantiated with
<c-table/>
on the HTML. - A list of animals is provided to the table, thanks to the
items
attribute:items={ state.animals }
; the table will automatically create headers, rows, and cells! ✅
Table Component with Custom Cells
Displaying custom components within cells is normal for production applications, such as: chips, checkboxes, or even buttons for calls to action.
This capability is enabled thanks to Slots: The <slot>
tag injects custom HTML templates in a child component from its parent. Let's change the following loop on the c-table.riot:
<td each={ val in Object.values(item) }>
{ val }
</td>
Replace it with the following expression:
<td each={ el in Object.entries(item) }>
<slot name="item" item={ item } column={ el?.[0] } value={ el?.[1] }></slot>
<template if={ slots.length === 0 }>
{ el?.[1] }
</template>
</td>
Let me explain what is happening here:
- Instead of using
Object.values
, it is usingObject.entries
to convert an object into a list of[key, value]
. For instance[{key: 1, name: "blue"}]
returns[["key", 1], ["name", "blue"]]
- One slot is created for each cell:
<slot name="item" column={ el?.[0] } item={ item } value={ el?.[1] }></slot>
. - Passing dynamic value to slots is called Higher Order Components: All attributes set on the slot tags will be available in their injected HTML templates. In our case, four attributes are created:
- The slot has an optional name, a Riot naming convention, without defining it, it would have the name
default
.It is not possible to define the name dynamically. - The item object of the row loop, is passed into the
item
attribute of the slot: It will be accessible on the component passed as slots!. - The item key is passed to the column attribute of the slot thanks to
column={ el?.[0] }
. - The item value is passed to the value attribute of the slot thanks to
value={ el?.[1] }
.
- The slot has an optional name, a Riot naming convention, without defining it, it would have the name
- If the slot does not exist, the cell's value is printed with
<template if={ slots.length === 0 }>{ el?.[1] }</template>
. Theif
directives can be used without a wrapper tag: Coupling theif
directive to the<template>
tag can render only the content of an if condition, in our case, the item's value (learn more about fragment condition on Riot documentation).
Now on the index.riot, let's print a Chip component for the Diet column:
<index-riot>
<div style="width:800px;padding:20px;">
<h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
<c-table items={ state.animals }>
<template slot="item">
<c-chip if={ column === 'diet' }>
{ value }
</c-chip>
<template if={ column !== 'diet' }>
{ value }
</template>
</template>
</c-table>
</div>
<script>
import cTable from "./components/c-table-full.riot"
import cChip from "./components/c-chip.riot"
export default {
components: {
cTable,
cChip
},
state: {
animals: [
{
id: 1,
name: 'African Elephant',
species: 'Loxodonta africana',
diet: 'Herbivore',
habitat: 'Savanna, Forests',
enabled: false
},
{
id: 2,
name: 'Lion',
species: 'Panthera leo',
diet: 'Carnivore',
habitat: 'Savanna, Grassland',
enabled: true
},
{
id: 3,
name: 'Hippopotamus',
species: 'Hippopotamus amphibius',
diet: 'Herbivore',
habitat: 'Riverbanks, Lakes',
enabled: false
}
]
}
}
</script>
</index-riot>
Code break-down:
- The custom chip is defined as a component into the
components:{}
Riot object and then loaded with<c-chip> Label </c-chip>
. - As soon as we use the
slot="item"
, for instance<c-chip slot="item">{ value }</c-chip>
, the Chip element will be injected into all cells. Using one single chip as a Slot is a bad idea: the chip will be displayed for all cells. That's not what we want! - Thanks to the three dynamic attributes (Higher Order Component magic ✨), we can control what is displayed:
- item is the object for the current row,
- column for the current header key,
- value for the cell's value.
- When multiple HTML elements are displayed in a slot, it is better to wrap them into a template tag, in our case:
<template slot="item"></template>
> - The most important part is:
<c-chip if={ column === 'diet' }>{ value }</c-chip><template if={ column !== 'diet' }>{ value }</template>
. If the current column is diet, the chip prints the value, otherwise, the raw value is displayed.
Here is the HTML result:
Table Component with Checkboxes
The process for inserting checkboxes into the table is the same as in the previous section: the checkbox component is passed as a Slot, and it must be displayed based on a column name.
In our case, the checkbox gets the Boolean value of the column enabled
.
Here is the HTML code for the index.riot:
<index-riot>
<div style="width:800px;padding:20px;">
<h4 style="margin-bottom:20px">Riot + BeerCSS</h4>
<c-table items={ state.animals }>
<template slot="item">
<c-checkbox if={ column === 'enabled' } value={ value } onchange={ (ev) => changed( item.id, ev.target.value ) } />
<c-chip if={ column === 'diet' }>
{ value }
</c-chip>
<template if={ column !== 'diet' && column !== 'enabled' }>
{ value }
</template>
</template>
</c-table>
</div>
<script>
import cTable from "./components/c-table-full.riot"
import cChip from "./components/c-chip.riot"
import cCheckbox from "./components/c-checkbox.riot"
export default {
changed (id, value) {
const _el = this.state.animals.find(el => el.id === id);
if (_el) {
_el.enabled = value;
this.update();
}
},
components: {
cTable,
cChip,
cCheckbox
},
state: {
animals: [
{
id: 1,
name: 'African Elephant',
species: 'Loxodonta africana',
diet: 'Herbivore',
habitat: 'Savanna, Forests',
enabled: false
},
{
id: 2,
name: 'Lion',
species: 'Panthera leo',
diet: 'Carnivore',
habitat: 'Savanna, Grassland',
enabled: true
},
{
id: 3,
name: 'Hippopotamus',
species: 'Hippopotamus amphibius',
diet: 'Herbivore',
habitat: 'Riverbanks, Lakes',
enabled: false
}
]
}
}
</script>
</index-riot>
Code breakdown:
- The custom checkbox is defined as a component into the
components:{}
Riot object and then loaded with<c-checkbox/>
. - The checkbox is printed only if the current column is
enabled
. - The checkbox value is defined thanks to
value={ value }
. - If a click occurs on the checkbox, the change event is emitted, and the
changed()
function is executed with two attributesonchange={ (ev) => changed( item.id, ev.target.value ) }
:- The function gets the item ID value as the first argument.
- As the second argument, the function gets the checkbox value.
- The
changed
function is executed: first, the current object is found thanks to the item ID, and finally the enabled value is updated.
Screenshot of the generated table with checkboxes:
Table Component Testing
It exists two methods for testing the Table component, and it is covered in two different articles:
Conclusion
Voilà 🎉 We made a Table Riot Component using Material Design elements with BeerCSS. A table can have many more capabilities, and new complete articles would be required to make selectable rows, custom headers, virtual scrolling, sorting, filtering, and more features.
The source code of the table is available on Github:
https://github.com/steevepay/riot-beercss/blob/main/components/c-table.riot
Have a great day! Cheers 🍻