Table Component with RiotJS (Material Design)
Tags: riotjs, javascript, beginners, frontend
Published: 2024-03-30 10:44:41

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](https://dev.to/steeve/riotjs-material-design-how-to-setup-beercss-269c).

> These articles form a series focusing on [RiotJS](https://riot.js.org/) paired with [BeerCSS](https://www.beercss.com/), 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 made with RiotJS and BeerCSS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rerh2258tkbluwubizbm.png)

## 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](https://www.beercss.com/#tables)) in `./components/c-table.riot`:
```html
<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:
1. 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 starting `c-` is a good naming.
2. The component can receive a list of object `items` as an attribute name, and can be used in the component with **props?.items**
3. To create cells, the [each](https://riot.js.org/documentation/#loops) 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}>`. The `item` 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.
4. For the table header, it uses `key` names of the first item of the list `props.items`: The function `Object.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>`.
5. 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](https://riot.js.org/documentation/#loops) Riot expression. The footer is displayed conditionally if the `props.footer` property exists.
6. The first character of each header is capitalised thanks to the CSS expression: **th::first-letter { text-transform:capitalize; }**
7. 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 class `stripes` is printed.

Finally, load and instantiate the **c-table.riot** in a front page named **index.riot**:
```html
<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:
1. Components are imported with `import cTable from "./components/c-table.riot";` then loaded in the **components:{}** Riot object.
2. The table is instantiated with `<c-table/>` on the HTML. 
3. 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! ✅


Here is the generated HTML:
![Table component made with RiotJS displaying a list of objects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/osxdf4y6cba4acpzhqa2.png)

## 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](https://riot.js.org/documentation/#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**:
```html
<td each={ val in Object.values(item) }>
  { val }
</td>
```
Replace it with the following expression:
```html
<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 using `Object.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](https://riot.js.org/api/#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.](https://github.com/riot/riot/issues/3002)
  * 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] }`.
* If the slot does not exist, the cell's value is printed with `<template if={ slots.length === 0 }>{ el?.[1] }</template>`. The `if` directives can be used without a wrapper tag: Coupling the `if` 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](https://riot.js.org/documentation/#fragments-conditional)). 

Now on the **index.riot**, let's print a Chip component for the Diet column:
```html
<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:
1. The custom chip is defined as a component into the `components:{}` Riot object and then loaded with `<c-chip> Label </c-chip>`.
2. 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!
3. Thanks to the three dynamic attributes ([Higher Order Component](https://riot.js.org/api/#higher-order-components) 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.
4. 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>`>
5. 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 Riot Component with custom chips components to print cells values](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w49h2uihmioo11ll0ek3.png)

## 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](https://riot.js.org/documentation/#slots), 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**:
```html
<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:
1. The custom checkbox is defined as a component into the `components:{}` Riot object and then loaded with `<c-checkbox/>`.
2. The checkbox is printed only if the current column is `enabled`.
3. The checkbox value is defined thanks to `value={ value }`.
4. If a click occurs on the checkbox, the **change** event is emitted, and the `changed()` function is executed with two attributes `onchange={ (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.
5. 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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpijojujb8lzsskhd891.png)

## Table Component Testing

It exists two methods for testing the Table component, and it is covered in two different articles:
* [Test with Vitest and Riot-SSR in a Node Environment](https://dev.to/steeve/riot-component-unit-test-with-vitest-node-env-hk0)
* [Test with Vitest in a JsDom Environment](https://dev.to/steeve/riot-component-unit-test-with-vitest-jsdom-env-182l)

## 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 🍻