可以实时在页面上复制、新增、删除、渲染、上传图片并预览的Angular_Material表格是如何实现的

152 阅读5分钟

代码块中的“……”代表省略了对本篇文章无用、过多的业务代码

需求

16列表格,均可以进行编辑和值输入
其中第一列为自动匹配框,获取鼠标焦点后展示下拉框,点击后渲染对应行的其他部分数据
第9列为一个图片上传,可以预览和删除
最后一列为操作按钮,可以复制和删除
表格下方有一个新建按钮,可以添加一行空数据
不同的列列宽不一样,需要单独设置
表格对接接口,可以保存,下次进入的时候读取

PS:为啥我会来写这个东西呢,因为这个页面是被调到安卓去的一个同事以前写的
他给每一行都设置了一个FormGroup,每个单元格设置了一个FormConctol,数据一旦多起来直接页面卡死
于是优化的重任就到我头上了
最恐怖的是所有的命名都是id+数字的格式,整个代码毫无有效注释
我还必须按着这样的数据结构去优化……

实现思路

MatTable(也就是Angular-Material的表格组件简称)没有某一行的索引或者指示器,于是我先给到来的数组中每条数据添加一个索引position,后续的复制、删除、新增都需要基于索引上使用

开始实现

先声明数组的结构(在这里我使用的是ngFor去渲染表格,当然也可以一列一列渲染)

// 声明表格类型
mlDataSource = {
    // 存放数据
    mlData: [...ELEMENT_DATA],
    // 定义表头
    TableHeaderColumns: ['', '面料', '编号', '材料编号', '品名'……],
    // 列宽
    FlexData: [0, 40, 2, 8, 7, 5, 5, 8, 8, 4, 4, 5, 4, 4, 4, 5, 5, 5],
    // 列模板
    displayedColumns: [……]
  };

给数据添加索引:

      // 获取面料数组
      const MLDATA = res['data']['products_materials'].ml_text;
      let i = 0;
      // 用于给表格数组赋值的临时数组,在forEach中直接将数据push进表格数组,表格不会加载
      const MLDATASRC = [];
      // 若没有数据,则push五条空数据
      if (MLDATA.length === 0) {
        MLDATASRC.push(……);    // ……中是五行空数据
      } else {
        // 将数组数据一一添加索引字段position
        MLDATA.forEach(index => {
          MLDATASRC.push({
            position: i,
            ……
            });
          i++;
        });
      }

在HTML中渲染,因为是ngFor循环渲染所以看起来会有些复杂,但其实逻辑非常简单
大部分单元格都是input框,所以根据表头字段判断是否为自动匹配、图片、操作即可

在这里我就直接贴上我的最终代码了,因为中间实现的步骤我并没有记录 被同事的代码恶心的头晕,外加周五想快点结束工时8小时硬是写了7个小时代码

<div class="tableBox mat-elevation-z8">
            <h2 style="margin-left: 15px;"> 面料 <span style="font-size:14px;margin-left: 10px;">(
                    模板要求不超过15行,超过行数无法保存到excel )</span></h2>
            <mat-table [dataSource]="mlDataSource['mlData']" class="ml_table" fusePerfectScrollbar>
                <ng-container matColumnDef="{{column}}"
                    *ngFor="let column of mlDataSource['displayedColumns']; let indexNum = index;">
                    <!-- fxFlex也是mlDataSource中的一个数组给的 -->
                    <mat-header-cell *matHeaderCellDef
                        fxFlex="{{mlDataSource['FlexData'][indexNum]}}">{{mlDataSource['TableHeaderColumns'][indexNum]}}</mat-header-cell>
                    <mat-cell *matCellDef="let row" fxFlex="{{mlDataSource['FlexData'][indexNum]}}">
                        <div fxFlex="97" *ngIf="column == 'id0'">
                            <!-- isSelected用于判断id0是否有值,若没有值则显示自动匹配框 -->
                            <self-autocomplete *ngIf="isSelected(row[column])" [searchList]="searchList"
                                [options]="materialList"
                                (receiveList)="receiveCompleteML($event,row)"></self-autocomplete>

                            <!-- isSelected用于判断id0是否有值,有值则将对应的值放进input框显示 -->
                            <div style="display: flex; justify-content: center;align-items: center; ">
                                <input style="width: 90%;" class="input_box" *ngIf="!isSelected(row[column])"
                                    [(ngModel)]="row[column]" [ngModelOptions]="{standalone: true}"
                                    [value]="row[column]" ngDefaultControl>
                                <!-- 该按钮和input框绑定同时出现和消失,可以清空id0的值,这样input框就会变为自动匹配框 -->
                                <button *ngIf="!isSelected(row[column])" matSuffix mat-icon-button aria-label="Clear"
                                    (click)="reset(row,'ml')">
                                    <mat-icon style="width: 16px;height: 16px;">close</mat-icon>
                                </button>
                            </div>
                        </div>
                        <input class="input_box" *ngIf="column !== 'button' && column !== 'id0' && column !== 'id14'"
                            [(ngModel)]="row[column]" [ngModelOptions]="{standalone: true}" [value]="row[column]"
                            ngDefaultControl>
                        <div *ngIf="column == 'id14'" class="img_box" style="width: 70px; height: 30px;">
                            <!-- 图片上传组件 -->
                            <upload-img *ngIf="showImgBox" [options]="row[column]" [detailType]="" [multiple]=false
                                (receiveImg)="importImg($event,row.position,'ml')">
                            </upload-img>
                        </div>
                        <div *ngIf="column == 'button'">
                            <span style="color:#039BE5;cursor: pointer;" (click)="copyData(row,'ml')">复制</span>
                            <span style="color:red;cursor: pointer; margin-left: 15px;"
                                (click)="removeData(row.position,'ml')">删除</span>
                        </div>
                    </mat-cell>
                </ng-container>
                <mat-header-row *matHeaderRowDef=" displayedColumns"></mat-header-row>
                <mat-row *matRowDef="let row; columns: displayedColumns;" matRipple>
                </mat-row>
            </mat-table>

            <div fxLayout="row" fxLayoutAlign="center center" style="margin-top:15px;">
                <button mat-raised-button class="mat-accent mt-5 mb-8" (click)="addData('ml')"> +
                    添加数据</button>
            </div>
        </div>

