├── _config.yml
├── .browserslistrc
├── babel.config.js
├── .eslintignore
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ └── css
│ │ └── tailwind.css
├── main.js
├── index.js
├── services
│ ├── debounce.js
│ ├── defaultClasses.js
│ └── CSSProcessor.js
├── components
│ ├── Cell.vue
│ ├── ChildrenButton.vue
│ ├── GroupCell.vue
│ ├── GroupRow.vue
│ ├── Row.vue
│ ├── HeaderCell.vue
│ ├── Table.vue
│ └── TableContainer.vue
├── mixins
│ ├── pagination.js
│ ├── flatten.js
│ ├── styling.js
│ ├── slots.js
│ ├── cell
│ │ ├── sortCell.js
│ │ └── cell.js
│ ├── exportData.js
│ ├── rows.js
│ ├── selection.js
│ ├── filter.js
│ ├── sort.js
│ ├── groupBy.js
│ ├── async.js
│ └── columns.js
├── App.vue
├── AsyncTableApp.vue
├── TableContainerApp.vue
└── BasicTableApp.vue
├── tailwind.config.js
├── tests
└── unit
│ ├── .eslintrc.js
│ ├── components
│ ├── ChildrenButton.test.js
│ ├── Row.test.js
│ ├── HeaderCell.test.js
│ ├── Cell.test.js
│ └── Table.test.js
│ └── services
│ └── CSSProcessor.test.js
├── .gitignore
├── .eslintrc.js
├── CONTRIBUTING.md
├── postcss.config.js
├── jest.config.js
├── LICENSE
├── package.json
├── CHANGELOG.md
└── README.md
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 8
4 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | tests/coverage
4 | babel.config.js
5 | tailwind.config.js
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/arnedesmedt/vue-ads-table-tree/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/css/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
6 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | prefix: 'vue-ads-',
3 | theme: {},
4 | variants: {},
5 | plugins: [],
6 | };
7 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './TableContainerApp';
3 |
4 | Vue.config.productionTip = false;
5 |
6 | new Vue({
7 | render: h => h(App),
8 | }).$mount('#app');
9 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './assets/css/tailwind.css';
2 |
3 | import VueAdsTableContainer from './components/TableContainer';
4 | import VueAdsTable from './components/Table';
5 |
6 | export default VueAdsTableContainer;
7 | export { VueAdsTable };
8 |
--------------------------------------------------------------------------------
/src/services/debounce.js:
--------------------------------------------------------------------------------
1 | export default (fn, time = 300) => {
2 | // Store active timeout
3 | let timeout;
4 |
5 | return (...args) => {
6 | clearTimeout(timeout);
7 |
8 | // Start a new timeout
9 | timeout = setTimeout(fn.bind(null, ...args), time);
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jest: true,
4 | },
5 | rules: {
6 | 'no-new': 'off',
7 | 'no-unused-expressions': 'off',
8 | 'no-template-curly-in-string': 'off',
9 | 'import/no-extraneous-dependencies': 'off',
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 |
5 | # test coverage
6 | tests/coverage
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw*
25 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 |
4 | env: {
5 | node: true,
6 | },
7 |
8 | extends: [
9 | 'plugin:vue/essential',
10 | '@vue/standard',
11 | 'eslint-config-ads',
12 | 'eslint-config-ads/vue',
13 | ],
14 |
15 | parserOptions: {
16 | parser: 'babel-eslint',
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/Cell.vue:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 |
4 | If you would like to contribute to the current project, follow these rules:
5 |
6 | - One pull request per issue (bug, feature, ...).
7 | - Create feature branches.
8 | - Test the changes if possible. Pull requests that doesn't pass the tests, will not be accepted (`npm run test:unit`).
9 | - Update the [README.md](README.md) file if necessary.
10 | - Update the [CHANGELOG.md](CHANGELOG.md) if necessary.
11 |
12 | Do you want to start now? Check the [issues tab](https://gitlab.com/arnedesmedt/vue-ads-table-tree/issues) in gitlab, fork and start coding!
13 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | tailwindcss: './tailwind.config.js',
5 | '@fullhuman/postcss-purgecss': {
6 | content: [
7 | './src/components/*.vue',
8 | './src/services/*.js',
9 | './node_modules/vue-ads-pagination/dist/vue-ads-pagination.common.js',
10 | './node_modules/vue-ads-form-builder/dist/vue-ads-form-builder.common.js',
11 | ],
12 | whitelistPatterns: [
13 | /^fa-sort(.*)$/,
14 | /^vue-ads-w-(\d+)\/4$/,
15 | ],
16 | },
17 | 'postcss-import': {},
18 | 'postcss-url': {},
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/src/mixins/pagination.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | start: {
4 | type: Number,
5 | validator: start => {
6 | return start >= 0 || start === null;
7 | },
8 | },
9 |
10 | end: {
11 | type: Number,
12 | validator: end => {
13 | return end >= 0 || end === null;
14 | },
15 | },
16 | },
17 |
18 | computed: {
19 | paginatedRows () {
20 | if (this.unresolved || (this.start === null && this.end === null)) {
21 | return this.sortedRows;
22 | }
23 |
24 | return this.sortedRows.slice(this.start, this.end);
25 | },
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | vue-ads-table-tree
10 |
11 |
12 |
13 | We're sorry but vue-ads-table-tree doesn't work properly without JavaScript enabled. Please enable it to continue.
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: [
3 | 'js',
4 | 'jsx',
5 | 'json',
6 | 'vue',
7 | ],
8 | transform: {
9 | '^.+\\.vue$': 'vue-jest',
10 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
11 | '^.+\\.jsx?$': 'babel-jest',
12 | },
13 | moduleNameMapper: {
14 | '^@/(.*)$': '/src/$1',
15 | },
16 | snapshotSerializers: [
17 | 'jest-serializer-vue',
18 | ],
19 | testMatch: [
20 | '/(tests/unit/**/*.test.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))',
21 | ],
22 | collectCoverage: true,
23 | collectCoverageFrom: [
24 | 'src/**/*.{js,vue}',
25 | '!src/*.{js,vue}',
26 | '!**/node_modules/**',
27 | ],
28 | coverageDirectory: '/tests/coverage',
29 | };
30 |
--------------------------------------------------------------------------------
/src/mixins/flatten.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export default {
4 | computed: {
5 | flattenedRows () {
6 | return this.flatten(this.groupedRows);
7 | },
8 | },
9 |
10 | methods: {
11 | async toggleChildren (row) {
12 | row._showChildren = !row._showChildren;
13 |
14 | if (!row._hasChildren) {
15 | return;
16 | }
17 |
18 | row._meta.loading = true;
19 | row._children = this.initRows(await this.callChildren(row), row);
20 | Vue.delete(row, '_hasChildren');
21 | row._meta.loading = false;
22 | },
23 |
24 | flatten (rows) {
25 | return rows
26 | .reduce((flattenedRows, row) => {
27 | return flattenedRows.concat([
28 | row,
29 | ...(row && row._showChildren ? this.flatten(row._meta.visibleChildren) : []),
30 | ]);
31 | }, []);
32 | },
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/src/mixins/styling.js:
--------------------------------------------------------------------------------
1 | import defaultClasses from '../services/defaultClasses';
2 | import CSSProcessor from '../services/CSSProcessor';
3 |
4 | export default {
5 | props: {
6 | classes: {
7 | type: Object,
8 | default: () => defaultClasses,
9 | },
10 | },
11 |
12 | watch: {
13 | classes: {
14 | handler: 'classesChanged',
15 | },
16 | },
17 |
18 | data () {
19 | return {
20 | cssProcessor: new CSSProcessor(this.columns.length, this.classes),
21 | };
22 | },
23 |
24 | computed: {
25 | tableClasses () {
26 | return this.classes.table || {};
27 | },
28 |
29 | headerRowClasses () {
30 | return this.cssProcessor.process(0);
31 | },
32 |
33 | infoClasses () {
34 | return this.classes.info || {};
35 | },
36 | },
37 |
38 | methods: {
39 | classesChanged (classes) {
40 | this.cssProcessor.classes = classes;
41 | },
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Arne De Smedt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/services/defaultClasses.js:
--------------------------------------------------------------------------------
1 | export default {
2 | table: {
3 | 'vue-ads-border': true,
4 | 'vue-ads-w-full': true,
5 | },
6 | info: {
7 | 'vue-ads-text-center': true,
8 | 'vue-ads-py-6': true,
9 | 'vue-ads-text-sm': true,
10 | 'vue-ads-border-t': true,
11 | },
12 | group: {
13 | 'vue-ads-font-bold': true,
14 | 'vue-ads-border-b': true,
15 | 'vue-ads-italic': true,
16 | },
17 | selected: {
18 | 'vue-ads-bg-teal-100': true,
19 | },
20 | 'all/': {
21 | 'hover:vue-ads-bg-gray-200': true,
22 | },
23 | 'all/all': {
24 | 'vue-ads-px-4': true,
25 | 'vue-ads-py-2': true,
26 | 'vue-ads-text-sm': true,
27 | },
28 | 'even/': {
29 | 'vue-ads-bg-gray-100': true,
30 | },
31 | 'odd/': {
32 | 'vue-ads-bg-white': true,
33 | },
34 | '0/': {
35 | 'vue-ads-bg-gray-100': false,
36 | 'hover:vue-ads-bg-gray-200': false,
37 | },
38 | '0/all': {
39 | 'vue-ads-px-4': true,
40 | 'vue-ads-py-2': true,
41 | 'vue-ads-text-left': true,
42 | },
43 | '0_-1/': {
44 | 'vue-ads-border-b': true,
45 | },
46 | '/0_-1': {
47 | 'vue-ads-border-r': true,
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/src/mixins/slots.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | slots: {
4 | type: Object,
5 | default: () => {
6 | return {};
7 | },
8 | },
9 | },
10 |
11 | computed: {
12 | currentSlots () {
13 | if (Object.keys(this.slots).length === 0) {
14 | return Object.assign(this.$slots, this.$scopedSlots);
15 | }
16 |
17 | return this.slots;
18 | },
19 |
20 | sortIconSlot () {
21 | return this.currentSlots['sort-icon'] || null;
22 | },
23 |
24 | toggleChildrenIconSlot () {
25 | return this.currentSlots['toggle-children-icon'] || null;
26 | },
27 |
28 | rowSlots () {
29 | let regexCell = new RegExp('^(' + this.columnProperties.join('|') + ')_', 'i');
30 | let regexRow = new RegExp('^_.+$', 'i');
31 | let slots = {};
32 |
33 | Object.keys(this.currentSlots)
34 | .forEach(slotKey => {
35 | if (this.columnProperties.includes(slotKey) || regexCell.test(slotKey) || regexRow.test(slotKey)) {
36 | slots[slotKey] = this.currentSlots[slotKey];
37 | }
38 | });
39 |
40 | return slots;
41 | },
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/mixins/cell/sortCell.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | sortIconSlot: {
4 | type: Function,
5 | default: null,
6 | },
7 | },
8 |
9 | computed: {
10 | sortable () {
11 | return [
12 | null,
13 | true,
14 | false,
15 | ].includes(this.column.direction);
16 | },
17 |
18 | sortIconClasses () {
19 | if (!this.sortable) {
20 | return {};
21 | }
22 |
23 | return {
24 | fa: true,
25 | 'vue-ads-ml-2': true,
26 | 'fa-sort': this.column.direction === null,
27 | 'fa-sort-down': this.column.direction === false,
28 | 'fa-sort-up': this.column.direction === true,
29 | };
30 | },
31 | },
32 |
33 | methods: {
34 | sortIcon (createElement) {
35 | return this.sortIconSlot ?
36 | this.sortIconSlot({
37 | direction: this.column.direction,
38 | }) :
39 | createElement(
40 | 'i',
41 | {
42 | class: this.sortIconClasses,
43 | on: {
44 | click: (event) => {
45 | event.stopPropagation();
46 | this.$emit('sort', this.column);
47 | },
48 | },
49 | },
50 | );
51 | },
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/src/mixins/exportData.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | exportColumns () {
4 | return this.columns.filter(column => column.export);
5 | },
6 | },
7 |
8 | methods: {
9 | exportTable (name, full) {
10 | if (!name) {
11 | return;
12 | }
13 |
14 | this.$emit(
15 | 'export',
16 | {
17 | fields: Object.assign({
18 | '#': '_order',
19 | }, this.exportFields()),
20 | data: this.exportData(full ? this.loadedRows : this.sortedRows, full),
21 | title: name,
22 | }
23 | );
24 | },
25 |
26 | exportFields () {
27 | return this.exportColumns
28 | .reduce((result, column) => {
29 | result[column.title] = column.property;
30 |
31 | return result;
32 | }, {});
33 | },
34 |
35 | exportData (rows, full, parent = '') {
36 | return rows
37 | .reduce((exportRows, row, index) => {
38 | let order = parent + (index + 1).toString();
39 | row._order = order + (parent === '' ? '-0' : '');
40 | return exportRows.concat([
41 | row,
42 | ...(full ?
43 | (row && row._children ? this.exportData(row._children, full, order + '-') : []) :
44 | (row && row._showChildren ? this.exportData(row._meta.visibleChildren, full, order + '-') : [])
45 | ),
46 | ]);
47 | }, []);
48 | },
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/ChildrenButton.vue:
--------------------------------------------------------------------------------
1 |
71 |
--------------------------------------------------------------------------------
/src/mixins/rows.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import uuid from 'uuid';
3 |
4 | export default {
5 | props: {
6 | rows: {
7 | type: Array,
8 | default: () => [],
9 | },
10 | },
11 |
12 | watch: {
13 | rows: {
14 | handler: 'rowsChanged',
15 | immediate: true,
16 | },
17 | },
18 |
19 | computed: {
20 | loadedRows () {
21 | return this.rows.filter(row => row);
22 | },
23 | },
24 |
25 | methods: {
26 | rowsChanged (rows, oldRows, parent) {
27 | this.initRows(rows, parent);
28 | },
29 |
30 | initRows (rows, parent) {
31 | rows
32 | .forEach((row, index) => this.initRow(row, parent, index));
33 | rows
34 | .filter(row => row._children.length > 0)
35 | .forEach(row => this.rowsChanged(row._children, null, row));
36 |
37 | return rows;
38 | },
39 |
40 | initRow (row, parent, index, groupColumn = null) {
41 | if (!row.hasOwnProperty('_children')) {
42 | Vue.set(row, '_children', []);
43 | }
44 |
45 | if (!row.hasOwnProperty('_showChildren')) {
46 | Vue.set(row, '_showChildren', false);
47 | }
48 |
49 | if (!row.hasOwnProperty('_selectable')) {
50 | let selectable = parent && parent.hasOwnProperty('_selectable') ? parent._selectable : this.selectable;
51 | Vue.set(row, '_selectable', selectable);
52 | }
53 |
54 | if (!row.hasOwnProperty('_meta')) {
55 | Vue.set(row, '_meta', {
56 | groupParent: 0,
57 | parent: parent ? parent._meta.parent + 1 : 0,
58 | uniqueIndex: uuid(),
59 | loading: false,
60 | visibleChildren: row._children,
61 | index,
62 | groupColumn,
63 | selected: false,
64 | });
65 | }
66 | },
67 | },
68 | };
69 |
--------------------------------------------------------------------------------
/src/mixins/selection.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | selectable: {
4 | type: Boolean,
5 | default: false,
6 | },
7 | },
8 |
9 | data () {
10 | return {
11 | firstSelectedRowId: undefined,
12 | };
13 | },
14 |
15 | methods: {
16 | clearSelection () {
17 | this.flatten(this.currentRows).forEach(row => row._meta.selected = false);
18 | },
19 |
20 | selectRows (rows) {
21 | rows.forEach(row => {
22 | if (row._selectable) {
23 | row._meta.selected = true;
24 | }
25 | });
26 | },
27 |
28 | selectRow (event, row, key) {
29 | if (! row._selectable) {
30 | return;
31 | }
32 |
33 | if (event.shiftKey) {
34 | let flatten = this.flatten(this.currentRows);
35 | let indexes = [
36 | row._meta.uniqueIndex,
37 | this.firstSelectedRowIndex,
38 | ];
39 | let minKey = Object.keys(flatten).find((key) => flatten[key]._meta.uniqueIndex === indexes[0]);
40 | let maxKey = Object.keys(flatten).find((key) => flatten[key]._meta.uniqueIndex === indexes[1]);
41 | let keys = [
42 | +minKey,
43 | +maxKey,
44 | ];
45 | [
46 | minKey,
47 | maxKey,
48 | ] = keys.sort((a, b) => a - b);
49 |
50 | this.clearSelection();
51 | this.selectRows(flatten.slice(minKey, maxKey + 1));
52 | } else {
53 | let oldSelected = row._meta.selected;
54 | if (! event.ctrlKey) {
55 | this.clearSelection();
56 | this.firstSelectedRowIndex = row._meta.uniqueIndex;
57 | }
58 |
59 | row._meta.selected = ! oldSelected;
60 |
61 | }
62 |
63 | this.$emit('selection-change', this.flatten(this.currentRows).filter(row => row._meta.selected));
64 | },
65 | },
66 | };
--------------------------------------------------------------------------------
/src/components/GroupCell.vue:
--------------------------------------------------------------------------------
1 |
84 |
--------------------------------------------------------------------------------
/src/mixins/filter.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | filter: {
4 | type: String,
5 | default: '',
6 | },
7 | },
8 |
9 | watch: {
10 | filter: {
11 | handler: 'filterChanged',
12 | immediate: true,
13 | },
14 | },
15 |
16 | computed: {
17 | isFiltering () {
18 | return this.filter !== '' && this.filterColumnProperties.length > 0;
19 | },
20 |
21 | filterRegex () {
22 | return new RegExp(this.filter, 'i');
23 | },
24 |
25 | filteredCurrentRows () {
26 | return this.unresolved ? this.currentRows.filter(row => row) : this.currentRows;
27 | },
28 |
29 | filteredRows () {
30 | if (this.unresolved) {
31 | return this.filteredCurrentRows;
32 | }
33 |
34 | // Always execute because of the children filtering.
35 | let filteredRows = Array.from(this.filteredCurrentRows).filter(this.rowMatch);
36 |
37 | if (this.isFiltering) {
38 | return filteredRows;
39 | }
40 |
41 | return this.filteredCurrentRows;
42 | },
43 | },
44 |
45 | methods: {
46 | async filterChanged () {
47 | this.clearSelection();
48 |
49 | this.totalFilteredRowsChanged(this.filteredRows.length);
50 |
51 | if (this.unresolved) {
52 | await this.handleUnresolved();
53 | }
54 | },
55 |
56 | totalFilteredRowsChanged (total) {
57 | this.$emit('total-filtered-rows-change', total);
58 | },
59 |
60 | rowMatch (row) {
61 | if (row === undefined) {
62 | return true;
63 | }
64 |
65 | row._meta.visibleChildren = row._children.filter(this.rowMatch);
66 |
67 | if (!this.isFiltering) {
68 | return true;
69 | }
70 |
71 | if (row._meta.visibleChildren.length > 0) {
72 | row._showChildren = true;
73 |
74 | return true;
75 | }
76 |
77 | return Object.keys(row)
78 | .filter(rowKey => this.filterColumnProperties.includes(rowKey))
79 | .filter(filterKey => this.filterRegex.test(row[filterKey]))
80 | .length > 0;
81 | },
82 | },
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/GroupRow.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
18 |
19 |
20 |
21 |
87 |
--------------------------------------------------------------------------------
/tests/unit/components/ChildrenButton.test.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 | import ChildrenButton from '../../../src/components/ChildrenButton';
3 |
4 | describe('ChildrenButton', () => {
5 | it('show the + sign by default', () => {
6 | const childrenButton = shallowMount(ChildrenButton, {
7 | propsData: {
8 | expanded: false,
9 | loading: false,
10 | },
11 | });
12 |
13 | expect(childrenButton.vm.classes['fa-plus-square']).toBeTruthy();
14 | expect(childrenButton.vm.classes['fa-minus-square']).toBeFalsy();
15 | });
16 |
17 | it('shows the - sign if the component is expanded', () => {
18 | const childrenButton = shallowMount(ChildrenButton, {
19 | propsData: {
20 | expanded: true,
21 | loading: false,
22 | },
23 | });
24 |
25 | expect(childrenButton.vm.classes['fa-plus-square']).toBeFalsy();
26 | expect(childrenButton.vm.classes['fa-minus-square']).toBeTruthy();
27 | });
28 |
29 | it('shows the loading sign if the component is loading', () => {
30 | const childrenButton = shallowMount(ChildrenButton, {
31 | propsData: {
32 | expanded: false,
33 | loading: true,
34 | },
35 | });
36 |
37 | expect(childrenButton.vm.classes['fa-ellipsis-h']).toBeTruthy();
38 | });
39 |
40 | it('uses the slot icon if the component is expanded', () => {
41 | const childrenButton = shallowMount(ChildrenButton, {
42 | propsData: {
43 | expanded: true,
44 | loading: false,
45 | iconSlot: props => {
46 | return `Test ${props.expanded ? 'open' : 'closed'}`;
47 | },
48 | },
49 | });
50 |
51 | expect(childrenButton.text()).toBe('Test open');
52 | });
53 |
54 | it('uses the slot icon if the component is closed', () => {
55 | const childrenButton = shallowMount(ChildrenButton, {
56 | propsData: {
57 | expanded: false,
58 | loading: false,
59 | iconSlot: props => {
60 | return `Test ${props.expanded ? 'open' : 'closed'}`;
61 | },
62 | },
63 | });
64 |
65 | expect(childrenButton.text()).toBe('Test closed');
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": false,
3 | "name": "vue-ads-table-tree",
4 | "description": "A vue table tree plugin.",
5 | "license": "MIT",
6 | "author": "Arne De Smedt (https://twitter.com/ArneSmedt)",
7 | "homepage": "https://github.com/arnedesmedt/vue-ads-table-tree",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/arnedesmedt/vue-ads-table-tree.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/arnedesmedt/vue-ads-table-tree/issues"
14 | },
15 | "version": "2.4.2",
16 | "main": "./dist/vue-ads-table-tree.common.js",
17 | "files": [
18 | "/dist"
19 | ],
20 | "scripts": {
21 | "build": "vue-cli-service build",
22 | "build:bundle": "vue-cli-service build --target lib --name vue-ads-table-tree ./src/index.js",
23 | "lint": "vue-cli-service lint",
24 | "package-lint": "prettier-package-json --write --tab-width=4 ./package.json",
25 | "serve": "vue-cli-service serve",
26 | "test:unit": "vue-cli-service test:unit",
27 | "preversion": "export NODE_ENV=production && npm run lint && npm run package-lint",
28 | "version": "npm run build:bundle",
29 | "postversion": "git push && git push --tags"
30 | },
31 | "dependencies": {
32 | "@fortawesome/fontawesome-free": "^5.11.2",
33 | "vue": "^2.6.10",
34 | "vue-ads-form-builder": "^0.1.15",
35 | "vue-ads-pagination": "^2.1.7",
36 | "vue-json-excel": "^0.2.98"
37 | },
38 | "devDependencies": {
39 | "@fullhuman/postcss-purgecss": "^1.3.0",
40 | "@vue/cli-plugin-babel": "^4.0.5",
41 | "@vue/cli-plugin-eslint": "^4.0.5",
42 | "@vue/cli-plugin-unit-jest": "^4.0.5",
43 | "@vue/cli-service": "^4.0.5",
44 | "@vue/eslint-config-standard": "^4.0.0",
45 | "@vue/test-utils": "^1.0.0-beta.29",
46 | "babel-core": "7.0.0-bridge.0",
47 | "babel-eslint": "^10.0.3",
48 | "babel-jest": "^24.9.0",
49 | "eslint": "^4.19.1",
50 | "eslint-config-ads": "^1.0.7",
51 | "eslint-plugin-vue": "^6.0.1",
52 | "postcss-import": "^12.0.1",
53 | "postcss-url": "^8.0.0",
54 | "prettier-package-json": "^2.1.0",
55 | "tailwindcss": "^1.1.3",
56 | "vue-template-compiler": "^2.6.10"
57 | },
58 | "keywords": [
59 | "component",
60 | "table",
61 | "tree",
62 | "vue"
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/Row.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
17 |
18 |
19 |
20 |
90 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | #### v2.3.0 - 07/03/2019
4 |
5 | - Reset the sort. The next click after sorting the rows ascending and descending will reset the sorting to
6 | the initial sort.
7 | - Group rows by a specific column value or use a closure that processes the column value first.
8 |
9 | #### v2.1.0 - 15/02/2019
10 |
11 | - Combined the basic table component and the async table component back to one component
12 | - Added an excel export
13 |
14 | #### v2.0.0 - 04/02/2019
15 |
16 | - Split the table tree in 3 components:
17 | - the basic table component
18 | - the async table component
19 | - the full component (with a filter box and a pagination component)
20 |
21 | #### v1.5.0 - 27/01/2019
22 |
23 | - Make it possible to show/hide columns.
24 |
25 | #### v1.4.0 - 27/01/2019
26 |
27 | - Add underscores as a prefix to the meta data of row objects.
28 | - Removed the possibility to don't store async items in the cache.
29 | - Don't create new objects from rows and columns but use them as they were created.
30 |
31 |
32 | #### v1.3.0 - 04/01/2019
33 |
34 | - make the properties of the rows reactive.
35 |
36 | #### v1.2.5 - 18/12/2018
37 |
38 | - Use system ui as the table tree font
39 | - Remove the table title by default because you will not use a title with the value 'title'.
40 |
41 | #### v1.2.4 - 07/12/2018
42 |
43 | - Bugfix on formatting the cell
44 | - Add the collapse/expand icon in a preferred column.
45 |
46 | #### v1.2.3 - 07/12/2018
47 |
48 | - Bugfix if an item was loaded on the zero index.
49 | - Only remove the pagination on filtering and loading.
50 |
51 | #### v1.2.2 - 27/11/2018
52 |
53 | - Small improvements
54 | - Update pagination component
55 |
56 | #### v1.2.1 - 24/11/2018
57 |
58 | - Removed the width property for columns because it's now possible with the styling object.
59 | - Removed the start and end properties used by the pagination template. Now you have to call the pagination pageChange method.
60 | - Seperate config files from the package.json
61 | - Update eslint
62 |
63 | #### v1.2.0 - 23/11/2018
64 |
65 | - Make a template of the filter input field and label.
66 | - Make a template of the pagination component.
67 | - Review the way of styling the table.
68 | - Don't call a method to execute the render of the table, but use computed properties.
69 |
70 | #### v1.1.0 - 23/08/2018
71 |
72 | - A table cell can be overwritten with a custom template. So it's possible to use components in the cell.
73 | - Added a timeout when typing in the filter field, if an async call has to be made.
74 |
75 | #### v1.0.0 - 15/08/2018
76 |
77 | - Initial release.
78 |
--------------------------------------------------------------------------------
/src/mixins/sort.js:
--------------------------------------------------------------------------------
1 | export default {
2 | computed: {
3 | sortedRows () {
4 | if (this.unresolved) {
5 | return this.filteredRows;
6 | }
7 |
8 | return this.sortRows(this.filteredRows);
9 | },
10 | },
11 |
12 | methods: {
13 | maxSortOrder () {
14 | return this.visibleColumns.reduce((max, column) => {
15 | return max < column.order ? column.order : max;
16 | }, 0);
17 | },
18 |
19 | sortRows (rows) {
20 | rows.sort((rowA, rowB) => {
21 | return rowA._meta.index - rowB._meta.index;
22 | });
23 |
24 | this.sortColumns
25 | .forEach(column => {
26 | let direction = column.direction ? 1 : -1;
27 | rows.sort((rowA, rowB) => {
28 | let sortValueA = rowA[column.property];
29 | let sortValueB = rowB[column.property];
30 |
31 | if (column.grouped && column.groupBy instanceof Function) {
32 | sortValueA = column.groupBy(sortValueA);
33 | sortValueB = column.groupBy(sortValueB);
34 | }
35 |
36 | if (typeof sortValueA === 'string' && typeof sortValueB === 'string') {
37 | return direction * ('' + sortValueA.localeCompare(sortValueB));
38 | }
39 |
40 | if (sortValueA < sortValueB) {
41 | return -direction;
42 | }
43 |
44 | if (sortValueA > sortValueB) {
45 | return direction;
46 | }
47 |
48 | return 0;
49 | });
50 | });
51 |
52 | rows
53 | .filter(row => row._meta.visibleChildren.length > 0)
54 | .forEach(row => {
55 | row._meta.visibleChildren = this.sortRows(row._meta.visibleChildren);
56 | });
57 |
58 | return rows;
59 | },
60 |
61 | async sort (column) {
62 | let wasFalse = column.direction === false;
63 | column.direction = wasFalse && !column.grouped ? null : !column.direction;
64 | if (! column.grouped) {
65 | column.order = this.maxSortOrder() + 1;
66 | }
67 |
68 | if (this.unresolved) {
69 | await this.handleUnresolved();
70 | }
71 | },
72 | },
73 | };
74 |
--------------------------------------------------------------------------------
/tests/unit/components/Row.test.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 |
3 | import Row from '../../../src/components/Row';
4 | import CSSProcessor from '../../../src/services/CSSProcessor';
5 |
6 | describe('Row', () => {
7 | let row;
8 | let columnA;
9 | let columnB;
10 | let cssProcessor;
11 |
12 |
13 | beforeEach(() => {
14 | columnA = {
15 | property: 'firstName',
16 | };
17 | columnB = {
18 | property: 'lastName',
19 | };
20 |
21 | cssProcessor = new CSSProcessor(2, {});
22 | cssProcessor.totalRows = 1;
23 |
24 | row = shallowMount(Row, {
25 | propsData: {
26 | row: {
27 | _id: 'arne',
28 | firstName: 'Arne',
29 | lastName: 'De Smedt',
30 | },
31 | columns: [
32 | columnA,
33 | columnB,
34 | ],
35 | rowIndex: 0,
36 | cssProcessor,
37 | },
38 | });
39 | });
40 |
41 | it('generates classes via the row index', () => {
42 | cssProcessor = new CSSProcessor(2, {
43 | '1/': {
44 | test: true,
45 | },
46 | });
47 | cssProcessor.totalRows = 1;
48 |
49 | row.setProps({
50 | cssProcessor,
51 | });
52 |
53 | expect(row.vm.rowClasses).toEqual({
54 | test: true,
55 | });
56 | });
57 |
58 | it('generates classes via the fixed row classes attribute', () => {
59 | row.setProps({
60 | row: {
61 | firstName: 'Arne',
62 | lastName: 'De Smedt',
63 | _classes: {
64 | row: {
65 | test: true,
66 | },
67 | },
68 | },
69 | });
70 |
71 | expect(row.vm.rowClasses).toEqual({
72 | test: true,
73 | });
74 | });
75 |
76 | it('selects the correct column slot', () => {
77 | row.setProps({
78 | slots: {
79 | firstName: 'test',
80 | lastName: 'test2',
81 | },
82 | });
83 |
84 | expect(row.vm.columnSlot(columnA)).toBe('test');
85 | });
86 |
87 | it('selects the correct cell slot', () => {
88 | row.setProps({
89 | slots: {
90 | firstName_arne: 'testcell',
91 | firstName: 'test',
92 | },
93 | });
94 |
95 | expect(row.vm.columnSlot(columnA)).toBe('testcell');
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/src/mixins/groupBy.js:
--------------------------------------------------------------------------------
1 | // TODO how to handle grouped data for async data
2 |
3 | export default {
4 | computed: {
5 | groupedRows () {
6 | if (this.paginatedRows.length === 0) {
7 | return this.paginatedRows;
8 | }
9 |
10 | return this.groupingRows(this.paginatedRows, 0);
11 | },
12 | },
13 |
14 | methods: {
15 | groupingRows (rows, groupColumnIndex) {
16 | if (groupColumnIndex === this.groupColumns.length) {
17 | return rows;
18 | }
19 |
20 | let column = this.groupColumns[groupColumnIndex];
21 |
22 | let previousValue = null;
23 | let groups = [];
24 | let groupedRows = [];
25 | let value;
26 |
27 |
28 | rows.forEach(row => {
29 | value = this.rowValue(row, column);
30 |
31 | if (previousValue === null) {
32 | previousValue = value;
33 | }
34 |
35 | if (value !== previousValue) {
36 | groups.push(this.createGroupRow(previousValue, column, groupedRows, groups.length, groupColumnIndex + 1));
37 |
38 | previousValue = value;
39 | groupedRows = [];
40 | }
41 |
42 | groupedRows.push(row);
43 | });
44 |
45 | groups.push(this.createGroupRow(value, column, groupedRows, groups.length, groupColumnIndex + 1));
46 |
47 | if (groupColumnIndex > 0) {
48 | groups.forEach(row => row._meta.groupParent += 1);
49 | }
50 |
51 | return groups;
52 | },
53 |
54 | rowValue (row, column) {
55 | let value = row[column.property];
56 |
57 | if (column.groupBy instanceof Function) {
58 | value = column.groupBy(value);
59 | }
60 |
61 | return value;
62 | },
63 |
64 | createGroupRow (value, column, groupedRows, groupLength, groupColumnIndex) {
65 | groupedRows.forEach(row => row._meta.groupParent = groupColumnIndex);
66 | groupedRows = this.groupingRows(groupedRows, groupColumnIndex);
67 |
68 | let groupRow = {
69 | [column.property]: value,
70 | _children: groupedRows,
71 | _showChildren: true,
72 | };
73 |
74 | this.initRow(groupRow, 0, groupLength, column);
75 |
76 | return groupRow;
77 | },
78 |
79 | async group (column) {
80 | column.grouped = !column.grouped;
81 | column.direction = column.grouped ? !column.direction : null;
82 | column.order = this.maxSortOrder() + 1;
83 |
84 | // Todo For now, no async data
85 | // if (this.unresolved) {
86 | // await this.handleUnresolved();
87 | // }
88 | },
89 | },
90 | };
91 |
--------------------------------------------------------------------------------
/src/components/HeaderCell.vue:
--------------------------------------------------------------------------------
1 |
122 |
--------------------------------------------------------------------------------
/src/mixins/async.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export default {
4 | props: {
5 | callRows: {
6 | type: Function,
7 | default: () => [],
8 | },
9 |
10 | callTempRows: {
11 | type: Function,
12 | default: () => [],
13 | },
14 |
15 | callChildren: {
16 | type: Function,
17 | default: () => [],
18 | },
19 | },
20 |
21 | data () {
22 | return {
23 | tempRows: [],
24 | loading: false,
25 | };
26 | },
27 |
28 | watch: {
29 | async indexesToLoad () {
30 | if (this.indexesToLoad.length === 0) {
31 | return;
32 | }
33 |
34 | if (!this.unresolved) {
35 | return await this.loadRows();
36 | }
37 |
38 | return await this.handleUnresolved();
39 | },
40 | },
41 |
42 | computed: {
43 | allRowsLoaded () {
44 | return this.loadedRows.length === this.rows.length;
45 | },
46 |
47 | allRowsFullyLoaded () {
48 | return this.allRowsLoaded && !this.rows.find(this.noChildrenLoaded);
49 | },
50 |
51 | unresolved () {
52 | return (
53 | (this.isFiltering && !this.allRowsFullyLoaded) ||
54 | (this.sortColumns.length > 0 && !this.allRowsLoaded)
55 | );
56 | },
57 |
58 | currentRows () {
59 | // this.loading because tempRows is empty while loading
60 | // this will trigger an total-filtered-rows-change of 0
61 | if (!this.unresolved || this.loading) {
62 | return this.rows;
63 | }
64 |
65 | return this.tempRows;
66 | },
67 |
68 | indexesToLoad () {
69 | let paginatedRows = this.currentRows.slice(this.start, this.end);
70 |
71 | return Array.from(paginatedRows)
72 | .map((row, index) => row === undefined ? index + this.start : undefined)
73 | .filter(index => index);
74 | },
75 | },
76 |
77 | methods: {
78 | noChildrenLoaded (row) {
79 | return row.hasOwnProperty('_hasChildren') && row._hasChildren;
80 | },
81 |
82 | async handleUnresolved () {
83 | this.loading = true;
84 | let result = await this.callTempRows(
85 | this.filter,
86 | this.sortColumns,
87 | this.start,
88 | this.end
89 | );
90 |
91 | let rows = Array.apply(null, Array(result.total || result.rows.length));
92 | rows.splice(
93 | this.start,
94 | this.end,
95 | ...result.rows
96 | );
97 | this.tempRows = rows;
98 | this.totalFilteredRowsChanged(result.total || result.rows.length);
99 | this.loading = false;
100 | },
101 |
102 | async loadRows () {
103 | this.loading = true;
104 | let rows = this.initRows(await this.callRows(this.indexesToLoad));
105 |
106 | let key;
107 | for (key in rows) {
108 | this.rows[this.indexesToLoad[key]] = rows[key];
109 | }
110 |
111 | Vue.set(this.rows, this.indexesToLoad[key], rows[key]);
112 | this.loading = false;
113 | },
114 | },
115 | };
116 |
--------------------------------------------------------------------------------
/tests/unit/services/CSSProcessor.test.js:
--------------------------------------------------------------------------------
1 | import CSSProcessor from '../../../src/services/CSSProcessor';
2 |
3 | describe('ClassProcessor', () => {
4 | let processor;
5 |
6 | beforeEach(() => {
7 | processor = new CSSProcessor(0, {});
8 | processor.totalRows = 5;
9 | });
10 |
11 | it('initializes the default params', () => {
12 | expect(processor.classes).toEqual({});
13 | expect(processor.totalColumns).toBe(0);
14 | });
15 |
16 | it('styles all rows', () => {
17 | processor.classes = {
18 | 'all/': {
19 | test: true,
20 | },
21 | };
22 |
23 | expect(processor.processedClasses[0].rows).toEqual([
24 | 0,
25 | 1,
26 | 2,
27 | 3,
28 | 4,
29 | ]);
30 |
31 | expect(processor.process(3)).toEqual({
32 | test: true,
33 | });
34 | });
35 |
36 | it('doesn\'t style rows out of range', () => {
37 | processor.classes = {
38 | 'all/': {
39 | test: true,
40 | },
41 | };
42 |
43 | expect(processor.process(5)).toEqual({});
44 | });
45 |
46 | it('styles odd rows', () => {
47 | processor.classes = {
48 | 'odd/': {
49 | test: true,
50 | },
51 | };
52 |
53 | expect(processor.processedClasses[0].rows).toEqual([
54 | 1,
55 | 3,
56 | ]);
57 | });
58 |
59 | it('styles even rows', () => {
60 | processor.classes = {
61 | 'even/': {
62 | test: true,
63 | },
64 | };
65 |
66 | expect(processor.processedClasses[0].rows).toEqual([
67 | 0,
68 | 2,
69 | 4,
70 | ]);
71 | });
72 |
73 | it('doesn\'t style any row if the total rows is not given', () => {
74 | processor = new CSSProcessor(0, {
75 | 'even/': {
76 | test: true,
77 | },
78 | });
79 |
80 | expect(processor.processedClasses[0].rows).toEqual([]);
81 | expect(processor.process(0)).toEqual({});
82 | });
83 |
84 | it('styles a specific row range', () => {
85 | processor.classes = {
86 | '1_4/': {
87 | test: true,
88 | },
89 | };
90 |
91 | expect(processor.processedClasses[0].rows).toEqual([
92 | 1,
93 | 2,
94 | 3,
95 | ]);
96 | });
97 |
98 | it('styles a some rows', () => {
99 | processor.classes = {
100 | '1,4/': {
101 | test: true,
102 | },
103 | };
104 |
105 | expect(processor.processedClasses[0].rows).toEqual([
106 | 1,
107 | 4,
108 | ]);
109 | });
110 |
111 | it('doesn\'t return a range if the start is lower than the end', function () {
112 | processor.classes = {
113 | '3_2/': {
114 | test: true,
115 | },
116 | };
117 |
118 | expect(processor.processedClasses[0].rows).toEqual([]);
119 | });
120 |
121 | it('doesn\'t return a range if the start and/or end are lower than 0', function () {
122 | processor.classes = {
123 | '-1_-2/': {
124 | test: true,
125 | },
126 | };
127 |
128 | expect(processor.processedClasses[0].rows).toEqual([]);
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/src/mixins/cell/cell.js:
--------------------------------------------------------------------------------
1 | import VueAdsChildrenButton from '../../components/ChildrenButton';
2 | import CSSProcessor from '../../services/CSSProcessor';
3 |
4 |
5 | export default {
6 | components: {
7 | VueAdsChildrenButton,
8 | },
9 |
10 | props: {
11 | row: {
12 | type: Object,
13 | required: true,
14 | },
15 |
16 | rowIndex: {
17 | type: Number,
18 | required: true,
19 | },
20 |
21 | column: {
22 | type: Object,
23 | required: true,
24 | },
25 |
26 | columnIndex: {
27 | type: Number,
28 | required: true,
29 | },
30 |
31 | cssProcessor: {
32 | type: CSSProcessor,
33 | required: true,
34 | },
35 |
36 | columnSlot: {
37 | type: Function,
38 | default: null,
39 | },
40 |
41 | toggleChildrenIconSlot: {
42 | type: Function,
43 | default: null,
44 | },
45 | },
46 |
47 | computed: {
48 | cellClasses () {
49 | return Object.assign(
50 | this.cssProcessor.process(null, this.columnIndex, this.column),
51 | this.cssProcessor.process(this.rowIndex + 1, this.columnIndex, this.row, this.column),
52 | this.cssProcessor.processFixed(this.row._classes, this.columnIndex, this.row, this.column)
53 | );
54 | },
55 |
56 | titleClasses () {
57 | return {
58 | 'vue-ads-cursor-pointer': this.hasCollapseIcon,
59 | };
60 | },
61 |
62 | style () {
63 | return {
64 | 'padding-left': (1 + (this.parent * 1.5)) + 'rem',
65 | };
66 | },
67 |
68 | parent () {
69 | let parent = 0;
70 |
71 | if (this.columnIndex === 0) {
72 | parent += this.row._meta.groupParent;
73 | }
74 |
75 | if (this.column.collapseIcon) {
76 | parent += this.row._meta.parent;
77 | }
78 |
79 | return parent;
80 | },
81 |
82 | collapsable () {
83 | return this.column.collapseIcon || this.groupCollapsable;
84 | },
85 |
86 | groupCollapsable () {
87 | return this.column.groupCollapsable && this.row._meta.groupColumn;
88 | },
89 |
90 | hasCollapseIcon () {
91 | return this.collapsable &&
92 | (this.row._meta.visibleChildren.length > 0 || this.row._hasChildren);
93 | },
94 |
95 | clickEvents () {
96 | return this.hasCollapseIcon ? {
97 | click: this.toggleChildren,
98 | } : {};
99 | },
100 | },
101 |
102 | methods: {
103 | value (createElement) {
104 | let elements = [];
105 |
106 | if (this.hasCollapseIcon) {
107 | elements.push(createElement(VueAdsChildrenButton, {
108 | props: {
109 | expanded: this.row._showChildren || false,
110 | loading: this.row._meta.loading || false,
111 | iconSlot: this.toggleChildrenIconSlot,
112 | },
113 | nativeOn: this.clickEvents,
114 | }));
115 | }
116 |
117 | if (this.columnSlot) {
118 | elements.push(this.columnSlot({
119 | row: this.row,
120 | column: this.column,
121 | }));
122 | } else if (this.column.property && this.row.hasOwnProperty(this.column.property)) {
123 | elements.push(this.row[this.column.property]);
124 | }
125 |
126 | return elements.length > 0 ? elements : [
127 | '',
128 | ];
129 | },
130 |
131 | toggleChildren (event) {
132 | event.stopPropagation();
133 | this.$emit('toggle-children');
134 | },
135 | },
136 | };
137 |
--------------------------------------------------------------------------------
/src/mixins/columns.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export default {
4 | props: {
5 | columns: {
6 | type: Array,
7 | required: true,
8 | },
9 | },
10 |
11 | watch: {
12 | columns: {
13 | handler: 'columnsChanged',
14 | immediate: true,
15 | },
16 | },
17 |
18 | computed: {
19 | visibleColumns () {
20 | return this.columns.filter(column => column.visible);
21 | },
22 |
23 | columnProperties () {
24 | return this.visibleColumns.map(column => column.property);
25 | },
26 |
27 | sortColumns () {
28 | return this.visibleColumns
29 | .filter(column => column.hasOwnProperty('direction') && column.direction !== null)
30 | .sort((columnA, columnB) => {
31 | if (columnA.grouped !== columnB.grouped) {
32 | return (!columnB.grouped | 0) - (!columnA.grouped | 0);
33 | }
34 |
35 | return columnA.grouped ? (columnB.order - columnA.order) : (columnA.order - columnB.order);
36 | });
37 | },
38 |
39 | nonGroupedColumns () {
40 | return this.visibleColumns.filter(column => !column.grouped || !column.hideOnGroup || column.collapseIcon);
41 | },
42 |
43 | groupColumns () {
44 | return this.visibleColumns
45 | .filter(column => column.groupable && column.grouped)
46 | .sort((columnA, columnB) => columnA.order - columnB.order);
47 | },
48 |
49 | filterColumnProperties () {
50 | return this.visibleColumns
51 | .filter(column => {
52 | return column.filterable;
53 | })
54 | .map(column => column.property);
55 | },
56 | },
57 |
58 | methods: {
59 | columnsChanged (columns) {
60 | let maxSortOrder = this.maxSortOrder();
61 |
62 | columns.forEach(column => {
63 | this.initColumn(column, maxSortOrder);
64 | if (column.order === maxSortOrder) {
65 | maxSortOrder++;
66 | }
67 | });
68 |
69 | if (columns.length > 0) {
70 | if (!columns.find(column => column.collapseIcon)) {
71 | Vue.set(columns[0], 'collapseIcon', true);
72 | }
73 | }
74 |
75 | // todo check to remove this to the styling mixin
76 | this.cssProcessor.totalColumns = this.nonGroupedColumns.length;
77 | },
78 |
79 | initColumn (column, order) {
80 | if (typeof column.property !== 'string') {
81 | Vue.set(column, 'property', '');
82 | }
83 |
84 | if (!column.hasOwnProperty('visible')) {
85 | Vue.set(column, 'visible', true);
86 | }
87 |
88 | if (!column.hasOwnProperty('export')) {
89 | Vue.set(column, 'export', true);
90 | }
91 |
92 | if (column.hasOwnProperty('order') || column.hasOwnProperty('direction')) {
93 | if (!Number.isInteger(column.order) || column.order < 0) {
94 | column.order = order;
95 | }
96 |
97 | if (!column.hasOwnProperty('direction')) {
98 | Vue.set(column, 'direction', null);
99 | }
100 | }
101 |
102 | if (!column.hasOwnProperty('groupable')) {
103 | Vue.set(
104 | column,
105 | 'groupable',
106 | (
107 | column.hasOwnProperty('grouped') ||
108 | column.hasOwnProperty('groupBy') ||
109 | column.hasOwnProperty('groupCollapsable') ||
110 | column.hasOwnProperty('hideOnGroup')
111 | )
112 | );
113 | }
114 |
115 | if (column.groupable && !column.hasOwnProperty('grouped')) {
116 | Vue.set(column, 'grouped', false);
117 | }
118 |
119 | if (column.groupable && !column.hasOwnProperty('groupCollapsable')) {
120 | Vue.set(column, 'groupCollapsable', true);
121 | }
122 |
123 | if (column.groupable && !column.hasOwnProperty('hideOnGroup')) {
124 | Vue.set(column, 'hideOnGroup', !(column.groupBy instanceof Function));
125 | }
126 | },
127 | },
128 | };
129 |
--------------------------------------------------------------------------------
/src/services/CSSProcessor.js:
--------------------------------------------------------------------------------
1 |
2 | export default class CSSProcessor {
3 | constructor (totalColumns, classes) {
4 | this._totalColumns = totalColumns;
5 | this._classes = classes;
6 | this.processClasses();
7 | }
8 |
9 | set classes (classes) {
10 | this._classes = classes;
11 | this.processClasses();
12 | }
13 |
14 | get classes () {
15 | return this._classes || {};
16 | }
17 |
18 | set totalRows (totalRows) {
19 | if (this._totalRows !== totalRows) {
20 | this._totalRows = totalRows;
21 | this.processClasses();
22 | }
23 | }
24 |
25 | get totalRows () {
26 | return this._totalRows || 0;
27 | }
28 |
29 | set totalColumns (totalColumns) {
30 | if (this._totalColumns !== totalColumns) {
31 | this._totalColumns = totalColumns;
32 | this.processClasses();
33 | }
34 | }
35 |
36 | get totalColumns () {
37 | return this._totalColumns || 0;
38 | }
39 |
40 | get processedClasses () {
41 | return this._processedClasses;
42 | }
43 |
44 | processClasses () {
45 | this._processedClasses = Object.keys(this.classes)
46 | .filter(key => key.includes('/'))
47 | .map(key => {
48 | let type = key.split('/');
49 | return {
50 | rows: this.toRange(type[0], this.totalRows),
51 | columns: this.toRange(type[1], this.totalColumns),
52 | value: this.classes[key],
53 | };
54 | });
55 | }
56 |
57 | toRange (selector, total) {
58 | if (selector === '' || total === 0) {
59 | return [];
60 | }
61 |
62 | switch (selector) {
63 | case 'all':
64 | return Array.from(Array(total).keys());
65 | case 'even':
66 | return Array.from(Array(total).keys()).filter(item => (item % 2) === 0);
67 | case 'odd':
68 | return Array.from(Array(total).keys()).filter(item => (item % 2) === 1);
69 | }
70 |
71 | return [].concat(...selector.split(',')
72 | .map(selector => selector.trim())
73 | .map(selector => {
74 | if (selector.includes('_')) {
75 | let range = selector.split('_')
76 | .map((index, key) => {
77 | if (index !== '') {
78 | return index;
79 | }
80 |
81 | return key === 0 ? 0 : total;
82 | })
83 | .map(index => Number.parseInt(index))
84 | .map(index => index < 0 ? total + index : index);
85 |
86 | if (range[0] < 0 || range[1] < 0 || range[0] > range[1]) {
87 | return null;
88 | }
89 |
90 | return Array.from(Array(range[1] - range[0]).keys())
91 | .map(number => number + range[0]);
92 | }
93 |
94 | return Number.parseInt(selector);
95 | })
96 | .filter(selector => selector !== null));
97 | }
98 |
99 | process (rowIndex = null, columnIndex = null, ...args) {
100 | return this.processedClasses
101 | .filter(classes => {
102 | return !((rowIndex === null && columnIndex === null) ||
103 | (columnIndex === null && classes.columns.length > 0) ||
104 | (rowIndex === null && classes.rows.length > 0) ||
105 | (columnIndex !== null && !classes.columns.includes(columnIndex)) ||
106 | (rowIndex !== null && !classes.rows.includes(rowIndex)));
107 | })
108 | .map(classes => CSSProcessor.processValue(classes.value, ...args))
109 | .reduce((result, classes) => Object.assign(result, classes), {});
110 | }
111 |
112 | static processValue (classes, ...args) {
113 | if (classes instanceof Function) {
114 | return classes(...args);
115 | }
116 |
117 | if (classes) {
118 | return classes;
119 | }
120 |
121 | return {};
122 | }
123 |
124 | processFixed (classes, columnIndex, ...args) {
125 | if (!classes) {
126 | return {};
127 | }
128 |
129 | return Object.keys(classes)
130 | .filter(key => key !== 'row')
131 | .filter(key => this.toRange(key, this.totalColumns).includes(columnIndex))
132 | .map(key => CSSProcessor.processValue(classes[key], ...args))
133 | .reduce((result, classes) => Object.assign(result, classes), {});
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/Table.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
12 |
22 |
23 |
24 |
25 |
26 |
29 |
33 |
34 | Loading...
35 |
36 |
37 | No results found
38 |
39 |
40 |
41 |
45 |
57 |
70 |
71 |
72 |
73 |
74 |
75 |
151 |
--------------------------------------------------------------------------------
/tests/unit/components/HeaderCell.test.js:
--------------------------------------------------------------------------------
1 | import {shallowMount} from '@vue/test-utils';
2 |
3 | import HeaderCell from '../../../src/components/HeaderCell';
4 | import CSSProcessor from '../../../src/services/CSSProcessor';
5 |
6 | describe('HeaderCell', () => {
7 | let headerCell;
8 | let cssProcessor;
9 |
10 | beforeEach(() => {
11 | cssProcessor = new CSSProcessor(2, {});
12 | cssProcessor.totalRows = 1;
13 |
14 | headerCell = shallowMount(HeaderCell, {
15 | propsData: {
16 | title: 'Title',
17 | columnIndex: 0,
18 | cssProcessor,
19 | },
20 | });
21 | });
22 |
23 | it('shows no sort icon if it\'s not sortable', () => {
24 | expect(headerCell.vm.sortIconClasses).toEqual({});
25 | });
26 |
27 | it('shows the sort icon if the column is sortable', () => {
28 | headerCell.setProps({
29 | sortable: true,
30 | direction: null,
31 | });
32 |
33 | expect(headerCell.vm.sortIconClasses['fa-sort']).toBeTruthy();
34 | expect(headerCell.vm.sortIconClasses['fa-sort-up']).toBeFalsy();
35 | expect(headerCell.vm.sortIconClasses['fa-sort-down']).toBeFalsy();
36 | });
37 |
38 | it('shows the desc sort icon if the column is desc sorted', () => {
39 | headerCell.setProps({
40 | sortable: true,
41 | direction: false,
42 | });
43 |
44 | expect(headerCell.vm.sortIconClasses['fa-sort']).toBeFalsy();
45 | expect(headerCell.vm.sortIconClasses['fa-sort-up']).toBeFalsy();
46 | expect(headerCell.vm.sortIconClasses['fa-sort-down']).toBeTruthy();
47 | });
48 |
49 | it('shows the asc sort icon if the column is asc sorted', () => {
50 | headerCell.setProps({
51 | sortable: true,
52 | direction: true,
53 | });
54 |
55 | expect(headerCell.vm.sortIconClasses['fa-sort']).toBeFalsy();
56 | expect(headerCell.vm.sortIconClasses['fa-sort-up']).toBeTruthy();
57 | expect(headerCell.vm.sortIconClasses['fa-sort-down']).toBeFalsy();
58 | });
59 |
60 | it('adds a cursor pointer if the column is sortable', () => {
61 | headerCell.setProps({
62 | sortable: true,
63 | direction: null,
64 | });
65 |
66 | expect(headerCell.vm.headerClasses).toEqual({
67 | 'vue-ads-cursor-pointer': true,
68 | });
69 | });
70 |
71 | it('doesn\'t add a cursor pointer if the column is not sortable', () => {
72 | headerCell.setProps({
73 | sortable: false,
74 | });
75 |
76 | expect(headerCell.vm.headerClasses).toEqual({
77 | 'vue-ads-cursor-pointer': false,
78 | });
79 | });
80 |
81 | it('returns the header classes with a column class', () => {
82 | cssProcessor = new CSSProcessor(2, {
83 | '/0': {
84 | test: true,
85 | },
86 | });
87 | cssProcessor.totalRows = 1;
88 |
89 | headerCell.setProps({
90 | cssProcessor,
91 | });
92 |
93 | expect(headerCell.vm.headerClasses).toEqual({
94 | 'vue-ads-cursor-pointer': false,
95 | test: true,
96 | });
97 | });
98 |
99 | it('returns the header classes without a column class if they don\'t match', () => {
100 | cssProcessor = new CSSProcessor(2, {
101 | '/1': {
102 | test: true,
103 | },
104 | });
105 | cssProcessor.totalRows = 1;
106 |
107 | headerCell.setProps({
108 | cssProcessor,
109 | });
110 |
111 | expect(headerCell.vm.headerClasses).toEqual({
112 | 'vue-ads-cursor-pointer': false,
113 | });
114 | });
115 |
116 | it('returns the header classes with a specific row class', () => {
117 | cssProcessor = new CSSProcessor(2, {
118 | '0/all': {
119 | test: true,
120 | },
121 | });
122 | cssProcessor.totalRows = 1;
123 |
124 | headerCell.setProps({
125 | cssProcessor,
126 | });
127 |
128 | expect(headerCell.vm.headerClasses).toEqual({
129 | 'vue-ads-cursor-pointer': false,
130 | test: true,
131 | });
132 | });
133 |
134 | it('emits a sort event if the header cell is clicked', () => {
135 | headerCell.trigger('click');
136 |
137 | expect(headerCell.emitted().sort).toBeTruthy();
138 | });
139 |
140 | it('uses the toggle children icon slot if direction is null', () => {
141 | headerCell.setProps({
142 | sortable: true,
143 | direction: null,
144 | sortIconSlot: props => `Test ${props.direction === null ? 'null' : (props.direction ? 'true' : 'false')}`,
145 | });
146 |
147 | expect(headerCell.text()).toContain('Test null');
148 | });
149 |
150 | it('uses the toggle children icon slot if direction is true', () => {
151 | headerCell.setProps({
152 | sortable: true,
153 | direction: true,
154 | sortIconSlot: props => `Test ${props.direction === null ? 'null' : (props.direction ? 'true' : 'false')}`,
155 | });
156 |
157 | expect(headerCell.text()).toContain('Test true');
158 | });
159 |
160 | it('uses the toggle children icon slot if direction is false', () => {
161 | headerCell.setProps({
162 | sortable: true,
163 | direction: false,
164 | sortIconSlot: props => `Test ${props.direction === null ? 'null' : (props.direction ? 'true' : 'false')}`,
165 | });
166 |
167 | expect(headerCell.text()).toContain('Test false');
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
15 |
16 | test cell - {{ props.row.name }}
17 |
18 | test column - {{ props.row.name }}
19 |
20 | test row - {{ props.row.name }}
21 | No results
22 | {{ props.direction === null ? 'null' : (props.direction ? 'up' : 'down') }}
23 | {{ props.expanded ? 'open' : 'closed' }}
24 |
25 |
26 |
27 |
28 |
218 |
--------------------------------------------------------------------------------
/src/components/TableContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
49 |
56 |
63 |
64 |
65 |
66 |
67 |
257 |
--------------------------------------------------------------------------------
/tests/unit/components/Cell.test.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 |
3 | import Cell from '../../../src/components/Cell';
4 | import CSSProcessor from '../../../src/services/CSSProcessor';
5 |
6 | describe('Cell', () => {
7 | let cell;
8 | let row;
9 | let cssProcessor;
10 | let column;
11 |
12 | beforeEach(() => {
13 | row = {
14 | firstName: 'arne',
15 | _meta: {
16 | parent: 0,
17 | visibleChildren: [
18 | 'test',
19 | ],
20 | },
21 | };
22 |
23 | column = {
24 | property: 'firstName',
25 | collapseIcon: true,
26 | };
27 |
28 | cssProcessor = new CSSProcessor(2, {});
29 | cssProcessor.totalRows = 1;
30 |
31 | cell = shallowMount(Cell, {
32 | propsData: {
33 | row,
34 | column,
35 | rowIndex: 0,
36 | columnIndex: 0,
37 | cssProcessor,
38 | },
39 | });
40 | });
41 |
42 | it('returns the default cell classes', () => {
43 | expect(cell.vm.cellClasses).toEqual({
44 | 'vue-ads-px-4': true,
45 | 'vue-ads-py-2': true,
46 | 'vue-ads-text-sm': true,
47 | });
48 | });
49 |
50 | it('returns the cell classes with a specific column class', () => {
51 | cssProcessor = new CSSProcessor(2, {
52 | '/0': {
53 | test: true,
54 | },
55 | });
56 | cssProcessor.totalRows = 1;
57 |
58 | cell.setProps({
59 | cssProcessor,
60 | });
61 |
62 | expect(cell.vm.cellClasses).toEqual({
63 | 'vue-ads-px-4': true,
64 | 'vue-ads-py-2': true,
65 | 'vue-ads-text-sm': true,
66 | test: true,
67 | });
68 | });
69 |
70 | it('only returns the default classes if the column class doesn\'t match', () => {
71 | cssProcessor = new CSSProcessor(2, {
72 | '/1': {
73 | test: true,
74 | },
75 | });
76 | cssProcessor.totalRows = 1;
77 |
78 | cell.setProps({
79 | cssProcessor,
80 | });
81 |
82 | expect(cell.vm.cellClasses).toEqual({
83 | 'vue-ads-px-4': true,
84 | 'vue-ads-py-2': true,
85 | 'vue-ads-text-sm': true,
86 | });
87 | });
88 |
89 | it('returns the cell classes with a specific cell class', () => {
90 | cssProcessor = new CSSProcessor(2, {
91 | '1/0': {
92 | test: true,
93 | },
94 | });
95 | cssProcessor.totalRows = 1;
96 |
97 | cell.setProps({
98 | cssProcessor,
99 | });
100 |
101 | expect(cell.vm.cellClasses).toEqual({
102 | 'vue-ads-px-4': true,
103 | 'vue-ads-py-2': true,
104 | 'vue-ads-text-sm': true,
105 | test: true,
106 | });
107 | });
108 |
109 | it('only returns the default classes if the cell class doesn\'t match', () => {
110 | cssProcessor = new CSSProcessor(2, {
111 | '1/1': {
112 | test: true,
113 | },
114 | });
115 | cssProcessor.totalRows = 1;
116 |
117 | cell.setProps({
118 | cssProcessor,
119 | });
120 |
121 | expect(cell.vm.cellClasses).toEqual({
122 | 'vue-ads-px-4': true,
123 | 'vue-ads-py-2': true,
124 | 'vue-ads-text-sm': true,
125 | });
126 | });
127 |
128 | it('returns the cell classes with fixed row classes', () => {
129 | cell.setProps({
130 | row: {
131 | _classes: {
132 | 0: {
133 | test: true,
134 | },
135 | },
136 | firstName: 'arne',
137 | _meta: {
138 | parent: 0,
139 | visibleChildren: [
140 | 'test',
141 | ],
142 | },
143 | },
144 | });
145 |
146 | expect(cell.vm.cellClasses).toEqual({
147 | 'vue-ads-px-4': true,
148 | 'vue-ads-py-2': true,
149 | 'vue-ads-text-sm': true,
150 | test: true,
151 | });
152 | });
153 |
154 | it('only returns the default cell classes if the column doesn\'t match the fixed row classes', () => {
155 | cell.setProps({
156 | row: {
157 | _classes: {
158 | 1: {
159 | test: true,
160 | },
161 | },
162 | firstName: 'arne',
163 | _meta: {
164 | parent: 0,
165 | visibleChildren: [
166 | 'test',
167 | ],
168 | },
169 | },
170 | });
171 |
172 | expect(cell.vm.cellClasses).toEqual({
173 | 'vue-ads-px-4': true,
174 | 'vue-ads-py-2': true,
175 | 'vue-ads-text-sm': true,
176 | });
177 | });
178 |
179 | it('changes the padding if the number of parents changes', () => {
180 | cell.setProps({
181 | row: {
182 | firstName: 'arne',
183 | _hasChildren: false,
184 | _meta: {
185 | parent: 1,
186 | visibleChildren: [],
187 | },
188 | },
189 | column: {
190 | collapseIcon: true,
191 | },
192 | });
193 |
194 | expect(cell.vm.style['padding-left']).toBe('2.5rem');
195 | });
196 |
197 | it('doesn\'t add padding if the cell has no collapse icon', () => {
198 | cell.setProps({
199 | row: {
200 | firstName: 'arne',
201 | _meta: {
202 | parent: 1,
203 | visibleChildren: [
204 | 'test',
205 | ],
206 | },
207 | },
208 | column: {
209 | collapseIcon: false,
210 | },
211 | });
212 |
213 | expect(cell.vm.style['padding-left']).toBe('1rem');
214 | });
215 |
216 | it('adds a toggle children button if the row has children and the column owns the button', () => {
217 | cell.setProps({
218 | row: {
219 | firstName: 'arne',
220 | _hasChildren: true,
221 | _meta: {
222 | parent: 0,
223 | visibleChildren: [],
224 | },
225 | },
226 | column: {
227 | property: 'firstName',
228 | collapseIcon: true,
229 | },
230 | });
231 |
232 | expect(cell.html()).toContain(' ');
233 | });
234 |
235 | it('creates a column slot', () => {
236 | cell.setProps({
237 | columnSlot: props => {
238 | return `Test: ${props.row.firstName}`;
239 | },
240 | });
241 |
242 | expect(cell.text()).toBe('Test: arne');
243 | });
244 |
245 | it('is empty with no matching properties', () => {
246 | cell.setProps({
247 | row: {
248 | lastName: 'de smedt',
249 | _meta: {
250 | parent: 0,
251 | visibleChildren: [
252 | 'test',
253 | ],
254 | },
255 | },
256 | });
257 |
258 | expect(cell.text()).toBe('');
259 | });
260 |
261 | it('emits toggle children, when clicking the toggle children button', () => {
262 | cell.find('span').trigger('click');
263 |
264 | expect(cell.emitted().toggleChildren).toBeTruthy();
265 | });
266 | });
267 |
--------------------------------------------------------------------------------
/src/AsyncTableApp.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
271 |
--------------------------------------------------------------------------------
/tests/unit/components/Table.test.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils';
2 |
3 | import Table from '../../../src/components/Table';
4 |
5 | describe('Table', () => {
6 | let table;
7 | let rows;
8 | let columns;
9 | let columnA;
10 | let columnB;
11 | let columnC;
12 |
13 | beforeEach(() => {
14 | rows = [
15 | {
16 | firstName: 'Arne',
17 | lastName: 'De Smedt',
18 | age: 27,
19 | },
20 | {
21 | firstName: 'Bruno',
22 | lastName: 'Vandenhende',
23 | age: 27,
24 | },
25 | {
26 | firstName: 'Tanja',
27 | lastName: 'Vandersyp',
28 | age: 25,
29 | },
30 | {
31 | firstName: 'Kris',
32 | lastName: 'Dejagere',
33 | age: 34,
34 | },
35 | ];
36 |
37 | columnA = {
38 | property: 'firstName',
39 | title: 'First Name',
40 | };
41 |
42 | columnB = {
43 | property: 'lastName',
44 | title: 'Last Name',
45 | };
46 |
47 | columnC = {
48 | property: 'age',
49 | title: 'Age',
50 | };
51 |
52 | columns = [
53 | columnA,
54 | columnB,
55 | columnC,
56 | ];
57 |
58 | table = shallowMount(Table, {
59 | propsData: {
60 | rows,
61 | columns,
62 | start: 0,
63 | end: 10,
64 | },
65 | });
66 | });
67 |
68 | it('adds the table classes', () => {
69 | table.setProps({
70 | classes: {
71 | table: {
72 | test: true,
73 | },
74 | },
75 | });
76 |
77 | expect(table.vm.tableClasses).toEqual({
78 | test: true,
79 | });
80 | expect(table.vm.infoClasses).toEqual({});
81 | });
82 |
83 | it('adds the header row classes', () => {
84 | table.setProps({
85 | classes: {
86 | '0/': {
87 | test: true,
88 | },
89 | },
90 | });
91 |
92 | expect(table.vm.headerRowClasses).toEqual({
93 | test: true,
94 | });
95 | });
96 |
97 | it('adds the info classes', () => {
98 | table.setProps({
99 | classes: {
100 | info: {
101 | test: true,
102 | },
103 | },
104 | });
105 |
106 | expect(table.vm.infoClasses).toEqual({
107 | test: true,
108 | });
109 | });
110 |
111 | it('has no results if the rows property is not set', () => {
112 | table = shallowMount(Table, {
113 | propsData: {
114 | columns,
115 | start: 0,
116 | end: 10,
117 | },
118 | });
119 |
120 | expect(table.vm.totalVisibleRows).toBe(0);
121 | });
122 |
123 | it('info is visible if loading is true', () => {
124 | table.setProps({
125 | loading: true,
126 | });
127 |
128 | expect(table.vm.infoVisible).toBeTruthy();
129 | });
130 |
131 | it('selects slots of the visible columns', () => {
132 | table = shallowMount(Table, {
133 | propsData: {
134 | rows,
135 | columns,
136 | start: 0,
137 | end: 10,
138 | },
139 | scopedSlots: {
140 | firstName: 'test - {{ props.row.firstName}} ',
141 | },
142 | });
143 |
144 | expect(Object.keys(table.vm.rowSlots)).toEqual([
145 | 'firstName',
146 | ]);
147 | });
148 |
149 | it('doesn\'t select slots of the invisible columns', () => {
150 | columnA.visible = false;
151 | table = shallowMount(Table, {
152 | propsData: {
153 | rows,
154 | columns,
155 | start: 0,
156 | end: 10,
157 | },
158 | scopedSlots: {
159 | firstName: 'test - {{ props.row.firstName}} ',
160 | lastName: 'test - {{ props.row.firstName}} ',
161 | },
162 | });
163 |
164 | expect(Object.keys(table.vm.rowSlots)).toEqual([
165 | 'lastName',
166 | ]);
167 | });
168 |
169 | it('shows only the visible columns', () => {
170 | columnB.visible = false;
171 |
172 | table = shallowMount(Table, {
173 | propsData: {
174 | rows,
175 | columns,
176 | },
177 | });
178 |
179 | expect(table.vm.visibleColumns.map(column => column.property)).toEqual([
180 | 'firstName',
181 | 'age',
182 | ]);
183 | });
184 |
185 | it('shows all rows without children or pagination', () => {
186 | expect(table.vm.flattenedRows).toHaveLength(4);
187 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
188 | 'Arne',
189 | 'Bruno',
190 | 'Tanja',
191 | 'Kris',
192 | ]);
193 | });
194 |
195 | it('shows just the first 2 rows if the total rows per page is 2', () => {
196 | table.setProps({
197 | start: 0,
198 | end: 2,
199 | });
200 |
201 | expect(table.vm.flattenedRows).toHaveLength(2);
202 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
203 | 'Arne',
204 | 'Bruno',
205 | ]);
206 | });
207 |
208 | it('sorts the first column by desc', () => {
209 | columnA.direction = false;
210 |
211 | table = shallowMount(Table, {
212 | propsData: {
213 | rows,
214 | columns,
215 | },
216 | });
217 |
218 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
219 | 'Tanja',
220 | 'Kris',
221 | 'Bruno',
222 | 'Arne',
223 | ]);
224 | });
225 |
226 | it('sorts non string types', () => {
227 | columnC.direction = false;
228 |
229 | table = shallowMount(Table, {
230 | propsData: {
231 | rows,
232 | columns,
233 | },
234 | });
235 |
236 | expect(table.vm.flattenedRows.map(row => row.age)).toEqual([
237 | 34,
238 | 27,
239 | 27,
240 | 25,
241 | ]);
242 | });
243 |
244 | it('sorts child rows', () => {
245 | columnA.direction = true;
246 |
247 | rows[0]._showChildren = true;
248 | rows[0]._children = [
249 | {
250 | firstName: 'Bruno',
251 | lastName: 'Vandenhende',
252 | age: 25,
253 | },
254 | {
255 | firstName: 'Arne',
256 | lastName: 'Vanleem',
257 | age: 26,
258 | },
259 | ];
260 |
261 | table = shallowMount(Table, {
262 | propsData: {
263 | rows,
264 | columns,
265 | },
266 | });
267 |
268 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
269 | 'Arne',
270 | 'Arne',
271 | 'Bruno',
272 | 'Bruno',
273 | 'Kris',
274 | 'Tanja',
275 | ]);
276 | });
277 |
278 | it('filters the table based on the first column', () => {
279 | columnA.filterable = true;
280 |
281 | table = shallowMount(Table, {
282 | propsData: {
283 | rows,
284 | columns,
285 | filter: 'n',
286 | },
287 | });
288 |
289 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
290 | 'Arne',
291 | 'Bruno',
292 | 'Tanja',
293 | ]);
294 | });
295 |
296 | it('filters child and parent if the parent doesn\'t match but only the child', () => {
297 | columnA.filterable = true;
298 | rows[0]._children = [
299 | {
300 | firstName: 'Bruno',
301 | lastName: 'Vandenhende',
302 | age: 25,
303 | },
304 | ];
305 |
306 | table = shallowMount(Table, {
307 | propsData: {
308 | rows,
309 | columns,
310 | filter: 'un',
311 | },
312 | });
313 |
314 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
315 | 'Arne',
316 | 'Bruno',
317 | 'Bruno',
318 | ]);
319 | });
320 |
321 | it('initializes columns without a property', () => {
322 | columns = [
323 | {
324 | title: 'Empty',
325 | },
326 | columnA,
327 | columnB,
328 | ];
329 |
330 | table = shallowMount(Table, {
331 | propsData: {
332 | rows,
333 | columns,
334 | },
335 | });
336 |
337 | expect(columns[0].property).toBe('');
338 | });
339 |
340 | it('initializes columns with an order but without a direction', () => {
341 | columnA.order = 1;
342 |
343 | table = shallowMount(Table, {
344 | propsData: {
345 | rows,
346 | columns,
347 | },
348 | });
349 |
350 | expect(columns[0].direction).toBeNull();
351 | });
352 |
353 | it('sorts a column and emits a sort event', () => {
354 | columnA.direction = null;
355 |
356 | table = shallowMount(Table, {
357 | propsData: {
358 | rows,
359 | columns,
360 | },
361 | });
362 |
363 | table.vm.sort(columnA);
364 |
365 | expect(table.emitted().sort).toBeTruthy();
366 | expect(columnA.direction).toBeTruthy();
367 | expect(columnA.order).toBe(1);
368 | });
369 |
370 | it('toggles the children', () => {
371 | rows[0]._showChildren = false;
372 |
373 | table = shallowMount(Table, {
374 | propsData: {
375 | rows,
376 | columns,
377 | },
378 | });
379 |
380 | table.vm.toggleChildren(rows[0]);
381 |
382 | expect(table.emitted()['toggle-children']).toBeTruthy();
383 | expect(rows[0]._showChildren).toBeTruthy();
384 | });
385 |
386 | it('temp rows are not filtered sorted or paginated', () => {
387 | columnA.direction = false;
388 | table = shallowMount(Table, {
389 | propsData: {
390 | rows,
391 | columns,
392 | filter: 'testje',
393 | temp: true,
394 | start: 0,
395 | end: 1,
396 | },
397 | });
398 |
399 | expect(table.vm.flattenedRows.map(row => row.firstName)).toEqual([
400 | 'Arne',
401 | 'Bruno',
402 | 'Tanja',
403 | 'Kris',
404 | ]);
405 | });
406 |
407 | it('doesn\'t filter rows with holes', () => {
408 | rows.length = 100;
409 | table = shallowMount(Table, {
410 | propsData: {
411 | rows,
412 | columns,
413 | },
414 | });
415 |
416 | expect(table.vm.filteredRows.length).toBe(100);
417 | });
418 | });
419 |
--------------------------------------------------------------------------------
/src/TableContainerApp.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
Selected Rows: {{selectedRowIds}}
32 |
33 | Sample action:
34 |
41 | Delete
42 |
43 |
44 |
45 |
46 |
47 |
48 |
418 |
--------------------------------------------------------------------------------
/src/BasicTableApp.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
599 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-ads-table-tree
2 |
3 | Vue ads table tree is a vue js table component, with a tree functionality.
4 | Here is a list of all the features of this table:
5 |
6 | - Filter rows based on the row values from specific columns or all columns.
7 | - Sort rows on one or multiple columns. The sortable columns are configurable.
8 | - Paginate the rows.
9 | - Group rows on one or multiple columns. You can group by value or by a custom range
10 | (therefore you have to use a custom callback)
11 | - Create a tree structure: Parent rows can have child rows, which in turn can have their own children, ...
12 | Child rows can be collapsed and expanded.
13 | - Not all rows has to be loaded on initialization. You can do async calls to load a range of parent rows or
14 | all the children of one parent row.
15 | - Select rows as in file manager.
16 | - Export all your table data, or the current state, to excel.
17 | - A lot of templates are used, so you can use custom components in rows, columns and or cells.
18 | All icons can also be customized. Be aware that the icon library [FontAwesome](https://fontawesome.com/) is not imported by the library.
19 | You will have to import it by yourself.
20 | - The layout of the table can fully customized. All rows, columns, cells could have their own layout.
21 | You can even apply a layout for one specific row that will stay fixed after sorting.
22 | - The columns and rows are reactive. The only restriction is that you can't add child rows
23 | after you've added them initial or via the async child loader.
24 |
25 | You can use 2 components:
26 | - The full component (VueAdsTableTree). It provides the table with all side input components.
27 | - The table component (VueAdsTable): It has no external input components, like a filter or paginator,
28 | but you can write your own container around it that suits your own needs.
29 | All functionality of the full component is available in the table component.
30 |
31 | ## Demo
32 |
33 | I've written a demo in [JSFiddle](https://jsfiddle.net/arnedesmedt/7my8L42q)
34 |
35 | ## Installation
36 |
37 | You can install the package via npm or yarn.
38 |
39 | #### NPM
40 |
41 | ```npm install vue-ads-table-tree --save```
42 |
43 | #### YARN
44 |
45 | ```yarn add vue-ads-table-tree```
46 |
47 | ## General objects for the components
48 |
49 | ### Columns
50 | Columns are listed in a column array as plain objects and can have the following arguments:
51 |
52 | - `property`: *(type: string, default: `''`)* The corresponding value will be shown in the column for the given row property.
53 | - `title`: *(type: string, default: `''`)* The title that will be shown in the header.
54 | - `filterable`: *(type: boolean, default: `false`)* Is this column filterable?
55 | - `visible`: *(type: boolean, default: `true`)* Is the column visible? Non-visible columns will not interact.
56 | That means if your column is filterable/sortable but not visible it won't filter or sort on it.
57 | - `direction`: *(type: boolean or null, default: `null`)* The initial sort direction. If null, the rows are not sorted by this column.
58 | If true, the sorting is ascending. If false, the sorting is descending. If all columns are null the rows will be
59 | sorted based on their `_order` value.
60 | - `order`: *(type: number)* Column order to sort the rows.
61 | - `collapseIcon`: *(type: boolean, default: `false`)* Indicates if this column will contain the collapse icon.
62 | - `groupable`: *(type: boolean, default: `false`)* Indicates if this column can be used to group rows.
63 | - `grouped`: *(type: boolean, default: `false`)* Group the rows by this column.
64 | - `groupCollapsable`: *(type: boolean, default: `true`)* Can the subrows of the groups be expanded and collapsed.
65 | - `hideOnGroup`: *(type: boolean, default: `true` if no `groupedBy` property is set)* Hide the column if the rows are grouped by this column.
66 | - `groupBy`: *(type: Function)* This function convert the cell value to another value. The row will be grouped by the returned value.
67 | It has one parameter:
68 | - `value`: *(type: mixed)* The default cell value.
69 |
70 | ### Rows
71 | Rows are listed in a row array as plain objects. Each object contains the row data and meta data. Meta data is prefixed with a `_`:
72 | - `_id`: *(type: string, default: `''`)* An identifier for this row. This is used to create slots for certain cells on the row.
73 | - `_children`: *(type: array, default: `[]`)* An array with plain child objects. The structure is the same as the parent list of rows.
74 | - `_hasChildren`: *(type: boolean, default: `false`)* This is usefull if you want to indicate that the row has children, but you will load them later via an async call.
75 | - `_showChildren`: *(type: boolean, default: `false`)* Indicates if the children has to be shown on initialization.
76 | If this is true, and `_hasChildren` is true, but no `_children` attribute is found, an async call will be initiated to call the children.
77 | - `_selectable`: *(type: boolean, default: The parent `_selectable` property or if no parent the `selectable` property of the table)*
78 | Mark if this row is selectable or not.
79 | - `_classes`: *(type: Object, default: `{}`)* Contains styling for the current row. It contains the following attributes:
80 | - `row`: *(type: Object)* A vue based object for adding classes.
81 | - ``: *(type: Object)* A vue based object for adding classes. The column selector works as explained in [styling](#styling),
82 | without the use of slashes to distinguish columns and rows.
83 |
84 | ### Styling
85 | All styling is done via a plain object. It contains a selector for a row/column/cell as key and a vue based object that contains classes.
86 | The latter ones can override the earlier ones:
87 | - The key is a selector. You have two type of selectors: fixed selectors and row/column/cell selectors:
88 | - fixed selectors:
89 | - `table`: Style the whole table.
90 | - `info`: Style the info row (shown while loading or no rows are found).
91 | - `group`: Style the group row.
92 | - `selected`: Style rows selected by end users.
93 | - row/column/cell selectors: These selectors are divided by a slash. So you can have a row selector and a column selector.
94 | The header row has index 0. The first data row has index 1. Some examples:
95 | - 'all': select all rows/columns.
96 | - 'even': select all even rows/columns.
97 | - 'odd': select all odd rows/columns.
98 | - '3': select row/column with index 3,
99 | - '5_7': select row/column 5 and 6,
100 | - '0_-1': select all rows/columns except the last one.
101 | - '1_': select all rows/columns except the first one.
102 | - '_4': select rows/columns 0,1,2,3.
103 | - '1_4,5_8': select rows/columns 1,2,3,5,6,7
104 | - The value is a vue based class object or a callable function that generates that vue based class object, depending on some given parameters.
105 | Those parameters depends on the key (the selector of rows, columns or rows and columns => cells).
106 | - Note that there is a difference between `1_3/all` and `1_3/`. The first will add the classes to all the cells of row 1 and 2.
107 | The latter will add the classes on the `` tags of row 1 and 2.
108 |
109 | #### Examples
110 | - `'0_-1/': {'test-row': true}` => will add the test class for all rows except the last one for all columns.
111 | - `'/1_3,5': {'test-column': true}` => will add the test-column class for column 1,2,5 on all rows.
112 | - `'even/1': {'cell': true}` => will add the cell class for column 1 on all even rows.
113 | - `'1_/3': (row, column) => {...}` => a callable function that returns a vue based class object, depending on the parameters row and column.
114 |
115 | ## Table component
116 |
117 | If you just need a normal table that you can customize fully by yourself, you can use the table component. It won't provide you a paginator,
118 | filter textbox, ... It's just a Vue component and all interaction happens via the properties or event handlers.
119 |
120 | If you want to use the table in a predefined container with a paginator, filter textbox see the [full component section](#fullComponent)
121 |
122 | ### Async features
123 |
124 | It is also possible to use this component without all data is already loaded. Therefore you will have to use the
125 | `call-` properties that will hold a callback function to load some additional data asynchronously.
126 |
127 | #### Load your root rows asynchronously
128 |
129 | In previous versions of the table tree, you needed to pass the total rows as a property. Now, you don't need
130 | that anymore. Just change the length of the rows array. For example you already have 3 root rows in your JSON array,
131 | but you want to load another 3 root rows asynchronously, well then set the length of your rows property to 6: `this.rows.length = 6`.
132 | Don't forget to add the `call-rows` property to your component. See [properties](#table_properties)
133 |
134 | #### Load your child rows asynchronously
135 |
136 | If you want to load your child rows later, just add the `_hasChildren` attribute to your row with `true` as value.
137 | It will add a toggle children icon and when you click it, the children will be loaded.
138 | Don't forget to add the `call-children` property to your component. See [properties](#table_properties)
139 |
140 | #### Sort or filter on a not fully loaded table
141 |
142 | If you're sorting/filtering while not all root/child rows are loaded. There is a small problem. We don't know if your sort
143 | or filter result will be the right one, because not all the data is loaded.
144 | Therefore the component will call the `call-temp-rows` function you need to pass via the property. See [properties](#table_properties)
145 |
146 | Be aware that the result of the function call is send to the table without any additional filtering, sorting or pagination.
147 | The rows will only expand if needed. You can see it as you pass temporarily rows to the table
148 |
149 | ### Properties
150 |
151 | - `columns`: *(type: array, required)* see [columns](#columns)
152 | - `rows`: *(type: array, default: `[]`)* see [rows](#rows)
153 | - `classes`: *(type: object, default: see [file](https://github.com/arnedesmedt/vue-ads-table-tree/blob/develop/src/services/defaultClasses.js))* see [styling](#styling)
154 | - `filter`: *(type: string, default: `''`)* Filter all row values on by this regex. Beware that if you change the filter,
155 | the total number of rows will decrease. So it's wisely to set the start index on 0 after you change the filter value.
156 | Then you will be able to properly display all the filtered rows.
157 | - `start`: *(type: number, default: `undefined`)* The start index to show only a slice of the rows.
158 | - `end`: *(type: number, default: `undefined`)* The end index to show only a slice of the rows.
159 | - `selectable`: *(type: boolean, default: true)* Enable/disable row selection.
160 | - `slots`: *(type: Object, default: {})* A list of slots that are passed from parent components.
161 | If this object doesn't contain any attributes, the default component slots will be used.
162 | - `export-name`: *(type: string, default: `''`)* The name of the export file to download. This is by default an empty string.
163 | If you change the name an export will be triggered. After the export happened, you can change the name back to an empty string.
164 | - `full-export`: *(type: boolean, default: true)* If this is true, all the known data of the table will be downloaded.
165 | If this is false, only the filtered and sorted data will be downloaded.
166 | - `call-rows`: *(type: Function, default: `() => []`)* This function will be called if additional root rows needs to be loaded.
167 | It will give you only one parameter:
168 | - `indexes`: *(type: array)* A list of indexes in the rows array you need to load.
169 | If you're sure all the items needs to be loaded, just take the first and last one and use them as a paramter.
170 | But in some cases, some rows will already been loaded. That's the reason why a list is taken instead of
171 | the first and last one.
172 | - `call-children`: *(type: Function, default: `() => []`)* This function will be called if child rows needs to be loaded.
173 | All child rows has to be loaded at once. There is no possibility to load a part of it later.It will give you only one parameter:
174 | - `parent`: *(type: Object)* The parent row that needs children.
175 | - `call-temp-rows`: *(type: Function, default: `() => []`)* The function will be called if the table is sorted and not all
176 | root rows are loaded or the table is filtered and not all root rows or child rows are loaded. The following parameters are passed to the function:
177 | - `filter`: *(type: string)* The current filter.
178 | - `sortColumns`: *(type: array)* The list of currently sorted columns ordered by sorting order.
179 | - `start`: *(type: Number)* The current start index, used for pagination.
180 | - `end`: *(type: Number)* The current end index, used for pagination.
181 |
182 | You need to return an simple javascript object with the following attributes:
183 | - `rows`: *(type: array)* A list of founded rows for the current parameters.
184 | - `total`: *(type: Number)* The total number of rows found with filtering and sorting, but without pagination.
185 | If this attribute is not set, the length of the rows attribute is taken.
186 |
187 |
188 | Start and end using the Array.slice method to show only a part of the rows. If you don't add them as properties,
189 | their value will be undefined and all the rows will be visible.
190 |
191 | ### Events
192 |
193 | - `total-filtered-rows-change`: This event will be triggered if due to filtering the total amount of rows changes.
194 | It contains one parameter:
195 | - `total`: *(type: Number)* The total number of filtered rows.
196 | - `export`: This event will be triggered if an export is initiated. It contains the following parameter that you can use with the [vue-json-excel package](https://www.npmjs.com/package/vue-json-excel)
197 | - `fields`: *(type: Object)* The fields of the export file.
198 | - `data`: *(type: array)* The rows of the export file.
199 | - `title`: *(type: string)* The name of the export file.
200 | - `selection-change`: This event will be triggered if the `selectable` property is `true` and one or more rows are selected. It contains one parameter:
201 | - `rows`: *(type: array)* The selected rows.
202 |
203 | ### Slots
204 |
205 | #### 1. Row/Column/Cell slot
206 |
207 | If you want to use your own template for all cells in a row, for all cells in a column or for a specific cell,
208 | you can use your own scoped slot with the following names (the first will overrule the latter):
209 | - `_`: Replace a specific cell for the column `` (assigned by the `property` attribute in the column object)
210 | and row `` (assigned by the `_id` attibute in the row object).
211 | - ``: Replace all column cells for the column `` (assigned by the `property` attribute in the column object).
212 | - `_`: Replace all row cells for the row `` (assigned by the `_id` attibute in the row object).
213 |
214 | All three contains a slot-scope which contains the following parameters:
215 | - `row`: The row object.
216 | - `column`: The column object.
217 |
218 | #### 2. No rows slot
219 |
220 | If no rows are loaded, the table displays 'No results found.'. You can replace this message by the scoped slot `no-rows`.
221 |
222 | #### 3. Sort icon slot
223 |
224 | If you want to customize the sort icon add a scoped slot with the name `sort-icon`.
225 | The scope contains only one parameter:
226 |
227 | - `direction`: *(type: boolean or null)* The direction is null if the column is not sorted, true if it's sorted asc and false if it's sorted desc.
228 |
229 | #### 4. Toggle children icon slot
230 |
231 | If you want to customize the toggle children icon add a scoped slot with the name `toggle-children-icon`.
232 | The scope contains two parameters:
233 |
234 | - `expanded`: *(type: boolean)* Indicates if the children are visible or not.
235 | - `loading`: *(type: boolean)* Indicates if the chilrend are currently loading.
236 |
237 | ### Example
238 |
239 | ```vue
240 |
241 |
242 |
254 |
255 | test cell - {{ props.row.name }}
256 |
257 | test column - {{ props.row.city }}
258 |
259 | test row - {{ props.row[props.column.property] }}
260 | Geen resultaten
261 | ({{ props.direction === null ? 'null' : (props.direction ? 'up' : 'down') }})
262 | [{{ props.expanded ? '-' : '+' }}]
263 |
264 |
265 |
266 |
267 |
456 |
457 |
462 | ```
463 |
464 | ## Full component
465 |
466 | The table container is the complete component it adds a filter box and and a paginator to the table.
467 | If your `call-rows` property is not empty, an async table component will be used. If the property is empty and basic table component will be used.
468 |
469 | ### Properties
470 |
471 | You can use the `columns`, `rows`, `filter`, `classes`, `selectable`, `full-export`, `call-rows`, `call-temp-rows` and `call-children` properties from the base and async table.
472 | But their are some additional properties:
473 |
474 | - `debounced-filter-time`: *(type: Number, default: 500)* The time in milliseconds to wait before the input value of the filter box is used.
475 | - `page`: *(type: Number, default: 0)* The initial page of the paginator.
476 | - `items-per-page`: *(type: Number, default: 10)* The number of items per page.
477 | - `export-name`: *(type: string, default: `''`)* The name of the export file to download. The trigger to export the table will be a click on the export button.
478 |
479 | ### Events
480 |
481 | The table container has 2 event:
482 |
483 | - `filter-change`: This is used to update the filter property outside the component.
484 | - `page-change`: This is used to update the page property outside the component.
485 |
486 | ### Slots
487 |
488 | All the slots of the [basic table](#basic_table_slots) can be used.
489 |
490 | And their are 2 additional slots:
491 |
492 | #### 1. Top
493 |
494 | A scoped slot that can be used to overwrite everything that is on the top of the table. It has the following scope:
495 | - `filter`: *(type: string)* The filter value.
496 | - `filterChanged`: *(type: Function)* Execute this function if the filter is changed. It will emit and filter-change and page-change event.
497 |
498 | #### 2. Bottom
499 |
500 | A scoped slot that can be used to overwrite everything that is on the bottom of the table. It has the following scope:
501 | - `total`: *(type: Number)* The total amount of rows.
502 | - `page`: *(type: Number)* The current page.
503 | - `itemsPerPage`: *(type: Number)* The number of rows per page.
504 | - `pageChanged`: *(type: Function)* Execute this function if the page is changed.
505 | - `rangeChanged`: *(type: Function)* Execute this function if the range is changed.
506 |
507 | ## Testing
508 |
509 | We use the jest framework for testing the table tree component. Run the following command to test it:
510 |
511 | ```
512 | npm run test:unit
513 | ```
514 |
515 | ## Changelog
516 |
517 | Read the [CHANGELOG](CHANGELOG.md) file to check what has changed.
518 |
519 | ## Issues
520 |
521 | If you have any issues (bugs, features, ...) on the current project, add them [here](https://gitlab.com/arnedesmedt/vue-ads-table-tree/issues/new).
522 |
523 | ## Contributing
524 |
525 | Do you like to contribute to this project? Please, read the [CONTRIBUTING](CONTRIBUTING.md) file.
526 |
527 | ## Social
528 |
529 | [1]: http://www.twitter.com/arnesmedt
530 | [1.1]: http://i.imgur.com/wWzX9uB.png (@ArneSmedt)
531 | - Follow me on [![alt text][1.1]][1]
532 |
533 | ## Donate
534 |
535 | Want to make a donation?
536 | That would be highly appreciated!
537 |
538 | Make a donation via [PayPal](https://www.paypal.me/arnedesmedt).
539 |
--------------------------------------------------------------------------------