近日,前端开发工具包 WijmoJS 发布最新版本,将所有组件和功能模块化,更加体现出 WijmoJS 的小巧、灵活和高效。这一版本的最大亮点,就是增加了“可在React Redux 应用程序中,编辑数据网格”。
上文中我们介绍了React Redux是什么,以及前端UI组件库WijmoJS 为何要在本次新版本发布中,增加在React Redux应用程序编辑数据网格的功能。
下面,我们将详细介绍:如何创建一个使用DataGrid来显示和编辑Redux Store中数组的简单示例。
该示例遵循标准的React-Redux应用程序结构,但采用了扁平化的文件夹结构,以使其更适合WijmoJS的在线演示站点。另外,由于示例站点对演示站点的要求,因此它使用SystemJS运行时加载器加载模块,而不是Webpack或类似的捆绑程序。
(React Redux应用程序中的可编辑DataGrid)
该应用程序具有一个包含两个FlexGrid控件的单一视图。
最上面的是由ImmutabilityProvider组件控制的可编辑DataGrid,该组件已从Redux Store绑定到阵列。这是用于检查功能的DataGrid(我们在本文中讨论的内容)。您可以通过从键盘上键入单元格值来编辑单元格值,使用网格行列表末尾的“新行”行来添加新项,或者通过选择它们并按Delete键来删除项。
您还可以粘贴剪贴板中的数据,或清除所选单元格区域中的多个单元格值。
值得一提的是,要使用Object.freeze() 函数冻结datagrid中显示的数据数组中的所有项目,以确保执行编辑时datagrid不会让数据改变。
除了编辑之外,您还可以根据需要转换数据-单击列标题进行排序,将列标题拖到datagrid上方的组面板中进行分组,然后单击列标题中的过滤器图标进行过滤。
第二个DataGrid是只读的。它不使用ImmutabilityProvider,而是使用其itemsSource属性直接绑定到商店的数组。该DataGrid可以帮助您检查通过顶部DataGrid所做的更改如何应用于Redux Store。
顶部DataGrid上方还有一个菜单,可用于更改数据阵列的大小。小型阵列可方便地检查您的更改如何应用于商店。更大的数组可用于评估编辑过程的性能。您可以选择一些与实际应用中期望的项目相似的项目,并评估其工作方式。
最初的应用程序全局状态在reducers.jsx文件中定义如下:
const itemCount = 5000; const initialState = { itemCount, items: getData(itemCount), idCounter: itemCount }
它包含具有随机生成的items数组,以及几个辅助属性-itemCount和idCounter,这些属性定义数组中的项数,而idCounter存储唯一的id值,该id值分配给新添加项的id属性。
items数组是由示例的DataGrid表示的数组,使用Object.freeze()函数冻结数组中的每个项目,以确保真正满足Redux提出的数据不变性要求。
Redux操作创建者函数在actions.jsx文件中定义:
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 });
有3个事件用于更改项目数组(ADD_ITEM,REMOVE_ITEM和CHANGE_ITEM)的操作,还有一个附加的CHANGE_COUNT事件,可使商店创建具有不同数量的全新项目数组。
每个事件都依赖于“action creator”事件。在ImmutabilityProvider.dataChanged事件处理程序(在GridView表示组件中)中调用这些函数,以通知商店有关datagrid中所做的数据更改。
对于项目更改操作,index属性包含items数组中受影响的索引项目, item属性保存了对item对象的引用。
应用程序根据上述动作定义一个单例的Reducer,对执行器全局状态进行更新。它在reducers.jsx文件中定义:
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; } }
根据Redux的要求,我们不会对现有项目数组及其项目的属性进行修改。如果添加或删除项目,会首先创建添加或删除该项目的克隆。如果该操作要求我们更新现有项目,我们将创建一个新的数组,将更新后的项替换为更改项的克隆。
我们使用 \@ grapecity / wijmo.grid.immutable模块中的copyObject函数来克隆对象。如果它是由浏览器实现的,它将有效地使用Object.assign函数;如果没有,则使用自定义实现(例如,在IE中)。
要处理REMOVE_ITEM和CHANGE_ITEM操作,我们需要知道items数组中受此更改影响的现有项目及其索引。在此示例中,我们使用最简单,也是最快的方法来执行此操作:该项目的索引在操作数据的index属性中传递(ImmutabilityProvider.dataChanged事件为您带来此信息!)。
如果上述方法不起作用,则可以在操作数据时传递将要更改的原始项目,并使用items.indexOf()方法找到其索引,或按商品ID搜索。
对于CHANGE_ITEM操作,您不仅需要知道将要更改的现有项目,还需要知道该项目的新属性值。通过为您提供包含新项目属性值的克隆对象,ImmutabilityProvider.dataChanged事件的数据也带来了此信息,此克隆对象在操作的item属性中传递,并且由reducer用于创建具有新属性值的新克隆项目,以在克隆items数组中使用它而不是旧的对象。
请注意,对于添加到克隆项数组中的任何克隆项,我们调用Object.freeze以保护该项免于意外情况。
该示例的UI在GridView.jsx文件的单个GridView表示性组件中实现。
按照Redux的React绑定中的惯例,我们将其与容器组件(在GridViewContainer.jsx文件中实现的GridViewContainer)一起使用。后者只是前者的包装,目的是向GridView提供来自Redux Store的必要数据。
数据是datagrid中表示的items数组,以及动作创建者函数(addItemAction,removeItemAction等),通过this.props对象,GridView可以将其作为道具使用。
这是GridViewContainer的实现方式:
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);
GridView演示性组件使用组件的render方法中的以下代码添加了带有关联的ImmutabilityProvider的FlexGrid组件:
import * as wjFlexGrid from '@grapecity/wijmo.react.grid'; import * as wjGridFilter from '@grapecity/wijmo.react.grid.filter'; import { DataChangeEventArgs, DataChangeAction } from '@grapecity/wijmo.grid.immutable'; import { ImmutabilityProvider } from '@grapecity/wijmo.react.grid.immutable'; …… <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> <wjFlexGrid.FlexGridColumn binding="start" header="Date" format="d"></wjFlexGrid.FlexGridColumn> <wjFlexGrid.FlexGridColumn binding="end" header="Time" format="t"></wjFlexGrid.FlexGridColumn> <wjFlexGrid.FlexGridColumn binding="country" header="Country"></wjFlexGrid.FlexGridColumn> <wjFlexGrid.FlexGridColumn binding="product" header="Product"></wjFlexGrid.FlexGridColumn> <wjFlexGrid.FlexGridColumn binding="sales" header="Sales" format="n2"></wjFlexGrid.FlexGridColumn> <wjFlexGrid.FlexGridColumn binding="downloads" header="Downloads" format="n0"></wjFlexGrid.FlexGridColumn> <wjFlexGrid.FlexGridColumn binding="active" header="Active" width={80}></wjFlexGrid.FlexGridColumn> </wjFlexGrid.FlexGrid>
如您所见,ImmutabilityProvider的itemsSource属性绑定到this.props.items属性,该属性包含来自全局应用程序状态的items数组。
在每次Store reducer生成该数组的新克隆以应用用户修改时,会使用新的数组实例自动更新this.props.items,并且ImmutabilityProvider将使FlexGrid更新其内容以映射修改。
每当用户更改保存到datagrid中的数据时,都会调用ImmutabilityProvider的dataChanged事件。它绑定到onGridDataChanged处理函数,该函数实现如下:
onGridDataChanged(s: ImmutabilityProvider, e: DataChangeEventArgs) { switch (e.action) { case DataChangeAction.Add: this.props.addItemAction(e.newItem); break; case DataChangeAction.Remove: this.props.removeItemAction(e.oldItem, e.itemIndex); break; case DataChangeAction.Change: this.props.changeItemAction(e.newItem, e.itemIndex); break; default: throw 'Unknown data action' } }
处理程序只调用一个适当的动作创建器函数,由于使用了GridViewContainer容器组件,该函数也可以通过this.props对象通过GridView组件使用。动作数据是从DataChangeEventArgs类型的事件参数中检索的,它带来有关已执行的更改操作(action属性,可以采用“添加”、“删除”或“更改”值)信息,源数组中受影响项目的索引,以及对受影响项目的引用操作)。
请注意:“更改”是一个特殊操作,它同时使用了oldItem和newItem属性。 oldItem包含必须更改其属性值的原始(未更改)项目,而newItem包含具有新属性值的原始克隆项目。
因此,具有直接附加的ImmutabilityProvider的FlexGrid不会触发直接改变源数组的操作,而是使用事件提供的数据触发dataChanged事件,该事件调用适当的操作创建者函数,将操作分派到Redux商店,然后到达该商店的reducer。
示例程序将使用更改的数据创建该数组的克隆,并且该数组的新副本在绑定到ImmutabilityProvider.itemsSource属性的this.props.items属性中可用。 ImmutabilityProvider检测到此新数组实例,并使FlexGrid刷新其内容。
该视图包括一个Menu组件,该组件允许用户更改在DataGrid中显示数组的大小。更改其值会导致Redux Store创建指定长度的新项目数组。
以下代码,可将菜单使用组件的render方法添加到视图中:
import * as wjInput from '@grapecity/wijmo.react.input'; .... <wjInput.Menu header='Items number' 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>
菜单的value属性绑定到全局Redux状态的itemCount属性,该状态包含当前items数组的长度。
当用户在下拉列表中选择另一个值时,将触发itemClicked事件并调用onCountChanged事件处理函数,该函数如下:
onCountChanged(s: wjcInput.Menu) { this.props.changeCountAction(s.selectedValue); }
处理程序仅调用changeCountAction操作创建者函数,将新的数组长度作为操作数据传递。这迫使Store reducer创建一个指定长度的新items数组。视图的另一个UI元素是只读datagrid,它仅显示items数组的内容。
该DataGrid具有关联的“显示数据”复选框元素,该元素允许用户临时将DataGrid与数据阵列断开连接。这是组件的render方法中的JSX,它添加了这些组件:
<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>
“显示数据”复选框是受控组件,它将值存储在组件状态的showStoreData属性中。
我们在这里使用本地组件来存储此值,但是,如果您希望将所有内容存储在全局Redux状态中,没有问题,可以轻松地将其移动到那里。
请注意,FlexGrid.itemsSource属性有条件地绑定到Store的items数组,或者绑定到null值,具体取决于showStoreData属性值。
应用程序的入口点是app.jsx文件,我们将所有应用程序片段放在一起并运行根App组件:
import * as React from 'react'; import * as ReactDOM from 'react-dom'; 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<any, any> { render() { return <Provider store={store}> <GridViewContainer /> </Provider>; } } ReactDOM.render(<App />, document.getElementById('app'));
截至目前,我们已经创建了一个应用程序APP,并将其传递给我们的reducer。然后,使用GridViewContainer容器组件,依次呈现GridView组件,并将其作为道具传递给全局Store数据。
我们用react-redux Provider组件包装应用程序组件树,这样就可以从任何应用程序组件中轻松访问存储项目。
FlexGrid DataGrid以及关联的ImmutabilityProvider组件可以最大程度的满足您的需求:创建基于Redux应用程序状态管理和可编辑的DataGrid。
借助 WijmoJS 前端开发工具包 ,您可以在应用程序UI中使用可编辑的DataGrid,而不会影响Redux对数据不变性的要求。即使在相当大的数据上,此解决方案也具备优秀的性能。
在Redux应用程序中将datagrid用作数据编辑控件几乎与使用输入控件一样简单,在输入控件中,您只需将控件值绑定到全局状态值,并在控件的“ value”中分配一个具有新值的动作已更改”事件即可。
文中的示例会加入 WijmoJS 的学习指南,请点击此处查看。如果您也希望借助WijmoJS的前端开发工具包,进一步提升企业IT部门的生产效率,欢迎访问官网,下载试用。