├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── docs ├── CNAME ├── app.js ├── build │ └── app.js ├── index.html ├── table-component.css └── webpack.config.js ├── package.json ├── src ├── classes │ ├── Column.js │ └── Row.js ├── components │ ├── Pagination.vue │ ├── TableCell.js │ ├── TableColumn.vue │ ├── TableColumnHeader.vue │ ├── TableComponent.vue │ └── TableRow.vue ├── expiring-storage.js ├── helpers.js ├── index.js └── settings.js ├── tests ├── .eslintrc ├── bootstrap.js ├── components │ ├── Pagination.test.js │ ├── TableComponent.test.js │ └── __snapshots__ │ │ ├── Pagination.test.js.snap │ │ └── TableComponent.test.js.snap ├── concerns │ ├── __snapshots__ │ │ └── filtering.test.js.snap │ ├── caching.test.js │ ├── expiringStorage.test.js │ ├── filtering.test.js │ ├── settings.test.js │ └── sorting.test.js ├── createVm.js ├── html-serializer.js └── mocks │ └── LocalStorageMock.js ├── webpack.base.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "safari >= 7"], 6 | "uglify": true 7 | }, 8 | "modules": "umd" 9 | }] 10 | ], 11 | "plugins": [ 12 | "transform-object-rest-spread", 13 | ["transform-runtime", { 14 | "polyfill": true, 15 | "regenerator": true 16 | }] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [{package.json,*.scss,*.css}] 15 | indent_size = 2 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "spatie" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | node_modules 3 | npm-debug.log* 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __tests__ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `vue-table-component` will be documented in this file. 4 | 5 | ## 1.9.2 - 2018-03-14 6 | - added `row-click` event 7 | 8 | ## 1.9.1 - 2018-02-26 9 | - Better IE support 10 | 11 | ## 1.9.0 - 2018-02-08 12 | - Better pagination component 13 | - Fixed global settings that weren't always applies 14 | - Fixed sorting with null values 15 | 16 | ## 1.8.1 - 2018-01-02 17 | - Fixed column contents with properties retrieved with dot notation 18 | 19 | ## 1.8.0 - 2017-11-15 20 | - Added a per-row click listener `` 21 | - Removed lodash dependency for a leaner build size 22 | - Republished package due to build issues 23 | 24 | ## 1.7.0 - 2017-11-02 25 | - Added named slot `tfoot` to display table footer information, receives row data as scoped properties 26 | 27 | ## 1.6.1 - 2017-09-25 28 | - Fixed a bug that didn't rerender the table when a column was changed 29 | 30 | ## 1.6.0 - 2017-09-24 31 | - Added `tbody-class` prop 32 | 33 | ## 1.5.0 - 2017-09-21 34 | - Added `thead-class` prop 35 | 36 | ## 1.4.3 - 2017-08-30 37 | - Fixed a bug that didn't rerender the table when a column was changed 38 | 39 | ## 1.4.2 - 2017-08-29 40 | - Added `cache-key` prop to manually set a cache key for local storage state 41 | - The filter input is now hidden when there are no filterable rows 42 | 43 | ## 1.4.1 - 2017-08-29 44 | - Fixed `regeneratorRuntime` issues 45 | 46 | ## 1.4.0 - 2017-08-16 47 | - Fixed cell rendering: HTML is now escaped by default. If you want raw html, use the new scoped slots feature 48 | - Added scoped slot support to `table-columns` for custom column contents 49 | - Added the `filter-input-class` prop to the `table-component` component (`filterInputClass` in settings) 50 | - Added the `header-class` and `cell-class` props to the `table-column` component (`headerClass` and `cellClass` in settings) 51 | - Added a minified build: `vue-table-component/dist/index.min.js` 52 | - Fixed the parsing of the a date format that contains `:` 53 | 54 | ## 1.3.0 - 2017-08-07 55 | - Added `formatter` property 56 | 57 | ## 1.2.2 - 2017-08-04 58 | - Fix for displaying nested properties 59 | 60 | ## 1.2.1 - 2017-08-04 61 | - Fix async data retrieval and pagination 62 | 63 | ## 1.2.0 - 2017-08-02 64 | - Add async data retrieval and pagination 65 | 66 | ## 1.1.3 - 2017-06-29 67 | - Fixed a filter bug caused by null values 68 | 69 | ## 1.1.2 - 2017-06-28 70 | - Added optional `hidden` prop to table column component 71 | 72 | ## 1.1.1 - 2017-06-28 73 | - Fixed `filterNoResults` label 74 | 75 | ## 1.1.0 - 2017-06-23 76 | - Added `show-caption` prop 77 | 78 | ## 1.0.1- 2017-06-13 79 | - Fix default sort order 80 | - Fix ordering of numeric and date columns in Safari 81 | 82 | ## 1.0.0 - 2017-06-13 83 | - Initial release 84 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/spatie/vue-table-component). 6 | 7 | ## Pull Requests 8 | 9 | - Use the ES6 syntax. 10 | - Your patch won't be accepted if it doesn't pass the tests and lints (`npm run test`). 11 | - If there's a `/demo` section, try to add an example. 12 | - **Document any change in behaviour:** Make sure the `README.md`, `CHANGELOG.md` and any other relevant documentation are kept up-to-date. 13 | - **Consider our release cycle:** We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 14 | - **Create feature branches:** Don't ask us to pull from your master branch. 15 | - **One pull request per feature:** If you want to do more than one thing, send multiple pull requests. 16 | - **Send coherent history:** Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 17 | 18 | ## Running Tests 19 | 20 | ```bash 21 | jest 22 | ``` 23 | 24 | **Happy coding**! 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 🚨 **THIS PACKAGE HAS BEEN ABANDONED** 🚨 2 | 3 | We don't use this package anymore in our own projects and cannot justify the time needed to maintain it anymore. That's why we have chosen to abandon it. Feel free to fork our code and maintain your own copy or use one of the many alternatives. 4 | 5 | # A straightforward Vue component to filter and sort tables 6 | 7 | [![Latest Version on NPM](https://img.shields.io/npm/v/vue-table-component.svg?style=flat-square)](https://npmjs.com/package/vue-table-component) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 9 | [![Build Status](https://img.shields.io/travis/spatie/vue-table-component/master.svg?style=flat-square)](https://travis-ci.org/spatie/vue-table-component) 10 | [![npm](https://img.shields.io/npm/dt/vue-table-component.svg?style=flat-square)](https://www.npmjs.com/package/vue-table-component) 11 | 12 | --- 13 | 14 | 🚨 **WARNING: FEATURE FREEZE** 🚨 15 | 16 | Version 1 of this package has become very hard to maintain due to the way it's built up. We also have too many feature requests, which we can't all cater too. We're working on v2 of this package, and won't be adding any new features or accepting feature PR's for v1. 17 | 18 | --- 19 | 20 | This repo contains a Vue component that can render a filterable and sortable table. It aims to be very lightweight and easy to use. It has support for [retrieving data asynchronously and pagination](#retrieving-data-asynchronously). 21 | 22 | Here's an example of how you can use it: 23 | 24 | ```html 25 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | ``` 47 | 48 | A cool feature is that the table caches the used filter and sorting for 15 minutes. So if you refresh the page, the filter and sorting will still be used. 49 | 50 | ## Demo 51 | 52 | Want to see the component in action? No problem. [Here's a demo](http://vue-table-component.spatie.be). 53 | 54 | ## Installation 55 | 56 | You can install the package via yarn: 57 | 58 | ```bash 59 | yarn add vue-table-component 60 | ``` 61 | 62 | or npm: 63 | 64 | ```bash 65 | npm install vue-table-component --save 66 | ``` 67 | 68 | Next, you must register the component. The most common use case is to do that globally. 69 | 70 | ```js 71 | //in your app.js or similar file 72 | import Vue from 'vue'; 73 | import { TableComponent, TableColumn } from 'vue-table-component'; 74 | 75 | Vue.component('table-component', TableComponent); 76 | Vue.component('table-column', TableColumn); 77 | ``` 78 | 79 | Alternatively you can do this to register the components: 80 | 81 | ```js 82 | import TableComponent from 'vue-table-component'; 83 | 84 | Vue.use(TableComponent); 85 | ``` 86 | 87 | ## Browser Support 88 | 89 | `vue-table-component` has the same browser support as Vue (see https://github.com/vuejs/vue). However, you might need to polyfill the [`Array.prototype.find`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find#Polyfill) method for IE support. 90 | 91 | ## Usage 92 | 93 | Here's a simple example on how to use the component. 94 | 95 | ```html 96 | 106 | 107 | 108 | 109 | 110 | ``` 111 | 112 | This will render a table that is both filterable and sortable. A filter field will be displayed right above the table. If your data contains any html we will filter that out when filtering. You can sort the table by clicking on the column headers. By default it will remember the used filter and sorting for the next 15 minutes. 113 | 114 | ### Props 115 | 116 | You can pass these props to `table-component`: 117 | 118 | - `data`: (required) the data the component will operate on. This can either be an array or [a function](#retrieving-data-asynchronously) 119 | - `show-filter`: set this to `false` to not display the `filter` field. 120 | - `show-caption`: set this to `false` to not display the `caption` field which shows the current active filter. 121 | - `sort-by`: the property in data on which to initially sort. 122 | - `sort-order`: the initial sort order. 123 | - `cache-lifetime`: the lifetime in minutes the component will cache the filter and sorting. 124 | - `cache-key`: if you use multiple instances of `table-component` on the same page you must set this to a unique value per instance. 125 | - `table-class`: the passed value will be added to the `class` attribute of the rendered table 126 | - `thead-class`: the passed value will be added to the `class` attribute of the rendered table head. 127 | - `tbody-class`: the passed value will be added to the `class` attribute of the rendered table body. 128 | - `filter-placeholder`: the text used as a placeholder in the filter field 129 | - `filter-input-class`: additional classes that you will be applied to the filter text input 130 | - `filter-no-results`: the text displayed when the filtering returns no results 131 | 132 | For each `table-column` a column will be rendered. It can have these props: 133 | 134 | - `show`: (required) the property name in the data that needs to be shown in this column. 135 | - `formatter`: a function the will receive the value that will be displayed and all column properties. The return value of this function will be displayed. Here's [an example](#formatting-values) 136 | - `label`: the label that will be shown on top of the column. Set this to an empty string to display nothing. If this property is not present, the string passed to `show` will be used. 137 | - `data-type`: if your column should be sorted numerically set this to `numeric`. If your column contains dates set it to `date:` followed by the format of your date 138 | - `sortable`: if you set this to `false` then the column won't be sorted when clicking the column header 139 | - `sort-by`: you can set this to any property present in `data`. When sorting the column that property will be used to sort on instead of the property in `show`. 140 | - `filterable`: if this is set to `false` than this column won't be used when filtering 141 | - `filter-on`: you can set this to any property present in `data`. When filtering the column that property will be used to filter on instead of the property in `show`. 142 | - `hidden`: if you set this to `true` then the column will be hidden. This is useful when you want to sort by a field but don't want it to be visible. 143 | - `header-class`: the passed value will be added to the `class` attribute of the columns `th` element. 144 | - `cell-class`: the passed value will be added to the `class` attribute of the columns `td` element. 145 | 146 | ## Listeners 147 | 148 | The `table-component` currently emits one custom event: 149 | 150 | - `@rowClick`: is fired when a row is clicked. Receives the row data as it's event payload. 151 | 152 | ### Modifying the used texts and CSS classes 153 | 154 | If you want to modify the built in text or classes you can pass settings globally. 155 | You can use the [CSS](docs/table-component.css) from the docs as a starting point for your own styling. 156 | 157 | ```js 158 | import TableComponent from 'vue-table-component'; 159 | 160 | TableComponent.settings({ 161 | tableClass: '', 162 | theadClass: '', 163 | tbodyClass: '', 164 | filterPlaceholder: 'Filter table…', 165 | filterNoResults: 'There are no matching rows', 166 | }); 167 | ``` 168 | 169 | You can also provide the custom settings on Vue plugin install hook: 170 | 171 | ```js 172 | import Vue from 'vue'; 173 | import TableComponent from 'vue-table-component'; 174 | 175 | Vue.use(TableComponent, { 176 | tableClass: '', 177 | theadClass: '', 178 | tbodyClass: '', 179 | filterPlaceholder: 'Filter table…', 180 | filterNoResults: 'There are no matching rows', 181 | }); 182 | ``` 183 | 184 | ## Retrieving data asynchronously 185 | 186 | The component can fetch data in an asynchronous manner. The most common use case for this is fetching data from a server. 187 | 188 | To use the feature you should pass a function to the `data` prop. The function will receive an object with `filter`, `sort` and `page`. You can use these parameters to fetch the right data. The function should return an object with the following properties: 189 | 190 | - `data`: (required) the data that should be displayed in the table. 191 | - `pagination`: (optional) this should be an object with keys `currentPage` and `totalPages`. If `totalPages` is higher than 1 pagination links will be displayed. 192 | 193 | Here's an example: 194 | 195 | ```html 196 | 203 | 204 | 218 | ``` 219 | 220 | If you for some reason need to manually refresh the table data, you can call the `refresh` method on the component. 221 | 222 | ```html 223 | 224 | 225 | 226 | ``` 227 | 228 | ```js 229 | this.$refs.table.refresh(); 230 | ``` 231 | 232 | ## Formatting values 233 | 234 | You can format values before they get displayed by using scoped slots. Here's a quick example: 235 | 236 | ```html 237 | 245 | 246 | 247 | 250 | 251 | 252 | ``` 253 | 254 | Alternatively you can pass a function to the `formatter` prop. Here's an example Vue component that uses the feature. 255 | 256 | ```vue 257 | 263 | 264 | 273 | ``` 274 | 275 | This will display values `Hi, I am John` and `Hi, I am Paul`. 276 | 277 | ## Adding table footer `` information 278 | 279 | Sometimes it can be useful to add information to the bottom of the table like summary data. 280 | A slot named `tfoot` is available and it receives all of the `rows` data to do calculations on the fly or you can show data directly from whatever is available in the parent scope. 281 | 282 | ```html 283 | 285 | 286 | 287 | 295 | 296 | ``` 297 | 298 | OR 299 | 300 | ```vue 301 | 314 | 330 | ``` 331 | 332 | Note: `rows` slot scope data includes more information gathered by the Table Component (e.g. `columns`) and `rows.data` is where the original `data` information is located. 333 | 334 | ## Changelog 335 | 336 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 337 | 338 | ## Testing 339 | 340 | ```bash 341 | yarn test 342 | ``` 343 | 344 | ## Contributing 345 | 346 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 347 | 348 | ## Postcardware 349 | 350 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 351 | 352 | Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. 353 | 354 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 355 | 356 | ## Security 357 | 358 | If you discover any security related issues, please contact freek@spatie.be instead of using the issue tracker. 359 | 360 | ## Credits 361 | 362 | - [Freek Van der Herten](https://github.com/freekmurze) 363 | - [Sebastian De Deyne](https://github.com/sebdedeyne) 364 | - [All Contributors](../../contributors) 365 | 366 | The Pagination component was inspired by [this lesson on Laracasts.com](https://laracasts.com/series/lets-build-a-forum-with-laravel/episodes/16). 367 | 368 | ## Support us 369 | 370 | Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). 371 | 372 | Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). 373 | All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. 374 | 375 | ## License 376 | 377 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 378 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | vue-table-component.spatie.be -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { TableColumn, TableComponent } from '../src'; 3 | 4 | new Vue({ 5 | el: '#app', 6 | 7 | components: { 8 | TableColumn, 9 | TableComponent, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | vue-table-component 4 | 5 | 128 | 129 | 130 |
131 |

132 | Vue-table-component 133 |

134 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 155 | 156 | 166 | 167 | 168 |
169 |

About

170 |

171 | The table component above is powered by spatie/vue-table-component. 172 |
173 | The component allows for straightforward and lightweight sorting and filtering of table data.

174 | 175 |

The component will also remember the state when you reload or return to the same page within 15 min. 176 |

177 |
178 | 179 | 180 | Code on GitHub | 181 | Proudly presented by spatie.be 182 | 183 |
184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /docs/table-component.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:after, 3 | *:before { 4 | position: relative; 5 | box-sizing: border-box; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | .table-component { 11 | display: flex; 12 | flex-direction: column; 13 | margin: 4em 0; 14 | } 15 | 16 | .table-component__filter { 17 | align-self: flex-end; 18 | } 19 | 20 | .table-component__filter__field { 21 | padding: 0 1.25em 0 .75em; 22 | height: 2.5em; 23 | border: solid 2px #e0e0e0; 24 | border-radius: 2em; 25 | font-size: inherit; 26 | } 27 | 28 | .table-component__filter__clear { 29 | position: absolute; 30 | top: 0; 31 | right: 0; 32 | bottom: 0; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | width: 2em; 37 | color: #007593; 38 | font-weight: bold; 39 | cursor: pointer; 40 | } 41 | 42 | .table-component__filter__field:focus { 43 | outline: 0; 44 | border-color: #007593; 45 | } 46 | 47 | .table-component__table-wrapper { 48 | overflow-x: auto; 49 | margin: 1em 0; 50 | width: 100%; 51 | border: solid 1px #ddd; 52 | border-bottom: none; 53 | } 54 | 55 | .table-component__table { 56 | min-width: 100%; 57 | border-collapse: collapse; 58 | border-bottom: solid 1px #ddd; 59 | table-layout: fixed; 60 | } 61 | 62 | .table-component__table__caption { 63 | position: absolute; 64 | top: auto; 65 | left: -10000px; 66 | overflow: hidden; 67 | width: 1px; 68 | height: 1px; 69 | } 70 | 71 | .table-component__table th, 72 | .table-component__table td { 73 | padding: .75em 1.25em; 74 | vertical-align: top; 75 | text-align: left; 76 | } 77 | 78 | .table-component__table th { 79 | background-color: #e0e0e0; 80 | color: #999; 81 | text-transform: uppercase; 82 | white-space: nowrap; 83 | font-size: .85em; 84 | } 85 | 86 | .table-component__table tbody tr:nth-child(even) { 87 | background-color: #f0f0f0; 88 | } 89 | 90 | .table-component__table a { 91 | color: #007593; 92 | } 93 | 94 | .table-component__message { 95 | color: #999; 96 | font-style: italic; 97 | } 98 | 99 | .table-component__th--sort, 100 | .table-component__th--sort-asc, 101 | .table-component__th--sort-desc { 102 | text-decoration: underline; 103 | cursor: pointer; 104 | -webkit-user-select: none; 105 | -moz-user-select: none; 106 | -ms-user-select: none; 107 | user-select: none; 108 | } 109 | 110 | .table-component__th--sort-asc:after, 111 | .table-component__th--sort-desc:after { 112 | position: absolute; 113 | left: .25em; 114 | display: inline-block; 115 | color: #bbb; 116 | } 117 | 118 | .table-component__th--sort-asc:after { 119 | content: '↑'; 120 | } 121 | 122 | .table-component__th--sort-desc:after { 123 | content: '↓'; 124 | } 125 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | 4 | module.exports = merge(require('../webpack.base'), { 5 | context: __dirname, 6 | 7 | entry: './app.js', 8 | 9 | output: { 10 | path: path.resolve(__dirname, 'build'), 11 | filename: 'app.js', 12 | publicPath: '/build/', 13 | }, 14 | 15 | resolve: { 16 | alias: { 17 | vue: 'vue/dist/vue.js', 18 | }, 19 | }, 20 | 21 | devServer: { 22 | contentBase: __dirname, 23 | port: 2000, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-table-component", 3 | "version": "1.9.2", 4 | "description": "A straightforward Vue component to filter and sort tables", 5 | "main": "dist/index.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "start": "webpack-dev-server --config docs/webpack.config.js", 9 | "demo": "NODE_ENV=production webpack --config docs/webpack.config.js", 10 | "build": "rm -rf dist && NODE_ENV=production webpack", 11 | "lint": "eslint src __tests__ --ext .js,.vue --fix; exit 0", 12 | "prepublish": "npm run test; npm run build", 13 | "test": "jest" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/spatie/vue-table-component.git" 18 | }, 19 | "keywords": [ 20 | "spatie" 21 | ], 22 | "author": "Freek Van der Herten", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/spatie/vue-table-component/issues" 26 | }, 27 | "homepage": "https://github.com/spatie/vue-table-component", 28 | "peerDependencies": { 29 | "moment": "^2.18.1", 30 | "vue": "^2.5.0" 31 | }, 32 | "devDependencies": { 33 | "babel-core": "^6.24.1", 34 | "babel-loader": "^7.0.0", 35 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 36 | "babel-plugin-transform-runtime": "^6.23.0", 37 | "babel-preset-env": "^1.4.0", 38 | "css-loader": "^0.28.1", 39 | "diffable-html": "^2.1.0", 40 | "eslint": "^4.0.0", 41 | "eslint-config-spatie": "^2.0.0", 42 | "jest": "^19.0.0", 43 | "jest-serializer-html": "^4.0.0", 44 | "jest-vue-preprocessor": "^0.2.0", 45 | "moment": "^2.18.1", 46 | "simulant": "^0.2.2", 47 | "vue": "^2.5.0", 48 | "vue-loader": "^12.0.3", 49 | "vue-template-compiler": "^2.3.0", 50 | "webpack": "^2.3.3", 51 | "webpack-dev-server": "^2.4.2", 52 | "webpack-merge": "^4.1.0" 53 | }, 54 | "jest": { 55 | "testRegex": "test.js$", 56 | "moduleFileExtensions": [ 57 | "js", 58 | "vue" 59 | ], 60 | "setupFiles": [ 61 | "/tests/bootstrap" 62 | ], 63 | "snapshotSerializers": [ 64 | "/tests/html-serializer" 65 | ], 66 | "transform": { 67 | "^.+\\.js$": "/node_modules/babel-jest", 68 | ".*\\.(vue)$": "/node_modules/jest-vue-preprocessor" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/classes/Column.js: -------------------------------------------------------------------------------- 1 | import { pick } from '../helpers'; 2 | 3 | export default class Column { 4 | constructor(columnComponent) { 5 | const properties = pick(columnComponent, [ 6 | 'show', 'label', 'dataType', 'sortable', 'sortBy', 'filterable', 7 | 'filterOn', 'hidden', 'formatter', 'cellClass', 'headerClass', 8 | ]); 9 | 10 | for (const property in properties) { 11 | this[property] = columnComponent[property]; 12 | } 13 | 14 | this.template = columnComponent.$scopedSlots.default; 15 | } 16 | 17 | 18 | isFilterable() { 19 | return this.filterable; 20 | } 21 | 22 | getFilterFieldName() { 23 | return this.filterOn || this.show; 24 | } 25 | 26 | isSortable() { 27 | return this.sortable; 28 | } 29 | 30 | getSortPredicate(sortOrder, allColumns) { 31 | const sortFieldName = this.getSortFieldName(); 32 | 33 | const sortColumn = allColumns.find(column => column.show === sortFieldName); 34 | 35 | const dataType = sortColumn.dataType; 36 | 37 | if (dataType.startsWith('date') || dataType === 'numeric') { 38 | 39 | return (row1, row2) => { 40 | const value1 = row1.getSortableValue(sortFieldName); 41 | const value2 = row2.getSortableValue(sortFieldName); 42 | 43 | if (sortOrder === 'desc') { 44 | return value2 < value1 ? -1 : 1; 45 | } 46 | 47 | return value1 < value2 ? -1 : 1; 48 | }; 49 | } 50 | 51 | return (row1, row2) => { 52 | const value1 = row1.getSortableValue(sortFieldName); 53 | const value2 = row2.getSortableValue(sortFieldName); 54 | 55 | if (sortOrder === 'desc') { 56 | return value2.localeCompare(value1); 57 | } 58 | 59 | return value1.localeCompare(value2); 60 | }; 61 | } 62 | 63 | getSortFieldName() { 64 | return this.sortBy || this.show; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/classes/Row.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { get } from '../helpers'; 3 | 4 | export default class Row { 5 | constructor(data, columns) { 6 | this.data = data; 7 | this.columns = columns; 8 | } 9 | 10 | getValue(columnName) { 11 | return get(this.data, columnName); 12 | } 13 | 14 | getColumn(columnName) { 15 | return this.columns.find(column => column.show === columnName); 16 | } 17 | 18 | getFilterableValue(columnName) { 19 | const value = this.getValue(columnName); 20 | 21 | if (! value) { 22 | return ''; 23 | } 24 | 25 | return value.toString().toLowerCase(); 26 | } 27 | 28 | getSortableValue(columnName) { 29 | const dataType = this.getColumn(columnName).dataType; 30 | 31 | let value = this.getValue(columnName); 32 | 33 | if (value === undefined || value === null) { 34 | return ''; 35 | } 36 | 37 | if (value instanceof String) { 38 | value = value.toLowerCase(); 39 | } 40 | 41 | if (dataType.startsWith('date')) { 42 | const format = dataType.replace('date:', ''); 43 | 44 | return moment(value, format).format('YYYYMMDDHHmmss'); 45 | } 46 | 47 | if (dataType === 'numeric') { 48 | return value; 49 | } 50 | 51 | return value.toString(); 52 | } 53 | 54 | passesFilter(filter) { 55 | return this.columns 56 | .filter(column => column.isFilterable()) 57 | .map(column => this.getFilterableValue(column.getFilterFieldName())) 58 | .filter(filterableValue => filterableValue.indexOf(filter.toLowerCase()) >= 0) 59 | .length; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 117 | -------------------------------------------------------------------------------- /src/components/TableCell.js: -------------------------------------------------------------------------------- 1 | export default { 2 | functional: true, 3 | 4 | props: ['column', 'row'], 5 | 6 | render(createElement, { props }) { 7 | const data = {}; 8 | 9 | if (props.column.cellClass) { 10 | data.class = props.column.cellClass; 11 | } 12 | 13 | if (props.column.template) { 14 | return createElement('td', data, props.column.template(props.row.data)); 15 | } 16 | 17 | data.domProps = {}; 18 | data.domProps.innerHTML = props.column.formatter(props.row.getValue(props.column.show), props.row.data); 19 | 20 | return createElement('td', data); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/TableColumn.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | -------------------------------------------------------------------------------- /src/components/TableColumnHeader.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 76 | -------------------------------------------------------------------------------- /src/components/TableComponent.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 324 | -------------------------------------------------------------------------------- /src/components/TableRow.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /src/expiring-storage.js: -------------------------------------------------------------------------------- 1 | class ExpiringStorage { 2 | get(key) { 3 | const cached = JSON.parse( 4 | localStorage.getItem(key) 5 | ); 6 | 7 | if (! cached) { 8 | return null; 9 | } 10 | 11 | const expires = new Date(cached.expires); 12 | 13 | if (expires < new Date()) { 14 | localStorage.removeItem(key); 15 | return null; 16 | } 17 | 18 | return cached.value; 19 | } 20 | 21 | has(key) { 22 | return this.get(key) !== null; 23 | } 24 | 25 | set(key, value, lifeTimeInMinutes) { 26 | const currentTime = new Date().getTime(); 27 | 28 | const expires = new Date(currentTime + lifeTimeInMinutes * 60000); 29 | 30 | localStorage.setItem(key, JSON.stringify({ value, expires })); 31 | } 32 | } 33 | 34 | export default new ExpiringStorage(); 35 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export function classList(...classes) { 2 | return classes 3 | .map(c => Array.isArray(c) ? c : [c]) 4 | .reduce((classes, c) => classes.concat(c), []); 5 | } 6 | 7 | export function get(object, path) { 8 | if (! path) { 9 | return object; 10 | } 11 | 12 | if (object === null || typeof object !== 'object') { 13 | return object; 14 | } 15 | 16 | const [pathHead, pathTail] = path.split(/\.(.+)/); 17 | 18 | return get(object[pathHead], pathTail); 19 | } 20 | 21 | export function pick(object, properties) { 22 | return properties.reduce((pickedObject, property) => { 23 | pickedObject[property] = object[property]; 24 | return pickedObject; 25 | }, {}); 26 | } 27 | 28 | export function range(from, to) { 29 | return [...Array(to - from)].map((_, i) => i + from); 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import TableComponent from './components/TableComponent'; 2 | import TableColumn from './components/TableColumn'; 3 | import Pagination from './components/Pagination'; 4 | import { mergeSettings } from './settings'; 5 | 6 | export default { 7 | install(Vue, options = {}) { 8 | mergeSettings(options); 9 | 10 | Vue.component('table-component', TableComponent); 11 | Vue.component('table-column', TableColumn); 12 | Vue.component('pagination', Pagination); 13 | }, 14 | 15 | settings(settings) { 16 | mergeSettings(settings); 17 | }, 18 | }; 19 | 20 | export { TableComponent, TableColumn }; 21 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | const settings = { 2 | tableClass: '', 3 | theadClass: '', 4 | tbodyClass: '', 5 | headerClass: '', 6 | cellClass: '', 7 | filterInputClass: '', 8 | filterPlaceholder: 'Filter table…', 9 | filterNoResults: 'There are no matching rows', 10 | }; 11 | 12 | export function mergeSettings(newSettings) { 13 | for(const setting in newSettings) { 14 | settings[setting] = newSettings[setting]; 15 | } 16 | } 17 | 18 | export default settings; 19 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js'; 2 | import TableComponent from '../src'; 3 | import LocalStorageMock from './mocks/LocalStorageMock'; 4 | 5 | /* 6 | * Set up a localStorage mock implementation, and bind it to the window. Don't 7 | * forget to clear the storage before every one, since this is only executed 8 | * once per test run. 9 | */ 10 | 11 | const localStorage = new LocalStorageMock(); 12 | 13 | window.localStorage = localStorage; 14 | 15 | /* 16 | * We'll globally install the table component as a Vue plugin so we don't need 17 | * to worry about importing the components for every test. 18 | */ 19 | 20 | Vue.use(TableComponent); 21 | 22 | Vue.config.productionTip = false; 23 | -------------------------------------------------------------------------------- /tests/components/Pagination.test.js: -------------------------------------------------------------------------------- 1 | import Pagination from '../../src'; 2 | import Vue from 'vue/dist/vue.js'; 3 | 4 | describe('Pagination', () => { 5 | Vue.use(Pagination); 6 | 7 | it('can mount without pagination data', async () => { 8 | document.body.innerHTML = ` 9 |
10 | 11 |
12 | `; 13 | 14 | await createVm(); 15 | 16 | expect(document.body.innerHTML).toMatchSnapshot(); 17 | }); 18 | 19 | it('will not display when there is only one page', async () => { 20 | 21 | document.body.innerHTML = ` 22 |
23 | 28 | 29 | 30 |
31 | `; 32 | 33 | await createVm(); 34 | 35 | expect(document.body.innerHTML).toMatchSnapshot(); 36 | }); 37 | 38 | it('will render links when there is more than one page', async () => { 39 | 40 | document.body.innerHTML = ` 41 |
42 | 47 | 48 | 49 |
50 | `; 51 | 52 | await createVm(); 53 | 54 | expect(document.body.innerHTML).toMatchSnapshot(); 55 | }); 56 | 57 | it('can set the active page', async () => { 58 | 59 | document.body.innerHTML = ` 60 |
61 | 66 | 67 | 68 |
69 | `; 70 | 71 | await createVm(); 72 | 73 | expect(document.body.innerHTML).toMatchSnapshot(); 74 | }); 75 | 76 | it('can render large pagination', async () => { 77 | 78 | document.body.innerHTML = ` 79 |
80 | 85 | 86 | 87 |
88 | `; 89 | 90 | await createVm(); 91 | 92 | expect(document.body.innerHTML).toMatchSnapshot(); 93 | }); 94 | }); 95 | 96 | async function createVm() { 97 | const vm = new Vue({ 98 | el: '#app', 99 | }); 100 | 101 | await Vue.nextTick(() => { 102 | }); 103 | 104 | const table = vm.$children[0]; 105 | 106 | return table; 107 | } 108 | -------------------------------------------------------------------------------- /tests/components/TableComponent.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js'; 2 | import simulant from 'simulant'; 3 | import TableComponent from '../../src/'; 4 | 5 | describe('TableComponent', () => { 6 | beforeEach(() => { 7 | localStorage.clear(); 8 | }); 9 | 10 | afterEach(() => { 11 | TableComponent.settings({ 12 | filterNoResults: 'There are no matching rows', 13 | }); 14 | }); 15 | 16 | it('can mount', async () => { 17 | document.body.innerHTML = ` 18 |
19 | 21 | 22 | 23 |
24 | `; 25 | 26 | await createVm(); 27 | 28 | expect(document.body.innerHTML).toMatchSnapshot(); 29 | }); 30 | 31 | it('can display nested properties', async () => { 32 | document.body.innerHTML = ` 33 |
34 | 36 | 37 | 38 |
39 | `; 40 | 41 | await createVm(); 42 | 43 | expect(document.body.innerHTML).toMatchSnapshot(); 44 | }); 45 | 46 | it('accepts a function to format values', async () => { 47 | document.body.innerHTML = ` 48 |
49 | 51 | 52 | 53 |
54 | `; 55 | 56 | await createVm({ 57 | methods: { 58 | formatter(value, properties) { 59 | return `Formatted: ${value}`; 60 | }, 61 | }, 62 | }); 63 | 64 | expect(document.body.innerHTML).toMatchSnapshot(); 65 | }); 66 | 67 | it('supports a scoped slot inside the table column', async () => { 68 | document.body.innerHTML = ` 69 |
70 | 72 | 73 | 76 | 77 | 78 |
79 | `; 80 | 81 | await createVm(); 82 | 83 | expect(document.body.innerHTML).toMatchSnapshot(); 84 | }); 85 | 86 | it('supports a named slot to display a tfoot section', async () => { 87 | document.body.innerHTML = ` 88 |
89 | 91 | 92 | 98 | 99 |
100 | `; 101 | 102 | await createVm(); 103 | 104 | expect(document.body.innerHTML).toMatchSnapshot(); 105 | }); 106 | 107 | it('has an prop to disable the filter', async () => { 108 | document.body.innerHTML = ` 109 |
110 | 112 | 113 | 114 |
115 | `; 116 | 117 | await createVm(); 118 | 119 | expect(document.body.innerHTML).toMatchSnapshot(); 120 | }); 121 | 122 | it('has an prop to disable the caption', async () => { 123 | document.body.innerHTML = ` 124 |
125 | 127 | 128 | 129 |
130 | `; 131 | 132 | await createVm(); 133 | 134 | expect(document.body.innerHTML).toMatchSnapshot(); 135 | }); 136 | 137 | it('will use the property name as a column heading if label is not set', async () => { 138 | document.body.innerHTML = ` 139 |
140 | 142 | 143 | 144 |
145 | `; 146 | 147 | await createVm(); 148 | 149 | expect(document.body.innerHTML).toMatchSnapshot(); 150 | }); 151 | 152 | it('won\'t use the property name as a column heading if label is an empty string', async () => { 153 | document.body.innerHTML = ` 154 |
155 | 157 | 158 | 159 |
160 | `; 161 | 162 | await createVm(); 163 | 164 | expect(document.body.innerHTML).toMatchSnapshot(); 165 | }); 166 | 167 | it('can display a custom message when filtering results in no results', async () => { 168 | document.body.innerHTML = ` 169 |
170 | 173 | 174 | 175 |
176 | `; 177 | 178 | const table = await createVm(); 179 | 180 | table.filter = 'this returns nothing'; 181 | 182 | await Vue.nextTick(); 183 | 184 | expect(document.body.innerHTML).toMatchSnapshot(); 185 | }); 186 | 187 | it('can display a custom message from global settings for no matching results', async () => { 188 | TableComponent.settings({ 189 | filterNoResults: 'There are no matching results', 190 | }); 191 | 192 | document.body.innerHTML = ` 193 |
194 | 195 | 196 | 197 |
198 | `; 199 | 200 | await createVm(); 201 | 202 | expect(document.body.innerHTML).toMatchSnapshot(); 203 | }); 204 | 205 | it('can display a custom placeholder in the filter field', async () => { 206 | document.body.innerHTML = ` 207 |
208 | 211 | 212 | 213 |
214 | `; 215 | 216 | const table = await createVm(); 217 | 218 | table.filter = 'this returns nothing'; 219 | 220 | await Vue.nextTick(); 221 | 222 | expect(document.body.innerHTML).toMatchSnapshot(); 223 | }); 224 | 225 | it('can accept a function to fetch the data', async () => { 226 | const serverResponse = () => { 227 | return { 228 | data: [{ firstName: 'John' }, { id: 2, firstName: 'Paul' }], 229 | }; 230 | }; 231 | 232 | document.body.innerHTML = ` 233 |
234 | 236 | 237 | 238 |
239 | `; 240 | 241 | await createVm(); 242 | 243 | await Vue.nextTick(); 244 | 245 | expect(document.body.innerHTML).toMatchSnapshot(); 246 | }); 247 | 248 | it('can render pagination when the server responds with pagination data', async () => { 249 | const serverResponse = () => { 250 | return { 251 | data: [{ firstName: 'John' }, { id: 2, firstName: 'Paul' }], 252 | 253 | pagination: { 254 | totalPages: 4, 255 | currentPage: 2, 256 | }, 257 | }; 258 | }; 259 | 260 | document.body.innerHTML = ` 261 |
262 | 264 | 265 | 266 |
267 | `; 268 | 269 | await createVm(); 270 | 271 | await Vue.nextTick(); 272 | 273 | expect(document.body.innerHTML).toMatchSnapshot(); 274 | }); 275 | 276 | it('clicking a link in the pagination will rerender the table', async () => { 277 | const serverResponse = ({ page }) => { 278 | return { 279 | data: [{ firstName: `John ${page}` }, { id: 2, firstName: `Paul ${page}` }], 280 | 281 | pagination: { 282 | totalPages: 4, 283 | currentPage: page, 284 | }, 285 | }; 286 | }; 287 | 288 | document.body.innerHTML = ` 289 |
290 | 292 | 293 | 294 |
295 | `; 296 | 297 | await createVm(); 298 | 299 | await Vue.nextTick(); 300 | 301 | expect(document.body.innerHTML).toMatchSnapshot(); 302 | 303 | const thirdPageLink = document.getElementsByClassName('page-link')[2]; 304 | 305 | await simulant.fire(thirdPageLink, 'click'); 306 | 307 | await Vue.nextTick(); 308 | await Vue.nextTick(); 309 | 310 | expect(document.body.innerHTML).toMatchSnapshot(); 311 | }); 312 | 313 | it('can add extra classes to the table, the cells and the headers', async () => { 314 | document.body.innerHTML = ` 315 |
316 | 322 | 328 | 329 |
330 | `; 331 | 332 | await createVm(); 333 | 334 | expect(document.body.innerHTML).toMatchSnapshot(); 335 | }); 336 | 337 | it('can update columns', async () => { 338 | document.body.innerHTML = ` 339 |
340 | 341 | 342 | 343 |
344 | `; 345 | 346 | const vm = new Vue({ 347 | el: '#app', 348 | data: { 349 | label: 'First name', 350 | }, 351 | }); 352 | 353 | await Vue.nextTick(); 354 | 355 | expect(document.body.innerHTML).toMatchSnapshot(); 356 | 357 | vm.label = 'Something else'; 358 | 359 | await Vue.nextTick(); 360 | 361 | expect(document.body.innerHTML).toMatchSnapshot(); 362 | }); 363 | }); 364 | 365 | async function createVm(options = {}) { 366 | const vm = new Vue({ 367 | el: '#app', 368 | ...options, 369 | }); 370 | 371 | await Vue.nextTick(() => { 372 | }); 373 | 374 | const table = vm.$children[0]; 375 | 376 | return table; 377 | } 378 | -------------------------------------------------------------------------------- /tests/components/__snapshots__/Pagination.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pagination can mount without pagination data 1`] = ` 4 | 5 |
6 | 7 |
8 | 9 | `; 10 | 11 | exports[`Pagination can render large pagination 1`] = ` 12 | 13 |
14 | 64 |
65 | 66 | `; 67 | 68 | exports[`Pagination can set the active page 1`] = ` 69 | 70 |
71 | 106 |
107 | 108 | `; 109 | 110 | exports[`Pagination will not display when there is only one page 1`] = ` 111 | 112 |
113 | 114 |
115 | 116 | `; 117 | 118 | exports[`Pagination will render links when there is more than one page 1`] = ` 119 | 120 |
121 | 156 |
157 | 158 | `; 159 | -------------------------------------------------------------------------------- /tests/components/__snapshots__/TableComponent.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableComponent accepts a function to format values 1`] = ` 4 | 5 |
6 |
7 |
8 | 12 | 13 |
14 |
15 | 16 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 48 | 49 | 50 | 51 | 52 |
20 | Table not sorted 21 |
28 | First name 29 |
35 | Formatted: 36 | 37 | John 38 | 39 |
43 | Formatted: 44 | 45 | Paul 46 | 47 |
53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 |
61 | 62 | `; 63 | 64 | exports[`TableComponent can accept a function to fetch the data 1`] = ` 65 | 66 |
67 |
68 |
69 | 73 | 74 |
75 |
76 | 77 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 103 | 104 | 105 | 106 | 107 |
81 | Table not sorted 82 |
89 | First name 90 |
96 | John 97 |
101 | Paul 102 |
108 |
109 | 110 |
111 | 112 |
113 | 114 |
115 |
116 | 117 | `; 118 | 119 | exports[`TableComponent can add extra classes to the table, the cells and the headers 1`] = ` 120 | 121 |
122 |
123 |
124 | 128 | 129 |
130 |
131 | 132 | 138 | 139 | 140 | 146 | 147 | 148 | 149 | 150 | 153 | 154 | 155 | 158 | 159 | 160 | 161 | 162 |
136 | Table not sorted 137 |
144 | First name 145 |
151 | John 152 |
156 | Paul 157 |
163 |
164 | 165 |
166 | 167 |
168 | 169 |
170 |
171 | 172 | `; 173 | 174 | exports[`TableComponent can display a custom message from global settings for no matching results 1`] = ` 175 | 176 |
177 |
178 |
179 | 183 | 184 |
185 |
186 | 187 | 193 | 194 | 195 | 201 | 202 | 203 | 204 | 205 | 206 | 207 |
191 | Table not sorted 192 |
199 | First name 200 |
208 |
209 |
210 | There are no matching results 211 |
212 |
213 | 214 |
215 | 216 |
217 |
218 | 219 | `; 220 | 221 | exports[`TableComponent can display a custom message when filtering results in no results 1`] = ` 222 | 223 |
224 |
227 |
228 | 232 | 233 | × 234 | 235 |
236 |
237 | 238 | 244 | 245 | 246 | 252 | 253 | 254 | 255 | 256 | 257 | 258 |
242 | Table not sorted 243 |
250 | First name 251 |
259 |
260 |
261 | There are no matching rows 262 |
263 |
264 | 265 |
266 | 267 |
268 |
269 | 270 | `; 271 | 272 | exports[`TableComponent can display a custom placeholder in the filter field 1`] = ` 273 | 274 |
275 |
278 |
279 | 283 | 284 | × 285 | 286 |
287 |
288 | 289 | 295 | 296 | 297 | 303 | 304 | 305 | 306 | 307 | 308 | 309 |
293 | Table not sorted 294 |
301 | First name 302 |
310 |
311 |
312 | There are no matching rows 313 |
314 |
315 | 316 |
317 | 318 |
319 |
320 | 321 | `; 322 | 323 | exports[`TableComponent can display nested properties 1`] = ` 324 | 325 |
326 |
327 |
328 | 332 | 333 |
334 |
335 | 336 | 342 | 343 | 344 | 350 | 351 | 352 | 353 | 354 | 357 | 358 | 359 | 362 | 363 | 364 | 365 | 366 |
340 | Table not sorted 341 |
348 | First name 349 |
355 | John 356 |
360 | Paul 361 |
367 |
368 | 369 |
370 | 371 |
372 | 373 |
374 |
375 | 376 | `; 377 | 378 | exports[`TableComponent can mount 1`] = ` 379 | 380 |
381 |
382 |
383 | 387 | 388 |
389 |
390 | 391 | 397 | 398 | 399 | 405 | 406 | 407 | 408 | 409 | 412 | 413 | 414 | 417 | 418 | 419 | 420 | 421 |
395 | Table not sorted 396 |
403 | First name 404 |
410 | John 411 |
415 | Paul 416 |
422 |
423 | 424 |
425 | 426 |
427 | 428 |
429 |
430 | 431 | `; 432 | 433 | exports[`TableComponent can render pagination when the server responds with pagination data 1`] = ` 434 | 435 |
436 |
437 |
438 | 442 | 443 |
444 |
445 | 446 | 452 | 453 | 454 | 460 | 461 | 462 | 463 | 464 | 467 | 468 | 469 | 472 | 473 | 474 | 475 | 476 |
450 | Table not sorted 451 |
458 | First name 459 |
465 | John 466 |
470 | Paul 471 |
477 |
478 | 479 |
480 | 481 |
482 | 522 |
523 |
524 | 525 | `; 526 | 527 | exports[`TableComponent can update columns 1`] = ` 528 | 529 |
530 |
531 |
532 | 536 | 537 |
538 |
539 | 540 | 546 | 547 | 548 | 554 | 555 | 556 | 557 | 558 | 561 | 562 | 563 | 566 | 567 | 568 | 569 | 570 |
544 | Table not sorted 545 |
552 | First name 553 |
559 | John 560 |
564 | Paul 565 |
571 |
572 | 573 |
574 | 575 |
576 | 577 |
578 |
579 | 580 | `; 581 | 582 | exports[`TableComponent can update columns 2`] = ` 583 | 584 |
585 |
586 |
587 | 591 | 592 |
593 |
594 | 595 | 601 | 602 | 603 | 609 | 610 | 611 | 612 | 613 | 616 | 617 | 618 | 621 | 622 | 623 | 624 | 625 |
599 | Table not sorted 600 |
607 | Something else 608 |
614 | John 615 |
619 | Paul 620 |
626 |
627 | 628 |
629 | 630 |
631 | 632 |
633 |
634 | 635 | `; 636 | 637 | exports[`TableComponent clicking a link in the pagination will rerender the table 1`] = ` 638 | 639 |
640 |
641 |
642 | 646 | 647 |
648 |
649 | 650 | 656 | 657 | 658 | 664 | 665 | 666 | 667 | 668 | 671 | 672 | 673 | 676 | 677 | 678 | 679 | 680 |
654 | Table not sorted 655 |
662 | First name 663 |
669 | John 1 670 |
674 | Paul 1 675 |
681 |
682 | 683 |
684 | 685 |
686 | 726 |
727 |
728 | 729 | `; 730 | 731 | exports[`TableComponent clicking a link in the pagination will rerender the table 2`] = ` 732 | 733 |
734 |
735 |
736 | 740 | 741 |
742 |
743 | 744 | 750 | 751 | 752 | 758 | 759 | 760 | 761 | 762 | 765 | 766 | 767 | 770 | 771 | 772 | 773 | 774 |
748 | Table not sorted 749 |
756 | First name 757 |
763 | John 3 764 |
768 | Paul 3 769 |
775 |
776 | 777 |
778 | 779 |
780 | 820 |
821 |
822 | 823 | `; 824 | 825 | exports[`TableComponent has an prop to disable the caption 1`] = ` 826 | 827 |
828 |
829 |
830 | 834 | 835 |
836 |
837 | 838 | 839 | 840 | 841 | 847 | 848 | 849 | 850 | 851 | 854 | 855 | 856 | 859 | 860 | 861 | 862 | 863 |
845 | First name 846 |
852 | John 853 |
857 | Paul 858 |
864 |
865 | 866 |
867 | 868 |
869 | 870 |
871 |
872 | 873 | `; 874 | 875 | exports[`TableComponent has an prop to disable the filter 1`] = ` 876 | 877 |
878 |
879 | 880 |
881 | 882 | 888 | 889 | 890 | 896 | 897 | 898 | 899 | 900 | 903 | 904 | 905 | 908 | 909 | 910 | 911 | 912 |
886 | Table not sorted 887 |
894 | First name 895 |
901 | John 902 |
906 | Paul 907 |
913 |
914 | 915 |
916 | 917 |
918 | 919 |
920 |
921 | 922 | `; 923 | 924 | exports[`TableComponent supports a named slot to display a tfoot section 1`] = ` 925 | 926 |
927 |
928 |
929 | 933 | 934 |
935 |
936 | 937 | 943 | 944 | 945 | 951 | 952 | 953 | 954 | 955 | 958 | 959 | 960 | 963 | 964 | 965 | 966 | 967 | 970 | 973 | 974 | 975 |
941 | Table not sorted 942 |
949 | First name 950 |
956 | John 957 |
961 | Paul 962 |
968 | Name count: 969 | 971 | 2 972 |
976 |
977 | 978 |
979 | 980 |
981 | 982 |
983 |
984 | 985 | `; 986 | 987 | exports[`TableComponent supports a scoped slot inside the table column 1`] = ` 988 | 989 |
990 |
991 |
992 | 996 | 997 |
998 |
999 | 1000 | 1006 | 1007 | 1008 | 1014 | 1015 | 1016 | 1017 | 1018 | 1021 | 1022 | 1023 | 1026 | 1027 | 1028 | 1029 | 1030 |
1004 | Table not sorted 1005 |
1012 | First name 1013 |
1019 | John slot 1020 |
1024 | Paul slot 1025 |
1031 |
1032 | 1033 |
1034 | 1035 |
1036 | 1037 |
1038 |
1039 | 1040 | `; 1041 | 1042 | exports[`TableComponent will use the property name as a column heading if label is not set 1`] = ` 1043 | 1044 |
1045 |
1046 | 1047 |
1048 | 1049 | 1055 | 1056 | 1057 | 1063 | 1064 | 1065 | 1066 | 1067 | 1070 | 1071 | 1072 | 1075 | 1076 | 1077 | 1078 | 1079 |
1053 | Table not sorted 1054 |
1061 | firstName 1062 |
1068 | John 1069 |
1073 | Paul 1074 |
1080 |
1081 | 1082 |
1083 | 1084 |
1085 | 1086 |
1087 |
1088 | 1089 | `; 1090 | 1091 | exports[`TableComponent won't use the property name as a column heading if label is an empty string 1`] = ` 1092 | 1093 |
1094 |
1095 | 1096 |
1097 | 1098 | 1104 | 1105 | 1106 | 1111 | 1112 | 1113 | 1114 | 1115 | 1118 | 1119 | 1120 | 1123 | 1124 | 1125 | 1126 | 1127 |
1102 | Table not sorted 1103 |
1110 |
1116 | John 1117 |
1121 | Paul 1122 |
1128 |
1129 | 1130 |
1131 | 1132 |
1133 | 1134 |
1135 |
1136 | 1137 | `; 1138 | -------------------------------------------------------------------------------- /tests/concerns/__snapshots__/filtering.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Filtering can add a custom html class on the filter input 1`] = ` 4 | 5 |
6 |
7 |
8 | 12 | 13 |
14 |
15 | 16 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 42 | 43 | 44 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 |
20 | Table sorted by lastName (descending) 21 |
28 | First name 29 |
35 | John 36 |
40 | Paul 41 |
45 | George 46 |
50 | Ringo 51 |
57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 |
65 | 66 | `; 67 | 68 | exports[`Filtering hides the filter when no columns are filterable 1`] = ` 69 | 70 |
71 |
72 | 73 |
74 | 75 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 102 | 103 | 106 | 107 | 108 | 111 | 112 | 113 | 114 | 115 |
79 | Table sorted by lastName (descending) 80 |
87 | First name 88 |
94 | John 95 |
99 | Paul 100 |
104 | George 105 |
109 | Ringo 110 |
116 |
117 | 118 |
119 | 120 |
121 | 122 |
123 |
124 | 125 | `; 126 | -------------------------------------------------------------------------------- /tests/concerns/caching.test.js: -------------------------------------------------------------------------------- 1 | import expiringStorage from '../../src/expiring-storage'; 2 | import simulant from 'simulant'; 3 | import createVm from '../createVm'; 4 | 5 | describe('Caching', () => { 6 | beforeEach(() => { 7 | localStorage.clear(); 8 | 9 | document.body.innerHTML = ` 10 |
11 |
12 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | `; 26 | 27 | const dateClass = Date; 28 | 29 | // eslint-disable-next-line no-global-assign 30 | Date = function (dateString) { 31 | return new dateClass(dateString || '2017-01-01T00:00:00.000Z'); 32 | }; 33 | }); 34 | 35 | it('will cache the used filter and sorting', async () => { 36 | const cacheContent = { 37 | filter: 'Paul', 38 | sort:{ 39 | fieldName: 'firstName', 40 | order:'asc', 41 | }, 42 | }; 43 | 44 | expiringStorage.set('vue-table-component.test', cacheContent, 5); 45 | 46 | progressTime(4); 47 | 48 | const table = await createVm(); 49 | 50 | expect(table.filter).toEqual('Paul'); 51 | expect(table.sort.fieldName).toEqual('firstName'); 52 | expect(table.sort.order).toEqual('asc'); 53 | }); 54 | 55 | it('will not use the cache when it has expired', async () => { 56 | const cacheContent = { 57 | filter: 'Paul', 58 | sort:{ 59 | fieldName: 'firstName', 60 | order:'asc', 61 | }, 62 | }; 63 | 64 | expiringStorage.set('vue-table-component.test', cacheContent, 5); 65 | 66 | progressTime(6); 67 | 68 | const table = await createVm(); 69 | 70 | expect(table.filter).toEqual(''); 71 | expect(table.sort.fieldName).toEqual(''); 72 | expect(table.sort.order).toEqual(''); 73 | }); 74 | 75 | it('will cache the filter', async () => { 76 | await createVm(table => { 77 | table.filter = 'cache this'; 78 | }); 79 | 80 | const localStorageContents = JSON.parse(localStorage.getItem('vue-table-component.test')); 81 | 82 | expect(localStorageContents.value.filter).toBe('cache this'); 83 | }); 84 | 85 | it('will cache a the sort column', async () => { 86 | await createVm(); 87 | 88 | const songsColumnHeader = document.getElementsByTagName('th')[2]; 89 | await simulant.fire(songsColumnHeader, 'click'); 90 | 91 | const localStorageContents = JSON.parse(localStorage.getItem('vue-table-component.test')); 92 | 93 | expect(localStorageContents.value.sort.fieldName).toBe('songs'); 94 | }); 95 | }); 96 | 97 | function progressTime(minutes) { 98 | const currentTime = (new Date()).getTime(); 99 | 100 | const newTime = new Date(currentTime + (minutes * 60000)); 101 | 102 | const originalDateClass = Date; 103 | 104 | // eslint-disable-next-line no-global-assign 105 | Date = function (dateString) { 106 | return new originalDateClass(dateString || newTime.toISOString()); 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /tests/concerns/expiringStorage.test.js: -------------------------------------------------------------------------------- 1 | import expiringStorage from '../../src/expiring-storage'; 2 | 3 | describe('expiringStorage', () => { 4 | beforeEach(() => { 5 | localStorage.clear(); 6 | 7 | const dateClass = Date; 8 | 9 | // eslint-disable-next-line no-global-assign 10 | Date = function (dateString) { 11 | return new dateClass(dateString || '2017-01-01T00:00:00.000Z'); 12 | }; 13 | }); 14 | 15 | it('sets keys in the local storage', () => { 16 | expiringStorage.set('my-key', 'my-value', 5); 17 | 18 | const localStorageContents = JSON.parse(localStorage.getItem('my-key')); 19 | 20 | expect(localStorageContents.value).toBe('my-value'); 21 | expect(localStorageContents.expires).toBe('2017-01-01T00:05:00.000Z'); 22 | }); 23 | 24 | it('remembers values by key', () => { 25 | expiringStorage.set('my-key', 'my-value', 5); 26 | 27 | expect(expiringStorage.get('my-key')).toEqual('my-value'); 28 | }); 29 | 30 | it('returns null if the value has expired ', () => { 31 | expiringStorage.set('my-key', 'my-value', 5); 32 | 33 | progressTime(5); 34 | 35 | expect(expiringStorage.get('my-key')).toEqual('my-value'); 36 | 37 | progressTime(1); 38 | 39 | expect(expiringStorage.get('my-key')).toBeNull(); 40 | }); 41 | 42 | it('returns null for unknown keys', () => { 43 | expect(expiringStorage.get('unknown-key')).toBeNull(); 44 | }); 45 | 46 | it('can determine it contains a value with the given key', () => { 47 | expect(expiringStorage.has('my-key')).toEqual(false); 48 | 49 | expiringStorage.set('my-key', 'my-value', 5); 50 | 51 | expect(expiringStorage.has('my-key')).toEqual(true); 52 | }); 53 | }); 54 | 55 | function progressTime(minutes) { 56 | const currentTime = (new Date()).getTime(); 57 | 58 | const newTime = new Date(currentTime + (minutes * 60000)); 59 | 60 | const originalDateClass = Date; 61 | 62 | // eslint-disable-next-line no-global-assign 63 | Date = function (dateString) { 64 | return new originalDateClass(dateString || newTime.toISOString()); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /tests/concerns/filtering.test.js: -------------------------------------------------------------------------------- 1 | import createVm from '../createVm'; 2 | 3 | describe('Filtering', () => { 4 | beforeEach(() => { 5 | window.localStorage.clear(); 6 | 7 | document.body.innerHTML = ` 8 |
9 | 17 | 18 | 19 | 20 |
21 | `; 22 | }); 23 | 24 | it('can filter data', async () => { 25 | const table = await createVm(table => { 26 | table.filter = 'Paul'; 27 | }); 28 | 29 | expect(table.displayedRows).toHaveLength(1); 30 | expect(table.displayedRows[0].data.firstName).toBe('Paul'); 31 | }); 32 | 33 | it('can filter data in a case-insensitive way', async () => { 34 | const table = await createVm(table => { 35 | table.filter = 'paul'; 36 | }); 37 | 38 | expect(table.displayedRows).toHaveLength(1); 39 | expect(table.displayedRows[0].data.firstName).toBe('Paul'); 40 | }); 41 | 42 | it('will display a message if there are no matching rows', async () => { 43 | const table = await createVm(table => { 44 | table.filter = 'there are no rows that will match this'; 45 | }); 46 | 47 | expect(table.displayedRows).toHaveLength(0); 48 | }); 49 | 50 | it('will not use columns that are not filterable', async () => { 51 | // Note: Only the firstName field is filterable 52 | // 53 | 54 | const table = await createVm(table => { 55 | table.filter = 'Lennon'; 56 | }); 57 | 58 | expect(table.displayedRows).toHaveLength(0); 59 | }); 60 | 61 | it('can filter on another property', async () => { 62 | document.body.innerHTML = ` 63 |
64 | 72 | 73 | 74 |
75 | `; 76 | 77 | const table = await createVm(table => { 78 | table.filter = '70'; 79 | }); 80 | 81 | expect(table.displayedRows).toHaveLength(1); 82 | expect(table.displayedRows[0].data.firstName).toBe('Paul'); 83 | }); 84 | 85 | it('can add a custom html class on the filter input', async () => { 86 | document.body.innerHTML = ` 87 |
88 | 97 | 98 | 99 |
100 | `; 101 | 102 | await createVm(); 103 | 104 | expect(document.body.innerHTML).toMatchSnapshot(); 105 | }); 106 | 107 | it('hides the filter when no columns are filterable', async () => { 108 | document.body.innerHTML = ` 109 |
110 | 118 | 119 | 120 |
121 | `; 122 | 123 | await createVm(); 124 | 125 | expect(document.body.innerHTML).toMatchSnapshot(); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/concerns/settings.test.js: -------------------------------------------------------------------------------- 1 | import TableComponent from '../../src/'; 2 | import settings from '../../src/settings'; 3 | 4 | describe('settings', () => { 5 | it('can update settings', () => { 6 | TableComponent.settings({ 7 | tableClass: 'table', 8 | theadClass: 'table-head', 9 | tbodyClass: 'table-body', 10 | }); 11 | 12 | expect(settings.tableClass).toBe('table'); 13 | expect(settings.theadClass).toBe('table-head'); 14 | expect(settings.tbodyClass).toBe('table-body'); 15 | expect(settings.filterPlaceholder).toBe('Filter table…'); 16 | expect(settings.filterNoResults).toBe('There are no matching rows'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/concerns/sorting.test.js: -------------------------------------------------------------------------------- 1 | import simulant from 'simulant'; 2 | import createVm from '../createVm'; 3 | 4 | function setDocumentInnerHtml({ sortBy, sortOrder }) { 5 | document.body.innerHTML = ` 6 |
7 | 15 | 16 | 17 | 18 | 19 |
20 | `; 21 | } 22 | 23 | describe('Sorting', () => { 24 | beforeEach(() => { 25 | window.localStorage.clear(); 26 | }); 27 | 28 | it('can sort the data by a column', async () => { 29 | setDocumentInnerHtml({ sortBy: 'firstName', sortOrder: 'asc' }); 30 | 31 | const table = await createVm(); 32 | 33 | expect(table.displayedRows[0].data.firstName).toBe('George'); 34 | expect(table.displayedRows[1].data.firstName).toBe('John'); 35 | expect(table.displayedRows[2].data.firstName).toBe('Paul'); 36 | expect(table.displayedRows[3].data.firstName).toBe('Ringo'); 37 | }); 38 | 39 | it('can sort the data with by a column in a different order', async () => { 40 | setDocumentInnerHtml({ sortBy: 'songs', sortOrder: 'desc' }); 41 | 42 | const table = await createVm(); 43 | 44 | expect(table.displayedRows[0].data.firstName).toBe('John'); 45 | expect(table.displayedRows[1].data.firstName).toBe('Paul'); 46 | expect(table.displayedRows[2].data.firstName).toBe('George'); 47 | expect(table.displayedRows[3].data.firstName).toBe('Ringo'); 48 | }); 49 | 50 | it('it will change the sort order when clicking the header of the column with the active sort', async () => { 51 | setDocumentInnerHtml({ sortBy: 'firstName', sortOrder: 'asc' }); 52 | 53 | const table = await createVm(); 54 | 55 | const firstNameColumnHeader = document.getElementsByTagName('th')[0]; 56 | await simulant.fire(firstNameColumnHeader, 'click'); 57 | 58 | expect(table.sort.order).toBe('desc'); 59 | }); 60 | 61 | it('will sort the data ascending if the header of of column without the active sort is clicked', async () => { 62 | setDocumentInnerHtml({ sortBy: 'firstName', sortOrder: 'desc' }); 63 | 64 | const table = await createVm(); 65 | 66 | const lastNameColumnHeader = document.getElementsByTagName('th')[1]; 67 | await simulant.fire(lastNameColumnHeader, 'click'); 68 | 69 | expect(table.sort.fieldName).toBe('lastName'); 70 | expect(table.sort.order).toBe('asc'); 71 | }); 72 | 73 | it('will not sort data when clicking a non-sortable column header', async () => { 74 | setDocumentInnerHtml({ sortBy: 'firstName', order: 'asc' }); 75 | 76 | const table = await createVm(); 77 | 78 | const songsColumnHeader = document.getElementsByTagName('th')[2]; 79 | await simulant.fire(songsColumnHeader, 'click'); 80 | 81 | expect(table.sort.fieldName).toBe('firstName'); 82 | }); 83 | 84 | it('wont break if a sortable column has no data', async () => { 85 | document.body.innerHTML = ` 86 |
87 | 93 | 94 | 95 | 96 | 97 |
98 | `; 99 | 100 | const table = await createVm(); 101 | 102 | const lastNameColumnHeader = document.getElementsByTagName('th')[1]; 103 | await simulant.fire(lastNameColumnHeader, 'click'); 104 | 105 | expect(table.sort.fieldName).toBe('lastName'); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/createVm.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js'; 2 | 3 | export default async function createVm(callback = null) { 4 | const vm = new Vue({ el: '#app' }); 5 | 6 | await Vue.nextTick(); 7 | 8 | const table = vm.$children[0]; 9 | 10 | if (callback) { 11 | callback(table); 12 | 13 | await Vue.nextTick(); 14 | } 15 | 16 | return table; 17 | } 18 | -------------------------------------------------------------------------------- /tests/html-serializer.js: -------------------------------------------------------------------------------- 1 | const toDiffableHtml = require('diffable-html'); 2 | 3 | module.exports = { 4 | test: object => typeof object === 'string' && object.trim()[0] === '<', 5 | print: toDiffableHtml, 6 | }; 7 | -------------------------------------------------------------------------------- /tests/mocks/LocalStorageMock.js: -------------------------------------------------------------------------------- 1 | export default class LocalStorageMock { 2 | constructor() { 3 | this.store = {}; 4 | } 5 | 6 | getAll() { 7 | return this.store; 8 | } 9 | 10 | getItem(key) { 11 | return this.store[key] || null; 12 | } 13 | 14 | setItem(key, value) { 15 | this.store[key] = value.toString(); 16 | } 17 | 18 | clear() { 19 | this.store = {}; 20 | } 21 | 22 | removeItem(key) { 23 | delete this.store[key]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webpack.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js/, 6 | loaders: ['babel-loader'], 7 | exclude: /node_modules/, 8 | }, 9 | { 10 | test: /\.vue$/, 11 | loaders: ['vue-loader'], 12 | exclude: /node_modules/, 13 | }, 14 | ], 15 | }, 16 | 17 | resolve: { 18 | extensions: ['.js', '.vue'], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | 5 | module.exports = merge(require('./webpack.base'), { 6 | context: __dirname, 7 | 8 | entry: { 9 | 'index': './src/index.js', 10 | 'index.min': './src/index.js', 11 | }, 12 | 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: '[name].js', 16 | library: 'vue-table-component', 17 | libraryTarget: 'umd', 18 | }, 19 | 20 | externals: [ 21 | 'moment', 'vue', 22 | ], 23 | 24 | plugins: [ 25 | new webpack.optimize.UglifyJsPlugin({ 26 | include: /\.min\.js$/, 27 | minimize: true, 28 | }), 29 | ], 30 | }); 31 | --------------------------------------------------------------------------------