|
1 | | -# Vue2 Datatable |
2 | | -> 做 Vue.js 下最好的 Datatable 组件 |
3 | | -
|
4 | | -## § 前言 |
5 | | -任何开源的 Datatable 都未必能满足所有的业务需求(否则也不会有这个项目了) |
6 | | -本 README 致力于让您在理解组件设计以及源码的基础上,可自行定制出适合您业务需求的 Datatable |
7 | | - |
8 | | -## § 快速体验 |
9 | | -我们以 [`example/Basic/index.vue`](example/Basic/index.vue) 为例,效果见 [demo](https://kenberkeley.github.io/vue2-datatable/example-dist) |
| 1 | +# Vue2 Datatable |
10 | 2 |
|
11 | | -```html |
12 | | -<template> |
13 | | - <div> |
14 | | - <code>query: {{ query }}</code> |
15 | | - <datatable v-bind="$data" /> |
16 | | - <!-- 上面的写法比下面的要优雅,来源见 https://github.com/vuejs/vue/issues/4962 |
17 | | - <datatable |
18 | | - :columns="columns" |
19 | | - :data="data" |
20 | | - :total="total" |
21 | | - :query="query"> |
22 | | - </datatable> |
23 | | - --> |
24 | | - </div> |
25 | | -</template> |
26 | | -<script> |
27 | | -import Datatable from 'vue2-datatable' |
28 | | -import mockData from '../_mockData' |
| 3 | +> The Best Datatable for Vue.js 2.x which never sucks |
29 | 4 |
|
30 | | -export default { |
31 | | - components: { Datatable }, |
32 | | - data: () => ({ |
33 | | - columns: [ |
34 | | - { title: 'User ID', field: 'uid', sort: true }, |
35 | | - { title: 'Username', field: 'name' }, |
36 | | - { title: 'Age', field: 'age', sort: true }, |
37 | | - { title: 'Email', field: 'email' }, |
38 | | - { title: 'Country', field: 'country' } |
39 | | - ], |
40 | | - data: [], |
41 | | - total: 0, |
42 | | - query: {} |
43 | | - }), |
44 | | - watch: { |
45 | | - query: { |
46 | | - handler (query) { |
47 | | - mockData(query).then(({ rows, total }) => { |
48 | | - this.data = rows |
49 | | - this.total = total |
50 | | - }) |
51 | | - }, |
52 | | - deep: true |
53 | | - } |
54 | | - } |
55 | | -} |
56 | | -</script> |
57 | | -``` |
58 | | - |
59 | | -## § 依赖 |
60 | | -* BootStrap 3.x + Font Awesome 4.x(全局引入) |
61 | | -* [lodash / orderBy](https://lodash.com/docs/4.17.4#orderBy) |
62 | | -* [replace-with](https://github.com/kenberkeley/replace-with) |
63 | | - |
64 | | -注:BootStrap 以及 Font Awesome 的可替换性极强,您完全可以使用其他库替代(一般就是改一下类名即可) |
65 | | - |
66 | | -## § 详解 |
67 | | - |
68 | | -### ⊙ 整体构造 |
69 | | -本 Datatable 的源码目录树 [`lib/`](lib/) 如下: |
70 | | - |
71 | | -``` |
72 | | -lib/ |
73 | | - ├─ HeaderSettings/ # 表头设置 |
74 | | - │ ├─ ColumnGroup.vue # 表头设置分栏组件 |
75 | | - │ └─ index.vue # 表头设置主体 |
76 | | - ├─ HeadSort.vue # 排序 |
77 | | - ├─ LimitSelect.vue # 每页显示记录数下拉选择框 |
78 | | - ├─ MultiSelect.vue # 行首多选框 |
79 | | - ├─ Pagination.vue # 分页 |
80 | | - └─ index.vue # Datatable 主体 |
81 | | -``` |
82 | | - |
83 | | -以 [`example/Advanced/index.vue`](example/Advanced/index.vue) 的 [demo](https://kenberkeley.github.io/vue2-datatable/example-dist/#advanced) 为例,标出对应的组件如下图所示: |
84 | | - |
85 | | - |
86 | | - |
87 | | -### ⊙ 配置项 |
88 | | -> 【 Vue.js 小技巧 】 |
89 | | -> 若 `HelloWorld` 组件中定义 `props: { hi: Boolean }` |
90 | | -> 则 `<hello-world hi />` 等同于 `<hello-world :hi="true" />` |
91 | | -> 显然地,前者在写法上更加优雅 |
92 | | -
|
93 | | -[`lib/index.vue`](lib/index.vue) 中的 `props` 如下: |
94 | | - |
95 | | -```js |
96 | | -props: { |
97 | | - columns: { type: Array, required: true }, // 列定义 |
98 | | - data: { type: Array, required: true }, // 当页纪录 (rows) |
99 | | - total: { type: Number, required: true }, // 记录总数 |
100 | | - query: { type: Object, required: true }, // 查询对象 |
101 | | - selection: Array, // 多项选择的容器 |
102 | | - summary: Object, // 汇总行数据 (summary row) |
103 | | - HeaderSettings: { type: Boolean, default: true }, // 是否显示表头设置组件 |
104 | | - Pagination: { type: Boolean, default: true }, // 是否显示分页相关组件 |
105 | | - xprops: Object, // 额外传给动态组件的东东 |
106 | | - supportBackup: Boolean, // 是否支持使用 LocalStorage 保存表头设置 |
107 | | - supportNested: Boolean, // 是否支持内嵌组件 (nested component) |
108 | | - tableBordered: Boolean // 是否添加 .table-bordered 类到 <table> 元素 |
109 | | -} |
110 | | -``` |
111 | | - |
112 | | -下面仅讲解 `columns` / `data` / `query` / `selection` / `xprops` 以及三种动态组件(`thComp` / `tdComp` / `nested component`) |
113 | | - |
114 | | -*** |
115 | | - |
116 | | -#### `:columns` |
117 | | -我们来对比一下 [`example/`](example/) 中的 [`Basic`](example/Basic/index.vue) 与 [`Advanced`](example/Advanced/index.vue) 的 `columns` 定义: |
118 | | - |
119 | | -```js |
120 | | -// example/Basic - 简单写法 |
121 | | -columns: [ |
122 | | - { title: 'User ID', field: 'uid', sort: true }, |
123 | | - { title: 'Username', field: 'name' }, |
124 | | - { title: 'Age', field: 'age', sort: true }, |
125 | | - { title: 'Email', field: 'email' }, |
126 | | - { title: 'Country', field: 'country' } |
127 | | -] |
128 | | - |
129 | | -// example/Advanced - 标准写法 |
130 | | -columns: [{ |
131 | | - groupName: 'Normal', |
132 | | - columns: [ |
133 | | - { title: 'Email', field: 'email', visible: false, thComp: 'FilterTh', tdComp: 'Email' }, |
134 | | - { title: 'Username', field: 'name', thComp: 'FilterTh' }, |
135 | | - { title: 'Country', field: 'country', thComp: 'FilterTh' }, |
136 | | - { title: 'IP', field: 'ip', visible: false, tdComp: 'IP' } |
137 | | - ] |
138 | | -}, { |
139 | | - groupName: 'Sortable', |
140 | | - columns: [ |
141 | | - { title: 'User ID', field: 'uid', sort: true, visible: 'true', weight: 1 }, |
142 | | - { title: 'Age', field: 'age', sort: true }, |
143 | | - { title: 'Create time', field: 'createTime', sort: true, |
144 | | - thClass: 'w-240', tdClass: 'w-240', thComp: 'CreatetimeTh', tdComp: 'CreatetimeTd' } |
145 | | - ] |
146 | | -}, { |
147 | | - groupName: 'Extra (radio)', |
148 | | - type: 'radio', |
149 | | - columns: [ |
150 | | - { title: 'Operation', tdComp: 'Opt' }, |
151 | | - // don't forget to set the columns below `visible: false`, since the `type` is `radio` |
152 | | - { title: 'Color', field: 'color', explain: 'Favorite color', visible: false, tdComp: 'Color' }, |
153 | | - { title: 'Language', field: 'lang', visible: false, thComp: 'FilterTh' }, |
154 | | - { title: 'PL', field: 'programLang', explain: 'Programming Language', visible: false, thComp: 'FilterTh' } |
155 | | - ] |
156 | | -}] |
157 | | -``` |
158 | | - |
159 | | -实际上 `Basic` 的这种简写最终都会被转为 `Advanced` 的标准形式(见源码 [`lib/index.vue`](lib/index.vue) 中的 `computed.columns$`) |
160 | | - |
161 | | -下面列出 `columns[i]` 中的可配置项: |
162 | | - |
163 | | -| 参数 | 说明 | 类型 | 可选项 | 默认值 | 是否必须 | |
164 | | -|---------|--------------------------------------------------|----------------|---------------------------|--------|----------| |
165 | | -| title | 显示名称 | String | - | - | 否 | |
166 | | -| field | 字段名称 | String | - | - | 否 | |
167 | | -| explain | 说明文字 | String | - | - | 否 | |
168 | | -| sort | 是否支持排序 | Boolean | - | false | 否 | |
169 | | -| weight | 显示排名权重 | Number | - | 0 | 否 | |
170 | | -| visible | 是否显示(若为字符串类型则禁止设置该列显隐状态) | Boolean / String | true / false / 'true' / 'false' | true | 否 | |
171 | | -| thClass | 用于 `<th>` 的类名 | String | - | - | 否 | |
172 | | -| thStyle | 用于 `<th>` 的内联样式 | String | - | - | 否 | |
173 | | -| thComp | 用于 `<th>` 的动态组件名 | String | - | - | 否 | |
174 | | -| tdClass | 用于 `<td>` 的类名 | String | - | - | 否 | |
175 | | -| tdStyle | 用于 `<td>` 的内联样式 | String | - | - | 否 | |
176 | | -| tdComp | 用于 `<td>` 的动态组件名 | String | - | - | 否 | |
177 | | - |
178 | | ->【 JS 小技巧 】 |
179 | | -> |
180 | | -```js |
181 | | -cols.map(col => { |
182 | | - if (!col.weight) col.weight = 0 |
183 | | - return col |
184 | | -}) |
185 | | -// 利用逗号运算符,可以把上面的代码缩写为一行 |
186 | | -cols.map(col => ((col.weight = col.weight || 0), col)) |
187 | | -``` |
188 | | -*** |
189 | | - |
190 | | -#### `:data` |
191 | | -实际上该项应该叫 `rows` 才合理,但主流的 Datatable 都是这样称呼,我也不能免俗 |
192 | | -本身该项是没啥好讲的,但本 Datatable 支持**无限递归内嵌组件**,靠的就是在这里做文章 |
193 | | -在此把源码 `lib/index.vue` 中的 `computed.data$` 直接搬出来: |
194 | | - |
195 | | -```js |
196 | | -data$ () { |
197 | | - const { data } = this |
198 | | - if (this.supportNested) { |
199 | | - // support nested components with extra magic |
200 | | - data.forEach(item => { |
201 | | - if (!item.__nested__) { |
202 | | - this.$set(item, '__nested__', { |
203 | | - comp: '', // name of nested component |
204 | | - visible: false, |
205 | | - $toggle (comp, visible) { |
206 | | - switch (arguments.length) { |
207 | | - case 0: |
208 | | - this.visible = !this.visible |
209 | | - break |
210 | | - case 1: |
211 | | - switch (typeof comp) { |
212 | | - case 'boolean': |
213 | | - this.visible = comp |
214 | | - break |
215 | | - case 'string': |
216 | | - this.comp = comp |
217 | | - this.visible = !this.visible |
218 | | - break |
219 | | - } |
220 | | - break |
221 | | - case 2: |
222 | | - this.comp = comp |
223 | | - this.visible = visible |
224 | | - break |
225 | | - } |
226 | | - } |
227 | | - }) |
228 | | - Object.defineProperty(item, '__nested__', { enumerable: false }) |
229 | | - } |
230 | | - }) |
231 | | - } |
232 | | - return data |
233 | | -} |
234 | | -``` |
235 | | - |
236 | | -由源码可知,我们对 `data (rows)` 内的各个元素 `item (row)` 设置了一个不可遍历属性 `__nested__`,包含以下三个属性: |
237 | | - |
238 | | -| 参数 | 说明 | 类型 | 可选项 | 默认值 | |
239 | | -|---------|--------------|----------|-------------------------------------------------------------|--------| |
240 | | -| comp | 内嵌组件名 | String | - | '' | |
241 | | -| visible | 是否显示 | Boolean | true / false | false | |
242 | | -| $toggle | 便捷操作函数 | Function | $toggle(comp) / $toggle(visible) / $toggle(comp, visible) | - | |
243 | | - |
244 | | -将 `__nested__` 作为 `props.nested` 传入到对应的 `tdComp` 与 `nested component` 中 |
245 | | -由此,在对应的动态组件内部即可通过 `nested.$toggle` 实现对 `nested component` 的控制 |
246 | | -(当然,您要直接操作 `row.__nested__.$toggle` 也是没问题的,都是同一个东西) |
247 | | - |
248 | | -*** |
249 | | - |
250 | | -#### `:query` |
251 | | -让我们来看看 Datatable 是如何初始化 `query` 的(见源码 [`lib/index.vue`](lib/index.vue) 中的 `created` 钩子函数): |
252 | | - |
253 | | -```js |
254 | | -created () { // init query |
255 | | - const { query } = this |
256 | | - const q = { limit: 10, offset: 0, sort: '', order: '', ...query } |
257 | | - Object.keys(q).forEach(key => this.$set(query, key, q[key])) |
258 | | -} |
259 | | -``` |
260 | | - |
261 | | -一般情况下,您只需要传入一个空对象 `{}` 即可。若还有其他查询条件(例如 `search`),则以下两种方式二选一: |
262 | | - 1. 在初始化时就传入 `{ search: '' }`(推荐) |
263 | | - 2. 自行使用 [`Vue.set / $vm.$set`](https://vuejs.org/v2/api/#Vue-set) 设置:`this.$set(this.query, 'search', '')` |
264 | | - |
265 | | -上述两种方式均在 `example/Advanced` 中有所体现,最终目的都是让额外的查询属性变成[响应式](https://vuejs.org/v2/guide/reactivity.html)的 |
266 | | -(其中第 2 种见 [`example/Advanced/comps/th-Filter.vue`](example/Advanced/comps/th-Filter.vue) 中的 `methods.search`) |
267 | | - |
268 | | -在此提一个常见的需求:实现刷新后保持查询条件 |
269 | | -最常见的解决方案是**同步查询条件到 URL**。拿 `example/Basic` 来说: |
270 | | - |
271 | | -```html |
272 | | -<template> |
273 | | - <div> |
274 | | - <code>query: {{ query }}</code> |
275 | | - <datatable v-bind="$data" /> |
276 | | - </div> |
277 | | -</template> |
278 | | -<script> |
279 | | -import Datatable from 'vue2-datatable' |
280 | | -import mockData from '../_mockData' |
281 | | -
|
282 | | -export default { |
283 | | - components: { Datatable }, |
284 | | - data () { |
285 | | - return { |
286 | | - columns: [ |
287 | | - { title: 'User ID', field: 'uid', sort: true }, |
288 | | - { title: 'Username', field: 'name' }, |
289 | | - { title: 'Age', field: 'age', sort: true }, |
290 | | - { title: 'Email', field: 'email' }, |
291 | | - { title: 'Country', field: 'country' } |
292 | | - ], |
293 | | - data: [], |
294 | | - total: 0, |
295 | | - query: this.$route.query // 初始化时传入 URL query(在业务中请注意安全性) |
296 | | - } |
297 | | - }, |
298 | | - watch: { |
299 | | - // 同步本地 query 到 URL query |
300 | | - query: { |
301 | | - handler (query) { |
302 | | - this.$router.push({ path: this.$route.path, query }) |
303 | | - }, |
304 | | - deep: true |
305 | | - }, |
306 | | - // 通过监听 URL query 的变化来重新获取数据 |
307 | | - '$route.query' (query) { |
308 | | - mockData(query).then(({ rows, total }) => { |
309 | | - this.data = rows |
310 | | - this.total = total |
311 | | - }) |
312 | | - } |
313 | | - } |
314 | | -} |
315 | | -</script> |
316 | | -``` |
317 | | - |
318 | | -*** |
319 | | - |
320 | | -#### `:selection` |
321 | | -一般情况下,您只需要传入一个空数组 `[]` 即可 |
322 | | -若有行被勾选,则对应的 `row` 将会进入到 `selection` 中 |
323 | | -假如您的产品经理要求默认就是全部勾选,也是没问题的。就以 `example/Advanced` 为例: |
324 | | - |
325 | | -```js |
326 | | -methods: { |
327 | | - handleQueryChange () { |
328 | | - mockData(this.query).then(({ rows, total, summary }) => { |
329 | | - this.data = rows |
330 | | - this.total = total |
331 | | - this.summary = summary |
332 | | - |
333 | | - // 就是这么简单! |
334 | | - this.$nextTick(() => this.selection = [...this.data]) |
335 | | - }) |
336 | | - } |
337 | | -} |
338 | | -``` |
339 | | - |
340 | | -*** |
341 | | - |
342 | | -#### `:xprops` |
343 | | -由于 `thComp / tdComp / nested component` 都是通过动态组件实现 |
344 | | -而业务需求又是不固定的,很可能需要传入很多额外的 props |
345 | | -那么,源码 [`lib/index.vue`](lib/index.vue) 中的模板,很有可能会演变成这样子: |
346 | | - |
347 | | -```html |
348 | | -<component |
349 | | - ... |
350 | | - :XXX="XXX" |
351 | | - :YYY="YYY" |
352 | | - :ZZZ="ZZZ" |
353 | | - :XYZ="XYZ" |
354 | | - :ZYX="ZYX" |
355 | | - ...><!-- 100+ extra props --> |
356 | | -</component> |
357 | | -``` |
358 | | - |
359 | | -再三斟酌下,我引入了 `xprops`,用于承载这些额外的 props,以避免污染源码 |
360 | | - |
361 | | -最常见的用处是,传入一个仅限于当前 Datatable 内部使用的 [eventbus](https://vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication),这样的话就不需要区分命名空间了 |
362 | | -(可以实现无限递归嵌套的 `example/Advanced` 就是通过这种方式来避免不必要的麻烦) |
363 | | - |
364 | | -## 深入 |
365 | | - |
366 | | -## 设计理念 |
367 | | -full ES5 |
368 | | -关键:扁平的动态组件(全局化 局部化) |
369 | | -反范式:允许子组件修改状态,毋须引入鸡肋的状态管理 |
370 | | -xprops 传递其余属性 |
371 | | -源码技巧 缩短 .map |
372 | | -同步 URL 技巧 |
| 5 | +[Documentation](https://OneWayTech.github.com/vue2-datatable/docs/_book) | |
| 6 | +[Online examples](https://OneWayTech.github.com/vue2-datatable/examples/dist) |
0 commit comments