(原创)mat-table-with-d3的示例以及code历程

2019-04-11  本文已影响0人  mona_alwyn

代码repo地址:https://github.com/kxc1573/mat-table-with-d3-sample

foreword

Recently, I got my first formal frontend task to implement a tree-table by mat-table and to draw chart of detail row by d3.
The technologies involved included Angular, Material table, animation and d3, they were all new to me.
I quickly picked up these skills with examples had been implemented by my colleagues.
But I still had doubts about some details, so I implemented a new demo from zero.
The code is mat-table-with-d3-sample, and I write this document to record the implementation process.

1. 需求设定

假设我们需要实现一个这样的表:

2. 初始化(branch step0)

1)项目初始化

Angular的开发可以参考教程(Tutorial-ESTutorial-CN)。

2)读取数据

post.service中实现数据读取操作,代码如下:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'

@Injectable({
  providedIn: 'root'
})
export class PostService {

  constructor(private http: HttpClient) { }

  readData() {
    return this.http.get('assets/data.json')
  }
}

Key Point:

post.readData()
    .subscribe( res => {
        console.log(res);
    })

3)页面初始化

使用mat-cardmat-grid-list进行页面布局,具体参考mat-card文档mat-grid-list文档
mat-grid-list的使用如下:

<mat-grid-list cols="3" rowHeight="40px">
    <mat-grid-tile [colspan]="2" [rowspan]="1">
    </mat-grid-tile>
    <mat-grid-tile [colspan]="1" [rowspan]="1">
    </mat-grid-tile>
<mat-grid-list>

Key Point:

此处效果如图2


图2

3. mat-table简单实现(branch step1)

1) 表格实现

上一步中已经完成了数据的提供和页面的布局,剩下的就是实现表格展示数据了。
一般的table是逐行实现的,下面是w3school的一个示例:

<table border="1">
  <tr>
    <th>Month</th>
    <th>Savings</th>
  </tr>
  <tr>
    <td>January</td>
    <td>$100</td>
  </tr>
</table>

mat-table的实现思路则不一样:它是逐列来定义的,包括表头和单元格数据;然后再按行提供数据来完成表格的渲染。
下面是demo的表格代码,具体参考mat-table文档

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

  <!-- Name Column -->
  <ng-container matColumnDef="name">
    <th mat-header-cell class="cell_name" *matHeaderCellDef>Name</th>
    <td mat-cell *matCellDef="let element">{{element.name}}</td>
  </ng-container>

  <!-- Score Columns -->
  <ng-container matColumnDef="{{column}}" *ngFor="let column of dataHeader">
    <th mat-header-cell class="cell_content" *matHeaderCellDef>{{column}}</th>
    <td mat-cell *matCellDef="let element"> {{element[column]}}</td>
  </ng-container>

  <tr mat-header-row *matHeaderRowDef="headers"></tr>
  <tr mat-row *matRowDef="let element; columns: headers;"
      class="example-element-row">
  </tr>
</table>

Key Point:

2)数据处理

为了能在表格中展示,当然需要将读取到的数据进行相应的修改,如何修改就不细说了。上文提到过数据读取是异步操作,这就意味着在数据返回之前页面就已经完成渲染了,拿到数据之后需要刷新页面才能将数据显示出来。而通过dataSource来提供数据是不会主动检查数据的更新的,原文是这么说的:

 If you are providing a data array directly to the table, don't forget to call renderRows() on the table, since it will not automatically check the array for changes.

但是具体如何调用,却没有说明,也是困扰了我许久,幸得同事ZhenYi指点迷津,代码如下:

import { ViewChild } from '@angular/core';

...

  @ViewChild(MatTable) table: MatTable<any>;

  ...

  this.table.renderRows();

...

此处的效果图如下


图3

4.嵌入动画事件显示子行数据(branch step2

经过上一步,基本的表格已经实现了,但只有一层数据,这一步要实现的效果就是点击后展开第二层数据。
由于之前的demo中有用到angular/animations来实现点击展开事件,所以我也依葫芦画瓢地用了一番,然后由于当时对mat-table的理解不够,第二层数据是通过原生的table嵌入在动画事件中来实现的。
angular/animations也是一个巨坑,animation文档我也没看,不过读了这篇angular-animations 动画 BrowserAnimationsModule 详解,这里就不详细说了。

这一部分的代码改动主要就是三部分:

1)在table.component.ts中定义触发器

import { animate, state, style, transition, trigger } from '@angular/animations';

...

  animations: [
    trigger('detailExpand', [
      state('collapsed', style({height: '0px', minHeight: '0', visibility: 'hidden'})),
      state('expanded', style({height: '*'})),
      transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
    ]),
  ]

2)在table.componenet.html添加子行数据的行并绑定事件