其中self-autocomplete组件非常简单就不贴代码了,就是一个自动匹配框,点击后返回对应的值(但同事写的这个组件复用性极差,我最开始用他写的,但到后面如果更换工作量太大,不得不一直用他的)
但自动匹配的触发事件需要渲染对应的值到同一行的框中:

// 监听输入匹配下拉
  receiveCompleteML(list, row): void {
    if (list && list.value) {
      const arr = this.toArry(list.value);
      // 将下拉框对应的数据覆盖对应position的数据
      this.mlDataSource['mlData'].splice(row['position'], 1, {
        position: row['position'],
        id0: list['value'],
        id15: row['id15'],
        id16: arr[0],
        ……
      });

      // 刷新表格
      this.mlTable.renderRows();
    }
  }

这时候出现了一个非常严重的问题,就是第一列自动匹配框虽然选中了,但渲染另外几个单元格后,表格刷新,自动匹配框没有传值(同事写的垃圾组件),自动匹配框就不会显示值了,所以这里我采用了一个组件切换的方式:
也就是当id0(第一列的字段)为空时,显示自动匹配框,当不为空时,显示input框,input框可以设置[(ngmodel)]绑定id0显示,这样就既可以使用自动匹配,又可以显示对应的值了。

html中

    <!-- isSelected用于判断id0是否有值,若没有值则显示自动匹配框 -->
    <self-autocomplete *ngIf="isSelected(row[column])" [searchList]="searchList 
    [options]="materialList" (receiveList)="receiveCompleteML($event,row)"></self-autocomplete>
    <!-- isSelected用于判断id0是否有值,有值则将对应的值放进input框显示 -->
    <div style="display: flex; justify-content: center;align-items: center; ">
        <input style="width: 90%;" class="input_box" *ngIf="!isSelected(row[column])"
        [(ngModel)]="row[column]" [ngModelOptions]="{standalone: true}"
        [value]="row[column]" ngDefaultControl>
    <!-- 该按钮和input框绑定同时出现和消失,可以清空id0的值,这样input框就会变为自动匹配框 -->
    <button *ngIf="!isSelected(row[column])" matSuffix mat-icon-button aria-label="Clear"
        (click)="reset(row,'ml')">
        <mat-icon style="width: 16px;height: 16px;">close</mat-icon>
    </button>
</div>

函数实现

// 当自动匹配框选中后,切换为input框
  // 之所以这么做是因为自动匹配框没有字段,选中后再刷新表格就会显示为空值
  isSelected(row): boolean {
    let result: boolean;
    if (row === '' || row === undefined || row === null) {
      result = true;
    } else {
      result = false;
    }

    return result;
  }

后面的X是重置按钮,重置的思路也很简单,用索引判断数据的位置,使用splice删除对应的数据,再新增一条id0为空其他不变的数据即可。

复制、删除、新增的思路

复制:获取当前索引的数据,将一条索引为当前数组length的相同数据push进数组

// 面料表格的复制按钮
  copyData(row, type): void {
      // 复制一条数据push进去并设置好对应的索引
      this.mlDataSource['mlData'].push({
        position: this.mlDataSource['mlData'].length,
        id0: row['id0'],
        id15: row['id15'],
        ……
      });
      // 刷新表格
      this.mlTable.renderRows();
  }

删除:获取当前索引的数据,删除,数据前半部分的数据索引不变,后半部分的数据索引每条都-1

// 面料表格的删除按钮
  removeData(position, type): void {
    // 用于存储在索引之前的数据
    const mlData1 = [];
    // 用于存储在索引之后的数据
    const mlData2 = [];
      // 删除对应索引的数据
      this.mlDataSource['mlData'].splice(position, 1);
      // 将索引前后的数据分别临时存储起来
      this.mlDataSource['mlData'].forEach(index => {
        if (Number(index['position']) < Number(position)) {
          mlData1.push(index);
        } else {
          mlData2.push(index);
        }
      });

      // 先将索引前的数据放进表格数组里
      this.mlDataSource['mlData'] = [...mlData1];
      // 将索引后的数据索引依次-1后放进数组
      mlData2.forEach(index => {
        this.mlDataSource['mlData'].push({
          position: index['position'] - 1,
          id0: index['id0'],
          ……
        });
      });

      // 刷新表格
      this.mlTable.renderRows();
  }

新增:直接在数据末尾push一条索引为当前表格length的空数据即可,很简单所以就不贴代码了

总结

这次优化让我领悟到了写注释和命名规范的重要性(虽然我平时确实写,但以后会写的更详细了)以及,工作时间久并不能代表思路和能力就强,也有可能是真的水货。
在技术上,MatTable我觉得一直不太好用的地方就是无法进行实时操作,只能用于只读渲染,原来只是当时自己的能力不足,现在经过这次优化等下一次类似需求的出现就可以更加熟练了!