Editable Redux TransposedGrid (React)

TransposedGrid normally updates the underlying data array with the changes made by a user via the grid. This approach doesn't work with state managements systems like Redux, which require the data immutability.

This problem can be solved by the ImmutabilityProvider extension component. Being attached to the transposedGrid control, and bound to a data array from the Redux Store, this component changes the transposedGrid behavior in the following ways:

  • Allows a user to edit the data via the transposedGrid in a usual manner (change item values.

  • Prevents the transposedGrid from mutating the underlying data array in response to user edits. Instead, it triggers the dataChanged event, which can be used to dispatch data change actions to the Redux Store.

Learn about FlexGrid | TransposedGrid API Reference

This example uses React.

import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import ReactDOM from 'react-dom/client'; import React from 'react'; import { createStore } from 'redux'; import { Provider as _Provider } from 'react-redux'; const Provider = _Provider; import './app.css'; import { appReducer } from './reducers'; import { TransposedGridViewContainer } from './TransposedGridViewContainer'; // Create global Redux Store const store = createStore(appReducer); function App() { return (<Provider store={store}> <TransposedGridViewContainer /> </Provider>); } 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>Immutable Data/Redux</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>
h1, h2, h3, h4, h5, h6 { font-weight: 300; } .header { background-color: #00C1D5; margin-bottom: 14px; padding: 12px 0px; color: #dcf3f6; } .header h1 { font-size: 40px; line-height: 1; margin: 8px 0 5px 0; color: #fff; } .header img { float: left; margin: 0 10px 5px 0; } h3 { margin: 30px 0 10px -12px; } h1, h2, h3, h4, h5, h6 { color: #026974; } .content { width: 60%; margin: 30px 0 10px 12px; } .detail { margin-left: 100px; } .wj-flexgrid { max-height: 200px; } .wj-menu { margin-bottom: 6px; }
const countries = ['US', 'Germany', 'UK', 'Japan', 'Italy', 'Greece']; const products = ['Widget', 'Gadget', 'Doohickey']; export function getData(count = 5) { const data = []; const dt = new Date(); // add count items for (let i = 0; i < count; i++) { // constants used to create data items let date = new Date(dt.getFullYear(), i % 12, 25, i % 24, i % 60, i % 60), countryId = Math.floor(Math.random() * countries.length), productId = Math.floor(Math.random() * products.length); // create the item let item = { id: i, start: date, end: date, country: countries[countryId], product: products[productId], sales: Math.random() * 10000, downloads: Math.round(Math.random() * 10000), active: i % 4 === 0 }; // make item immutable Object.freeze(item); // add the item to the list data.push(item); } // return the data return data; }
import React, { useState } from 'react'; import useEvent from 'react-use-event-hook'; import * as wjInput from '@mescius/wijmo.react.input'; import * as wjFlexGrid from '@mescius/wijmo.react.grid'; import * as wjTransposedGrid from '@mescius/wijmo.react.grid.transposed'; import * as wjcGridImmutable from '@mescius/wijmo.grid.immutable'; import { ImmutabilityProvider } from '@mescius/wijmo.react.grid.immutable'; import '@mescius/wijmo.touch'; // add touch support on mobile devices // Presentation component with an editable Redux TransposedGrid export function TransposedGridView(props) { const [showStoreData, setShowStoreData] = useState(true); const onCountChanged = useEvent((s) => { props.changeCountAction(s.selectedValue); }); // Dispatches data change actions to the Redux Store in response to // user edits made via the grid. // Note: TransposedGrid with ImmutabilityProvider only triggers Change events // (Add and Remove are not supported by TransposedGrid) const onGridDataChanged = useEvent((s, e) => { if (e.action === wjcGridImmutable.DataChangeAction.Change) { props.changeItemAction(e.newItem, e.itemIndex); } }); return (<main className="container-fluid"> <h4> Editable TransposedGrid with Immutable Data (Redux) </h4> <div> <p> This <b>editable</b> <i>TransposedGrid</i> component has an{' '} <i>ImmutabilityProvider</i> component as its child. The latter is bound to the <i>items</i> array from the Redux Store, using its <b>itemsSource</b> property. It defines a handler for the{' '} <b>ImmutabilityProvider.dataChanged</b> event, which is triggered when a user edits data via the grid, and is used to dispatch data change <i>actions</i> to the Redux Store. </p> <p> <b>TransposedGrid Architecture:</b> Unlike regular FlexGrid where rows represent data items, in TransposedGrid <b>columns represent data items</b> and <b>rows represent properties</b>. This transposed layout is ideal for comparing multiple items side-by-side or viewing object properties in a property-sheet style. </p> <p> The items in the Redux Store array are frozen using the <b>Object.freeze()</b>{' '} method, to ensure that the grid doesn't mutate the underlying data. User edits don't mutate the data directly. Instead, the data change <i>actions</i> called from the <b>dataChanged</b> event handler cause Redux Store <i>reducers</i> to update the <i>items</i> array in the global State. Because the <i>ImmutabilityProvider.itemsSource</i> property is bound directly to this array, it detects the applied changes and causes <b>TransposedGrid</b> to update its content to reflect the changes. </p> <p> <b>Important Limitations:</b> TransposedGrid does not support: </p> <ul> <li><b>Adding/Deleting items</b> - Only editing existing items is supported</li> <li><b>Multi-cell paste</b> - Only single-cell paste operations are allowed</li> <li><b>Sorting and Grouping</b> - These features are disabled in TransposedGrid</li> </ul> <div> <wjInput.Menu header='Item Count' value={props.itemCount} itemClicked={onCountChanged}> <wjInput.MenuItem value={5}>5</wjInput.MenuItem> <wjInput.MenuItem value={20}>20</wjInput.MenuItem> <wjInput.MenuItem value={50}>50</wjInput.MenuItem> <wjInput.MenuItem value={100}>100</wjInput.MenuItem> <wjInput.MenuItem value={200}>200 (Performance Test)</wjInput.MenuItem> </wjInput.Menu> </div> </div> <div> <wjTransposedGrid.TransposedGrid autoGenerateRows={true} headersVisibility="Row"> <ImmutabilityProvider itemsSource={props.items} dataChanged={onGridDataChanged}/> </wjTransposedGrid.TransposedGrid> </div> <div> <h4> Check data in the Store </h4> <p> This <b>read-only</b> FlexGrid shows the same data array from the Redux Store in standard (non-transposed) layout, allowing you to verify that the update operations are working correctly. </p> <p> Notice that when you edit a cell in the TransposedGrid above (e.g., column 2 which represents item[1]), the corresponding row in the FlexGrid below (row 2) updates immediately, demonstrating the Redux Store synchronization. </p> <input type="checkbox" checked={showStoreData} onChange={(e) => { setShowStoreData(e.target.checked); }}/> {' '} <b>Show data</b> <wjFlexGrid.FlexGrid itemsSource={showStoreData ? props.items : null} isReadOnly/> </div> </main>); }
// GridViewContainer container component for the GridView presentation component. import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { TransposedGridView } from './TransposedGridView'; import { changeItemAction, changeCountAction } from './actions'; const mapStateToProps = (state) => ({ items: state.items, itemCount: state.itemCount }); const mapDispatchToProps = (dispatch) => { return bindActionCreators({ changeItemAction, changeCountAction }, dispatch); }; export const TransposedGridViewContainer = connect(mapStateToProps, mapDispatchToProps)(TransposedGridView);
export const changeItemAction = (item, index) => ({ type: 'CHANGE_ITEM', item, index }); export const changeCountAction = (count) => ({ type: 'CHANGE_COUNT', count });
import { getData } from './data'; import { copyObject } from '@mescius/wijmo.grid.immutable'; const itemCount = 20; const initialState = { itemCount, items: getData(itemCount), idCounter: itemCount }; export const appReducer = (state = initialState, action) => { switch (action.type) { case 'CHANGE_ITEM': { let items = state.items, index = action.index, oldItem = items[index], // create a cloned item with the property changes applied clonedItem = Object.freeze(copyObject({}, oldItem, action.item)); return copyObject({}, state, { // items array clone with the updated item items: items.slice(0, index). concat([clonedItem]). concat(items.slice(index + 1)) }); } case 'CHANGE_COUNT': { // create a brand new state with a new data let ret = copyObject({}, state, { itemCount: action.count, items: getData(action.count), idCounter: action.count }); return ret; } default: return state; } };
(function (global) { window.process = { env: { NODE_ENV: "production" } } 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.style': 'npm:@mescius/wijmo.grid.style/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/index.js', 'react-dom': 'npm:react-dom/index.js', 'react-dom/client': 'npm:react-dom/client.js', "scheduler": "npm:scheduler/index.js", 'redux': 'npm:redux/dist/redux.legacy-esm.js', 'react-redux': 'npm:react-redux/dist/react-redux.legacy-esm.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", "use-sync-external-store": "npm:use-sync-external-store" }, // packages tells the System loader how to load when no filename and/or no extension packages: { src: { defaultExtension: 'jsx' }, "node_modules": { defaultExtension: 'js' }, } }); })(this);