这里首先将两层数据定义为parentRowchildRow,并添加了一个扩展标志位属性expand
然后加了when: isParentRowwhen: isChildRow作为过滤条件
其次增加了是否expandedElement的判断
还添加了点击事件Click,其中包含了当前行expand取反和数据更新updateChildRow操作
最后就是添加了childRow专属的行,所提供的数据同parentRow是不一样的,因为内部的实现也是不一致的。

  <tr mat-row *matRowDef="let element; let i = dataIndex; columns: headers; when: isParentRow"
      class="example-element-row"
      [class.example-expanded-row]="expandedElement === element"
      (click)="element['expand'] = !element['expand']; updateChildRow(i, element)">
  </tr>
  <tr mat-row *matRowDef="let element; columns: ['expandedDetail'];  when: isChildRow"
      class="example-detail-row"></tr>

我认为isParentRowisChildRowupdateChildRow代码调用实现包含了函数式编程的思想。

3)用原生table在展开行中嵌入实现子行数据

这里关注点应该就是虽然同样为ng-container,但expandedDetail的格式是自成一格的,不只是定义一列,而是定义所有通过动画展开的行的全部格式,这与上面代码中columns: ['expandedDetail']是对应的。

  <!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
  <ng-container matColumnDef="expandedDetail">
    <td mat-cell *matCellDef="let element" [attr.colspan]="headers.length"
        class="zero2three">
      <div class="example-element-detail"
           [@detailExpand]="element['expand'] ? 'expanded' : 'collapsed'">
        <table>
          <tbody>
            <tr style="background-color: white; text-align: center">
              <td style="width:40px"></td>
              <td style="width:180px">{{element.name}}</td>
              <td style="width:58px" *ngFor="let column of dataHeader">{{element[column]}}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </td>
  </ng-container> 

4)Bug:行间存有黑线

如图4,如果parentRow未展开,那么行间就存在黑线,这是因为虽然childRow通过animation定义的高度为0px,但在html也是占了一行的,且行高1px,因而隐藏了几条childRow就有相应高度的黑线。

图4

5.动态更新数据显示子行数据(branch step3

对于行间黑线的问题,多次尝试从CSS角度解决均失败,最后依然是在阅读同事的代码时开窍——应该从数据的动态更新入手,具体参考代码中的updateChildExpand方法实现即可。

这里列出代码中我感觉好用的两个js编程技巧:

6. 使用D3动态画图(branch step4

step2step3中不同的展开childRow方法结合起来,就得到了我们实现tree-table-with-chart的思路:
点击parentRow更新dataSource展示childRow,点击childRow通过animation渲染D3动态绘制的detail chart
为了保证每次渲染都是正确的,dataSource都会由updateChildExpand(i, element)updateDetailGraph(element)两个方法进行更新,内部逻辑不复杂也不简单,此处不细说。

1)D3画图

D3的全称是Data-Driven Documents,用来画矢量图的,是一个贼拉牛逼的前端神器,具体的看D3官网吧,这里大致说一下我理解的画图实现。
首先当然是安装d3库,简单的npm install d3即可。
基本实现过程为

具体代码查看createChart方法的实现吧,看懂了就可以自行裁剪,看不懂去啃官方文档则更好了。

2)为每个childRow匹配一个detail chart

上面画图过程中说到,第一步就是要根据selectorId来新建一个svg,每一个childRow都要有自己的detail chart,那么就需要相应独立的selectorId。这里采用的方法是在数据处理时为每行数据加一个position属性作为行标,这样在html中通过如下代码即实现了自适应生成slectorId的功能了。

      <div class="example-element-detail"
           [@detailExpand]="element['expand'] ? 'expanded' : 'collapsed'">
        <div class='gia-chart-wrapper' style="width: 720px; height: 360px; float: left;">
          <div class="gia-chart-{{element['position']}}"></div>
        </div>
      </div>

Key Point:

<table mat-table [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8">
...
</table>

7.添加datepickerbranch step5

到了step4基本就实现预期目标了,不过当前设定的数据全部是2018年的,如果想显示更多年份数据的话,我们可以通过datepicker来选择时间,可以实现为只选择year的,同样Angular material有自己的matDatepicker,具体用法还是看matDatepicker文档
这里要强调的依然是一个bug,如图6所示,点击datepicker后的弹窗位置是不对的。

图6
最终在另一个同事HongYi的调研下找到了该issue的讨论和解答:angular-material-datepicker-popup-position

8.改进方向?

able's default role is grid, and it can be changed to treegrid through role attribute.
表格的默认角色是 grid,可以通过 role 属性来把它改为 treegrid

一个gridtreegrid让我有了些想法,不过怎么都没找到相应的解释,只能作罢。

9.最后

放上昨天在朋友圈看到的一张图“假如让写编程书的那群人来出数学书”,确实好多编程教程的基本操作就是先写Hello World,然后接着就是实战了。

上一篇 下一篇

猜你喜欢

热点阅读