用 TypeScript 写一个轻量级的 UI 框架之十三:Grid 表格组件(下)

脏数据处理

Grid 的多行数据,修改后要提交到后台。如果大批量的数据一次性提交到后台恐怕不大合理。如果只是修改过那行的数据才提交过去,是比较合理的方式。这些修改的数据,我们称为“脏数据(Dirty data)”。脏数据本质也是一个 map、一个 json。

/**
 * 某个记录的修改过的部分,其中 id 用于标识,
 * 其余字段就是修改过的 key & value
 */
interface DirtyData extends JsonParam {
    /**
     * 修改过记录的 id 标识
     */
    id: string;
}

在 Grid 里实现脏数据机制的要点是:如何知晓哪些是修改过数据,其余的问题就不复杂了。

Vue 提供 watch 监视数据的变化,——这特性很符合我们解决这个问题的要求。我们把有变化的值保存到一个 map 之中,这个 map 又带有 id 所以知道是修改哪一行的记录。如果是 Vue 传统的配置项写法,很难符合我们 Grid 大批量数据的方式,总不能手工逐个登记吧?不过幸好 Vue 提供 $watch() 手动方法登记监视数据。

mounted(): void {
    for (var i in this.rowData) // 监视每个字段
        this.$watch('rowData.' + i, makeWatch.call(this, i));
}

makeWatch 是生成监视函数的高阶函数。

/**
 * 生成该字段的 watch 函数
 * 
 * @param this 
 * @param field 
 */
function makeWatch(this: GridEditRow, field: string): (_new: any) => void {
    return function (this: GridEditRow, _new: any) {
        let arr: GridRecord[] = this.$parent.list,
            data!: GridRecord;

        for (let i = 0, j = arr.length; i < j; i++) {// 已知 id 找到原始数据
            if (this.id && (String(arr[i].id) == this.id)) {
                data = arr[i];
                break;
            }
        }

        if (!data)
            throw '找不到匹配的实体!目标 id: ' + this.id;

        if (!data.dirty)
            data.dirty = { id: this.id };

        data.dirty[field] = _new; // 保存新的值,key 是字段名
    }
}

当前设计是在原 Grid 记录那一行添加 dirty 的 map 记录脏数据。

/**
 * 行记录数据
 */
interface GridRecord extends BaseObject {
    /**
     * 修改过记录
     */
    dirty?: DirtyData;
}

或者是否没必要找到原始数据呢?有待优化~~

剩下的 CRUD 代码就简单了,举个例子。

/**
 * 保存脏数据
 */
onDirtySaveClk(): void {
    let dirties: GridRecord[] = getDirty.call(this);

    if (!dirties.length) {
        msg.show('没有修改过的记录');
        return;
    }

    dirties.forEach((item: GridRecord) => {
        xhr.put(`${this.apiUrl}/${item.id}/`, (j: RepsonseResult) => {
            if (j.isOk) {
                this.list.forEach((item: GridRecord) => { // clear
                    if (item.dirty)
                        delete item.dirty;
                });

                msg.show('修改记录成功');
            }
        }, item.dirty);
    });
}

动态组件渲染

笔者学艺未精,在未理解 Vue 动态组件渲染之前,走了一段弯路:避开了原生的方法,去用另外一个动态组件的方法来做,也算是一种别致的方法。这种方法使用过程如下,算是在这里记录一下。

首先定义一个组件:

/**
 * 动态组件
 */
Vue.component('aj-cell-renderer', {
	props: {
		html: { type: String, default: '' },
		form: Object
	},
	render(h: any) {
		if (this.html.indexOf('<aj-') != -1) {
			let com = Vue.extend({
				template: this.html,
				props: {
					// form: Object
				}
			});

			return h(com, {
				props: {
					// form: this.form
				}
			});
		} else {
			return this._v(this.html); // html
		}
	}
});

使用组件:

<aj-cell-renderer v-if="!isEditMode || !cellRenderer.editMode" :html="cellRenderer.renderer(rowData)"></aj-cell-renderer>

