├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE.md ├── README.md ├── bili.config.js ├── demo ├── App.vue ├── Screenshot.png ├── index.js └── users.json ├── package-lock.json ├── package.json └── src ├── DataTable.vue ├── helpers ├── get.js ├── is.js └── validators.js └── mixins ├── Alignable.js ├── Selectable.js └── Sortable.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Default editor's settings. 4 | [*] 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = space 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Handle sources as text and force Unix line endings 2 | * text=auto eol=lf 3 | 4 | # Disable diff for lock file 5 | package-lock.json -diff 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js modules 2 | node_modules/ 3 | 4 | # Log files 5 | *.log 6 | *.log.* 7 | 8 | # OS stuff 9 | .DS_Store 10 | 11 | # Yarn ~srry 12 | yarn.lock 13 | 14 | # Generated CSS & JavaScript sources. 15 | dist/ 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vitor Luiz Cavalcanti 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Data Tablee 2 | 3 | ![[JavaScript Style Guide][2]][3] 4 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FVitorLuizC%2Fvue-data-tablee.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FVitorLuizC%2Fvue-data-tablee?ref=badge_shield) 5 | 6 | Yeap, another Vue table component. This one is based on [vue-good-table][0], a 7 | simple and pretty table component. 8 | 9 | Screenshot of a styled DataTable component 15 | 16 | ## Install 17 | 18 | Install from npm. 19 | 20 | ```sh 21 | npm install vue-data-tablee 22 | ``` 23 | 24 | With `Vue.use` function declare vue-data-tablee components. 25 | 26 | ```js 27 | import 'vue-data-tablee/dist/vue-data-tablee.css' 28 | import Vue from 'vue' 29 | import DataTablee from 'vue-data-tablee' 30 | 31 | Vue.use(DataTablee) 32 | ``` 33 | 34 | You can also import just components you need, without installing globally. 35 | 36 | ```vue 37 | 40 | 41 | 49 | ``` 50 | 51 | ## Component Props 52 | 53 | Name | Type | Default | About 54 | ---- | ---- | ------- | ----- 55 | cols | `Array.` | `[]` | [Cols][4] list. 56 | rows | `Array.` | `[]` | Rows list. 57 | align | `'center' \| 'right' \| 'left'` | `'left'` | Global column alignment option. 58 | empty | `String` | `''` | Empty cell's character. 59 | selectable | `Boolean` | `false` | Add checkbox column to select a row. It emits event on change 60 | sort | `Boolean \| Function.(a:*, b:*):Number` | `true` | Global sort option. Could enable/disable sort or use a custom [sort function][5]. 61 | sortExternal | `Boolean` | `false` | Only change sort and arrow. Useful to sort outsite component. 62 | ## Cols properties 63 | 64 | Name | Type | About 65 | ---- | ---- | ----- 66 | label | `String` | Column label (`{{ label }}`). 67 | field | `String` | Property name, or property path. Ex `'user.contact.phone'`. 68 | align | `'center' \| 'right' \| 'left'` | Column alignment option. Stronger than global sort 69 | width | `Number` | Column width. 70 | hidden | `Boolean` | Defines if column is hidden. 71 | headerClass | `String` | Adds this class to column header cell. 72 | contentClass | `String` | Adds this class to columns content cells. 73 | sort | `Boolean \| Function.(a:*, b:*):Number` | Could enable/disable column sort or use a custom [sort function][5]. Stronger than global sort 74 | 75 | ## Events 76 | 77 | Name | Payload | About 78 | ----- | ------- | ----- 79 | select | `Array.` | Emitted on select row (`selectable` option). 80 | sort | `{ column:Col, sortment:('ascending'\|'descending') }` | Emitted on sort change. 81 | 82 | ## License 83 | 84 | Released under [MIT license][1]. 85 | 86 | [0]: https://github.com/xaksis/vue-good-table 87 | [1]: ./LICENSE.md 88 | [2]: https://standardjs.com 89 | [3]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 90 | [4]: /#cols-properties 91 | [5]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Description 92 | 93 | 94 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FVitorLuizC%2Fvue-data-tablee.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FVitorLuizC%2Fvue-data-tablee?ref=badge_large) 95 | -------------------------------------------------------------------------------- /bili.config.js: -------------------------------------------------------------------------------- 1 | const { name } = require('./package.json') 2 | const vue = require('rollup-plugin-vue').default 3 | const css = require('rollup-plugin-css-only') 4 | 5 | module.exports = { 6 | js: 'buble', 7 | input: 'src/DataTable.vue', 8 | banner: true, 9 | format: ['umd', 'umd-min', 'cjs', 'es'], 10 | filename: name + '[suffix].js', 11 | plugins: [ 12 | css({ output: `dist/${name}.css` }), 13 | vue({ css: false }) 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /demo/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VitorLuizC/vue-data-tablee/29e5c0052384b468021362ee705e2a698c9d521a/demo/Screenshot.png -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import DataTablee from '../' 4 | 5 | Vue.use(DataTablee) 6 | 7 | global.vue = new Vue({ 8 | el: '#app', 9 | render: h => h(App) 10 | }) 11 | -------------------------------------------------------------------------------- /demo/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "daa3daeb-91c6-42f8-aaf4-4e80c297c000", 4 | "name": "Darsie Tellett", 5 | "email": "dtellett1@behance.net", 6 | "gender": "Male", 7 | "birth_date": "1990-07-12", 8 | "address": { 9 | "country": "United States", 10 | "zip": "84130", 11 | "province": "Utah", 12 | "city": "Salt Lake City", 13 | "street": "Schurz", 14 | "number": "08529" 15 | } 16 | }, 17 | { 18 | "id": "37742e75-6839-461a-8727-d00b4333176a", 19 | "name": "Hilarius Jeaffreson", 20 | "email": "hjeaffreson2@ted.com", 21 | "gender": "Male", 22 | "birth_date": "1993-06-13", 23 | "address": { 24 | "country": "United States", 25 | "zip": "34290", 26 | "province": "Florida", 27 | "city": "North Port", 28 | "street": "Milwaukee", 29 | "number": "0" 30 | } 31 | }, 32 | { 33 | "id": "a6ebf9ee-3cc5-4384-b73c-594a79207774", 34 | "name": "Lyman Haxley", 35 | "email": "lhaxley3@newyorker.com", 36 | "gender": 10, 37 | "birth_date": "1972-12-29", 38 | "address": { 39 | "country": "United States", 40 | "zip": "77260", 41 | "province": "Texas", 42 | "city": "Houston", 43 | "street": "Scoville", 44 | "number": "9514" 45 | } 46 | }, 47 | { 48 | "id": "77b39998-188a-4967-a960-12372308345c", 49 | "name": "Lock Kearney", 50 | "email": "lkearney4@ucsd.edu", 51 | "gender": "Male", 52 | "birth_date": "1983-01-17", 53 | "address": { 54 | "country": "United States", 55 | "zip": "43215", 56 | "province": "Ohio", 57 | "city": "Columbus", 58 | "street": "5th", 59 | "number": "36" 60 | } 61 | }, 62 | { 63 | "id": "df88b137-fc34-4bd4-b32a-595f79867906", 64 | "name": "Ame Blachford", 65 | "email": "ablachford5@miitbeian.gov.cn", 66 | "gender": "Female", 67 | "birth_date": "1974-03-04", 68 | "address": { 69 | "country": "United States", 70 | "zip": "32803", 71 | "province": "Florida", 72 | "city": "Orlando", 73 | "street": "Hovde", 74 | "number": "6251" 75 | } 76 | }, 77 | { 78 | "id": "d18a89e9-a707-4bed-9b75-73a1e712714b", 79 | "name": "Bobbye Seldon", 80 | "email": "bseldon6@artisteer.com", 81 | "gender": "Male", 82 | "birth_date": "1993-07-23", 83 | "address": { 84 | "country": "United States", 85 | "zip": "83757", 86 | "province": "Idaho", 87 | "city": "Boise", 88 | "street": "Delaware", 89 | "number": "9754" 90 | } 91 | } 92 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-data-tablee", 3 | "version": "0.13.0", 4 | "description": "A very simple table component.", 5 | "main": "dist/vue-data-tablee.cjs.js", 6 | "module": "dist/vue-data-tablee.es.js", 7 | "browser": "dist/vue-data-tablee.min.js", 8 | "scripts": { 9 | "test": "standard", 10 | "start": "poi demo/index.js", 11 | "build": "bili", 12 | "prepare": "npm run build && npm test" 13 | }, 14 | "files": [ 15 | "dist/", 16 | "README.md", 17 | "LICENSE.md" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/VitorLuizC/vue-data-tablee.git" 22 | }, 23 | "keywords": [ 24 | "data-tablee", 25 | "data-table", 26 | "data-grid", 27 | "table", 28 | "vue" 29 | ], 30 | "author": { 31 | "url": "https://vitorluizc.github.io/", 32 | "name": "Vitor Cavalcanti", 33 | "email": "vitorluizc@outlook.com" 34 | }, 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/VitorLuizC/vue-data-tablee/issues" 38 | }, 39 | "homepage": "https://github.com/VitorLuizC/vue-data-tablee#readme", 40 | "standard": { 41 | "ignore": [ 42 | "dist/" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "bili": "^3.1.2", 47 | "node-sass": "^4.9.2", 48 | "poi": "^10.2.9", 49 | "rollup-plugin-css-only": "^0.4.0", 50 | "rollup-plugin-vue": "^4.3.1", 51 | "standard": "^11.0.1", 52 | "vue": "^2.5.16", 53 | "vue-template-compiler": "^2.5.16" 54 | }, 55 | "dependencies": { 56 | "object-take": "^0.1.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DataTable.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 165 | 166 | 250 | 251 | -------------------------------------------------------------------------------- /src/helpers/get.js: -------------------------------------------------------------------------------- 1 | import is from './is' 2 | import get from 'object-take' 3 | 4 | const DEFAULT_VALIDATE = (value) => !is(value, 'Null') 5 | 6 | /** 7 | * Get value from first object. 8 | * @param {string} name 9 | * @param {Deep[]} objects 10 | * @param {function(*):boolean} [validate] 11 | * @returns {*} 12 | */ 13 | export const getProperty = (name, objects, validate = DEFAULT_VALIDATE) => { 14 | const properties = objects.map((object) => get(object, name)) 15 | const property = properties.find((property) => validate(property)) 16 | return property 17 | } 18 | 19 | export default get 20 | -------------------------------------------------------------------------------- /src/helpers/is.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check value's constructor name. 3 | * @param {*} value 4 | * @param {string} constructor 5 | * @returns {boolean} 6 | */ 7 | export default (value, constructor) => { 8 | const is = Object.prototype.toString.call(value) === `[object ${constructor}]` 9 | return is 10 | } 11 | -------------------------------------------------------------------------------- /src/helpers/validators.js: -------------------------------------------------------------------------------- 1 | import is from './is' 2 | 3 | /** 4 | * Creates a validator function that checks is value is included in values. 5 | * @param {Array} values 6 | * @returns {function(*):boolean} 7 | */ 8 | const includes = (values) => (value) => values.includes(value) 9 | 10 | const ALIGNMENTS = ['right', 'left', 'center'] 11 | 12 | /** 13 | * Checks if value is an alignment. 14 | */ 15 | export const isAlignment = includes(ALIGNMENTS) 16 | 17 | /** 18 | * Checks if value is a list of objects. 19 | * @param {*} value 20 | * @returns {boolean} 21 | */ 22 | export const isContent = (value) => { 23 | const isObject = (value) => is(value, 'Object') 24 | const isContent = is(value, 'Array') && value.every(isObject) 25 | return isContent 26 | } 27 | -------------------------------------------------------------------------------- /src/mixins/Alignable.js: -------------------------------------------------------------------------------- 1 | import { getProperty } from '../helpers/get' 2 | import { isAlignment } from '../helpers/validators' 3 | 4 | const Alignable = ({ cols = 'cols' } = {}) => ({ 5 | props: { 6 | /** 7 | * Default cell's alignment. 8 | */ 9 | align: { 10 | type: String, 11 | default: 'left', 12 | validator: isAlignment 13 | } 14 | }, 15 | methods: { 16 | /** 17 | * Get column's alignment. 18 | * @param {number} index 19 | * @returns {('right'|'left'|'center')} 20 | */ 21 | $getAlignment (index) { 22 | const col = this[cols][index] 23 | const alignment = getProperty('align', [col, this._props], isAlignment) 24 | return alignment 25 | } 26 | } 27 | }) 28 | 29 | export default Alignable 30 | -------------------------------------------------------------------------------- /src/mixins/Selectable.js: -------------------------------------------------------------------------------- 1 | const Selectable = ({ rows = 'rows' } = {}) => ({ 2 | props: { 3 | selectable: Boolean 4 | }, 5 | 6 | data () { 7 | return { 8 | selectedRows: [], 9 | lastClicked: undefined 10 | } 11 | }, 12 | 13 | computed: { 14 | isSelectedAll () { 15 | const isNotEmpty = !!this[rows].length 16 | const isEqualsLength = isNotEmpty && this[rows].length === this.selectedRows.length 17 | const isSelectedAll = isEqualsLength && this[rows].every((row) => { 18 | return this.selectedRows.includes(row) 19 | }) 20 | return isSelectedAll 21 | } 22 | }, 23 | 24 | watch: { 25 | rows () { 26 | this.selectedRows = [] 27 | } 28 | }, 29 | 30 | methods: { 31 | /** 32 | * Check if a row is selected. 33 | * @param {object} row 34 | * @returns {boolean} 35 | */ 36 | isSelected (row) { 37 | const isSelected = !!this.selectedRows.find((selected) => selected === row) 38 | return isSelected 39 | }, 40 | 41 | /** 42 | * Set row active. 43 | * @param {object} row 44 | * @param {object} event 45 | */ 46 | select (row, event) { 47 | if (event.shiftKey && this.lastClicked !== row) { 48 | this.multipleSelect(row) 49 | } else { 50 | this.selectedRows = this.isSelected(row) 51 | ? this.selectedRows.filter((selected) => selected !== row) 52 | : [ ...this.selectedRows, row ] 53 | } 54 | this.lastClicked = row 55 | this.emitSelected() 56 | }, 57 | 58 | /** 59 | * Set multiple rows active. 60 | * @param {object} row 61 | */ 62 | multipleSelect (row) { 63 | const s1 = this[rows].indexOf(row) 64 | const s2 = this[rows].indexOf(this.lastClicked) 65 | const [start, end] = [s1, s2].sort() 66 | const range = Array(end - start + 1).fill().map((_, index) => start + index) 67 | this.selectedRows = this[rows].filter((_, index) => range.includes(index)) 68 | }, 69 | 70 | /** 71 | * Set all rows active. 72 | * @param {Event} event 73 | */ 74 | selectAll (event) { 75 | this.selectedRows = this.isSelectedAll ? [] : [ ...this[rows] ] 76 | this.emitSelected() 77 | }, 78 | 79 | /** 80 | * Emit selected rows. 81 | */ 82 | emitSelected () { 83 | this.$emit('select', this.selectedRows) 84 | } 85 | } 86 | }) 87 | 88 | export default Selectable 89 | -------------------------------------------------------------------------------- /src/mixins/Sortable.js: -------------------------------------------------------------------------------- 1 | import is from '../helpers/is' 2 | import get, { getProperty } from '../helpers/get' 3 | 4 | /** 5 | * Default sort function. 6 | * @param {(string|number)} a 7 | * @param {(string|number)} b 8 | * @returns {number} 9 | */ 10 | const DEFAULT_SORT = (a = '', b = '') => { 11 | const isNumbers = is(a, 'Number') && is(b, 'Number') 12 | if (isNumbers) { 13 | return a - b 14 | } else { 15 | return String(a || '').localeCompare(String(b || '')) 16 | } 17 | } 18 | 19 | const Sortable = ({ cols = 'cols', rows = 'rows' } = {}) => ({ 20 | props: { 21 | /** 22 | * Defines if it sort and optionally define's default sort function. 23 | */ 24 | sort: { 25 | type: [Boolean, Function], 26 | default: true 27 | }, 28 | 29 | /** 30 | * Defines if sort just emit events. 31 | */ 32 | sortExternal: Boolean 33 | }, 34 | 35 | data () { 36 | return { 37 | sorter: null, 38 | sortment: 'ascending' 39 | } 40 | }, 41 | 42 | computed: { 43 | $sortedRows () { 44 | const isSorted = is(this.sorter, 'Number') 45 | if (!isSorted || this.sortExternal) { 46 | return [ ...this[rows] ] 47 | } 48 | 49 | const sorted = [ ...this[rows] ].sort((a, b) => this.$sort(a, b)) 50 | return sorted 51 | } 52 | }, 53 | 54 | methods: { 55 | /** 56 | * Check if column is sorting. 57 | * @param {number} index 58 | * @returns {boolean} 59 | */ 60 | $isSorting (index) { 61 | const isSorting = this.sorter === index 62 | return isSorting 63 | }, 64 | 65 | /** 66 | * Check if a column is sortable. 67 | * @param {number} index 68 | * @returns {boolean} 69 | */ 70 | $isSortable (index) { 71 | const isSortable = !!this.$getSort(index) 72 | return isSortable 73 | }, 74 | 75 | /** 76 | * Get column's sort setting. 77 | * @param {number} index 78 | * @returns {(boolean|function)} 79 | */ 80 | $getSort (index) { 81 | const col = this[cols][index] 82 | const sort = getProperty('sort', [col, this._props]) 83 | return sort 84 | }, 85 | 86 | /** 87 | * Get column sort classes. 88 | * @param {number} index 89 | * @returns {(string|Array.)[]} 90 | */ 91 | $getSortClasses (index) { 92 | const isSortable = this.$isSortable(index) 93 | const isSorting = this.$isSorting(index) 94 | const classes = { 95 | '-sorting': isSorting, 96 | '-sortable': isSortable, 97 | '-unsortable': !isSortable, 98 | ['-' + this.sortment]: isSorting 99 | } 100 | return classes 101 | }, 102 | 103 | /** 104 | * Get column arrow's. 105 | * @param {number} index 106 | * @returns {('▼'|'▲'|'')} 107 | */ 108 | $getArrow (index) { 109 | const isSorting = this.$isSorting(index) 110 | if (!isSorting) return '▲' 111 | const arrow = this.sortment === 'ascending' ? '▲' : '▼' 112 | return arrow 113 | }, 114 | 115 | /** 116 | * Sort a column or change its sortment. 117 | * @param {number} index 118 | */ 119 | $setSorter (index) { 120 | const isSorter = this.$isSorting(index) 121 | const isSortable = this.$isSortable(index) 122 | 123 | if (!isSortable) { 124 | return 125 | } 126 | 127 | const column = this[cols][index] 128 | const sortment = !isSorter || this.sortment === 'descending' ? 'ascending' : 'descending' 129 | this.sortment = sortment 130 | this.sorter = index 131 | this.$emit('sort', { column, sortment }) 132 | }, 133 | 134 | /** 135 | * Returns diference between values. 136 | * This is the sort function. 137 | * @param {object} rowA 138 | * @param {object} rowB 139 | * @returns {number} 140 | */ 141 | $sort (rowA, rowB) { 142 | const custom = this.$getSort(this.sorter) 143 | const sort = is(custom, 'Function') ? custom : DEFAULT_SORT 144 | const path = this[cols][this.sorter].field 145 | const number = sort(get(rowA, path), get(rowB, path)) 146 | const result = number * (this.sortment === 'ascending' ? 1 : -1) 147 | return result 148 | } 149 | } 150 | }) 151 | 152 | export default Sortable 153 | --------------------------------------------------------------------------------