ReactJS实现可编辑表格的几个思路

@vrqq  April 14, 2020

Part1 双向绑定

官方是不推崇双向绑定的,好用是真好用,不过我们先明确几个原则

  • 举个例子,我们有 "编辑组件TextInput" 和一个"页面class Page",数据落在Page.state.a.b.c.d里
  • 绑定的set/get只能在"更新-显示"链的最末端,也就是说当TextInput实现双向绑定以后,TextInput其内部就没有用户代码了
  • 初衷是绑定,数据当然就落在远端Page,通过给props方式把数据递给TextInput
  • 为保证显示一致,须达到<TextInput value={this.state.a.b.c.d} />`这种效果
  • 为了更新 TextInput要调用page.setState()
  • 不用考虑琐碎的的性能问题,使用ImmutableJS (我用的便携版Seamless-Immutable)

    // path[0] = this
    // return path[0].state.path[1].path[2]...;
    let getValue = (path) => {
      let obj = path[0].state;
      for (let i=1; i<path.length; i++) {
          if (path[i] in obj)
              obj = obj[path[i]];
          else
              return null;
      }
      return obj;
    }
    
    // path[0] = this
    // path[0].state.path[1].path[2]... = newvalue
    let setValue = (path, newvalue) => {
      let laststate = SImmutable(path[0].state[path[1]]);
      let nextstate;
      if (path.length > 2)
          nextstate = laststate.setIn(path.slice(2), newvalue);
      else
          nextstate = newvalue;
      let obj={}; obj[path[1]] = nextstate;
      // console.log(obj);
      path[0].setState(obj);
    }
    
    export function EphText (props) {
      return (
          <input type="text" {...props} style={my_style}
              value = {getValue(props.ephbind)}
              onChange={ e => {setValue(props.ephbind, e.target.value);} }
          />
      );
    }

Part2 存储好看 还是 用户开发友好

我们想实现如下功能:

  • 最后一行作为空行,当用户修改当前行时候,在后面新增一个空行
  • 删除中间一行,不引起其他行reRender
  • 提交数据时候比较容易读到直接submit
  • 表格使用[row1:{}, row2:{}, ...]这种方式而不是二维数组

先来一个:类似Java的无所不包Class模式

  • 最容易想到
  • 建立Storage类,存储表格在state中的path
  • 这种思想源自统筹一切的class,关于表格数据的一切操作都被class覆盖
  • 当然数据格式还是用户自己管理,是字符串还是object,StorageClass不关心
  • 我感觉代码太长,因为StorageClass写死开发者不可控,不能一两句能说清楚,不能一眼看懂数据变化关系和效率
    使用时候大致如下:

    export class Storage 
    {
      constructor(path, rowtemplate) {}
      set(r, c, newval) {} //set value
      rowHash() {} //get row hash
      get(r, c) {} //get value
      rmRow(r) {}  //remove a row
      renderArray(); //array for render
      serialize(); //tojson
    }
    
    ptr = new Storage([path]);
    <MyTable title=['col1', 'col2']>
    {ptr.renderArray().map((row, index) => 
      <tr key={ptr.rowHash(index)}>
      <td><TextInput bind={ptr, row} /></td>
      <td><TextInput value={ptr.get(row, 2)} onChange={e=>{ptr.set(row, 2, e.target.value);}} /></td>
      </tr>
    )}
    </MyTable>

第二个问题,实现删除一行其他行不渲染?

  • 当然通过key规避,如果key是数组index,那删了肯定会重新渲染
  • 如果数组是数据hash,需要证明unique
  • 我的方案是把删掉的行设为null,数组位置还占着,渲染时跳过

下一个问题,想实现“最后一行作为新增功能”,那这个空行 要不要进入Page.state.mytable存储

  • 存储:渲染省事,当onChange最后一行每个元素时 自动copy&push
  • 但是存储要在Ajax.fetch()后就遍历所有数据点,提交前还要把最后一行清了
  • 不存储:Table就要想个办法多渲染一行

我的一个办法,取个巧

  • Page.state.mytable不存储最新一行的数据,而改用tabletmpl变量存储待插入行的模板
  • 将待插入行渲染时加入渲染数组,带key排序
  • 尽量不违背一眼看穿原则,看下面代码就明白了

    const table2_tmpl = {name1:"", name2:""};
    <MyTable title={['name1', 'name2']}>
    {[...this.state.demotable, table2_tmpl].map( (row, index) => {
      if (row == null)
          return;
      let islastrow = (index==this.state.demotable.length?row:index);
      return (
      <tr key={index}>
          <td>{index+1}</td>
          <td><TextInput bind={[this, 'demotable', islastrow, 'name1']} /></td>
          <td><TextInput bind={[this, 'demotable', islastrow, 'name2']} /></td>
          <td></td>
          <td><DeleteButton bind={[this, 'demotable', index]}/></td>
      </tr>
      );
    })}
    </MyTable>

仅有一条评论

  1. 1 1

    11

添加新评论