实话说笔者也搞不懂这个组件的原理(网上摘抄回来的代码),总之可以输入字符串的组件如 cellRenderer.renderer(rowData) 返回的结果:<aj-avatar /> 便可动态渲染组件了。

这是老的方法,在查阅 Vue 文档的时候记得 Vue 本身就支持动态组件的,不知道当时编码遇到什么状况,居然调不出原生方法,现在重构之后,代码更加清晰,于是试着重新使用原生方法,谁知道一调就通,——笔者也百思不得其解。新方法如下。

<span v-if="cellRenderer && cellRenderer.isComponent">
   <component v-if="!isEditMode || !cellRenderer.editMode" v-bind:is="cellRenderer.renderer(rowData)"></component>
   <component v-if="isEditMode && cellRenderer.editMode"   v-bind:is="cellRenderer.editRenderer(rowData)"></component>
</span>

关键在于 component 的 v-bind:isv-bind,分别对应组件的名字及其 props,事件也是同样道理,使用 v-on。调用者的配置和例子如下。


/**
 * 复杂的单元格渲染器
 */
interface CellRendererConfig {
    /**
     * 传入一个 Vue 的组件去参与渲染
     */
    cmpName: string;

    /**
     * 编辑状态下使用何种组件?
     */
    editCmpName?: string;

    /**
     * 组件所依赖的 props 
     */
    cmpProps: (j: JsonParam) => JsonParam;

    /**
     * 可否被编辑编辑
     */
    editMode: boolean;

    key?: CellRendererKey;

    /**
     * 数据类型
     */
    type?: String | Number | Boolean;
}

let avatar: CellRendererConfig = {
    cmpName: "aj-avatar",
    editMode: false,
    cmpProps(data: JsonParam): JsonParam {

        let avatar = <string>data.avatar,
            prefix = '';//'${aj_allConfig.uploadFile.imgPerfix}'

        avatar = "https://static001.geekbang.org/account/avatar/00/10/10/51/9fedfd70.jpg?x-oss-process=image/resize,w_200,h_200";

        if (!avatar)
            return { avatar: "" };

        if (avatar.indexOf('http') === -1)
            avatar = prefix + avatar;

        return { avatar };
    }
};

回归简单,还是原生 Vue 的 API 方法好。

新建行记录

在 Table 中插入一个新的 <tr> 元素专用于新建记录,这个行也是一个标准 Vue 组件(位于 inline-edit-row-creat.ts),用法如下。

<tr v-show="grid.showAddNew" 
	is="aj-grid-inline-edit-row-create" 
	:columns="['name', 'rate', null]" create-api=".">
</tr>

为了更好地参与到 <table> 元素中,还是使用 <tr> 元素比较妥当,然后通过 Vue 提供的 is 指令即可等价于声明组件。使用该组件要什么哪些字段是可以创建的,在属性 columns 中指定。

这里稍微说说题外话,Vue 一大特色是与标签强烈耦合,话说前端 Web 开发,很多时候都是在和 HTML 打交道,最终产物还是在浏览器渲染出来的 DOM,而不管你使用何种手段,是后台语言 Java 还是 JavaScript、还是 TypeScript……终究还是化身成为 DOM。一个稍有规模的工程,一般都不会只用一种语言来从头写到尾,我们开发者要处理多种语言是家常便饭的事情,最难的就是如何协调好它们。也就是说,用一种语言去控制另外一种语言,是非常常见的,有时它可能还不是一种语言,只算是小型的 DSL(Domain-specific language 领域特定语言)。

类似的场景,好比 SQL,我们一般都不会直接去写,而通过 ORM 等方法去控制 SQL——最终数据库只认 SQL。如今换作 Vue,其目的同样也是,怎么更好地控制 HTML 标签。Vue 给出的各种手段,例如标签化的组件、各种方式的数据绑定、事件绑定、props 属性输入……可以看出 Vue 的立场还是非常尊重标签的,从而鼓励大家使用标签这种声明式的编码方法去构建 UI,某种角度讲,它就是一种“用标签生成标签”的理论。

