V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
GrapeCityChina
V2EX  ›  React

如何为 React Redux 应用程序,创建可编辑的数据网格(二)

  •  
  •   GrapeCityChina ·
    GrapeCity · 2020-04-08 17:09:16 +08:00 · 1160 次点击
    这是一个创建于 1450 天前的主题,其中的信息可能已经有所发展或是发生改变。

    近日,前端开发工具包 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 上方还有一个菜单,可用于更改数据阵列的大小。小型阵列可方便地检查您的更改如何应用于商店。更大的数组可用于评估编辑过程的性能。您可以选择一些与实际应用中期望的项目相似的项目,并评估其工作方式。

    状态定义 State

    最初的应用程序全局状态在 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 提出的数据不变性要求。

    事件 Actions

    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

    应用程序根据上述动作定义一个单例的 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 以保护该项免于意外情况。

    数据预览组件 :Presentational 和 Container

    该示例的 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 部门的生产效率,欢迎访问官网,下载试用。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   990 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 20:06 · PVG 04:06 · LAX 13:06 · JFK 16:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.