Render Components in Table Cells

Render Components in Table Cells

Introduction

Sometimes when we use Table component we want to display some of the cells differently. For example, we want to render a custom component instead just the value of the row. In this topic, we are going to use a custom component in the cells for a given column.

Setup React Environment

The easiest way is via create-react-app. Open a terminal and type:

npx create-react-app smart-app
cd smart-app

  1. open src/App.js and remove everything inside the div with className App
    <div className="App"></div>
  2. remove import logo from './logo.svg';
  3. remove everything from App.css
  4. remove src/logo.svg

Setup Smart UI

Smart UI for React is distributed as smart-webcomponents-react NPM package

  1. Open the terminal and install the package:
    npm install smart-webcomponents-react
  2. Import the CSS in the App.js
    import 'smart-webcomponents-react/source/styles/smart.default.css';
    import './App.css';
    
    function App() {
        return (
            <div className="App"></div>
        );
    }
    
    export default App;

Adding Smart.Table

Let's add Smart.Table in our App component. To use Smart.Table we have to import the component this way:

import { Table } from 'smart-webcomponents-react/table';

Paste this to create a simple Table, which we will use as a starting point:

App.js
import 'smart-webcomponents-react/source/styles/smart.default.css';
import './App.css';

import { Table } from 'smart-webcomponents-react/table';

function App() {

    const tableSettings = {
        dataSource: new window.Smart.DataAdapter({
            dataSource: generateData(10),
            dataFields: [
                'id: number',
                'productName: string',
                'quantity: number',
                'price: number',
                'date: date'
            ]
        }),
        columns: [
            { label: 'id', dataField: 'id', dataType: 'number' },
            { label: 'Product Name', dataField: 'productName', dataType: 'string' },
            { label: 'Quantity', dataField: 'quantity', dataType: 'number' },
            { label: 'Price', dataField: 'price', dataType: 'number' },
            { label: 'Date Purchased', dataField: 'date', dataType: 'date' }
        ]
    }

    return (
        <div className="App">
            <Table {...tableSettings} />
        </div>
    );
}

function generateData(rowscount = 100, hasNullValues) {
    // prepare the data
    const data = [];

    const firstNames =
    [
        "Andrew", "Nancy", "Shelley", "Regina", "Yoshi", "Antoni", "Mayumi", "Ian", "Peter", "Lars", "Petra", "Martin", "Sven", "Elio", "Beate", "Cheryl", "Michael", "Guylene"
    ];

    const lastNames =
    [
        "Fuller", "Davolio", "Burke", "Murphy", "Nagase", "Saavedra", "Ohno", "Devling", "Wilson", "Peterson", "Winkler", "Bein", "Petersen", "Rossi", "Vileid", "Saylor", "Bjorn", "Nodier"
    ];

    const productNames =
    [
        "Black Tea", "Green Tea", "Caffe Espresso", "Doubleshot Espresso", "Caffe Latte", "White Chocolate Mocha", "Caramel Latte", "Caffe Americano", "Cappuccino", "Espresso Truffle", "Espresso con Panna", "Peppermint Mocha Twist"
    ];

    const priceValues =
    [
        "2.25", "1.5", "3.0", "3.3", "4.5", "3.6", "3.8", "2.5", "5.0", "1.75", "3.25", "4.0"
    ];

    for (let i = 0; i < rowscount; i++) {
    const row = {};
    const productindex = Math.floor(Math.random() * productNames.length);
    const price = parseFloat(priceValues[productindex]);
    const quantity = Math.round(Math.random() * 10);

    row["id"] = i;
    row["reportsto"] = Math.floor(Math.random() * firstNames.length);
    if (i % Math.floor(Math.random() * firstNames.length) === 0) {
        row["reportsto"] = null;
    }

    row["available"] = productindex % 2 === 0;
    if (hasNullValues === true && productindex % 2 !== 0) {
        const random = Math.floor(Math.random() * rowscount);
        row["available"] = i % random === 0 ? null : false;
    }

    row["firstName"] = firstNames[Math.floor(Math.random() * firstNames.length)];
    row["lastName"] = lastNames[Math.floor(Math.random() * lastNames.length)];
    row["name"] = row["firstName"] + " " + row["lastName"];
    row["productName"] = productNames[productindex];
    row["price"] = price;
    row["quantity"] = quantity;
    row["total"] = price * quantity;

    const date = new Date();
    date.setFullYear(2023, Math.floor(Math.random() * 11), Math.floor(Math.random() * 27));
    date.setHours(0, 0, 0, 0);
    row["date"] = date;

    data[i] = row;
    }

    return data;
}

