Row Details

Sometimes rows are bound to data objects that contain more information than would fit easily on a regular grid.

In these scenarios, you may want to use the FlexGridDetailProvider class that is included with the wijmo.grid.detail module. The FlexGridDetailProvider will create a details section for each row that users can expand to see the rest of the data.

You can add anything you want to the detail row, including other grids. The sample below shows both how to display the data in another grid, as well as within standard HTML elements.

Learn about FlexGrid | FlexGrid API Reference

import 'bootstrap.css'; import '@grapecity/wijmo.styles/wijmo.css'; import './styles.css'; // import * as wjCore from '@grapecity/wijmo'; import * as wjOdata from '@grapecity/wijmo.odata'; import * as wjGrid from '@grapecity/wijmo.grid'; import * as wjGridDetail from '@grapecity/wijmo.grid.detail'; import * as wjInput from '@grapecity/wijmo.input'; // // document.readyState === 'complete' ? init() : window.onload = init; // function init() { // // get OData categories and products const url = 'https://services.odata.org/Northwind/Northwind.svc'; const categories = new wjOdata.ODataCollectionView(url, 'Categories', { fields: ['CategoryID', 'CategoryName', 'Description'] }); const products = new wjOdata.ODataCollectionView(url, 'Products'); // // shared column definitions const categoryColumns = [ { binding: 'CategoryName', header: 'Category Name', width: '*' }, { binding: 'Description', header: 'Description', width: '2*' } ]; // // get products for a given category const _catToProductMap = new Map(); function getProducts(categoryID) { let categoryProducts = _catToProductMap.get(categoryID); if (!categoryProducts) { categoryProducts = products.items.filter(product => (product.CategoryID === categoryID)); _catToProductMap.set(categoryID, categoryProducts); } return categoryProducts; } // // // grid with HTML detail const htmlDetail = new wjGrid.FlexGrid('#htmlDetail', { autoGenerateColumns: false, columns: categoryColumns, itemsSource: categories, isReadOnly: true, }); // // html detail provider const dpHtml = new wjGridDetail.FlexGridDetailProvider(htmlDetail, { // use animation when showing details isAnimated: true, // // create detail cells for a given row createDetailCell: function (row) { // build detail content for the current category const cat = row.dataItem; const prods = getProducts(cat.CategoryID); let html = `ID: <b>${cat.CategoryID}</b><br />`; html += `Name: <b>${cat.CategoryName}</b><br />`; html += `Description: <b>${cat.Description}</b><br />`; html += `Products: <b>${prods.length} items</b><br />`; html += '<ol>'; prods.forEach(product => { html += `<li>${product.ProductName}</li>`; }); html += '</ol>'; // // create and return detail cell const cell = document.createElement('div'); cell.innerHTML = html; return cell; }, }); // // // grid with grid detail const gridDetail = new wjGrid.FlexGrid('#gridDetail', { autoGenerateColumns: false, columns: categoryColumns, itemsSource: categories, isReadOnly: true, }); // // grid detail provider const initialOptions = { // specifies when and how the row details are displayed detailVisibilityMode: wjGridDetail.DetailVisibilityMode.ExpandSingle, // // maximum height of the detail rows, in pixels maxHeight: null, // // whether to use animation when showing row details isAnimated: true, // // action to perform when the ENTER key is pressed keyActionEnter: wjGridDetail.KeyAction.None, // // callback function that determines whether a row has details rowHasDetail: null, // // function that creates detail cells createDetailCell: function (row) { const cell = document.createElement('div'); new wjGrid.FlexGrid(cell, { headersVisibility: wjGrid.HeadersVisibility.Column, isReadOnly: true, autoGenerateColumns: false, itemsSource: getProducts(row.dataItem.CategoryID), columns: [ { header: 'ID', binding: 'ProductID' }, { header: 'Name', binding: 'ProductName' }, { header: 'Qty/Unit', binding: 'QuantityPerUnit' }, { header: 'Unit Price', binding: 'UnitPrice' }, { header: 'Discontinued', binding: 'Discontinued' }, ], }); return cell; }, }; const dpGrid = new wjGridDetail.FlexGridDetailProvider(gridDetail, initialOptions); // // create detailVisibilityMode selector const detailVisibilityModeMenu = new wjInput.Menu('#detailVisibilityMode', { selectedIndexChanged: (s, e) => { if (s.selectedIndex > -1) { formatMenuHeader(s); dpGrid.detailVisibilityMode = s.selectedValue; } }, header: 'detailVisibilityMode', displayMemberPath: 'header', selectedValuePath: 'value', itemsSource: [ { header: 'Code', value: wjGridDetail.DetailVisibilityMode.Code }, { header: 'Selection', value: wjGridDetail.DetailVisibilityMode.Selection }, { header: 'ExpandSingle', value: wjGridDetail.DetailVisibilityMode.ExpandSingle }, { header: 'ExpandMulti', value: wjGridDetail.DetailVisibilityMode.ExpandMulti }, ], }); detailVisibilityModeMenu.selectedValue = initialOptions.detailVisibilityMode; // // create maxHeight selector const maxHeightMenu = new wjInput.Menu('#maxHeight', { selectedIndexChanged: (s, e) => { if (s.selectedIndex > -1) { formatMenuHeader(s); dpGrid.maxHeight = s.selectedValue; } }, header: 'maxHeight', displayMemberPath: 'header', selectedValuePath: 'value', itemsSource: [ { header: 'null', value: null }, { header: '100', value: 100 }, { header: '200', value: 200 }, { header: '300', value: 300 }, { header: '400', value: 400 }, { header: '500', value: 500 }, ], }); maxHeightMenu.selectedValue = initialOptions.maxHeight; // // create isAnimated selector const isAnimatedMenu = new wjInput.Menu('#isAnimated', { selectedIndexChanged: (s, e) => { if (s.selectedIndex > -1) { formatMenuHeader(s); dpGrid.isAnimated = s.selectedValue; } }, header: 'isAnimated', displayMemberPath: 'header', selectedValuePath: 'value', itemsSource: [ { header: 'False', value: false }, { header: 'True', value: true }, ], }); isAnimatedMenu.selectedValue = initialOptions.isAnimated; // // create keyActionEnter selector const keyActionEnterMenu = new wjInput.Menu('#keyActionEnter', { selectedIndexChanged: (s, e) => { if (s.selectedIndex > -1) { formatMenuHeader(s); dpGrid.keyActionEnter = s.selectedValue; } }, header: 'keyActionEnter', displayMemberPath: 'header', selectedValuePath: 'value', itemsSource: [ { header: 'None', value: wjGridDetail.KeyAction.None }, { header: 'ToggleDetail', value: wjGridDetail.KeyAction.ToggleDetail }, ], }); keyActionEnterMenu.selectedValue = initialOptions.keyActionEnter; // // create rowDetails selector const rowDetailsMenu = new wjInput.Menu('#rowDetails', { selectedIndexChanged: (s, e) => { if (s.selectedIndex > -1) { formatMenuHeader(s); dpGrid.rowHasDetail = s.selectedValue ? null : (row => !(row.dataItem.CategoryID % 2)); } }, header: 'rowDetails', displayMemberPath: 'header', selectedValuePath: 'value', itemsSource: [ { header: 'All', value: true }, { header: 'Even rows only', value: false }, ], }); rowDetailsMenu.selectedValue = initialOptions.rowHasDetail; // // function formatMenuHeader(menu) { let index = menu.header.indexOf(':'); if (index !== -1) { menu.header = menu.header.substring(0, menu.header.indexOf(':')) + wjCore.format(': <b>{header}</b>', menu.selectedItem); } else { menu.header = menu.header + wjCore.format(': <b>{header}</b>', menu.selectedItem); } } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo FlexGrid Row Details</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- SystemJS --> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <div class="container-fluid"> <h3>HTML in Row Details</h3> <p> This grid shows product categories on each row. Expanding the rows shows an HTML element with information about the products in that category: </p> <div id="htmlDetail"></div> <h3>Grids in Row Details</h3> <p> You can add anything you want to the detail rows, including other grids. This example shows the same categories, but the detail row uses another grid to show the products: </p> <div id="gridDetail"></div> <div class="grid-options"> <div id="detailVisibilityMode"></div> <div id="maxHeight"></div> <div id="isAnimated"></div> <div id="keyActionEnter"></div> <div id="rowDetails"></div> </div> </div> </body> </html>
.wj-flexgrid { max-height: 350px; } body { margin-bottom: 20pt; } .grid-options .wj-menu { margin: 0.25em 0.5em 0.25em 0; }
(function (global) { System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: 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', '@grapecity/wijmo': 'npm:@grapecity/wijmo/index.js', '@grapecity/wijmo.input': 'npm:@grapecity/wijmo.input/index.js', '@grapecity/wijmo.styles': 'npm:@grapecity/wijmo.styles', '@grapecity/wijmo.cultures': 'npm:@grapecity/wijmo.cultures', '@grapecity/wijmo.chart': 'npm:@grapecity/wijmo.chart/index.js', '@grapecity/wijmo.chart.analytics': 'npm:@grapecity/wijmo.chart.analytics/index.js', '@grapecity/wijmo.chart.animation': 'npm:@grapecity/wijmo.chart.animation/index.js', '@grapecity/wijmo.chart.annotation': 'npm:@grapecity/wijmo.chart.annotation/index.js', '@grapecity/wijmo.chart.finance': 'npm:@grapecity/wijmo.chart.finance/index.js', '@grapecity/wijmo.chart.finance.analytics': 'npm:@grapecity/wijmo.chart.finance.analytics/index.js', '@grapecity/wijmo.chart.hierarchical': 'npm:@grapecity/wijmo.chart.hierarchical/index.js', '@grapecity/wijmo.chart.interaction': 'npm:@grapecity/wijmo.chart.interaction/index.js', '@grapecity/wijmo.chart.radar': 'npm:@grapecity/wijmo.chart.radar/index.js', '@grapecity/wijmo.chart.render': 'npm:@grapecity/wijmo.chart.render/index.js', '@grapecity/wijmo.chart.webgl': 'npm:@grapecity/wijmo.chart.webgl/index.js', '@grapecity/wijmo.chart.map': 'npm:@grapecity/wijmo.chart.map/index.js', '@grapecity/wijmo.gauge': 'npm:@grapecity/wijmo.gauge/index.js', '@grapecity/wijmo.grid': 'npm:@grapecity/wijmo.grid/index.js', '@grapecity/wijmo.grid.detail': 'npm:@grapecity/wijmo.grid.detail/index.js', '@grapecity/wijmo.grid.filter': 'npm:@grapecity/wijmo.grid.filter/index.js', '@grapecity/wijmo.grid.search': 'npm:@grapecity/wijmo.grid.search/index.js', '@grapecity/wijmo.grid.grouppanel': 'npm:@grapecity/wijmo.grid.grouppanel/index.js', '@grapecity/wijmo.grid.multirow': 'npm:@grapecity/wijmo.grid.multirow/index.js', '@grapecity/wijmo.grid.transposed': 'npm:@grapecity/wijmo.grid.transposed/index.js', '@grapecity/wijmo.grid.transposedmultirow': 'npm:@grapecity/wijmo.grid.transposedmultirow/index.js', '@grapecity/wijmo.grid.pdf': 'npm:@grapecity/wijmo.grid.pdf/index.js', '@grapecity/wijmo.grid.sheet': 'npm:@grapecity/wijmo.grid.sheet/index.js', '@grapecity/wijmo.grid.xlsx': 'npm:@grapecity/wijmo.grid.xlsx/index.js', '@grapecity/wijmo.grid.selector': 'npm:@grapecity/wijmo.grid.selector/index.js', '@grapecity/wijmo.grid.cellmaker': 'npm:@grapecity/wijmo.grid.cellmaker/index.js', '@grapecity/wijmo.nav': 'npm:@grapecity/wijmo.nav/index.js', '@grapecity/wijmo.odata': 'npm:@grapecity/wijmo.odata/index.js', '@grapecity/wijmo.olap': 'npm:@grapecity/wijmo.olap/index.js', '@grapecity/wijmo.rest': 'npm:@grapecity/wijmo.rest/index.js', '@grapecity/wijmo.pdf': 'npm:@grapecity/wijmo.pdf/index.js', '@grapecity/wijmo.pdf.security': 'npm:@grapecity/wijmo.pdf.security/index.js', '@grapecity/wijmo.viewer': 'npm:@grapecity/wijmo.viewer/index.js', '@grapecity/wijmo.xlsx': 'npm:@grapecity/wijmo.xlsx/index.js', '@grapecity/wijmo.undo': 'npm:@grapecity/wijmo.undo/index.js', '@grapecity/wijmo.interop.grid': 'npm:@grapecity/wijmo.interop.grid/index.js', '@grapecity/wijmo.touch': 'npm:@grapecity/wijmo.touch/index.js', '@grapecity/wijmo.cloud': 'npm:@grapecity/wijmo.cloud/index.js', '@grapecity/wijmo.barcode': 'npm:@grapecity/wijmo.barcode/index.js', '@grapecity/wijmo.barcode.common': 'npm:@grapecity/wijmo.barcode.common/index.js', '@grapecity/wijmo.barcode.composite': 'npm:@grapecity/wijmo.barcode.composite/index.js', '@grapecity/wijmo.barcode.specialized': 'npm:@grapecity/wijmo.barcode.specialized/index.js', 'jszip': 'npm:jszip/dist/jszip.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' }, // packages tells the System loader how to load when no filename and/or no extension packages: { src: { defaultExtension: 'js' }, "node_modules": { defaultExtension: 'js' }, } }); })(this);