最近在做新版表格的列拖动,虽然以前做过一个版本,但是实现的比较粗糙,目前还是有一些问题(拖拽卡顿,拖动后的宽度不符合预期),所以在做新版表格的时候就想着这块需要重新设计。
column-resize
表格列拖动定义为一个独立的模块,命名为column-resize。
目前的方案参照cdk-experimental
、material-experimental
的实现,目前还属于实验性的功能,而且是基于Angular 9.0之后的版本开发的,虽然还不是很稳定,但是他们的方案还是比较先进的,我认为主要有一下几个点值得学习:
- business abstraction(业务抽象)
- rxjs流的运用
- angular new feature(Angular新特性)
business abstraction
列拖动调整宽度本身是表格的功能,简单的做法肯定是在表格上直接支持,通过参数控制拖拽是否可用,这也是我们第一版的做法,代码在表格组件上直接撸,material-design则是提供一个无侵入的指令,如果想支持拖拽只需要把对应指令加上就OK,所有拖拽逻辑的实现完全在附加指令上完成。
我也是照这个思路做的,使用大概示例:
table附加theColumnResize指令
<div theTable theColumnResize [context]="context">
<ng-template
[ngTemplateOutlet]="context.childrenTemplate"
[ngTemplateOutletContext]="context"
></ng-template>
</div>
cell 附加 theResizable 指令
<ng-template #tableCell let-context="context">
<td theTd theResizable [context]="context">
<ng-template
[ngTemplateOutlet]="context.childrenTemplate"
[ngTemplateOutletContext]="context"
></ng-template>
</td>
</ng-template>
rxjs 流的运用
column-resize可以称得上是复杂交互的场景,下面我简单的介绍下用到的用到的流(有机会在做个图):
- resize前的流:mouseover、mouseleave
这个主要处理表格的鼠标事件,然后动态创建和销毁拖拽标准线
- resize处理流:mousedown、mousemove、mouseup、keyup(实现按
esc
取消拖动)
这是拖拽的标准事件,而且mousemove、mouseup、keyup事件一般绑定在document上
- resize通知流:resizeCanceled、resizeCompleted、triggerResize
这个是分发拖动状态的流,这样设计就意味着我实现拖动的逻辑和具体修改表格列的逻辑以及其它细节处理可以在任何合适的地方,只需要订阅对应的流就可以。
- resize状态流:tableCellHoveredOrActiveDistinct
这个流设计的比较精密
首先这个流是一个组合流,组合了单元格的hovered状态和handle-thumb的激活状态(拖动中),当hovered 或者 active时 对应的handle-thumb保持不变,否则执行销毁(handle-thumb是通过overlay动态创建组件实现,所以销毁这个词),而且同一时间有一个单元格对应handle-thumb组件会被创建。
组合的主要逻辑
readonly tableCellHoveredOrActiveDistinct = combineLatest(
this.tableCellHovered.pipe(startWith(null), distinctUntilChanged()),
this.overlayHandleActiveForCell.pipe(startWith(null), distinctUntilChanged())
).pipe(
skip(1), // Ignore initial [null, null] emission.
map(([hovered, active]) => active || hovered),
distinctUntilChanged(),
share()
);
private readonly _tableCellHoveredDistinctReenterZone = this.tableCellHoveredOrActiveDistinct.pipe(this._enterZone(), share());
单元格各自状态的订阅,根据单元格对应的DOM引用进行distnct,简直完美!
/**
* Emits whether the specified row should show its overlay controls.
* Emission occurs within the NgZone.
*/
resizeOverlayVisibleForTableCell(cell: Element): Observable<boolean> {
if (cell !== this._lastSeenCell) {
this._lastSeenCell = cell;
this._lastSeenCellHover = this._tableCellHoveredDistinctReenterZone.pipe(
map(hoveredCell => hoveredCell === cell),
distinctUntilChanged(),
share()
);
}
return this._lastSeenCellHover!;
}
提供了一个rxjs的operator,DOM事件监控时是要runOutsideAngular,订阅的逻辑处理需要运行在变化检测之内。
private _enterZone<T>(): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) =>
new Observable<T>(observer =>
source.subscribe({
next: value => this.ngZone.run(() => observer.next(value)),
error: err => observer.error(err),
complete: () => observer.complete()
})
);
}
angular new feature
这里简单介绍下,我感觉惊讶的代码没错这里定义了一个抽象类,而且它是一个指令了
/**
* Base class for ColumnResize directives which attach to mat-table elements to
* provide common events and services for column resizing.
*/
@Directive()
export abstract class ColumnResize implements AfterViewInit, OnDestroy {
ngAfterViewInit() {
}
ngOnDestroy() {
// this.destroyed.next();
// this.destroyed.complete();
}
}
继承上面的抽象类
@Directive({
selector: 'table[cdk-table][columnResize]',
providers: [
...TABLE_PROVIDERS,
{provide: ColumnResize, useExisting: CdkColumnResize},
],
})
export class CdkColumnResize extends ColumnResize {
constructor(
readonly columnResizeNotifier: ColumnResizeNotifier,
readonly elementRef: ElementRef<HTMLElement>,
protected readonly eventDispatcher: HeaderRowEventDispatcher,
protected readonly ngZone: NgZone,
protected readonly notifier: ColumnResizeNotifierSource) {
super();
}
}
按照我的理解Angular基类中是写不了组件声明周期的钩子函数的,这里他们这么做了,我按照这种做法在项目中试了试,发现编译报错(Angular 8.*),所以猜测这是Angular9以后的新支持的特性。
其它
这里想介绍从column-resize学到的另外一个知识,这个灵感来源于matColumnDef
,这是mateiral表格列定义指令,看下它的使用
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef [matResizableMaxWidthPx]="100"> No. </th>
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
</ng-container>
最初看到的时候不理解这是什么操作,后面发下这个Def有点厉害,可以解决我遇到的一个痛点。
就是当我需要在两个或者多个同级组件共享一些服务的时候,我可以定义一个他们的容器指令,把服务挂在它上面,然后使用这个指令包裹这些同级组件,不产生任何副作用,同时使用ng-container也不产生任何额外DOM,可以说完美。这是一个思路问题,以前没想到可以这么做。
---- 更新
@Hello Money 这是实现效果图(评论中好像不能发动图),我是针对富文本编辑器场景做的表格