편집 가능한 Redux TransposedGrid (React)

TransposedGrid는 일반적으로 사용자가 그리드를 통해 변경한 내용을 기반 데이터 배열에 직접 반영합니다. 하지만 이러한 방식은 데이터 불변성을 요구하는 Redux와 같은 상태 관리 시스템에서는 적합하지 않습니다.

이 문제는 ImmutabilityProvider 확장 컴포넌트를 사용하여 해결할 수 있습니다. 이 컴포넌트를 TransposedGrid 컨트롤에 연결하고 Redux Store 의 데이터 배열에 바인딩하면, TransposedGrid의 동작이 다음과 같이 변경됩니다.

  • 사용자는 TransposedGrid를 통해 일반적인 방식으로 데이터를 편집할 수 있습니다(항목 값 변경).

  • TransposedGrid가 사용자 편집에 따라 기본 데이터 배열을 직접 변경하는 것을 방지합니다. 대신, dataChanged 이벤트를 트리거하며, 이 이벤트를 사용해 Redux Store로 데이터 변경 액션을 디스패치할 수 있습니다.

FlexGrid 알아보기 | TransposedGrid API

이 데모는 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);