export default App;

In the component from above, there is a function for generating random data. We are using this function in the dataAdapter of the tableSettings. As you can see, this is a very basic table which we are going to upgrade.

Creating React Component

To render a component in a cell, we should first have one. Create a new folder inside the src folder called Components.

Now add a new file called ProgressDisplay.js. That will be a component for displaying the progress of some properties. In our case, we will show the availability of the product based on its quantity.

Our component will accept four properties: text, value, min and max. The text is for the property, which is displayed, the value is the current value and the min and max are the value to which the current value is compared.

Here is the ProgressDisplay.js:

import React from 'react'

import { ProgressBar } from 'smart-webcomponents-react/progressbar';

const ProgressDisplay = ({ text, value, min, max }) => {
    return (
        <div>
            <div>The {text} is: {value}</div>
            <ProgressBar value={value} min={min} max={max} style={{ width: "100%" }} />
        </div>
    )
}

export default ProgressDisplay

Using formatFunction

To customize the cells for a given column, we should use the formatFunction. This function accepts 'settings'. 'settings' is an object which contains:

{ value: any, row: string | number, column: string, cell: HTMLTableCellElement, data: Proxy(Object), template?: any }

To render a custom component we should attach an element to the template property. The template property is used to render the element or the template which is assigned to it.

We will assign an element to the template, but how to render a component? We have to use 'createRoot', which is imported from react-dom/client. 'createRoot' accepts an element, which will be the container for our component. In the 'createRoot', we will pass the element that is assigned to settings. After creating a root with the function, we can render our ProgressDisplay in the container using the render method of the root. Take a look at the column definition:

columns: [
    { label: 'id', dataField: 'id', dataType: 'number' },
    { label: 'Product Name', dataField: 'productName', dataType: 'string' },
    { 
        label: 'Quantity', 
        dataField: 'quantity', 
        dataType: 'number',
        formatFunction: (settings) => {
            const rootElement = document.createElement('div');
            settings.template = rootElement
            
            const root = createRoot(rootElement)
            root.render(<ProgressDisplay text='quantity' value={settings.data.quantity} min={0} max={10}/>)
        }
    },
    { label: 'Price', dataField: 'price', dataType: 'number' },
    { label: 'Date Purchased', dataField: 'date', dataType: 'date' }
]

The ProgressDisplay displays the availability of the quantity for each product. That quantity can be from 0 to 10 which is why the min and max are 0 and 10. The current value is the quantity of the product.

Forward Component Ref

To go even further, we can pass a ref to the ProgressBar in the ProgressDisplay.js. This is done with the React built-in forwardRef function. The forwardRef accepts a render function. The render function accepts props and ref and returns a React Component. Basically the forwardRef takes a React Component but not with one argument (props) but two (props, ref). This ref can be passed to every element or component inside the render function.

To pass a ref to our Smart.ProgressBar we should edit our ProgressDisplay.js this way:

import React, { forwardRef } from 'react'

import { ProgressBar } from 'smart-webcomponents-react/progressbar';

const ProgressDisplay = forwardRef(({ text, value, min, max }, ref) => {
    return (
        <div>
            <div>The {text} is: {value}</div>
            <ProgressBar ref={ref} value={value} min={min} max={max} style={{ width: "100%" }} />
        </div>
    )
})

export default ProgressDisplay

Now our ProgressDisplay.js can accept a ref and that ref will be passed to the Smart.ProgressBar

To create one use the built-in createRef(). This is useful if we want tо further customize the ProgressBar.

This should be the new formatFunction.

formatFunction: (settings) => {
   
    const rootElement = document.createElement('div');
    settings.template = rootElement

    const root = createRoot(rootElement)

    const progressBarRef = createRef();
    
    root.render(<ProgressDisplay ref={progressBarRef} text='quantity' value={settings.data.quantity} min={0} max={10}/>)

    setTimeout(() => {
        //Customize the ProgressBar inside the ProgressDisplay
        //progressBarRef.current....
    }, 50)
}

Final Result

See the final components below:

import 'smart-webcomponents-react/source/styles/smart.default.css';
import './App.css';

import { createRoot } from 'react-dom/client';
import { createRef } from 'react';

