Cell Templates (React)

Wijmo interops for popular frameworks allow you to define custom content for MultiRow cells declaratively, by using each frameworks' own template markup in conjunction with Wijmo's MultiRowCellTemplate component.

Such templates may include arbitrary components and HTML elements, with property and event bindings. All types of grid cells can be defined using templates (data cells, header cells, editors, etc).

This sample shows a grid with the templates for all cell types. The templates can be dynamically enabled or disabled by toggling the checkboxes.

Learn about MultiRow | MultiRow API Reference

This example uses React.

import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import ReactDOM from 'react-dom/client'; import React, { useRef, useState } from 'react'; import * as wjInput from '@mescius/wijmo.react.input'; import { MultiRow, MultiRowCell, MultiRowCellGroup, MultiRowCellTemplate } from '@mescius/wijmo.react.grid.multirow'; import * as wjcCore from '@mescius/wijmo'; import './app.css'; import { cv, countries } from './data'; function App() { const [state, setState] = useState({ countries: countries, customBottomLeft: false, customCell: true, customCellEdit: true, customColumnHeader: true, customGroup: true, customGroupHeader: true, customRowHeader: true, customRowHeaderEdit: true, customTopLeft: true, highlightDownloads: true, itemsSource: cv, }); const mrRef = useRef(null); // The template used in more than one column is defined as a method. // The rest of the template functions are inlined in MultiRowCellTemplate // definitions. const totalTemplate = (context) => { return <React.Fragment> {context.col.header}: {wjcCore.Globalize.formatNumber(context.value, 'N0')} </React.Fragment>; }; // Another template function. const rowHeaderTemplate = (context) => { return context.row.index / context.row.grid.rowsPerItem + 1; }; return (<div className="container-fluid"> <p> Use a <b>MultiRowCellTemplate</b> component to define a cell template. The <b>cellType</b> property specifies the type of cell represented by the template, and the <b>template</b> <i>render prop</i> accepts a function that renders the cells content. The function parameter receives an object with the cell-specific data, including the data item (<b>item</b>), row (<b>row</b>), and column (<b>col</b>) that the cell represents. </p> <p> Note that column-specific templates should be defined as children of the corresponding <b>MultiRowCell</b> component, while the others are defined as children of the <b>MultiRow</b> component. </p> <MultiRow ref={mrRef} multiRowGroupHeaders={true} itemsSource={state.itemsSource}> { // To conditionally disable a template, we can just assign // the 'template' property with a null value. } <MultiRowCellTemplate cellType="TopLeft" template={state.customTopLeft ? (context) => '№' : null}/> { // An alternative way to conditionally disable a template is // to exclude the MultiRowCellTemplate element from the rendering // element tree. } {state.customBottomLeft ? <MultiRowCellTemplate cellType="BottomLeft" template={() => <span>&#931;</span>}/> : null} { // The template function definition can be inlined like in the templates // above, or defined as a component method like shown in this template. } <MultiRowCellTemplate cellType="RowHeader" template={state.customRowHeader ? rowHeaderTemplate : null}/> <MultiRowCellTemplate cellType="RowHeaderEdit" template={state.customRowHeaderEdit ? () => '...' : null}/> <MultiRowCellGroup header="Identity"> <MultiRowCell binding='id' header='ID'> <MultiRowCellTemplate cellType="ColumnHeader" template={state.customColumnHeader ? () => <i>ID</i> : null}/> </MultiRowCell> <MultiRowCell binding='date' header='Date' colspan={1} width={140}> <MultiRowCellTemplate cellType="CellEdit" template={state.customCellEdit ? (context) => { return (<wjInput.InputDate className="cell-editor" value={context.value} valueChanged={(idt) => context.value = idt.value}> </wjInput.InputDate>); } : null}/> </MultiRowCell> </MultiRowCellGroup> <MultiRowCellGroup header="Statistics" colspan={2}> <MultiRowCell header="Country" binding="country" colspan={2}> <MultiRowCellTemplate cellType="ColumnHeader" template={state.customColumnHeader ? () => <i>Country</i> : null}/> <MultiRowCellTemplate cellType="Cell" template={state.customCell ? (context) => { return <React.Fragment> <img src={`resources/${context.item.country}.png`}/> {context.item.country} </React.Fragment>; } : null}/> <MultiRowCellTemplate cellType="CellEdit" template={state.customCellEdit ? (context) => { return <wjInput.ComboBox className="cell-editor" itemsSource={state.countries} isEditable={false} selectedValue={context.value} selectedIndexChanged={(cb) => context.value = cb.selectedValue}> </wjInput.ComboBox>; } : null}/> <MultiRowCellTemplate cellType="GroupHeader" template={state.customGroupHeader ? (context) => { return <React.Fragment> <input type="checkbox" checked={context.row.isCollapsed} onChange={e => context.row.isCollapsed = e.target.checked}/>&nbsp; {context.item.name} ({context.item.items.length} items) </React.Fragment>; } : null}/> </MultiRowCell> <MultiRowCell header="Downloads" binding="downloads" width={170} aggregate="Sum"> <MultiRowCellTemplate cellType="Cell" template={state.customCell ? (context) => { let style = { fontWeight: 700, color: '' }, downloads = context.item.downloads; if (state.highlightDownloads) { style.color = downloads > 10000 ? 'green' : 'red'; } return <span style={style}> {downloads} </span>; } : null}/> <MultiRowCellTemplate cellType="CellEdit" template={state.customCellEdit ? (context) => { return <wjInput.InputNumber className="cell-editor" step={1} value={context.value} valueChanged={(inpNum) => context.value = inpNum.value}> </wjInput.InputNumber>; } : null}/> { // The same totalTemplate render function is used in two // different templates. Because of this, it's better to define it // as the component method, and assign the 'template' property // with the reference to it. } <MultiRowCellTemplate cellType="Group" template={state.customGroup ? totalTemplate : null}/> </MultiRowCell> <MultiRowCell header="Sales" binding="sales" width={170} aggregate="Sum"> <MultiRowCellTemplate cellType="Cell" template={state.customCell ? (context) => { let style = { fontWeight: 700, color: '' }, sales = context.item.sales; if (state.highlightDownloads) { style.color = sales > 3000 ? 'green' : 'red'; } return <span style={style}> {sales} </span>; } : null}/> <MultiRowCellTemplate cellType="CellEdit" template={state.customCellEdit ? (context) => { return <wjInput.InputNumber className="cell-editor" step={1} value={context.value} valueChanged={(inpNum) => context.value = inpNum.value}> </wjInput.InputNumber>; } : null}/> { // The same totalTemplate render function is used in two // different templates. Because of this, it's better to define it // as the component method, and assign the 'template' property // with the reference to it. } <MultiRowCellTemplate cellType="Group" template={state.customGroup ? totalTemplate : null}/> </MultiRowCell> </MultiRowCellGroup> </MultiRow> <div className="checkbox-list"> <div className="checkbox-list-title">Cell level templates:</div> <div className="checkbox-cell"> <label className="checkbox"> <input type="checkbox" checked={state.customCell} onChange={e => setState(Object.assign(Object.assign({}, state), { customCell: e.target.checked }))}/> Cell </label> <label className="checkbox"> <input type="checkbox" checked={state.customCellEdit} onChange={e => setState(Object.assign(Object.assign({}, state), { customCellEdit: e.target.checked }))}/> CellEdit </label> </div> <div className="checkbox-cell"> <label className="checkbox"> <input type="checkbox" checked={state.customColumnHeader} onChange={e => setState(Object.assign(Object.assign({}, state), { customColumnHeader: e.target.checked }))}/> ColumnHeader </label> </div> <div className="checkbox-cell"> <label className="checkbox"> <input type="checkbox" checked={state.customGroupHeader} onChange={e => setState(Object.assign(Object.assign({}, state), { customGroupHeader: e.target.checked }))}/> GroupHeader </label> <label className="checkbox"> <input type="checkbox" checked={state.customGroup} onChange={e => setState(Object.assign(Object.assign({}, state), { customGroup: e.target.checked }))}/> Group </label> </div> <div className="checkbox-list-title">MultiRow level templates:</div> <div className="checkbox-cell"> <label className="checkbox"> <input type="checkbox" checked={state.customTopLeft} onChange={e => setState(Object.assign(Object.assign({}, state), { customTopLeft: e.target.checked }))}/> TopLeft </label> </div> <div className="checkbox-cell"> <label className="checkbox"> <input type="checkbox" checked={state.customRowHeader} onChange={e => setState(Object.assign(Object.assign({}, state), { customRowHeader: e.target.checked }))}/> RowHeader </label> <label className="checkbox"> <input type="checkbox" checked={state.customRowHeaderEdit} onChange={e => setState(Object.assign(Object.assign({}, state), { customRowHeaderEdit: e.target.checked }))}/> RowHeaderEdit </label> </div> </div> </div>); } const container = document.getElementById('app'); if (container) { const root = ReactDOM.createRoot(container); root.render(<App />); }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Cell Templates</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.40/system.src.js" integrity="sha512-G6mEj6h18+m3MvzdviSDfPle/TfH0//cXcB33AKlNR/Rha0yQsKefDZKRTkIZos97HEGq2JMV1RT5ybMoQ3WsQ==" crossorigin="anonymous"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div id="app"></div> </body> </html>
label { margin-right: 3px; } .checkbox-list { padding: 0 20px; } .checkbox-cell { display: table-cell; padding-right: 50px; } .checkbox-list .checkbox { margin: 10px 0 0 0; } .checkbox-list-title { font-size: 18px; margin: 10px 0px 2px -20px; } .wj-flexgrid { max-height: 350px; max-width: 600px; margin: 10px 0; } .cell-editor { position: absolute; left: 0px; width: 100%; border-radius: 0px; margin: -4px 0 -3px 0; }
import * as wjcCore from '@mescius/wijmo'; function getData() { const data = []; for (var i = 0; i < 30; i++) { data.push({ id: i, date: new Date(2015, Math.floor(i / countries.length) % 12, (Math.floor(i / countries.length) + 1) % 28), country: countries[i % countries.length], countryMapped: i % countries.length, downloads: Math.round(Math.random() * 20000), sales: Math.round(Math.random() * 10000 * 100) / 100, expenses: Math.random() * 5000, checked: i % 9 == 0 }); } return data; } export function getCountries() { return ['US', 'Germany', 'UK', 'Japan', 'Italy', 'Greece']; } export function getCv(data) { var dataCv = new wjcCore.CollectionView(data); dataCv.groupDescriptions.push(new wjcCore.PropertyGroupDescription('country')); return dataCv; } export const countries = ['US', 'Germany', 'UK', 'Japan', 'Italy', 'Greece']; export const cv = getCv(getData());
(function (global) { System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: true, react: true }, meta: { '*.css': { loader: 'css' } }, paths: { // paths serve as alias 'npm:': 'node_modules/' }, // map tells the System loader where to look for things map: { 'jszip': 'npm:jszip/dist/jszip.js', '@mescius/wijmo': 'npm:@mescius/wijmo/index.js', '@mescius/wijmo.input': 'npm:@mescius/wijmo.input/index.js', '@mescius/wijmo.styles': 'npm:@mescius/wijmo.styles', '@mescius/wijmo.cultures': 'npm:@mescius/wijmo.cultures', '@mescius/wijmo.chart': 'npm:@mescius/wijmo.chart/index.js', '@mescius/wijmo.chart.analytics': 'npm:@mescius/wijmo.chart.analytics/index.js', '@mescius/wijmo.chart.animation': 'npm:@mescius/wijmo.chart.animation/index.js', '@mescius/wijmo.chart.annotation': 'npm:@mescius/wijmo.chart.annotation/index.js', '@mescius/wijmo.chart.finance': 'npm:@mescius/wijmo.chart.finance/index.js', '@mescius/wijmo.chart.finance.analytics': 'npm:@mescius/wijmo.chart.finance.analytics/index.js', '@mescius/wijmo.chart.hierarchical': 'npm:@mescius/wijmo.chart.hierarchical/index.js', '@mescius/wijmo.chart.interaction': 'npm:@mescius/wijmo.chart.interaction/index.js', '@mescius/wijmo.chart.radar': 'npm:@mescius/wijmo.chart.radar/index.js', '@mescius/wijmo.chart.render': 'npm:@mescius/wijmo.chart.render/index.js', '@mescius/wijmo.chart.webgl': 'npm:@mescius/wijmo.chart.webgl/index.js', '@mescius/wijmo.chart.map': 'npm:@mescius/wijmo.chart.map/index.js', '@mescius/wijmo.gauge': 'npm:@mescius/wijmo.gauge/index.js', '@mescius/wijmo.grid': 'npm:@mescius/wijmo.grid/index.js', '@mescius/wijmo.grid.detail': 'npm:@mescius/wijmo.grid.detail/index.js', '@mescius/wijmo.grid.filter': 'npm:@mescius/wijmo.grid.filter/index.js', '@mescius/wijmo.grid.search': 'npm:@mescius/wijmo.grid.search/index.js', '@mescius/wijmo.grid.grouppanel': 'npm:@mescius/wijmo.grid.grouppanel/index.js', '@mescius/wijmo.grid.multirow': 'npm:@mescius/wijmo.grid.multirow/index.js', '@mescius/wijmo.grid.transposed': 'npm:@mescius/wijmo.grid.transposed/index.js', '@mescius/wijmo.grid.transposedmultirow': 'npm:@mescius/wijmo.grid.transposedmultirow/index.js', '@mescius/wijmo.grid.pdf': 'npm:@mescius/wijmo.grid.pdf/index.js', '@mescius/wijmo.grid.sheet': 'npm:@mescius/wijmo.grid.sheet/index.js', '@mescius/wijmo.grid.xlsx': 'npm:@mescius/wijmo.grid.xlsx/index.js', '@mescius/wijmo.grid.selector': 'npm:@mescius/wijmo.grid.selector/index.js', '@mescius/wijmo.grid.cellmaker': 'npm:@mescius/wijmo.grid.cellmaker/index.js', '@mescius/wijmo.grid.immutable': 'npm:@mescius/wijmo.grid.immutable/index.js', '@mescius/wijmo.touch': 'npm:@mescius/wijmo.touch/index.js', '@mescius/wijmo.cloud': 'npm:@mescius/wijmo.cloud/index.js', '@mescius/wijmo.nav': 'npm:@mescius/wijmo.nav/index.js', '@mescius/wijmo.odata': 'npm:@mescius/wijmo.odata/index.js', '@mescius/wijmo.olap': 'npm:@mescius/wijmo.olap/index.js', '@mescius/wijmo.rest': 'npm:@mescius/wijmo.rest/index.js', '@mescius/wijmo.pdf': 'npm:@mescius/wijmo.pdf/index.js', '@mescius/wijmo.pdf.security': 'npm:@mescius/wijmo.pdf.security/index.js', '@mescius/wijmo.viewer': 'npm:@mescius/wijmo.viewer/index.js', '@mescius/wijmo.xlsx': 'npm:@mescius/wijmo.xlsx/index.js', '@mescius/wijmo.undo': 'npm:@mescius/wijmo.undo/index.js', '@mescius/wijmo.interop.grid': 'npm:@mescius/wijmo.interop.grid/index.js', '@mescius/wijmo.barcode': 'npm:@mescius/wijmo.barcode/index.js', '@mescius/wijmo.barcode.common': 'npm:@mescius/wijmo.barcode.common/index.js', '@mescius/wijmo.barcode.composite': 'npm:@mescius/wijmo.barcode.composite/index.js', '@mescius/wijmo.barcode.specialized': 'npm:@mescius/wijmo.barcode.specialized/index.js', "@mescius/wijmo.react.chart.analytics": "npm:@mescius/wijmo.react.chart.analytics/index.js", "@mescius/wijmo.react.chart.animation": "npm:@mescius/wijmo.react.chart.animation/index.js", "@mescius/wijmo.react.chart.annotation": "npm:@mescius/wijmo.react.chart.annotation/index.js", "@mescius/wijmo.react.chart.finance.analytics": "npm:@mescius/wijmo.react.chart.finance.analytics/index.js", "@mescius/wijmo.react.chart.finance": "npm:@mescius/wijmo.react.chart.finance/index.js", "@mescius/wijmo.react.chart.hierarchical": "npm:@mescius/wijmo.react.chart.hierarchical/index.js", "@mescius/wijmo.react.chart.interaction": "npm:@mescius/wijmo.react.chart.interaction/index.js", "@mescius/wijmo.react.chart.radar": "npm:@mescius/wijmo.react.chart.radar/index.js", "@mescius/wijmo.react.chart": "npm:@mescius/wijmo.react.chart/index.js", "@mescius/wijmo.react.core": "npm:@mescius/wijmo.react.core/index.js", '@mescius/wijmo.react.chart.map': 'npm:@mescius/wijmo.react.chart.map/index.js', "@mescius/wijmo.react.gauge": "npm:@mescius/wijmo.react.gauge/index.js", "@mescius/wijmo.react.grid.detail": "npm:@mescius/wijmo.react.grid.detail/index.js", "@mescius/wijmo.react.grid.filter": "npm:@mescius/wijmo.react.grid.filter/index.js", "@mescius/wijmo.react.grid.grouppanel": "npm:@mescius/wijmo.react.grid.grouppanel/index.js", '@mescius/wijmo.react.grid.search': 'npm:@mescius/wijmo.react.grid.search/index.js', "@mescius/wijmo.react.grid.multirow": "npm:@mescius/wijmo.react.grid.multirow/index.js", "@mescius/wijmo.react.grid.sheet": "npm:@mescius/wijmo.react.grid.sheet/index.js", '@mescius/wijmo.react.grid.transposed': 'npm:@mescius/wijmo.react.grid.transposed/index.js', '@mescius/wijmo.react.grid.transposedmultirow': 'npm:@mescius/wijmo.react.grid.transposedmultirow/index.js', '@mescius/wijmo.react.grid.immutable': 'npm:@mescius/wijmo.react.grid.immutable/index.js', "@mescius/wijmo.react.grid": "npm:@mescius/wijmo.react.grid/index.js", "@mescius/wijmo.react.input": "npm:@mescius/wijmo.react.input/index.js", "@mescius/wijmo.react.olap": "npm:@mescius/wijmo.react.olap/index.js", "@mescius/wijmo.react.viewer": "npm:@mescius/wijmo.react.viewer/index.js", "@mescius/wijmo.react.nav": "npm:@mescius/wijmo.react.nav/index.js", "@mescius/wijmo.react.base": "npm:@mescius/wijmo.react.base/index.js", '@mescius/wijmo.react.barcode.common': 'npm:@mescius/wijmo.react.barcode.common/index.js', '@mescius/wijmo.react.barcode.composite': 'npm:@mescius/wijmo.react.barcode.composite/index.js', '@mescius/wijmo.react.barcode.specialized': 'npm:@mescius/wijmo.react.barcode.specialized/index.js', 'jszip': 'npm:jszip/dist/jszip.js', 'react': 'npm:react/umd/react.production.min.js', 'react-dom': 'npm:react-dom/umd/react-dom.production.min.js', 'react-dom/client': 'npm:react-dom/umd/react-dom.production.min.js', 'redux': 'npm:redux/dist/redux.min.js', 'react-redux': 'npm:react-redux/dist/react-redux.min.js', 'bootstrap.css': 'npm:bootstrap/dist/css/bootstrap.min.css', 'css': 'npm:systemjs-plugin-css/css.js', 'plugin-babel': 'npm:systemjs-plugin-babel/plugin-babel.js', 'systemjs-babel-build':'npm:systemjs-plugin-babel/systemjs-babel-browser.js', "react-use-event-hook": "npm:react-use-event-hook/dist/esm/useEvent.js", }, // packages tells the System loader how to load when no filename and/or no extension packages: { src: { defaultExtension: 'jsx' }, "node_modules": { defaultExtension: 'js' }, } }); })(this);