import 'bootstrap.css';
import '@grapecity/wijmo.styles/wijmo.css';
import './app.css';
//
//React/Redux
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
//Application
import { appReducer } from './reducers';
import { GridViewContainer } from './GridViewContainer';
// Create global Redux Store
const store = createStore(appReducer);
class App extends React.Component {
render() {
return <Provider store={store}>
<GridViewContainer />
</Provider>;
}
}
setTimeout(() => {
const container = document.getElementById('app');
if (container) {
const root = ReactDOM.createRoot(container);
root.render(<App />);
}
}, 100);
<!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="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('./src/app');
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>
body {
background-color: #f8f8f8;
font-family: -apple-system-font, 'Segoe UI', 'Roboto', sans-serif;
margin-bottom: 50px;
}
h1, h2, h3, h4, h5, h6 {
font-family: -apple-system-font, 'Segoe UI Light', 'Segoe UI', 'Roboto', sans-serif;
font-weight: 300;
}
.header {
background-color: #48A9C5;
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,
.wj-grouppanel {
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;
}
// React
import * as React from 'react';
//
// Wijmo
import * as wjInput from '@grapecity/wijmo.react.input';
import * as wjFlexGrid from '@grapecity/wijmo.react.grid';
import * as wjGroupPanel from '@grapecity/wijmo.react.grid.grouppanel';
import * as wjGridFilter from '@grapecity/wijmo.react.grid.filter';
import '@grapecity/wijmo.touch'; // add touch support on mobile devices
//
// Wijmo ImmutabilityProvider
import { DataChangeAction } from '@grapecity/wijmo.grid.immutable';
import { ImmutabilityProvider } from '@grapecity/wijmo.react.grid.immutable';
//
// Presentation component with an editable Redux grid
export class GridView extends React.Component {
constructor(props) {
super(props);
this.onCountChanged = this.onCountChanged.bind(this);
this.onGridInitialized = this.onGridInitialized.bind(this);
this.onGridDataChanged = this.onGridDataChanged.bind(this);
this.groupPanelRef = React.createRef();
// We store local UI related data in the local state, for simplicity,
// to not bloat global store with irrelevant data.
this.state = {
showStoreData: true
};
}
render() {
return <div className='container-fluid'>
<h4>
Editable FlexGrid without data source mutation
</h4>
<div>
<p>
This <b>editable</b> <i>FlexGrid</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 also 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>
The items in the Redux Store array are frozen using the <b>Object.freeze()</b>{' '}
method, to make sure that FlexGrid really doesn't change the underlying data.
User edits in datagrid don't mutate the underlying 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>FlexGrid</b> to update
its content to reflect the changes.
Notice that the overall performance of this seemingly complex process is nice,
the edits are applied instantly.
</p>
<p>
This way you get a usual data editing experience in the datagrid.
But instead of directly mutating the underlying data array, the updates are
performed via the Redux Store <i>reducers</i> mechanism.
You can also sort, group, and filter the data as usual.
</p>
<div>
<wjInput.Menu header='Item Count' value={this.props.itemCount} itemClicked={this.onCountChanged}>
<wjInput.MenuItem value={5}>5</wjInput.MenuItem>
<wjInput.MenuItem value={50}>50</wjInput.MenuItem>
<wjInput.MenuItem value={100}>100</wjInput.MenuItem>
<wjInput.MenuItem value={500}>500</wjInput.MenuItem>
<wjInput.MenuItem value={5000}>5,000</wjInput.MenuItem>
<wjInput.MenuItem value={10000}>10,000</wjInput.MenuItem>
<wjInput.MenuItem value={50000}>50,000</wjInput.MenuItem>
<wjInput.MenuItem value={100000}>100,000</wjInput.MenuItem>
</wjInput.Menu>
</div>
<wjGroupPanel.GroupPanel ref={this.groupPanelRef} placeholder="Drag columns here to create groups."/>
</div>
<div>
<wjFlexGrid.FlexGrid allowAddNew allowDelete initialized={this.onGridInitialized}>
<ImmutabilityProvider itemsSource={this.props.items} dataChanged={this.onGridDataChanged}/>
<wjGridFilter.FlexGridFilter />
<wjFlexGrid.FlexGridColumn binding="id" header="ID" width={80} isReadOnly={true}/>
<wjFlexGrid.FlexGridColumn binding="start" header="Date" format="d"/>
<wjFlexGrid.FlexGridColumn binding="end" header="Time" format="t"/>
<wjFlexGrid.FlexGridColumn binding="country" header="Country"/>
<wjFlexGrid.FlexGridColumn binding="product" header="Product"/>
<wjFlexGrid.FlexGridColumn binding="sales" header="Sales" format="n2"/>
<wjFlexGrid.FlexGridColumn binding="downloads" header="Downloads" format="n0"/>
<wjFlexGrid.FlexGridColumn binding="active" header="Active" width={80}/>
</wjFlexGrid.FlexGrid>
</div>
<div>
<h4>
Check data in the Store
</h4>
<p>
This <b>read-only</b> grid shows the same data array from the Redux Store,
to allow you controlling how the update operations go.
</p>
<p>
If you evaluate performance of the data change operations on a big array,
you may want to disconnect it from the data by means of the
checkbox below, to not bring additional performance penalties caused
by this grid refresh.
</p>
<input type="checkbox" checked={this.state.showStoreData} onChange={(e) => {
this.setState({ showStoreData: e.target.checked });
}}/>
{' '}
<b>Show data</b>
<wjFlexGrid.FlexGrid itemsSource={this.state.showStoreData ? this.props.items : null} isReadOnly/>
</div>
</div>;
}
onCountChanged(s) {
this.props.changeCountAction(s.selectedValue);
}
onGridInitialized(s) {
// Attach group panel
this.groupPanelRef.current.control.grid = s;
}
// Dispatches data change actions to the Redux Store in response to
// user edits made via the grid.
onGridDataChanged(s, e) {
switch (e.action) {
case DataChangeAction.Add:
this.props.addItemAction(e.newItem);
break;
case DataChangeAction.Remove:
this.props.removeItemAction(e.newItem, e.itemIndex);
break;
case DataChangeAction.Change:
this.props.changeItemAction(e.newItem, e.itemIndex);
break;
default:
throw 'Unknown data action';
}
}
}
// GridViewContainer container component for the GridView presentation component.
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { GridView } from './GridView';
import { addItemAction, removeItemAction, changeItemAction, changeCountAction } from './actions';
const mapStateToProps = state => ({
items: state.items,
itemCount: state.itemCount
});
const mapDispatchToProps = dispatch => {
return bindActionCreators({
addItemAction, removeItemAction, changeItemAction, changeCountAction
}, dispatch);
};
export const GridViewContainer = connect(mapStateToProps, mapDispatchToProps)(GridView);
export const addItemAction = (item) => ({
type: 'ADD_ITEM',
item
});
export const removeItemAction = (item, index) => ({
type: 'REMOVE_ITEM',
item,
index
});
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 '@grapecity/wijmo.grid.immutable';
const itemCount = 5000;
const initialState = {
itemCount,
items: getData(itemCount),
idCounter: itemCount
};
export const appReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_ITEM':
{
// make a clone of the new item which will be added to the
// items array, and assigns its 'id' property with a unique value.
let newItem = Object.freeze(copyObject({}, action.item, { id: state.idCounter }));
return copyObject({}, state, {
// items array clone with a new item added
items: state.items.concat([newItem]),
// increment 'id' counter
idCounter: state.idCounter + 1
});
}
case 'REMOVE_ITEM':
{
let items = state.items, index = action.index;
return copyObject({}, state, {
// items array clone with the item removed
items: items.slice(0, index).concat(items.slice(index + 1))
});
}
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) {
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',
'@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.grid.immutable': 'npm:@grapecity/wijmo.grid.immutable/index.js',
'@grapecity/wijmo.touch': 'npm:@grapecity/wijmo.touch/index.js',
'@grapecity/wijmo.cloud': 'npm:@grapecity/wijmo.cloud/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.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',
"@grapecity/wijmo.react.chart.analytics": "npm:@grapecity/wijmo.react.chart.analytics/index.js",
"@grapecity/wijmo.react.chart.animation": "npm:@grapecity/wijmo.react.chart.animation/index.js",
"@grapecity/wijmo.react.chart.annotation": "npm:@grapecity/wijmo.react.chart.annotation/index.js",
"@grapecity/wijmo.react.chart.finance.analytics": "npm:@grapecity/wijmo.react.chart.finance.analytics/index.js",
"@grapecity/wijmo.react.chart.finance": "npm:@grapecity/wijmo.react.chart.finance/index.js",
"@grapecity/wijmo.react.chart.hierarchical": "npm:@grapecity/wijmo.react.chart.hierarchical/index.js",
"@grapecity/wijmo.react.chart.interaction": "npm:@grapecity/wijmo.react.chart.interaction/index.js",
"@grapecity/wijmo.react.chart.radar": "npm:@grapecity/wijmo.react.chart.radar/index.js",
"@grapecity/wijmo.react.chart": "npm:@grapecity/wijmo.react.chart/index.js",
"@grapecity/wijmo.react.core": "npm:@grapecity/wijmo.react.core/index.js",
'@grapecity/wijmo.react.chart.map': 'npm:@grapecity/wijmo.react.chart.map/index.js',
"@grapecity/wijmo.react.gauge": "npm:@grapecity/wijmo.react.gauge/index.js",
"@grapecity/wijmo.react.grid.detail": "npm:@grapecity/wijmo.react.grid.detail/index.js",
"@grapecity/wijmo.react.grid.filter": "npm:@grapecity/wijmo.react.grid.filter/index.js",
"@grapecity/wijmo.react.grid.grouppanel": "npm:@grapecity/wijmo.react.grid.grouppanel/index.js",
'@grapecity/wijmo.react.grid.search': 'npm:@grapecity/wijmo.react.grid.search/index.js',
"@grapecity/wijmo.react.grid.multirow": "npm:@grapecity/wijmo.react.grid.multirow/index.js",
"@grapecity/wijmo.react.grid.sheet": "npm:@grapecity/wijmo.react.grid.sheet/index.js",
'@grapecity/wijmo.react.grid.transposed': 'npm:@grapecity/wijmo.react.grid.transposed/index.js',
'@grapecity/wijmo.react.grid.transposedmultirow': 'npm:@grapecity/wijmo.react.grid.transposedmultirow/index.js',
'@grapecity/wijmo.react.grid.immutable': 'npm:@grapecity/wijmo.react.grid.immutable/index.js',
"@grapecity/wijmo.react.grid": "npm:@grapecity/wijmo.react.grid/index.js",
"@grapecity/wijmo.react.input": "npm:@grapecity/wijmo.react.input/index.js",
"@grapecity/wijmo.react.olap": "npm:@grapecity/wijmo.react.olap/index.js",
"@grapecity/wijmo.react.viewer": "npm:@grapecity/wijmo.react.viewer/index.js",
"@grapecity/wijmo.react.nav": "npm:@grapecity/wijmo.react.nav/index.js",
"@grapecity/wijmo.react.base": "npm:@grapecity/wijmo.react.base/index.js",
'@grapecity/wijmo.react.barcode.common': 'npm:@grapecity/wijmo.react.barcode.common/index.js',
'@grapecity/wijmo.react.barcode.composite': 'npm:@grapecity/wijmo.react.barcode.composite/index.js',
'@grapecity/wijmo.react.barcode.specialized': 'npm:@grapecity/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'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
src: {
defaultExtension: 'jsx'
},
"node_modules": {
defaultExtension: 'js'
},
}
});
})(this);