import { Table } from 'smart-webcomponents-react/table';
import ProgressDisplay from './Components/ProgressDisplay';

function App() {

    const tableSettings = {
        dataSource: new window.Smart.DataAdapter({
            dataSource: generateData(10),
            dataFields: [
            'id: number',
            'productName: string',
            'quantity: number',
            'price: number',
            'date: date'
            ]
        }),
        columns: [
            { label: 'id', dataField: 'id', dataType: 'number' },
            { label: 'Product Name', dataField: 'productName', dataType: 'string' },
            {
            label: 'Quantity',
            dataField: 'quantity',
            dataType: 'number',
            formatFunction: (settings) => {
                
                const rootElement = document.createElement('div');
                settings.template = rootElement

                const root = createRoot(rootElement)

                const progressBarRef = createRef();
                
                root.render(<ProgressDisplay ref={progressBarRef} text='quantity' value={settings.data.quantity} min={0} max={10} />)
                
                setTimeout(() => {
                    //Customize the ProgressBar inside the ProgressDisplay
                    //progressBarRef.current....
                }, 50)
            }
            },
            { label: 'Price', dataField: 'price', dataType: 'number' },
            { label: 'Date Purchased', dataField: 'date', dataType: 'date' }
        ]
    }

    return (
    <div className="App">
        <Table {...tableSettings}/>
    </div>
    );
}

function generateData(rowscount = 100, hasNullValues) {
    // prepare the data
    const data = [];

    const firstNames =
    [
        "Andrew", "Nancy", "Shelley", "Regina", "Yoshi", "Antoni", "Mayumi", "Ian", "Peter", "Lars", "Petra", "Martin", "Sven", "Elio", "Beate", "Cheryl", "Michael", "Guylene"
    ];

    const lastNames =
    [
        "Fuller", "Davolio", "Burke", "Murphy", "Nagase", "Saavedra", "Ohno", "Devling", "Wilson", "Peterson", "Winkler", "Bein", "Petersen", "Rossi", "Vileid", "Saylor", "Bjorn", "Nodier"
    ];

    const productNames =
    [
        "Black Tea", "Green Tea", "Caffe Espresso", "Doubleshot Espresso", "Caffe Latte", "White Chocolate Mocha", "Caramel Latte", "Caffe Americano", "Cappuccino", "Espresso Truffle", "Espresso con Panna", "Peppermint Mocha Twist"
    ];

    const priceValues =
    [
        "2.25", "1.5", "3.0", "3.3", "4.5", "3.6", "3.8", "2.5", "5.0", "1.75", "3.25", "4.0"
    ];

    for (let i = 0; i < rowscount; i++) {
    const row = {};
    const productindex = Math.floor(Math.random() * productNames.length);
    const price = parseFloat(priceValues[productindex]);
    const quantity = Math.round(Math.random() * 10);

    row["id"] = i;
    row["reportsto"] = Math.floor(Math.random() * firstNames.length);
    if (i % Math.floor(Math.random() * firstNames.length) === 0) {
        row["reportsto"] = null;
    }

    row["available"] = productindex % 2 === 0;
    if (hasNullValues === true && productindex % 2 !== 0) {
        const random = Math.floor(Math.random() * rowscount);
        row["available"] = i % random === 0 ? null : false;
    }

    row["firstName"] = firstNames[Math.floor(Math.random() * firstNames.length)];
    row["lastName"] = lastNames[Math.floor(Math.random() * lastNames.length)];
    row["name"] = row["firstName"] + " " + row["lastName"];
    row["productName"] = productNames[productindex];
    row["price"] = price;
    row["quantity"] = quantity;
    row["total"] = price * quantity;

    const date = new Date();
    date.setFullYear(2023, Math.floor(Math.random() * 11), Math.floor(Math.random() * 27));
    date.setHours(0, 0, 0, 0);
    row["date"] = date;

    data[i] = row;
    }

    return data;
}

export default App;
import React, { forwardRef } from 'react'

import { ProgressBar } from 'smart-webcomponents-react/progressbar';

const ProgressDisplay = forwardRef(({ text, value, min, max }, ref) => {
    return (
        <div>
            <div>The {text} is: {value}</div>
            <ProgressBar ref={ref} value={value} min={min} max={max} style={{ width: "100%" }} />
        </div>
    )
})

export default ProgressDisplay

The final result is the following: