Table Component with RiotJS (Material Design)

Steeve - Mar 30 - - Dev Community

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 made with RiotJS and BeerCSS

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

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 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 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:

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

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

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

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

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: 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] }.
  • 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).

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

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

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

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

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 🍻

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