“用一种语言去控制另外一种语言”,——我们无法回避,也司空见惯——焦点是如何更优雅地去处理、控制、协调,那才是最难的。幸运地是,Vue 向我们给出的答案尚算不赖,甚至让笔者觉得,后端的 SQL/ORM 则没有那么优雅的方案了……

工具条

工具条 Toolbar 如下图所示。它本身提供一些默认的按钮(CRUD)。
在这里插入图片描述

另外你也可以通过 Vue 的 slot 机制添加自定义的按钮。

<div class="box">
	<!-- 菜单工具栏-->
	<aj-entity-toolbar :create="false" :save="false"> 
		<li onclick="window.open('${ctx}/user/register/');"> 用户注册</li>
		<li class="fa fa-user-o" onclick="USER_GROUP.$refs.layer.show();"> 用户组</li>
		<li class="fa fa-user-circle-o" onclick="ASSIGN_RIGHT.$refs.assignRight.show();"> 角色分配</li>
	</aj-entity-toolbar>
</div>

效果如下图所示。

原理

该组件不复杂,源码如下所示(位于 toolbar.ts)。

namespace aj.list.grid {
    /**
     * 工具条
     */
    export interface GridToolbar {
        $el: HTMLElement;
        $parent: Grid;
    }

    /**
     * 工具条
     */
    Vue.component('aj-entity-toolbar', {
        template: html`
            <div class="toolbar">
                <form v-if="search" class="right">
                    <input type="text" name="keyword" placeholder="请输入关键字" size="12" />
                    <button @click="doSearch"><i class="fa fa-search" style="color:#417BB5;"></i>搜索</button>
                </form>
                <aj-form-between-date v-if="betweenDate" class="right"></aj-form-between-date>
                <ul>
                    <li v-if="create" @click="$emit('on-create-btn-clk')"><i class="fa fa-plus" style="color:#0a90f0;"></i> 新建</li>
                    <li v-if="save" @click="$emit('on-save-btn-clk')"><i class="fa fa-floppy-o"
                            style="color:rgb(205, 162, 4);"></i>保存</li>
                    <li v-if="deleBtn" @click="$emit('on-delete-btn-clk')"><i class="fa fa-trash-o" style="color:red;"></i> 删除</li>
                    <li v-if="excel"><i class="fa fa-file-excel-o" style="color:green;"></i> 导出</li>
                    <slot></slot>
                </ul>
            </div>
        `,
        props: {
            betweenDate: { type: Boolean, default: true },
            create: { type: Boolean, default: true },
            save: { type: Boolean, default: true },
            excel: { type: Boolean, default: false },
            deleBtn: { type: Boolean, default: true },
            search: { type: Boolean, default: true }
        },
        methods: {
            /**
             * 获取关键字进行搜索
             * 
             * @param this 
             * @param ev 
             */
            doSearch(this: GridToolbar, ev: Event): void {
                ev.preventDefault();

                aj.apply(this.$parent.$store.extraParam, { keyword: form.utils.getFormFieldValue(this.$el, 'input[name=keyword]') });
                this.$parent.$store.reload();
            }
        }
    });
}

主要是子组件与父组件之间的事件通讯,采用 $emit() 触发事件,然后在 Grid 组件实例中捕获事件,自定义处理。为什么要自定义处理?因为按钮事件会按照实际情形有不同逻辑,硬是封装一起没必要,不如开放给用户去自定义。当然也可以作一些适当的封装,例如下例的“新建”按钮,点击后触发的事件是 Grid 默认的 onCreateClk() 方法。

<!-- 菜单工具栏-->
<aj-entity-toolbar @on-create-btn-clk="grid.onCreateClk" @on-save-btn-clk="grid.onDirtySaveClk"></aj-entity-toolbar>

这里的 grid 指向 aj-grid 组件实例,而不是最外层的 Vue 实例。值得注意的是它通过 Vue “插槽 slot”暴露出来的:<template v-slot="{grid}">,在 aj-grid 组件创建 slot 时候就如此声明了,注意 v-bind:grid="this" 指向当前 Grid 实例。

template = '<div class="aj-grid"><slot v-bind:grid="this"></slot></div>';
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页