├── .github └── workflows │ ├── pr-next.yml │ └── release-next.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── PACKAGES.md ├── README.md ├── commitlint.config.js ├── coverage ├── badge-next.svg └── coverage-summary.json ├── dist ├── vue-dataset.es.js └── vue-dataset.umd.js ├── docs ├── .vuepress │ ├── client.js │ ├── components │ │ ├── Example1.vue │ │ ├── Example2.vue │ │ └── Example3.vue │ ├── config.js │ ├── styles │ │ └── styles.scss │ └── utilities │ │ └── index.js ├── components │ └── index.md ├── examples │ ├── cards │ │ └── index.md │ └── datatable │ │ └── index.md ├── index.md └── installation │ └── index.md ├── eslint.config.js ├── example-data └── users.json ├── favicon.ico ├── index.html ├── jsconfig.json ├── lint-staged.config.js ├── package.json ├── playground ├── App.vue ├── assets │ └── logo.png ├── components │ └── HelloWorld.vue ├── helpers │ └── index.js ├── layouts │ └── default │ │ ├── Default.vue │ │ ├── DefaultFooter.vue │ │ └── DefaultNav.vue ├── main.js ├── router │ └── index.js ├── scss │ ├── app.scss │ └── variables.scss ├── store │ └── index.js └── views │ ├── CompositionApi.vue │ └── OptionsApi.vue ├── pnpm-lock.yaml ├── prettier.config.js ├── release.config.js ├── src ├── Dataset.vue ├── DatasetInfo.vue ├── DatasetItem.vue ├── DatasetPager.vue ├── DatasetSearch.vue ├── DatasetShow.vue ├── helpers │ └── index.js ├── i18n │ └── en.js └── index.js ├── tests ├── unit │ ├── Dataset.spec.js │ ├── DatasetInfo.spec.js │ ├── DatasetItem.spec.js │ ├── DatasetPager.spec.js │ ├── DatasetSearch.spec.js │ ├── DatasetShow.spec.js │ ├── __snapshots__ │ │ └── DatasetItem.spec.js.snap │ ├── helpers │ │ ├── createPagingRange.spec.js │ │ ├── debounce.spec.js │ │ ├── fieldFilter.spec.js │ │ ├── fieldSorter.spec.js │ │ ├── findAny.spec.js │ │ ├── isEmptyObject.spec.js │ │ └── testData.js │ └── index.spec.js └── utils.js └── vite.config.js /.github/workflows/pr-next.yml: -------------------------------------------------------------------------------- 1 | name: pr-next 2 | 3 | on: 4 | push: 5 | branches: [next] 6 | 7 | jobs: 8 | ci: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node: [20] 15 | 16 | steps: 17 | - name: Checkout 🛎 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup node env 🏗 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version-file: '.nvmrc' 26 | check-latest: true 27 | 28 | - name: Setup pnpm 📦 29 | uses: pnpm/action-setup@v4 30 | with: 31 | package_json_file: package.json 32 | 33 | - name: Install dependencies 📦 34 | run: pnpm install 35 | 36 | - name: Run linter 👀 37 | run: pnpm run lint-fix 38 | 39 | - name: Run tests 🧪 40 | run: pnpm run test:unit 41 | 42 | - name: Build 43 | run: pnpm run build 44 | -------------------------------------------------------------------------------- /.github/workflows/release-next.yml: -------------------------------------------------------------------------------- 1 | name: release-next 2 | 3 | on: 4 | push: 5 | branches: [next] 6 | 7 | jobs: 8 | ci: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest] 14 | node: [20] 15 | 16 | steps: 17 | - name: Checkout 🛎 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup node env 🏗 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version-file: '.nvmrc' 26 | check-latest: true 27 | 28 | - name: Setup pnpm 📦 29 | uses: pnpm/action-setup@v4 30 | with: 31 | package_json_file: package.json 32 | 33 | - name: Install dependencies 📦 34 | run: pnpm install 35 | 36 | - name: Install semantic-release extra plugins 📦 37 | run: pnpm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git 38 | 39 | - name: Run linter 👀 40 | run: pnpm run lint-fix 41 | 42 | - name: Run tests 🧪 43 | run: pnpm run test:unit-coverage 44 | 45 | - name: Build 46 | run: pnpm run build 47 | 48 | - name: Release 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | run: npx semantic-release 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | public 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | # Custom ignores 25 | .editorconfig 26 | .prettierrc 27 | 28 | # Vuepress 29 | .temp 30 | .cache -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.7.0](https://github.com/kouts/vue-dataset/compare/v3.6.6...v3.7.0) (2024-10-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * added dsShowEntries provider ([c9e0ef2](https://github.com/kouts/vue-dataset/commit/c9e0ef2b826eb9fc78056a4828a52b217181f6d7)) 7 | * updated npm packages ([d71ffa9](https://github.com/kouts/vue-dataset/commit/d71ffa9b234f4b679867bcaa593ded7901a8619c)) 8 | 9 | 10 | ### Features 11 | 12 | * exposed dsSearch ([79c6db7](https://github.com/kouts/vue-dataset/commit/79c6db783b11c85291bfd1d712ea10291fa3bd2d)) 13 | 14 | ## [3.6.6](https://github.com/kouts/vue-dataset/compare/v3.6.5...v3.6.6) (2024-08-22) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * updated vue-example ([4be3c6f](https://github.com/kouts/vue-dataset/commit/4be3c6ff334b57e5eaf4e91daa50172355aece48)) 20 | 21 | ## [3.6.5](https://github.com/kouts/vue-dataset/compare/v3.6.4...v3.6.5) (2024-08-21) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * fixed components path ([bc32301](https://github.com/kouts/vue-dataset/commit/bc3230192a5cb52a06ae1fcd7ecbf0ee7dffe434)) 27 | * fixed example component name ([4c76a06](https://github.com/kouts/vue-dataset/commit/4c76a06e6166ec2c0565b76c358e5954fdee163b)) 28 | 29 | ## [3.6.4](https://github.com/kouts/vue-dataset/compare/v3.6.3...v3.6.4) (2024-08-19) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * fixed docs examples styling ([9ca8ce0](https://github.com/kouts/vue-dataset/commit/9ca8ce02175ef2b8ad9aed172e6780c58582557b)) 35 | * updated eslint config, added vitest ([b7b905a](https://github.com/kouts/vue-dataset/commit/b7b905a0cb8651ae4dd8d56555434df7bcc40b98)) 36 | * updated vuepress-plugin-vue-example ([68251a1](https://github.com/kouts/vue-dataset/commit/68251a153d2e7792b1340e12e515468014709a2c)) 37 | * updated vuepress-plugin-vue-example ([6f7d7f2](https://github.com/kouts/vue-dataset/commit/6f7d7f2853d0ea7f6606bba74f2e48ebac9b3f98)) 38 | 39 | ## [3.6.3](https://github.com/kouts/vue-dataset/compare/v3.6.2...v3.6.3) (2024-05-12) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * swtched to pnpm ([dff098d](https://github.com/kouts/vue-dataset/commit/dff098df9e5c1e558e608ea93e40be7a8d4fc2cb)) 45 | 46 | ## [3.6.2](https://github.com/kouts/vue-dataset/compare/v3.6.1...v3.6.2) (2024-05-04) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * fixed vuepress config ([8b885c2](https://github.com/kouts/vue-dataset/commit/8b885c27b7ba7a620781add3f868bf382889df98)) 52 | 53 | ## [3.6.1](https://github.com/kouts/vue-dataset/compare/v3.6.0...v3.6.1) (2023-08-31) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * added update dsData example, improved filterFields description ([fbe918a](https://github.com/kouts/vue-dataset/commit/fbe918a32fb4f99d47ddb0410c703990a867d575)) 59 | 60 | # [3.6.0](https://github.com/kouts/vue-dataset/compare/v3.5.4...v3.6.0) (2023-08-29) 61 | 62 | 63 | ### Features 64 | 65 | * **filter:** passing all data fields to filterfields function ([a2d50e5](https://github.com/kouts/vue-dataset/commit/a2d50e50e05dd8faafa843e6c808b26416950f24)) 66 | 67 | ## [3.5.4](https://github.com/kouts/vue-dataset/compare/v3.5.3...v3.5.4) (2023-06-04) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * updated npm packages ([5b1cce4](https://github.com/kouts/vue-dataset/commit/5b1cce4645d7a556ba9a4a4a5444e2913c7a567f)) 73 | 74 | ## [3.5.3](https://github.com/kouts/vue-dataset/compare/v3.5.2...v3.5.3) (2023-03-05) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * updated npm packages ([ac5ddde](https://github.com/kouts/vue-dataset/commit/ac5ddde47ea4e24f9a11cc82fd0ab228c6057721)) 80 | 81 | ## [3.5.2](https://github.com/kouts/vue-dataset/compare/v3.5.1...v3.5.2) (2023-02-26) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * use deep watch instead of cimputed for data update ([97bf0c7](https://github.com/kouts/vue-dataset/commit/97bf0c7eb9187c9559af96a8ad24ef9c738111d1)) 87 | 88 | ## [3.5.1](https://github.com/kouts/vue-dataset/compare/v3.5.0...v3.5.1) (2023-02-20) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * fixed update:dsData event to emit the sorted data ([63cf64e](https://github.com/kouts/vue-dataset/commit/63cf64ede660860a20578b91abd681d1a420dfab)) 94 | 95 | # [3.5.0](https://github.com/kouts/vue-dataset/compare/v3.4.9...v3.5.0) (2023-02-05) 96 | 97 | 98 | ### Features 99 | 100 | * added update event ([409179c](https://github.com/kouts/vue-dataset/commit/409179cad28cf0069a9ae82992a35d99230b136a)) 101 | 102 | ## [3.4.9](https://github.com/kouts/vue-dataset/compare/v3.4.8...v3.4.9) (2022-11-06) 103 | 104 | 105 | ### Bug Fixes 106 | 107 | * updated eslint config and npm packages ([79653ee](https://github.com/kouts/vue-dataset/commit/79653ee4714916ca4d58dc6cdfa2b5ccc30bdc47)) 108 | 109 | ## [3.4.8](https://github.com/kouts/vue-dataset/compare/v3.4.7...v3.4.8) (2022-08-07) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * fixed set active page to last page ([fda0228](https://github.com/kouts/vue-dataset/commit/fda0228cef0b7cca7ec6b846b2cfcdccfcc692d8)) 115 | 116 | ## [3.4.7](https://github.com/kouts/vue-dataset/compare/v3.4.6...v3.4.7) (2022-07-19) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * fixed docs site ([343ed04](https://github.com/kouts/vue-dataset/commit/343ed0457d39df6b732f0e0dd5c6d467959717ea)) 122 | 123 | ## [3.4.6](https://github.com/kouts/vue-dataset/compare/v3.4.5...v3.4.6) (2022-07-17) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * updated vuepress ([d0a75f2](https://github.com/kouts/vue-dataset/commit/d0a75f20c2e1e54180fab445b9f4d9442770e666)) 129 | 130 | ## [3.4.5](https://github.com/kouts/vue-dataset/compare/v3.4.4...v3.4.5) (2022-05-07) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * fixed sass autoprefixer error ([fb54ee0](https://github.com/kouts/vue-dataset/commit/fb54ee011f5040d340c6387fba2a8d9fb128b96b)) 136 | * fixed updated eslint config and npm packages ([592d931](https://github.com/kouts/vue-dataset/commit/592d931abae97b33f99ead0868a459f3b0f25cab)) 137 | 138 | ## [3.4.4](https://github.com/kouts/vue-dataset/compare/v3.4.3...v3.4.4) (2022-04-20) 139 | 140 | 141 | ### Bug Fixes 142 | 143 | * updated npm packages ([0eb174e](https://github.com/kouts/vue-dataset/commit/0eb174ef2bf32d1c0c21c848da7752598a1bca27)) 144 | 145 | ## [3.4.3](https://github.com/kouts/vue-dataset/compare/v3.4.2...v3.4.3) (2022-01-22) 146 | 147 | 148 | ### Bug Fixes 149 | 150 | * added exports property ([cb8e30e](https://github.com/kouts/vue-dataset/commit/cb8e30e4e1ebb081e27e8f08d85bccad926c1249)) 151 | * added exports property ([a5441b6](https://github.com/kouts/vue-dataset/commit/a5441b606e12dbff7426a76931fde13cb7548fe5)) 152 | 153 | ## [3.4.2](https://github.com/kouts/vue-dataset/compare/v3.4.1...v3.4.2) (2022-01-08) 154 | 155 | 156 | ### Bug Fixes 157 | 158 | * fixed release assets ([c4462da](https://github.com/kouts/vue-dataset/commit/c4462da8ce45c1cb7c8f17dbe40b5578a3ff1e2f)) 159 | 160 | ## [3.4.1](https://github.com/kouts/vue-dataset/compare/v3.4.0...v3.4.1) (2022-01-08) 161 | 162 | 163 | ### Bug Fixes 164 | 165 | * added coverage badge ([51f5d82](https://github.com/kouts/vue-dataset/commit/51f5d826370b3a5b03533b2906bdc05d5b93be3c)) 166 | 167 | # [3.4.0](https://github.com/kouts/vue-dataset/compare/v3.3.0...v3.4.0) (2021-12-19) 168 | 169 | 170 | ### Features 171 | 172 | * pass rowData as 3rd param to the searchAs function ([ec3a81a](https://github.com/kouts/vue-dataset/commit/ec3a81ad79356f74ab5c1fac794377e59dc0dc18)) 173 | 174 | # [3.3.0](https://github.com/kouts/vue-dataset/compare/v3.2.0...v3.3.0) (2021-12-19) 175 | 176 | 177 | ### Bug Fixes 178 | 179 | * exposed dsIndexes through provide ([931e6cc](https://github.com/kouts/vue-dataset/commit/931e6cc01a974492ea97be0a6dd00cbc34f0fe93)) 180 | 181 | 182 | ### Features 183 | 184 | * exposed indexes to scope as `dsIndexes` ([000970a](https://github.com/kouts/vue-dataset/commit/000970a248c803895c20dbeb597f3fff2cf5c9b2)) 185 | 186 | # [3.2.0](https://github.com/kouts/vue-dataset/compare/v3.1.1...v3.2.0) (2021-12-14) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * typos ([8d40b51](https://github.com/kouts/vue-dataset/commit/8d40b5178c235110c73f8b10e53f1bf5d760e2f8)) 192 | 193 | 194 | ### Features 195 | 196 | * add JSDoc type info on props ([a1cfba7](https://github.com/kouts/vue-dataset/commit/a1cfba7987ddcd2c579b229abb214ada44f8115d)) 197 | 198 | ## [3.1.1](https://github.com/kouts/vue-dataset/compare/v3.1.0...v3.1.1) (2021-12-13) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * **docs:** remove non-existing css variable usage ([1f00506](https://github.com/kouts/vue-dataset/commit/1f005064b442ba1bc9de49bede940a713342c34d)) 204 | 205 | # [3.1.0](https://github.com/kouts/vue-dataset/compare/v3.0.0...v3.1.0) (2021-12-10) 206 | 207 | 208 | ### Features 209 | 210 | * pass functions in scope... search, showEntries, setActive ([13903f3](https://github.com/kouts/vue-dataset/commit/13903f379917ebf5cf4d67563fa22ce1896d806b)) 211 | 212 | # [3.0.0](https://github.com/kouts/vue-dataset/compare/v2.1.0...v3.0.0) (2021-11-10) 213 | 214 | 215 | * Merge pull request #45 from mesqueeb/next ([041dcae](https://github.com/kouts/vue-dataset/commit/041dcae9da75057eb744f07abf82f5833058e1ba)), closes [#45](https://github.com/kouts/vue-dataset/issues/45) 216 | 217 | 218 | ### BREAKING CHANGES 219 | 220 | * The Dataset provider component no longer renders a div. 221 | 222 | feat: Make Dataset truly renderless 223 | 224 | # [2.1.0](https://github.com/kouts/vue-dataset/compare/v2.0.2...v2.1.0) (2021-10-28) 225 | 226 | 227 | ### Features 228 | 229 | * added iteration index ([367c1c0](https://github.com/kouts/vue-dataset/commit/367c1c0986cc954ea87ed06528ee5ae5f6b8441f)) 230 | 231 | ## [2.0.2](https://github.com/kouts/vue-dataset/compare/v2.0.1...v2.0.2) (2021-10-16) 232 | 233 | 234 | ### Bug Fixes 235 | 236 | * fixed dsdata not updating ([f17a1d7](https://github.com/kouts/vue-dataset/commit/f17a1d79ce6b6c3724c57b6936ed6524b8bd79ce)) 237 | 238 | ## [2.0.1](https://github.com/kouts/vue-dataset/compare/v2.0.0...v2.0.1) (2021-09-19) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * fixed size badge url ([3d1f01f](https://github.com/kouts/vue-dataset/commit/3d1f01f2675483adb26494096f3a602d831e097a)) 244 | 245 | # [2.0.0](https://github.com/kouts/vue-dataset/compare/v1.2.0...v2.0.0) (2021-09-19) 246 | 247 | 248 | ### Features 249 | 250 | * added vue 3 support ([4d240a4](https://github.com/kouts/vue-dataset/commit/4d240a4782a9e4e9f087d91520935c41517075c2)) 251 | 252 | 253 | ### BREAKING CHANGES 254 | 255 | * added vue 3 support 256 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Giannis Koutsaftakis 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 | -------------------------------------------------------------------------------- /PACKAGES.md: -------------------------------------------------------------------------------- 1 | ## NPM packages version information 2 | 3 | - `bootstrap ^4.6.2 → ^5.2.2` 4 | Needed in order for examples to work correctly 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-dataset ![](https://img.badgesize.io/kouts/vue-dataset/next/dist/vue-dataset.umd.js.svg) ![](https://img.badgesize.io/kouts/vue-dataset/next/dist/vue-dataset.umd.js.svg?compression=gzip) ![](/coverage/badge-next.svg) 2 | 3 | 4 | --- 5 | 6 | :fire: **HEADS UP!** You're currently looking at vue-dataset `next` branch for **Vue.js 3**. 7 | If you're looking for a Vue.js 2 compatible version of vue-dataset, [please check out the `master` branch](https://github.com/kouts/vue-dataset/tree/master). 8 | 9 | --- 10 | 11 | A set of Vue.js 3 components to display datasets (lists) with filtering, paging, and sorting capabilities! 12 | Created with reusability in mind, so that one doesn't have to recreate the same functionality for lists over and over again. 13 | 14 | > vue-dataset does not impose any structure or layout limitations on your HTML, you can use divs, tables or anything you like to present your data. 15 | 16 | ## Features 17 | 18 | - Highly customizable DOM structure 19 | - Custom filtering based on the row values from all or specific data keys 20 | - "Search as" feature allows for searching using a custom search method 21 | - Multi "column" searching, search data keys are configurable 22 | - "Sort as" feature allows for sorting using a custom sorting method 23 | - Multi "column" sorting, sortable data keys are configurable 24 | - Pagination 25 | - Global search with debounce setting 26 | - Easy to extend with custom components 27 | 28 | ## Browsers support 29 | 30 | | [Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | 31 | | --------- | --------- | --------- | --------- | --------- | 32 | | Edge | last 2 versions | last 2 versions | last 2 versions| last 2 versions 33 | 34 | Documentation and examples 35 | https://next--vue-dataset-demo.netlify.app/ 36 | 37 | # Development 38 | 39 | In order to start development: 40 | 41 | ```sh 42 | pnpm i 43 | pnpm run dev 44 | ``` -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [1, 'always', 200], 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /coverage/badge-next.svg: -------------------------------------------------------------------------------- 1 | Coverage: 100%Coverage100% -------------------------------------------------------------------------------- /coverage/coverage-summary.json: -------------------------------------------------------------------------------- 1 | {"total": {"lines":{"total":519,"covered":519,"skipped":0,"pct":100},"statements":{"total":519,"covered":519,"skipped":0,"pct":100},"functions":{"total":27,"covered":27,"skipped":0,"pct":100},"branches":{"total":133,"covered":131,"skipped":0,"pct":98.49},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}} 2 | ,"/home/runner/work/vue-dataset/vue-dataset/src/Dataset.vue": {"lines":{"total":157,"covered":157,"skipped":0,"pct":100},"functions":{"total":12,"covered":12,"skipped":0,"pct":100},"statements":{"total":157,"covered":157,"skipped":0,"pct":100},"branches":{"total":33,"covered":33,"skipped":0,"pct":100}} 3 | ,"/home/runner/work/vue-dataset/vue-dataset/src/DatasetInfo.vue": {"lines":{"total":22,"covered":22,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":22,"covered":22,"skipped":0,"pct":100},"branches":{"total":7,"covered":7,"skipped":0,"pct":100}} 4 | ,"/home/runner/work/vue-dataset/vue-dataset/src/DatasetItem.vue": {"lines":{"total":31,"covered":31,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":31,"covered":31,"skipped":0,"pct":100},"branches":{"total":4,"covered":4,"skipped":0,"pct":100}} 5 | ,"/home/runner/work/vue-dataset/vue-dataset/src/DatasetPager.vue": {"lines":{"total":55,"covered":55,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":55,"covered":55,"skipped":0,"pct":100},"branches":{"total":23,"covered":21,"skipped":0,"pct":91.3}} 6 | ,"/home/runner/work/vue-dataset/vue-dataset/src/DatasetSearch.vue": {"lines":{"total":34,"covered":34,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":34,"covered":34,"skipped":0,"pct":100},"branches":{"total":3,"covered":3,"skipped":0,"pct":100}} 7 | ,"/home/runner/work/vue-dataset/vue-dataset/src/DatasetShow.vue": {"lines":{"total":43,"covered":43,"skipped":0,"pct":100},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":43,"covered":43,"skipped":0,"pct":100},"branches":{"total":4,"covered":4,"skipped":0,"pct":100}} 8 | ,"/home/runner/work/vue-dataset/vue-dataset/src/index.js": {"lines":{"total":7,"covered":7,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} 9 | ,"/home/runner/work/vue-dataset/vue-dataset/src/helpers/index.js": {"lines":{"total":160,"covered":160,"skipped":0,"pct":100},"functions":{"total":6,"covered":6,"skipped":0,"pct":100},"statements":{"total":160,"covered":160,"skipped":0,"pct":100},"branches":{"total":59,"covered":59,"skipped":0,"pct":100}} 10 | ,"/home/runner/work/vue-dataset/vue-dataset/src/i18n/en.js": {"lines":{"total":10,"covered":10,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":10,"covered":10,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} 11 | } 12 | -------------------------------------------------------------------------------- /dist/vue-dataset.es.js: -------------------------------------------------------------------------------- 1 | import { ref as _, computed as f, watch as C, provide as c, renderSlot as R, nextTick as j, inject as i, openBlock as h, createElementBlock as v, toDisplayString as u, createBlock as L, resolveDynamicComponent as B, withCtx as M, Fragment as A, renderList as F, createCommentVNode as K, createElementVNode as w, normalizeClass as k, withModifiers as N } from "vue"; 2 | const V = "..."; 3 | function z(t, n, s) { 4 | let e; 5 | return function() { 6 | const r = this, o = arguments; 7 | clearTimeout(e), e = setTimeout(function() { 8 | e = null, t.apply(r, o); 9 | }, n); 10 | }; 11 | } 12 | function O(t) { 13 | for (const n in t) 14 | return !1; 15 | return !0; 16 | } 17 | function G(t, n) { 18 | const e = [], r = []; 19 | let o; 20 | if (e.push(1), t <= 1) 21 | return e; 22 | for (let a = n - 2; a <= n + 2; a++) 23 | a < t && a > 1 && e.push(a); 24 | e.push(t); 25 | for (let a = 0; a < e.length; a++) 26 | o && (e[a] - o === 2 ? r.push(o + 1) : e[a] - o !== 1 && r.push(V)), r.push(e[a]), o = e[a]; 27 | return r; 28 | } 29 | function W(t, n = {}) { 30 | const s = []; 31 | let e; 32 | const r = t.length; 33 | return t = t.map(function(o, a) { 34 | return o[0] === "-" ? (s[a] = -1, o = o.substring(1)) : s[a] = 1, o; 35 | }), function(o, a) { 36 | for (e = 0; e < r; e++) { 37 | const d = t[e], b = n[d] ? n[d](o.value[d]) : o.value[d], m = n[d] ? n[d](a.value[d]) : a.value[d]; 38 | if (b > m) 39 | return s[e]; 40 | if (b < m) 41 | return -s[e]; 42 | } 43 | return 0; 44 | }; 45 | } 46 | function q(t, n) { 47 | for (const s in n) 48 | t = t.filter(function(e) { 49 | const r = e.value; 50 | for (const o in r) 51 | if (o === s) { 52 | if (typeof n[s] == "function") 53 | return n[s](r[o], r); 54 | if (n[s] === "" || r[o] === n[s]) 55 | return !0; 56 | } 57 | return !1; 58 | }); 59 | return t; 60 | } 61 | function H(t, n, s, e) { 62 | e = String(e).toLowerCase(); 63 | for (const r in s) 64 | if (t.length === 0 || t.indexOf(r) !== -1) { 65 | const o = String(s[r]).toLowerCase(); 66 | for (const a in n) 67 | if (a === r && typeof n[a] == "function") { 68 | const d = n[a](o, e, s); 69 | if (d === !0) 70 | return d; 71 | } 72 | if (o.indexOf(e) >= 0) 73 | return !0; 74 | } 75 | return !1; 76 | } 77 | const J = { 78 | show: "Show", 79 | entries: "entries", 80 | previous: "Previous", 81 | next: "Next", 82 | showing: "Showing", 83 | showingTo: "to", 84 | showingOf: "of", 85 | showingEntries: "entries" 86 | }, P = (t, n) => { 87 | const s = t.__vccOpts || t; 88 | for (const [e, r] of n) 89 | s[e] = r; 90 | return s; 91 | }, Q = { 92 | name: "Dataset", 93 | props: { 94 | dsData: { 95 | type: Array, 96 | default: () => [] 97 | }, 98 | dsFilterFields: { 99 | type: Object, 100 | default: () => ({}) 101 | }, 102 | dsSortby: { 103 | type: Array, 104 | default: () => [] 105 | }, 106 | dsSearchIn: { 107 | type: Array, 108 | default: () => [] 109 | }, 110 | dsSearchAs: { 111 | type: Object, 112 | default: () => ({}) 113 | }, 114 | dsSortAs: { 115 | type: Object, 116 | default: () => ({}) 117 | } 118 | }, 119 | emits: ["update:dsData"], 120 | /** 121 | * @param {{ 122 | * dsData: Record[]; 123 | * dsFilterFields: { [fieldId in string]: (columnValue: any) => boolean | any }; 124 | * dsSortby: string[]; 125 | * dsSearchIn: string[]; 126 | * dsSearchAs: { [id in string]: (columnValue: any, searchString: string) => boolean }; 127 | * dsSortAs: { [id in string]: (columnValue: any) => any }; 128 | * }} props 129 | */ 130 | setup(t, { emit: n }) { 131 | const s = _(1), e = _(""), r = _(10), o = _(J), a = _([]), d = (l) => { 132 | e.value = l; 133 | }, b = async (l) => { 134 | r.value = l, await j(), s.value > S.value && m(y.value[y.value.length - 1]); 135 | }, m = (l) => { 136 | s.value = l; 137 | }, T = f(() => a.value.slice(D.value, I.value)), y = f(() => G(S.value, s.value)), x = f(() => a.value.length), S = f(() => Math.ceil(x.value / r.value)), D = f(() => (s.value - 1) * r.value), I = f(() => s.value * r.value); 138 | return C(x, (l, g) => { 139 | m(1); 140 | }), C( 141 | () => [t.dsData, e, t.dsSortby, t.dsFilterFields, t.dsSearchIn, t.dsSearchAs, t.dsSortAs], 142 | () => { 143 | let l = []; 144 | !e.value && !t.dsSortby.length && O(t.dsFilterFields) ? l = t.dsData.map((g, E) => E) : (l = t.dsData.map((g, E) => ({ index: E, value: g })), O(t.dsFilterFields) || (l = q(l, t.dsFilterFields)), e.value && (l = l.filter((g) => H(t.dsSearchIn, t.dsSearchAs, g.value, e.value))), t.dsSortby.length && l.sort(W(t.dsSortby, t.dsSortAs)), l = l.map((g) => g.index)), a.value = l, n( 145 | "update:dsData", 146 | l.map((g) => t.dsData[g]) 147 | ); 148 | }, 149 | { 150 | immediate: !0, 151 | deep: !0 152 | } 153 | ), c("dsIndexes", a), c("search", d), c("showEntries", b), c("setActive", m), c("datasetI18n", o), c( 154 | "dsData", 155 | f(() => t.dsData) 156 | ), c("dsRows", T), c("dsSearch", e), c("dsShowEntries", r), c("dsPages", y), c("dsResultsNumber", x), c("dsPagecount", S), c("dsFrom", D), c("dsTo", I), c("dsPage", s), { 157 | dsIndexes: a, 158 | dsShowEntries: r, 159 | dsResultsNumber: x, 160 | dsPage: s, 161 | dsPagecount: S, 162 | dsFrom: D, 163 | dsTo: I, 164 | dsRows: T, 165 | dsPages: y, 166 | search: d, 167 | showEntries: b, 168 | setActive: m 169 | }; 170 | } 171 | }; 172 | function U(t, n, s, e, r, o) { 173 | return R(t.$slots, "default", { 174 | ds: { 175 | dsIndexes: e.dsIndexes, 176 | dsShowEntries: e.dsShowEntries, 177 | dsResultsNumber: e.dsResultsNumber, 178 | dsPage: e.dsPage, 179 | dsPagecount: e.dsPagecount, 180 | dsFrom: e.dsFrom, 181 | dsTo: e.dsTo, 182 | dsData: s.dsData, 183 | dsRows: e.dsRows, 184 | dsPages: e.dsPages, 185 | search: e.search, 186 | showEntries: e.showEntries, 187 | setActive: e.setActive 188 | } 189 | }); 190 | } 191 | const ge = /* @__PURE__ */ P(Q, [["render", U]]), X = { 192 | name: "DatasetInfo", 193 | setup() { 194 | const t = i("dsResultsNumber"), n = i("dsFrom"), s = i("dsTo"), e = f(() => t.value !== 0 ? n.value + 1 : 0), r = f(() => s.value >= t.value ? t.value : s.value); 195 | return { 196 | datasetI18n: i("datasetI18n"), 197 | dsResultsNumber: t, 198 | showing: e, 199 | showingTo: r 200 | }; 201 | } 202 | }; 203 | function Y(t, n, s, e, r, o) { 204 | return h(), v("div", null, u(e.datasetI18n.showing) + " " + u(e.showing) + " " + u(e.datasetI18n.showingTo) + " " + u(e.showingTo) + " " + u(e.datasetI18n.showingOf) + " " + u(e.dsResultsNumber) + " " + u(e.datasetI18n.showingEntries), 1); 205 | } 206 | const me = /* @__PURE__ */ P(X, [["render", Y]]), Z = { 207 | name: "DatasetItem", 208 | props: { 209 | tag: { 210 | type: String, 211 | default: "div" 212 | } 213 | }, 214 | setup() { 215 | const t = f(() => { 216 | const n = []; 217 | for (let s = i("dsFrom").value; s < i("dsTo").value; s++) 218 | n.push(s); 219 | return n; 220 | }); 221 | return { 222 | dsData: i("dsData"), 223 | dsRows: i("dsRows"), 224 | indexes: t 225 | }; 226 | } 227 | }; 228 | function p(t, n, s, e, r, o) { 229 | return h(), L(B(s.tag), null, { 230 | default: M(() => [ 231 | (h(!0), v(A, null, F(e.dsRows, (a, d) => R(t.$slots, "default", { 232 | row: e.dsData[a], 233 | rowIndex: a, 234 | index: e.indexes[d] 235 | })), 256)), 236 | e.dsRows.length ? K("", !0) : R(t.$slots, "noDataFound", { key: 0 }) 237 | ]), 238 | _: 3 239 | }); 240 | } 241 | const _e = /* @__PURE__ */ P(Z, [["render", p]]), $ = { 242 | name: "DatasetPager", 243 | setup() { 244 | const t = _(V), n = i("dsPage"), s = i("dsPagecount"), e = f(() => n.value === 1), r = f(() => n.value === s.value || s.value === 0); 245 | return { 246 | datasetI18n: i("datasetI18n"), 247 | setActive: i("setActive"), 248 | dsPages: i("dsPages"), 249 | dsPagecount: s, 250 | dsPage: n, 251 | morePages: t, 252 | disabledPrevious: e, 253 | disabledNext: r 254 | }; 255 | } 256 | }, ee = { class: "pagination" }, te = ["tabindex", "aria-disabled"], se = ["onClick"], ne = { 257 | key: 1, 258 | class: "page-link" 259 | }, ae = ["tabindex", "aria-disabled"]; 260 | function re(t, n, s, e, r, o) { 261 | return h(), v("ul", ee, [ 262 | w("li", { 263 | class: k(["page-item", e.disabledPrevious && "disabled"]) 264 | }, [ 265 | w("a", { 266 | class: "page-link", 267 | href: "#", 268 | tabindex: e.disabledPrevious ? "-1" : null, 269 | "aria-disabled": e.disabledPrevious ? "true" : null, 270 | onClick: n[0] || (n[0] = N((a) => e.setActive(e.dsPage !== 1 && e.dsPagecount !== 0 ? e.dsPage - 1 : e.dsPage), ["prevent"])) 271 | }, u(e.datasetI18n.previous), 9, te) 272 | ], 2), 273 | (h(!0), v(A, null, F(e.dsPages, (a, d) => (h(), v("li", { 274 | key: d, 275 | class: k(["page-item", a === e.dsPage && "active", a === e.morePages && "disabled"]) 276 | }, [ 277 | a !== e.morePages ? (h(), v("a", { 278 | key: 0, 279 | class: "page-link", 280 | href: "#", 281 | onClick: N((b) => e.setActive(a), ["prevent"]) 282 | }, u(a), 9, se)) : (h(), v("span", ne, u(a), 1)) 283 | ], 2))), 128)), 284 | w("li", { 285 | class: k(["page-item", e.disabledNext && "disabled"]) 286 | }, [ 287 | w("a", { 288 | class: "page-link", 289 | href: "#", 290 | tabindex: e.disabledNext ? "-1" : null, 291 | "aria-disabled": e.disabledNext ? "true" : null, 292 | onClick: n[1] || (n[1] = N((a) => e.setActive(e.dsPage !== e.dsPagecount && e.dsPagecount !== 0 ? e.dsPage + 1 : e.dsPage), ["prevent"])) 293 | }, u(e.datasetI18n.next), 9, ae) 294 | ], 2) 295 | ]); 296 | } 297 | const we = /* @__PURE__ */ P($, [["render", re]]), oe = { 298 | name: "DatasetSearch", 299 | props: { 300 | dsSearchPlaceholder: { 301 | type: String, 302 | default: "" 303 | }, 304 | wait: { 305 | type: Number, 306 | default: 0 307 | } 308 | }, 309 | setup(t) { 310 | const n = i("search"), s = _(""), e = z((r) => { 311 | n(r); 312 | }, t.wait); 313 | return { 314 | dsSearch: s, 315 | input: e 316 | }; 317 | } 318 | }, de = ["placeholder", "value"]; 319 | function le(t, n, s, e, r, o) { 320 | return h(), v("input", { 321 | type: "text", 322 | placeholder: s.dsSearchPlaceholder, 323 | class: "form-control", 324 | value: e.dsSearch, 325 | onInput: n[0] || (n[0] = (a) => e.input(a.target.value)) 326 | }, null, 40, de); 327 | } 328 | const be = /* @__PURE__ */ P(oe, [["render", le]]), ie = { 329 | name: "DatasetShow", 330 | props: { 331 | dsShowEntries: { 332 | type: Number, 333 | default: 10 334 | }, 335 | dsShowEntriesLovs: { 336 | type: Array, 337 | default: () => [ 338 | { value: 5, text: 5 }, 339 | { value: 10, text: 10 }, 340 | { value: 25, text: 25 }, 341 | { value: 50, text: 50 }, 342 | { value: 100, text: 100 } 343 | ] 344 | } 345 | }, 346 | emits: ["changed"], 347 | setup(t, { emit: n }) { 348 | const s = i("showEntries"), e = (r) => { 349 | n("changed", Number(r.target.value)), s(Number(r.target.value)); 350 | }; 351 | return s(Number(t.dsShowEntries)), { 352 | datasetI18n: i("datasetI18n"), 353 | change: e 354 | }; 355 | } 356 | }, ce = { class: "form-inline" }, ue = ["value"], fe = ["value"]; 357 | function he(t, n, s, e, r, o) { 358 | return h(), v("div", ce, [ 359 | w("label", null, u(e.datasetI18n.show), 1), 360 | w("select", { 361 | value: s.dsShowEntries, 362 | class: "form-control mr-1 ml-1", 363 | onChange: n[0] || (n[0] = (...a) => e.change && e.change(...a)) 364 | }, [ 365 | (h(!0), v(A, null, F(s.dsShowEntriesLovs, (a) => (h(), v("option", { 366 | key: a.value, 367 | value: a.value 368 | }, u(a.text), 9, fe))), 128)) 369 | ], 40, ue), 370 | w("label", null, u(e.datasetI18n.entries), 1) 371 | ]); 372 | } 373 | const Pe = /* @__PURE__ */ P(ie, [["render", he]]); 374 | export { 375 | ge as Dataset, 376 | me as DatasetInfo, 377 | _e as DatasetItem, 378 | we as DatasetPager, 379 | be as DatasetSearch, 380 | Pe as DatasetShow 381 | }; 382 | -------------------------------------------------------------------------------- /dist/vue-dataset.umd.js: -------------------------------------------------------------------------------- 1 | (function(c,e){typeof exports=="object"&&typeof module<"u"?e(exports,require("vue")):typeof define=="function"&&define.amd?define(["exports","vue"],e):(c=typeof globalThis<"u"?globalThis:c||self,e(c.VueDataset={},c.Vue))})(this,function(c,e){"use strict";const D="...";function k(n,s,a){let t;return function(){const r=this,i=arguments;clearTimeout(t),t=setTimeout(function(){t=null,n.apply(r,i)},s)}}function b(n){for(const s in n)return!1;return!0}function E(n,s){const t=[],r=[];let i;if(t.push(1),n<=1)return t;for(let o=s-2;o<=s+2;o++)o1&&t.push(o);t.push(n);for(let o=0;og)return a[t];if(m=0)return!0}return!1}const B={show:"Show",entries:"entries",previous:"Previous",next:"Next",showing:"Showing",showingTo:"to",showingOf:"of",showingEntries:"entries"},h=(n,s)=>{const a=n.__vccOpts||n;for(const[t,r]of s)a[t]=r;return a},j={name:"Dataset",props:{dsData:{type:Array,default:()=>[]},dsFilterFields:{type:Object,default:()=>({})},dsSortby:{type:Array,default:()=>[]},dsSearchIn:{type:Array,default:()=>[]},dsSearchAs:{type:Object,default:()=>({})},dsSortAs:{type:Object,default:()=>({})}},emits:["update:dsData"],setup(n,{emit:s}){const a=e.ref(1),t=e.ref(""),r=e.ref(10),i=e.ref(B),o=e.ref([]),d=l=>{t.value=l},m=async l=>{r.value=l,await e.nextTick(),a.value>S.value&&g(u.value[u.value.length-1])},g=l=>{a.value=l},P=e.computed(()=>o.value.slice(w.value,_.value)),u=e.computed(()=>E(S.value,a.value)),y=e.computed(()=>o.value.length),S=e.computed(()=>Math.ceil(y.value/r.value)),w=e.computed(()=>(a.value-1)*r.value),_=e.computed(()=>a.value*r.value);return e.watch(y,(l,f)=>{g(1)}),e.watch(()=>[n.dsData,t,n.dsSortby,n.dsFilterFields,n.dsSearchIn,n.dsSearchAs,n.dsSortAs],()=>{let l=[];!t.value&&!n.dsSortby.length&&b(n.dsFilterFields)?l=n.dsData.map((f,p)=>p):(l=n.dsData.map((f,p)=>({index:p,value:f})),b(n.dsFilterFields)||(l=I(l,n.dsFilterFields)),t.value&&(l=l.filter(f=>N(n.dsSearchIn,n.dsSearchAs,f.value,t.value))),n.dsSortby.length&&l.sort(x(n.dsSortby,n.dsSortAs)),l=l.map(f=>f.index)),o.value=l,s("update:dsData",l.map(f=>n.dsData[f]))},{immediate:!0,deep:!0}),e.provide("dsIndexes",o),e.provide("search",d),e.provide("showEntries",m),e.provide("setActive",g),e.provide("datasetI18n",i),e.provide("dsData",e.computed(()=>n.dsData)),e.provide("dsRows",P),e.provide("dsSearch",t),e.provide("dsShowEntries",r),e.provide("dsPages",u),e.provide("dsResultsNumber",y),e.provide("dsPagecount",S),e.provide("dsFrom",w),e.provide("dsTo",_),e.provide("dsPage",a),{dsIndexes:o,dsShowEntries:r,dsResultsNumber:y,dsPage:a,dsPagecount:S,dsFrom:w,dsTo:_,dsRows:P,dsPages:u,search:d,showEntries:m,setActive:g}}};function F(n,s,a,t,r,i){return e.renderSlot(n.$slots,"default",{ds:{dsIndexes:t.dsIndexes,dsShowEntries:t.dsShowEntries,dsResultsNumber:t.dsResultsNumber,dsPage:t.dsPage,dsPagecount:t.dsPagecount,dsFrom:t.dsFrom,dsTo:t.dsTo,dsData:a.dsData,dsRows:t.dsRows,dsPages:t.dsPages,search:t.search,showEntries:t.showEntries,setActive:t.setActive}})}const R=h(j,[["render",F]]),T={name:"DatasetInfo",setup(){const n=e.inject("dsResultsNumber"),s=e.inject("dsFrom"),a=e.inject("dsTo"),t=e.computed(()=>n.value!==0?s.value+1:0),r=e.computed(()=>a.value>=n.value?n.value:a.value);return{datasetI18n:e.inject("datasetI18n"),dsResultsNumber:n,showing:t,showingTo:r}}};function A(n,s,a,t,r,i){return e.openBlock(),e.createElementBlock("div",null,e.toDisplayString(t.datasetI18n.showing)+" "+e.toDisplayString(t.showing)+" "+e.toDisplayString(t.datasetI18n.showingTo)+" "+e.toDisplayString(t.showingTo)+" "+e.toDisplayString(t.datasetI18n.showingOf)+" "+e.toDisplayString(t.dsResultsNumber)+" "+e.toDisplayString(t.datasetI18n.showingEntries),1)}const V=h(T,[["render",A]]),C={name:"DatasetItem",props:{tag:{type:String,default:"div"}},setup(){const n=e.computed(()=>{const s=[];for(let a=e.inject("dsFrom").value;a[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.dsRows,(o,d)=>e.renderSlot(n.$slots,"default",{row:t.dsData[o],rowIndex:o,index:t.indexes[d]})),256)),t.dsRows.length?e.createCommentVNode("",!0):e.renderSlot(n.$slots,"noDataFound",{key:0})]),_:3})}const L=h(C,[["render",O]]),M={name:"DatasetPager",setup(){const n=e.ref(D),s=e.inject("dsPage"),a=e.inject("dsPagecount"),t=e.computed(()=>s.value===1),r=e.computed(()=>s.value===a.value||a.value===0);return{datasetI18n:e.inject("datasetI18n"),setActive:e.inject("setActive"),dsPages:e.inject("dsPages"),dsPagecount:a,dsPage:s,morePages:n,disabledPrevious:t,disabledNext:r}}},z={class:"pagination"},K=["tabindex","aria-disabled"],q=["onClick"],G={key:1,class:"page-link"},W=["tabindex","aria-disabled"];function H(n,s,a,t,r,i){return e.openBlock(),e.createElementBlock("ul",z,[e.createElementVNode("li",{class:e.normalizeClass(["page-item",t.disabledPrevious&&"disabled"])},[e.createElementVNode("a",{class:"page-link",href:"#",tabindex:t.disabledPrevious?"-1":null,"aria-disabled":t.disabledPrevious?"true":null,onClick:s[0]||(s[0]=e.withModifiers(o=>t.setActive(t.dsPage!==1&&t.dsPagecount!==0?t.dsPage-1:t.dsPage),["prevent"]))},e.toDisplayString(t.datasetI18n.previous),9,K)],2),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.dsPages,(o,d)=>(e.openBlock(),e.createElementBlock("li",{key:d,class:e.normalizeClass(["page-item",o===t.dsPage&&"active",o===t.morePages&&"disabled"])},[o!==t.morePages?(e.openBlock(),e.createElementBlock("a",{key:0,class:"page-link",href:"#",onClick:e.withModifiers(m=>t.setActive(o),["prevent"])},e.toDisplayString(o),9,q)):(e.openBlock(),e.createElementBlock("span",G,e.toDisplayString(o),1))],2))),128)),e.createElementVNode("li",{class:e.normalizeClass(["page-item",t.disabledNext&&"disabled"])},[e.createElementVNode("a",{class:"page-link",href:"#",tabindex:t.disabledNext?"-1":null,"aria-disabled":t.disabledNext?"true":null,onClick:s[1]||(s[1]=e.withModifiers(o=>t.setActive(t.dsPage!==t.dsPagecount&&t.dsPagecount!==0?t.dsPage+1:t.dsPage),["prevent"]))},e.toDisplayString(t.datasetI18n.next),9,W)],2)])}const J=h(M,[["render",H]]),Q={name:"DatasetSearch",props:{dsSearchPlaceholder:{type:String,default:""},wait:{type:Number,default:0}},setup(n){const s=e.inject("search"),a=e.ref(""),t=k(r=>{s(r)},n.wait);return{dsSearch:a,input:t}}},U=["placeholder","value"];function X(n,s,a,t,r,i){return e.openBlock(),e.createElementBlock("input",{type:"text",placeholder:a.dsSearchPlaceholder,class:"form-control",value:t.dsSearch,onInput:s[0]||(s[0]=o=>t.input(o.target.value))},null,40,U)}const Y=h(Q,[["render",X]]),Z={name:"DatasetShow",props:{dsShowEntries:{type:Number,default:10},dsShowEntriesLovs:{type:Array,default:()=>[{value:5,text:5},{value:10,text:10},{value:25,text:25},{value:50,text:50},{value:100,text:100}]}},emits:["changed"],setup(n,{emit:s}){const a=e.inject("showEntries"),t=r=>{s("changed",Number(r.target.value)),a(Number(r.target.value))};return a(Number(n.dsShowEntries)),{datasetI18n:e.inject("datasetI18n"),change:t}}},v={class:"form-inline"},$=["value"],ee=["value"];function te(n,s,a,t,r,i){return e.openBlock(),e.createElementBlock("div",v,[e.createElementVNode("label",null,e.toDisplayString(t.datasetI18n.show),1),e.createElementVNode("select",{value:a.dsShowEntries,class:"form-control mr-1 ml-1",onChange:s[0]||(s[0]=(...o)=>t.change&&t.change(...o))},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(a.dsShowEntriesLovs,o=>(e.openBlock(),e.createElementBlock("option",{key:o.value,value:o.value},e.toDisplayString(o.text),9,ee))),128))],40,$),e.createElementVNode("label",null,e.toDisplayString(t.datasetI18n.entries),1)])}const ne=h(Z,[["render",te]]);c.Dataset=R,c.DatasetInfo=V,c.DatasetItem=L,c.DatasetPager=J,c.DatasetSearch=Y,c.DatasetShow=ne,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"})}); 2 | -------------------------------------------------------------------------------- /docs/.vuepress/client.js: -------------------------------------------------------------------------------- 1 | import './styles/styles.scss' 2 | import 'vuepress-plugin-vue-example/style.css' 3 | import { defineClientConfig } from 'vuepress/client' 4 | import Dataset from '@/Dataset.vue' 5 | import DatasetInfo from '@/DatasetInfo.vue' 6 | import DatasetItem from '@/DatasetItem.vue' 7 | import DatasetPager from '@/DatasetPager.vue' 8 | import DatasetSearch from '@/DatasetSearch.vue' 9 | import DatasetShow from '@/DatasetShow.vue' 10 | 11 | export default defineClientConfig({ 12 | enhance({ app, router, siteData }) { 13 | app.component('Dataset', Dataset) 14 | app.component('DatasetShow', DatasetShow) 15 | app.component('DatasetSearch', DatasetSearch) 16 | app.component('DatasetPager', DatasetPager) 17 | app.component('DatasetItem', DatasetItem) 18 | app.component('DatasetInfo', DatasetInfo) 19 | }, 20 | setup() { 21 | // noop 22 | }, 23 | rootComponents: [], 24 | }) 25 | -------------------------------------------------------------------------------- /docs/.vuepress/components/Example1.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 71 | -------------------------------------------------------------------------------- /docs/.vuepress/components/Example2.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | 183 | 184 | 192 | -------------------------------------------------------------------------------- /docs/.vuepress/components/Example3.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 121 | 122 | 173 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | import { viteBundler } from '@vuepress/bundler-vite' 2 | import { docsearchPlugin } from '@vuepress/plugin-docsearch' 3 | import { registerComponentsPlugin } from '@vuepress/plugin-register-components' 4 | import { defaultTheme } from '@vuepress/theme-default' 5 | import * as path from 'path' 6 | import * as url from 'url' 7 | import { fileURLToPath } from 'url' 8 | import { defineUserConfig } from 'vuepress' 9 | import { vueExamplePlugin } from 'vuepress-plugin-vue-example' 10 | 11 | const examplesDir = fileURLToPath(new URL('./components', import.meta.url)) 12 | 13 | console.log(examplesDir) 14 | 15 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) 16 | 17 | export default defineUserConfig({ 18 | bundler: viteBundler(), 19 | plugins: [ 20 | registerComponentsPlugin({ 21 | componentsDir: examplesDir, 22 | }), 23 | vueExamplePlugin({ 24 | componentsDir: examplesDir, 25 | }), 26 | docsearchPlugin({ 27 | apiKey: 'a05c686d69be9a09e66f93b07bc7f855', 28 | indexName: 'next-vue-dataset', 29 | appId: 'BAXEDGK9R9', 30 | }), 31 | ], 32 | dest: 'public', 33 | title: 'vue-dataset', 34 | description: 'A vue component to display datasets with filtering, paging and sorting capabilities!', 35 | theme: defaultTheme({ 36 | contributors: false, 37 | repo: 'https://github.com/kouts/vue-dataset/tree/next', 38 | colorMode: 'light', 39 | colorModeSwitch: false, 40 | sidebar: [ 41 | { 42 | link: '/', 43 | text: 'Introduction', 44 | }, 45 | { 46 | link: '/installation/', 47 | text: 'Installation', 48 | }, 49 | { 50 | link: '/components/', 51 | text: 'Components', 52 | }, 53 | { 54 | text: 'Examples', 55 | collapsable: true, 56 | children: [ 57 | { 58 | link: '/examples/cards/', 59 | text: 'Cards', 60 | }, 61 | { 62 | link: '/examples/datatable/', 63 | text: 'Datatable', 64 | }, 65 | ], 66 | }, 67 | ], 68 | themePlugins: { 69 | prismjs: { 70 | theme: 'tomorrow', 71 | }, 72 | }, 73 | }), 74 | alias: { 75 | '@': path.resolve(__dirname, '../../src'), 76 | '@playground': path.resolve(__dirname, '../../playground'), 77 | '@root': path.resolve(__dirname, '../../'), 78 | }, 79 | }) 80 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/styles.scss: -------------------------------------------------------------------------------- 1 | $primary: #3eaf7c !default; 2 | $link-color: $primary !default; 3 | $link-hover-color: lighten($link-color, 15%) !default; 4 | 5 | /* Bootstrap imports */ 6 | @import 'bootstrap/scss/_functions'; 7 | @import 'bootstrap/scss/_variables'; 8 | 9 | @import 'bootstrap/scss/_mixins'; 10 | @import 'bootstrap/scss/_root'; 11 | @import 'bootstrap/scss/_reboot'; 12 | @import 'bootstrap/scss/_type'; 13 | /* 14 | @import 'bootstrap/scss/_images'; 15 | @import 'bootstrap/scss/_code'; 16 | */ 17 | @import 'bootstrap/scss/_grid'; 18 | @import 'bootstrap/scss/_tables'; 19 | @import 'bootstrap/scss/_forms'; 20 | @import 'bootstrap/scss/_buttons'; 21 | /* 22 | @import 'bootstrap/scss/_transitions'; 23 | @import 'bootstrap/scss/_dropdown'; 24 | */ 25 | @import 'bootstrap/scss/_button-group'; 26 | /* 27 | @import 'bootstrap/scss/_input-group'; 28 | */ 29 | @import 'bootstrap/scss/_custom-forms'; 30 | @import 'bootstrap/scss/_nav'; 31 | /* 32 | @import 'bootstrap/scss/_navbar'; 33 | */ 34 | @import 'bootstrap/scss/_card'; 35 | /* 36 | @import 'bootstrap/scss/_breadcrumb'; 37 | */ 38 | @import 'bootstrap/scss/_pagination'; 39 | @import 'bootstrap/scss/_badge'; 40 | /* 41 | @import 'bootstrap/scss/_jumbotron'; 42 | @import 'bootstrap/scss/_alert'; 43 | @import 'bootstrap/scss/_progress'; 44 | @import 'bootstrap/scss/_media'; 45 | @import 'bootstrap/scss/_list-group'; 46 | @import 'bootstrap/scss/_close'; 47 | @import 'bootstrap/scss/_toasts'; 48 | @import 'bootstrap/scss/_modal'; 49 | @import 'bootstrap/scss/_tooltip'; 50 | @import 'bootstrap/scss/_popover'; 51 | @import 'bootstrap/scss/_carousel'; 52 | @import 'bootstrap/scss/_spinners'; 53 | */ 54 | @import 'bootstrap/scss/_utilities'; 55 | /* 56 | @import 'bootstrap/scss/_print'; 57 | */ 58 | 59 | /* Max-widths */ 60 | .mw1 { 61 | max-width: 35px !important; 62 | } 63 | .mw2 { 64 | max-width: 90px !important; 65 | } 66 | .mw3 { 67 | max-width: 145px !important; 68 | } 69 | .mw4 { 70 | max-width: 200px !important; 71 | } 72 | .mw5 { 73 | max-width: 255px !important; 74 | } 75 | .mw6 { 76 | max-width: 310px !important; 77 | } 78 | .mw7 { 79 | max-width: 365px !important; 80 | } 81 | .mw8 { 82 | max-width: 420px !important; 83 | } 84 | .mw9 { 85 | max-width: 475px !important; 86 | } 87 | .mw10 { 88 | max-width: 530px !important; 89 | } 90 | .mw11 { 91 | max-width: 585px !important; 92 | } 93 | .mw12 { 94 | max-width: 640px !important; 95 | } 96 | .mw13 { 97 | max-width: 695px !important; 98 | } 99 | .mw14 { 100 | max-width: 750px !important; 101 | } 102 | .mw15 { 103 | max-width: 805px !important; 104 | } 105 | .mw16 { 106 | max-width: 860px !important; 107 | } 108 | .mw17 { 109 | max-width: 915px !important; 110 | } 111 | .mw18 { 112 | max-width: 970px !important; 113 | } 114 | blockquote p { 115 | color: #777; 116 | } 117 | 118 | /* Font sizes */ 119 | .font-16 { 120 | font-size: 16px; 121 | } 122 | 123 | /* Browsers support table */ 124 | #browsers-support + table .icon.outbound { 125 | display: none; 126 | } 127 | #browsers-support + table th { 128 | text-align: center; 129 | } 130 | #browsers-support + table th img { 131 | vertical-align: baseline; 132 | } 133 | 134 | /* Content width fix */ 135 | .theme-default-content:not(.custom) { 136 | box-sizing: content-box; 137 | } 138 | 139 | /* Custom width for dataset pages */ 140 | .dataset-page .theme-default-content:not(.custom) { 141 | max-width: 100%; 142 | } 143 | 144 | /* Fix links appearance */ 145 | .dataset-page .theme-default-content:not(.custom) ul.pagination a:hover { 146 | text-decoration: none; 147 | } 148 | 149 | /* Fix margin-top for first paragraph */ 150 | .theme-container .theme-default-content:not(.custom) > h2:first-child + p { 151 | margin-top: 0 !important; 152 | } 153 | 154 | /* Fix margin-bottom for headers */ 155 | .theme-container .theme-default-content:not(.custom) h2, 156 | .theme-container .theme-default-content:not(.custom) h3, 157 | .theme-container .theme-default-content:not(.custom) h4, 158 | .theme-container .theme-default-content:not(.custom) h5, 159 | .theme-container .theme-default-content:not(.custom) h6 { 160 | margin-bottom: 1rem; 161 | } 162 | 163 | /* Fix footer nav links */ 164 | .page-nav .nav-link, 165 | .page-meta .nav-link { 166 | display: inline; 167 | padding: 0; 168 | } 169 | 170 | /* Fix navbar links */ 171 | .theme-container .navbar a:hover { 172 | text-decoration: none; 173 | } 174 | 175 | .theme-container .navbar .navbar-items a:hover { 176 | border-bottom: none; 177 | } 178 | 179 | /* Fix stripped table rows colors */ 180 | .table.table-striped tr:nth-child(2n) { 181 | background-color: #f6f8fa; 182 | } 183 | 184 | /* Fix card titles */ 185 | .theme-container .theme-default-content:not(.custom) .card > .card-body > h5.card-title, 186 | .theme-container .theme-default-content:not(.custom) .card > .card-body > h6 { 187 | margin: 0px; 188 | padding: 0px; 189 | } 190 | 191 | .theme-container .theme-default-content:not(.custom) .card > .card-body h3 { 192 | margin: 0px 0px 8px 0px; 193 | padding: 0px; 194 | } 195 | 196 | /* Fix headings font-size */ 197 | h1 { 198 | font-size: 2.2rem; 199 | } 200 | 201 | h2 { 202 | font-size: 1.65rem; 203 | } 204 | 205 | h3 { 206 | font-size: 1.35rem; 207 | } 208 | 209 | /* Fix code blocks */ 210 | code { 211 | font-family: inherit; 212 | } 213 | 214 | /* Fix Dataset pagination links */ 215 | .theme-default-content ul.pagination li a { 216 | text-decoration: none; 217 | } 218 | 219 | /* Fix Dataset card title spacing */ 220 | .theme-default-content h5.card-title { 221 | position: relative; 222 | padding-top: 3rem; 223 | } 224 | 225 | /* Fix navbar item hover */ 226 | .vp-navbar-item > a:hover { 227 | border-bottom: none; 228 | } 229 | -------------------------------------------------------------------------------- /docs/.vuepress/utilities/index.js: -------------------------------------------------------------------------------- 1 | export const filterList = function (list = [], filter) { 2 | return list.filter(function (item) { 3 | for (const key in filter) { 4 | if (item[key] === undefined || item[key] !== filter[key]) { 5 | return false 6 | } 7 | } 8 | 9 | return true 10 | }) 11 | } 12 | 13 | export const clone = function (obj) { 14 | return JSON.parse(JSON.stringify(obj || {})) 15 | } 16 | 17 | export const isoDateToEuroDate = function (isoDate) { 18 | const parts = isoDate.split('-') 19 | 20 | return `${parts[2]}.${parts[1]}.${parts[0]}` 21 | } 22 | 23 | export const searchAsEuroDate = function (value, searchString) { 24 | const parts = searchString.split('.') 25 | const isoDate = `${parts[2]}-${parts[1]}-${parts[0]}` 26 | 27 | return isoDate === value 28 | } 29 | 30 | export const isoDateToDate = function (isoDate) { 31 | return new Date(isoDate) 32 | } 33 | -------------------------------------------------------------------------------- /docs/components/index.md: -------------------------------------------------------------------------------- 1 | ## Dataset 2 | 3 | The `dataset` component acts as the provider component of all the data and methods `vue-dataset` needs to function. 4 | It does so by using the provide/inject mechanism of Vue so that data is also accessible in nested levels down the component tree. 5 | 6 | Dataset takes the original data object as a prop and also some useful props as options that dictate how the data will be filtered, searched and sorted. 7 | 8 | ### Example 9 | 10 | ```vue 11 | 20 | ... 21 | 22 | ``` 23 | 24 | ### Props 25 | 26 | #### ds-data 27 | 28 | Type: `Array of Objects` 29 | Default: Empty Array 30 | 31 | This is the data object that `vue-dataset` will operate on. 32 | It must be an Array of Objects. 33 | 34 | ```js 35 | [ 36 | { 37 | firstName: 'John', 38 | lastName: 'Doe', 39 | birthDate: '2004-02-11' 40 | }, 41 | { 42 | firstName: 'George', 43 | lastName: 'Adams', 44 | birthDate: '2003-07-28' 45 | }, 46 | ... 47 | ] 48 | ``` 49 | 50 | #### ds-filter-fields 51 | 52 | Type: `Object` 53 | Default: Empty Object 54 | 55 | It defines how certain properties of the data object will be filtered. 56 | The object key denotes the data object property and the object value is a `value` or a `function` that will be used to filter 57 | that data property. 58 | 59 | For example this will filter the data by 60 | firstName "John" and all lastNames that start with the letter "D" 61 | 62 | ```js 63 | { 64 | firstName: 'John', 65 | lastName: startsWithD 66 | } 67 | ``` 68 | 69 | `startsWithD` can be a predicate function defined in your instance methods. 70 | The function takes two arguments, the value of the data object property and the current row data. 71 | 72 | ```js 73 | startsWithD (value, row) { 74 | return value.toLowerCase().startsWith('d') 75 | } 76 | ``` 77 | 78 | #### ds-sortby 79 | 80 | Type: `Array` 81 | Default: Empty Array 82 | 83 | It defines the data object properties by which the dataset object will be sorted. 84 | If a property is prefixed by `-` it will be sorted with descending order. 85 | 86 | For example this will sort the data by lastName 87 | 88 | ```html 89 | ['lastName'] 90 | ``` 91 | 92 | #### ds-search-in 93 | 94 | Type: `Array` 95 | Default: Empty Array 96 | 97 | It restricts the search to certain data object properties. 98 | If the `ds-search-in` array is empty (default), then all object properties will be searched. 99 | 100 | For example this will tell `Dataset` to perform search **only** in the `firstName` and `lastName` data object properties. 101 | 102 | ```html 103 | ['firstName', 'lastName'] 104 | ``` 105 | 106 | #### ds-search-as 107 | 108 | Type: `Object` 109 | Default: Empty Object 110 | 111 | It defines how certain properties of the data object will be searched. 112 | The object key denotes the data object property and the object value is a predicate `Function` that will be used to search 113 | that data property. The predicate function has access to the column value, the search string and the current row data. 114 | 115 | This is useful in situations when you are displaying a formatted value and you want the user to be able to search 116 | it inside the data object with the same format as it appears on-screen. 117 | 118 | For example this will set the birthDate attribute searchable by `searchAsEuroDate` method 119 | and will allow birthdate dates defined as YYYY-MM-DD format to be searched as DD.MM.YYYY format. 120 | 121 | ```js 122 | { 123 | birthDate: searchAsEuroDate 124 | } 125 | ``` 126 | 127 | Inside your instance methods 128 | 129 | ```js 130 | searchAsEuroDate: function (value, searchString, rowData) { 131 | const parts = searchString.split('.') 132 | const isoDate = `${parts[2]}-${parts[1]}-${parts[0]}` 133 | return isoDate === value 134 | } 135 | ``` 136 | 137 | #### ds-sort-as 138 | 139 | Type: `Object` 140 | Default: Empty Object 141 | 142 | It defines how certain properties of the data object will be sorted. 143 | The object key denotes the data object property and the object value is a `Function` that will be used to convert 144 | that data property so that it can be sorted correctly. This is useful in situations when you have values that can't be sorted natively 145 | such as currency or dates. 146 | 147 | For example this will apply the `sortAsDate` method to the birthDate attribute so that dates defined as YYYY-MM-DD format can be sorted 148 | correctly. 149 | 150 | ```js 151 | { 152 | birthDate: sortAsDate 153 | } 154 | ``` 155 | 156 | Inside your instance methods 157 | 158 | ```js 159 | sortAsDate: function (isoDate) { 160 | return new Date(isoDate) 161 | } 162 | ``` 163 | 164 | ### Provides 165 | 166 | Dataset `provides` reactive data and methods to the child components. 167 | You can leverage these using `inject` to create your own **custom child components**. 168 | 169 | ##### Reactive data provided by `vue-dataset` 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 |
PropertyTypeDescription
dsDataArray of ObjectsThe data object that contains all the data
dsIndexesArrayThe indexes of all the filtered data rows, regardless of paging
dsRowsArrayThe indexes of the data rows in the current displayed page
dsPagesArrayThe array used to create pagination links
dsResultsNumberNumberThe number of rows currently displaying
dsPagecountNumberThe number of pagination pages
dsFromNumberThe item "from" of paginated items currently displaying
dsToNumberThe item "to" of paginated items currently displaying
dsPageNumberThe number of the current page in pagination
dsShowEntriesNumberThe number of items to show in pagination
datasetI18nObjectAn object containing translation strings
237 | 238 | Example: 239 | 240 | ```vue 241 | 244 | 245 | 260 | ``` 261 | 262 | ##### Methods 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 |
NameParamsInput value/Description
searchStringThe value to search
showEntriesNumberThe number of items to show on each page
setActiveNumberThe number of the page to set as active
290 | 291 | ### Events 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 |
NameEmitsDescription
update:dsDataArray of ObjectsEmits the filtered dsData items, every time the internal computed results is changed.
309 | 310 | ### Scoped slot 311 | 312 | Dataset also provides several data via a `ds` object exposed from a a scoped slot. 313 | 314 | #### The scoped slot ds object 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 |
PropertyTypeDescription
dsDataArray of ObjectsThe data object that contains all the data
dsIndexesArrayThe indexes of all the filtered data rows, regardless of paging
dsRowsArrayThe indexes of the data rows in the current displayed page
dsPagesArrayThe array used to create pagination links
dsResultsNumberNumberThe number of rows currently displaying
dsPagecountNumberThe number of pagination pages
dsFromNumberThe item "from" of paginated items currently displaying
dsToNumberThe item "to" of paginated items currently displaying
dsPageNumberThe number of the current page in pagination
dsShowEntriesNumberThe number of items to show in pagination
377 | 378 | ## DatasetItem 379 | 380 | The `dataset-item` component is responsible for displaying the item rows of the dataset. 381 | Since it's a dynamic component it can take the form of any tag like `div`, `li`, `tr` etc. 382 | 383 | DatasetItem must be nested inside the Dataset component in order to work. 384 | It exposes one scoped slot with the the row's data and index and also one slot for the customization 385 | of the "no data found" message. 386 | 387 | ### Example 388 | 389 | ```vue 390 | 391 | 396 | 399 | 400 | ``` 401 | 402 | ### Props 403 | 404 | #### tag 405 | 406 | Type: `String` 407 | Default: div 408 | 409 | The HTML tag to render. 410 | 411 | ### Scoped slot 412 | 413 | DatasetItem also provides the row's data via a `row` object exposed from a a scoped slot. 414 | It also provides the row's original index, useful e.g if you want to delete an item. 415 | 416 | #### The scoped slot object 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 |
PropertyTypeDescription
rowObjectThe dataset row data
rowIndexNumberThe original index of the data row
indexNumberThe iteration index
444 | 445 | ### Named slot `noDataFound` 446 | 447 | DatasetItem provides a named slot to customize the "no data found" message. 448 | There's no default content for the slot. 449 | 450 | ## DatasetInfo 451 | 452 | The `dataset-info` component displays information about the range of items being displayed 453 | and the total number of items in the dataset. 454 | 455 | ### Example 456 | 457 | ```vue 458 | 459 | ``` 460 | 461 | ## DatasetPager 462 | 463 | The `dataset-pager` component displays the pagination controls. 464 | 465 | ### Example 466 | 467 | ```vue 468 | 469 | ``` 470 | 471 | ## DatasetShow 472 | 473 | The `dataset-show` component displays a select that is used to control how many items are visible simultaneously. 474 | Props can be used to customize the default number of visible items as well as the list for the select control. 475 | 476 | ### Example 477 | 478 | ```vue 479 | 480 | ``` 481 | 482 | ### Props 483 | 484 | #### ds-show-entries 485 | 486 | Type: `Number` 487 | Default: 10 488 | 489 | The selected number of items to show. 490 | 491 | #### ds-show-entries-lovs 492 | 493 | Type: `Array of Objects` 494 | Default: `[{ value: 5, text: 5 }, { value: 10, text: 10 }, { value: 25, text: 25 }, { value: 50, text: 50 }, { value: 100, text: 100 }]` 495 | 496 | The list of options for the select element. 497 | 498 | ## DatasetSearch 499 | 500 | The `dataset-search` component displays an input search form control used to search inside the dataset data. 501 | Props can be used to customize the placeholder text as well as the debounce wait time. 502 | 503 | ### Example 504 | 505 | ```vue 506 | 507 | ``` 508 | 509 | ### Props 510 | 511 | #### ds-search-placeholder 512 | 513 | Type: `String` 514 | Default: Empty String 515 | 516 | The placeholder text of the input control. 517 | 518 | #### wait 519 | 520 | Type: `Number` 521 | Default: 0 522 | 523 | The amount of time the debounce function waits after the last received input action before executing the search. 524 | -------------------------------------------------------------------------------- /docs/examples/cards/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageClass: dataset-page 3 | --- 4 | 5 |

Example with cards layout and custom filters

6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/examples/datatable/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageClass: dataset-page 3 | --- 4 | 5 |

Example with table layout

6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | pageClass: dataset-page 3 | --- 4 | 5 | # vue-dataset ![](https://img.badgesize.io/kouts/vue-dataset/next/dist/vue-dataset.umd.js) ![](https://img.badgesize.io/kouts/vue-dataset/next/dist/vue-dataset.umd.js.svg?compression=gzip) ![](../coverage/badge-next.svg) 6 | 7 | A set of Vue.js 3 components to display datasets (lists) with filtering, paging, and sorting capabilities! 8 | Created with reusability in mind, so that one doesn't have to recreate the same functionality for lists over and over again. 9 | 10 | > vue-dataset does not impose any structure or layout limitations on your HTML, you can use divs, tables or anything you like to present your data. 11 | 12 | ## Features 13 | 14 | - Highly customizable DOM structure 15 | - Custom filtering based on the row values from all or specific data keys 16 | - "Search as" feature allows for searching using a custom search method 17 | - Multi "column" searching, search data keys are configurable 18 | - "Sort as" feature allows for sorting using a custom sorting method 19 | - Multi "column" sorting, sortable data keys are configurable 20 | - Pagination 21 | - Global search with debounce setting 22 | - Easy to extend with custom components 23 | 24 | `vue-dataset` contains 6 components 25 | 26 | | Component | Description | 27 | | ---------------- | ----------------------------------------------------------------------------- | 28 | | `dataset` | Responsible for distributing data/methods to children (wrapper/data provider) | 29 | | `dataset-item` | Renders the dataset items | 30 | | `dataset-info` | Renders the paging information | 31 | | `dataset-pager` | Renders the paging buttons | 32 | | `dataset-search` | Renders the search input field | 33 | | `dataset-show` | Renders the "items per page" dropdown select | 34 | 35 |

Example using cards

36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/installation/index.md: -------------------------------------------------------------------------------- 1 | ## Using module bundlers 2 | 3 | Most likely you are using a module bundler like [Webpack](https://webpack.js.org/), which makes it easy to directly include `vue-dataset` into your project. 4 | 5 | Install `vue-dataset@next` via npm 6 | 7 | ```bash 8 | npm install vue-dataset@next --save 9 | ``` 10 | 11 | Use the `import` statement to include the `vue-dataset` components into your bundle. 12 | 13 | ```js 14 | import { Dataset, DatasetItem, DatasetInfo, DatasetPager, DatasetSearch, DatasetShow } from 'vue-dataset' 15 | ``` 16 | 17 | ## Using a script tag 18 | 19 | A `vue-dataset.umd.js` file suitable for including `vue-dataset` using a `script` tag into your page, is resides inside the `dist` folder. 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | This will expose a global `VueDataset` object that contains all the `vue-dataset` components. 26 | You can then register them globally e.g. 27 | 28 | ```js 29 | const app = Vue.createApp({...}) 30 | 31 | app.component('Dataset', VueDataset.Dataset) 32 | app.component('DatasetItem', VueDataset.DatasetItem) 33 | app.component('DatasetInfo', VueDataset.DatasetInfo) 34 | app.component('DatasetPager', VueDataset.DatasetPager) 35 | app.component('DatasetSearch', VueDataset.DatasetSearch) 36 | app.component('DatasetShow', VueDataset.DatasetShow)` 37 | ``` 38 | 39 | ## Translations 40 | 41 | It's possible to customize the texts of `vue-dataset` by extending the `Dataset` component using the following pattern. 42 | 43 | ```js 44 | import { Dataset } from 'vue-dataset' 45 | 46 | export default { 47 | ...Dataset, 48 | provide() { 49 | return { 50 | // Provide the translated texts here 51 | datasetI18n: { 52 | show: 'Show', 53 | entries: 'entries', 54 | previous: 'Previous', 55 | next: 'Next', 56 | showing: 'Showing', 57 | showingTo: 'to', 58 | showingOf: 'of', 59 | showingEntries: 'entries' 60 | } 61 | } 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@kouts/eslint-config' 2 | 3 | export default [ 4 | ...config({ 5 | ts: false, 6 | }), 7 | { 8 | name: 'project-ignores', 9 | ignores: ['!/docs/.vuepress'], 10 | }, 11 | { 12 | name: 'project-overrides', 13 | files: ['playground/**/*.{vue,js}', 'src/**/*.vue', 'docs/**/*.{vue,js}'], 14 | rules: { 15 | 'vue/multi-word-component-names': 'off', 16 | }, 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kouts/vue-dataset/edfbb1bd9fa109f026544af68102cfa27cdfb4ee/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%- title %> 10 | 11 | 12 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"], 6 | "@playground/*": ["./playground/*"], 7 | "@root/*": ["./*"] 8 | } 9 | }, 10 | "exclude": ["node_modules", "dist"] 11 | } -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '*.{js,vue}': ['npm run lint-fix'], 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-dataset", 3 | "description": "A vue 3 component to display datasets with filtering, paging and sorting capabilities!", 4 | "version": "0.0.0-semantic-release", 5 | "author": "Giannis Koutsaftakis", 6 | "license": "MIT", 7 | "repository": "https://github.com/kouts/vue-dataset", 8 | "packageManager": "pnpm@9.12.0", 9 | "type": "module", 10 | "keywords": [ 11 | "vue", 12 | "component", 13 | "dataset", 14 | "datatable" 15 | ], 16 | "main": "./dist/vue-dataset.umd.js", 17 | "module": "./dist/vue-dataset.es.js", 18 | "exports": { 19 | ".": { 20 | "import": "./dist/vue-dataset.es.js", 21 | "require": "./dist/vue-dataset.umd.js" 22 | } 23 | }, 24 | "unpkg": "dist/vue-dataset.umd.js", 25 | "sideEffects": [ 26 | "./**/*.scss", 27 | "./vuepress-plugin-vue-example/**/*.vue", 28 | "./docs/**/*.vue" 29 | ], 30 | "scripts": { 31 | "dev": "vite", 32 | "serve": "vite preview", 33 | "build": "vite build", 34 | "test:unit": "vitest", 35 | "test:unit-coverage": "vitest run --coverage && make-coverage-badge --output-path ./coverage/badge-next.svg", 36 | "lint": "eslint \"**/*.{vue,ts,js}\"", 37 | "lint-fix": "eslint --fix \"**/*.{vue,ts,js}\"", 38 | "prepare": "husky", 39 | "docs:dev": "vuepress dev docs", 40 | "docs:build": "vuepress build docs" 41 | }, 42 | "dependencies": { 43 | "vue": "^3.4.38" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^19.5.0", 47 | "@commitlint/config-conventional": "^19.5.0", 48 | "@kouts/eslint-config": "^1.3.14", 49 | "@vitejs/plugin-vue": "^5.1.4", 50 | "@vitest/coverage-v8": "^2.1.2", 51 | "@vue/compiler-sfc": "^3.4.38", 52 | "@vue/test-utils": "^2.4.6", 53 | "@vuepress/bundler-vite": "2.0.0-rc.15", 54 | "@vuepress/plugin-docsearch": "2.0.0-rc.41", 55 | "@vuepress/plugin-register-components": "2.0.0-rc.37", 56 | "@vuepress/theme-default": "^2.0.0-rc.41", 57 | "bootstrap": "^4.6.2", 58 | "eslint": "^9.11.1", 59 | "husky": "^9.1.6", 60 | "jsdom": "^25.0.1", 61 | "lint-staged": "^15.2.10", 62 | "make-coverage-badge": "^1.2.0", 63 | "prettier": "3.3.3", 64 | "prismjs": "^1.29.0", 65 | "sass": "~1.77.8", 66 | "vite": "^5.4.8", 67 | "vite-plugin-html": "^3.2.2", 68 | "vitest": "^2.1.2", 69 | "vue-router": "^4.4.5", 70 | "vuepress": "2.0.0-rc.15", 71 | "vuepress-plugin-vue-example": "3.0.17", 72 | "vuex": "^4.1.0" 73 | }, 74 | "resolutions": { 75 | "@typescript-eslint/utils": "^8.0.0", 76 | "vuepress": "2.0.0-rc.15" 77 | } 78 | } -------------------------------------------------------------------------------- /playground/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /playground/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kouts/vue-dataset/edfbb1bd9fa109f026544af68102cfa27cdfb4ee/playground/assets/logo.png -------------------------------------------------------------------------------- /playground/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /playground/helpers/index.js: -------------------------------------------------------------------------------- 1 | const MORE_PAGES = '...' 2 | 3 | function debounce(func, wait, immediate) { 4 | let timeout 5 | 6 | return function () { 7 | const context = this 8 | const args = arguments 9 | 10 | clearTimeout(timeout) 11 | if (immediate && !timeout) { 12 | func.apply(context, args) 13 | } 14 | timeout = setTimeout(function () { 15 | timeout = null 16 | if (!immediate) { 17 | func.apply(context, args) 18 | } 19 | }, wait) 20 | } 21 | } 22 | 23 | // https://jsperf.com/object-empty-ch/1 24 | function isEmptyObject(obj) { 25 | // eslint-disable-next-line no-unreachable-loop 26 | for (const key in obj) { 27 | return false 28 | } 29 | 30 | return true 31 | } 32 | 33 | function createPagingRange(nrOfPages, currentPage) { 34 | const delta = 2 35 | const range = [] 36 | const rangeWithDots = [] 37 | let length 38 | 39 | range.push(1) 40 | 41 | if (nrOfPages <= 1) { 42 | return range 43 | } 44 | 45 | for (let i = currentPage - delta; i <= currentPage + delta; i++) { 46 | if (i < nrOfPages && i > 1) { 47 | range.push(i) 48 | } 49 | } 50 | range.push(nrOfPages) 51 | 52 | for (let i = 0; i < range.length; i++) { 53 | if (length) { 54 | if (range[i] - length === 2) { 55 | rangeWithDots.push(length + 1) 56 | } else if (range[i] - length !== 1) { 57 | rangeWithDots.push(MORE_PAGES) 58 | } 59 | } 60 | rangeWithDots.push(range[i]) 61 | length = range[i] 62 | } 63 | 64 | return rangeWithDots 65 | } 66 | 67 | function fieldSorter(fields, dsSortAs = {}) { 68 | const dir = [] 69 | let i 70 | const length = fields.length 71 | 72 | fields = fields.map(function (o, i) { 73 | if (o[0] === '-') { 74 | dir[i] = -1 75 | o = o.substring(1) 76 | } else { 77 | dir[i] = 1 78 | } 79 | 80 | return o 81 | }) 82 | 83 | return function (a, b) { 84 | for (i = 0; i < length; i++) { 85 | const o = fields[i] 86 | const aVal = dsSortAs[o] ? dsSortAs[o](a.value[o]) : a.value[o] 87 | const bVal = dsSortAs[o] ? dsSortAs[o](b.value[o]) : b.value[o] 88 | 89 | if (aVal > bVal) { 90 | return dir[i] 91 | } 92 | if (aVal < bVal) { 93 | return -dir[i] 94 | } 95 | } 96 | 97 | return 0 98 | } 99 | } 100 | 101 | function fieldFilter(items, filterFields) { 102 | // Filter it by field 103 | for (const filterKey in filterFields) { 104 | // console.log(filterKey + ' -> ' + filterFields[filterKey]); 105 | items = items.filter(function (item) { 106 | const itemValue = item.value 107 | 108 | for (const itemKey in itemValue) { 109 | if (itemKey === filterKey) { 110 | if (typeof filterFields[filterKey] === 'function') { 111 | return filterFields[filterKey](itemValue[itemKey]) 112 | } 113 | if (filterFields[filterKey] === '') { 114 | return true 115 | } 116 | if (itemValue[itemKey] === filterFields[filterKey]) { 117 | return true 118 | } 119 | } 120 | } 121 | 122 | return false 123 | }) 124 | } 125 | 126 | return items 127 | } 128 | 129 | // Search method that also takes into account transformations needed 130 | function findAny(dsSearchIn, dsSearchAs, obj, str) { 131 | // Convert the search string to lower case 132 | str = String(str).toLowerCase() 133 | for (const key in obj) { 134 | if (dsSearchIn.length === 0 || dsSearchIn.indexOf(key) !== -1) { 135 | const value = String(obj[key]).toLowerCase() 136 | 137 | for (const field in dsSearchAs) { 138 | if (field === key) { 139 | // Found key in dsSearchAs so we pass the value and the search string to a search function 140 | // that returns true/false and we return that if true. 141 | /* Check if dsSearchAs is a function (passed from the template) */ 142 | if (typeof dsSearchAs[field] === 'function') { 143 | const res = dsSearchAs[field](value, str) 144 | 145 | if (res === true) { 146 | return res 147 | } 148 | } 149 | } 150 | } 151 | // If it doesn't return from above we perform a simple search 152 | if (value.indexOf(str) >= 0) { 153 | return true 154 | } 155 | } 156 | } 157 | 158 | return false 159 | } 160 | 161 | export { createPagingRange, debounce, fieldFilter, fieldSorter, findAny, isEmptyObject, MORE_PAGES } 162 | -------------------------------------------------------------------------------- /playground/layouts/default/Default.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 44 | -------------------------------------------------------------------------------- /playground/layouts/default/DefaultFooter.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /playground/layouts/default/DefaultNav.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /playground/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import Dataset from '@/Dataset.vue' 3 | import DatasetInfo from '@/DatasetInfo.vue' 4 | import DatasetItem from '@/DatasetItem.vue' 5 | import DatasetPager from '@/DatasetPager.vue' 6 | import DatasetSearch from '@/DatasetSearch.vue' 7 | import DatasetShow from '@/DatasetShow.vue' 8 | import App from './App.vue' 9 | import Default from './layouts/default/Default.vue' 10 | import { router } from './router' 11 | import { store } from './store' 12 | 13 | const app = createApp(App) 14 | 15 | app.component('LayoutDefault', Default) 16 | 17 | app.component('Dataset', Dataset) 18 | app.component('DatasetInfo', DatasetInfo) 19 | app.component('DatasetItem', DatasetItem) 20 | app.component('DatasetPager', DatasetPager) 21 | app.component('DatasetSearch', DatasetSearch) 22 | app.component('DatasetShow', DatasetShow) 23 | 24 | app.use(store) 25 | app.use(router) 26 | app.mount('#app') 27 | -------------------------------------------------------------------------------- /playground/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import OptionsApi from '../views/OptionsApi.vue' 3 | 4 | const history = createWebHashHistory() 5 | const routes = [ 6 | { 7 | path: '/', 8 | name: 'OptionsApi', 9 | component: OptionsApi, 10 | meta: { 11 | layout: 'default', 12 | }, 13 | }, 14 | { 15 | path: '/composition-api', 16 | name: 'CompositionApi', 17 | // route level code-splitting 18 | // this generates a separate chunk (about.[hash].js) for this route 19 | // which is lazy-loaded when the route is visited. 20 | component: () => import(/* webpackChunkName: "composition-api" */ '../views/CompositionApi.vue'), 21 | meta: { 22 | layout: 'default', 23 | }, 24 | }, 25 | ] 26 | const router = createRouter({ 27 | linkActiveClass: 'active', 28 | history, 29 | routes, 30 | }) 31 | 32 | export { router } 33 | -------------------------------------------------------------------------------- /playground/scss/app.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap source files (except functions, variables, mixins) 2 | 3 | @import "~bootstrap/scss/root"; 4 | @import "~bootstrap/scss/reboot"; 5 | @import "~bootstrap/scss/type"; 6 | @import "~bootstrap/scss/images"; 7 | @import "~bootstrap/scss/code"; 8 | @import "~bootstrap/scss/grid"; 9 | @import "~bootstrap/scss/tables"; 10 | @import "~bootstrap/scss/forms"; 11 | @import "~bootstrap/scss/buttons"; 12 | @import "~bootstrap/scss/transitions"; 13 | @import "~bootstrap/scss/dropdown"; 14 | @import "~bootstrap/scss/button-group"; 15 | @import "~bootstrap/scss/input-group"; 16 | @import "~bootstrap/scss/custom-forms"; 17 | @import "~bootstrap/scss/nav"; 18 | @import "~bootstrap/scss/navbar"; 19 | @import "~bootstrap/scss/card"; 20 | @import "~bootstrap/scss/breadcrumb"; 21 | @import "~bootstrap/scss/pagination"; 22 | @import "~bootstrap/scss/badge"; 23 | @import "~bootstrap/scss/jumbotron"; 24 | @import "~bootstrap/scss/alert"; 25 | @import "~bootstrap/scss/progress"; 26 | @import "~bootstrap/scss/media"; 27 | @import "~bootstrap/scss/list-group"; 28 | @import "~bootstrap/scss/close"; 29 | @import "~bootstrap/scss/toasts"; 30 | @import "~bootstrap/scss/modal"; 31 | @import "~bootstrap/scss/tooltip"; 32 | @import "~bootstrap/scss/popover"; 33 | @import "~bootstrap/scss/carousel"; 34 | @import "~bootstrap/scss/spinners"; 35 | @import "~bootstrap/scss/utilities"; 36 | @import "~bootstrap/scss/print"; -------------------------------------------------------------------------------- /playground/scss/variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $primary: #42b983; 3 | $body-color: #304455; 4 | $info: #73abfe; 5 | $gray-100: #f6f6f6; 6 | $text-muted: #4e6e8e; 7 | $gray-900: #273849; 8 | $dark: #273849; 9 | 10 | @import "~bootstrap/scss/functions"; 11 | @import "~bootstrap/scss/variables"; 12 | @import "~bootstrap/scss/mixins"; 13 | 14 | $navbar-dark-color: rgba($white, .7); -------------------------------------------------------------------------------- /playground/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | export const store = createStore({ 4 | state() { 5 | return { 6 | // Define state here... 7 | } 8 | }, 9 | mutations: {}, 10 | actions: {}, 11 | modules: {}, 12 | }) 13 | -------------------------------------------------------------------------------- /playground/views/CompositionApi.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 124 | -------------------------------------------------------------------------------- /playground/views/OptionsApi.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 125 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | import prettierConfig from '@kouts/eslint-config/prettier' 2 | 3 | export default prettierConfig 4 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | branches: [ 3 | 'master', 4 | { 5 | name: 'beta', 6 | prerelease: true, 7 | }, 8 | { 9 | name: 'next', 10 | channel: 'next', 11 | }, 12 | ], 13 | plugins: [ 14 | '@semantic-release/commit-analyzer', 15 | '@semantic-release/release-notes-generator', 16 | [ 17 | '@semantic-release/changelog', 18 | { 19 | changelogFile: 'CHANGELOG.md', 20 | }, 21 | ], 22 | '@semantic-release/npm', 23 | '@semantic-release/github', 24 | [ 25 | '@semantic-release/git', 26 | { 27 | assets: ['CHANGELOG.md', 'dist', 'coverage'], 28 | // eslint-disable-next-line no-template-curly-in-string 29 | message: 'chore(release): set `package.json` to ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 30 | }, 31 | ], 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /src/Dataset.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 197 | -------------------------------------------------------------------------------- /src/DatasetInfo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | -------------------------------------------------------------------------------- /src/DatasetItem.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 40 | -------------------------------------------------------------------------------- /src/DatasetPager.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 65 | -------------------------------------------------------------------------------- /src/DatasetSearch.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 42 | -------------------------------------------------------------------------------- /src/DatasetShow.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 52 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | const MORE_PAGES = '...' 2 | 3 | function debounce(func, wait, immediate) { 4 | let timeout 5 | 6 | return function () { 7 | const context = this 8 | const args = arguments 9 | 10 | clearTimeout(timeout) 11 | if (immediate && !timeout) { 12 | func.apply(context, args) 13 | } 14 | timeout = setTimeout(function () { 15 | timeout = null 16 | if (!immediate) { 17 | func.apply(context, args) 18 | } 19 | }, wait) 20 | } 21 | } 22 | 23 | // https://jsperf.com/object-empty-ch/1 24 | function isEmptyObject(obj) { 25 | // eslint-disable-next-line no-unreachable-loop 26 | for (const key in obj) { 27 | return false 28 | } 29 | 30 | return true 31 | } 32 | 33 | function createPagingRange(nrOfPages, currentPage) { 34 | const delta = 2 35 | const range = [] 36 | const rangeWithDots = [] 37 | let length 38 | 39 | range.push(1) 40 | 41 | if (nrOfPages <= 1) { 42 | return range 43 | } 44 | 45 | for (let i = currentPage - delta; i <= currentPage + delta; i++) { 46 | if (i < nrOfPages && i > 1) { 47 | range.push(i) 48 | } 49 | } 50 | range.push(nrOfPages) 51 | 52 | for (let i = 0; i < range.length; i++) { 53 | if (length) { 54 | if (range[i] - length === 2) { 55 | rangeWithDots.push(length + 1) 56 | } else if (range[i] - length !== 1) { 57 | rangeWithDots.push(MORE_PAGES) 58 | } 59 | } 60 | rangeWithDots.push(range[i]) 61 | length = range[i] 62 | } 63 | 64 | return rangeWithDots 65 | } 66 | 67 | function fieldSorter(fields, dsSortAs = {}) { 68 | const dir = [] 69 | let i 70 | const length = fields.length 71 | 72 | fields = fields.map(function (o, i) { 73 | if (o[0] === '-') { 74 | dir[i] = -1 75 | o = o.substring(1) 76 | } else { 77 | dir[i] = 1 78 | } 79 | 80 | return o 81 | }) 82 | 83 | return function (a, b) { 84 | for (i = 0; i < length; i++) { 85 | const o = fields[i] 86 | const aVal = dsSortAs[o] ? dsSortAs[o](a.value[o]) : a.value[o] 87 | const bVal = dsSortAs[o] ? dsSortAs[o](b.value[o]) : b.value[o] 88 | 89 | if (aVal > bVal) { 90 | return dir[i] 91 | } 92 | if (aVal < bVal) { 93 | return -dir[i] 94 | } 95 | } 96 | 97 | return 0 98 | } 99 | } 100 | 101 | function fieldFilter(items, filterFields) { 102 | // Filter it by field 103 | for (const filterKey in filterFields) { 104 | // console.log(filterKey + ' -> ' + filterFields[filterKey]); 105 | items = items.filter(function (item) { 106 | const itemValue = item.value 107 | 108 | for (const itemKey in itemValue) { 109 | if (itemKey === filterKey) { 110 | if (typeof filterFields[filterKey] === 'function') { 111 | return filterFields[filterKey](itemValue[itemKey], itemValue) 112 | } 113 | if (filterFields[filterKey] === '') { 114 | return true 115 | } 116 | if (itemValue[itemKey] === filterFields[filterKey]) { 117 | return true 118 | } 119 | } 120 | } 121 | 122 | return false 123 | }) 124 | } 125 | 126 | return items 127 | } 128 | 129 | // Search method that also takes into account transformations needed 130 | function findAny(dsSearchIn, dsSearchAs, rowData, str) { 131 | // Convert the search string to lower case 132 | str = String(str).toLowerCase() 133 | for (const key in rowData) { 134 | if (dsSearchIn.length === 0 || dsSearchIn.indexOf(key) !== -1) { 135 | const value = String(rowData[key]).toLowerCase() 136 | 137 | for (const field in dsSearchAs) { 138 | if (field === key) { 139 | // Found key in dsSearchAs so we pass the value and the search string to a search function 140 | // that returns true/false and we return that if true. 141 | /* Check if dsSearchAs is a function (passed from the template) */ 142 | if (typeof dsSearchAs[field] === 'function') { 143 | const res = dsSearchAs[field](value, str, rowData) 144 | 145 | if (res === true) { 146 | return res 147 | } 148 | } 149 | } 150 | } 151 | // If it doesn't return from above we perform a simple search 152 | if (value.indexOf(str) >= 0) { 153 | return true 154 | } 155 | } 156 | } 157 | 158 | return false 159 | } 160 | 161 | export { createPagingRange, debounce, fieldFilter, fieldSorter, findAny, isEmptyObject, MORE_PAGES } 162 | -------------------------------------------------------------------------------- /src/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | show: 'Show', 3 | entries: 'entries', 4 | previous: 'Previous', 5 | next: 'Next', 6 | showing: 'Showing', 7 | showingTo: 'to', 8 | showingOf: 'of', 9 | showingEntries: 'entries', 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Dataset from './Dataset.vue' 2 | import DatasetInfo from './DatasetInfo.vue' 3 | import DatasetItem from './DatasetItem.vue' 4 | import DatasetPager from './DatasetPager.vue' 5 | import DatasetSearch from './DatasetSearch.vue' 6 | import DatasetShow from './DatasetShow.vue' 7 | 8 | export { Dataset, DatasetInfo, DatasetItem, DatasetPager, DatasetSearch, DatasetShow } 9 | -------------------------------------------------------------------------------- /tests/unit/Dataset.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { nextTick, ref } from 'vue' 3 | import Dataset from '@/Dataset.vue' 4 | import DatasetInfo from '@/DatasetInfo.vue' 5 | import DatasetItem from '@/DatasetItem.vue' 6 | import DatasetPager from '@/DatasetPager.vue' 7 | import DatasetSearch from '@/DatasetSearch.vue' 8 | import DatasetShow from '@/DatasetShow.vue' 9 | import users from '../../example-data/users.json' 10 | import { clone, waitNT } from '../../tests/utils.js' 11 | 12 | const createWrapper = (props = {}) => { 13 | return mount(Dataset, { 14 | slots: { 15 | default: ` 16 | 17 | 18 | 19 | 22 | 27 | 28 | 29 | 30 | `, 31 | }, 32 | props, 33 | global: { 34 | components: { 35 | DatasetShow, 36 | DatasetSearch, 37 | DatasetInfo, 38 | DatasetItem, 39 | DatasetPager, 40 | }, 41 | }, 42 | }) 43 | } 44 | 45 | describe('Dataset', () => { 46 | it('renders the dataset container div', () => { 47 | const wrapper = createWrapper({ dsData: users }) 48 | const div = wrapper.find('div') 49 | 50 | expect(div.exists()).toBe(true) 51 | }) 52 | 53 | it('has the correct pagination values', async () => { 54 | const wrapper = createWrapper({ dsData: users }) 55 | const pagination = wrapper.findComponent(DatasetPager) 56 | const lis = pagination.findAll('ul > li') 57 | const arr = lis.map((li) => li.text()) 58 | 59 | expect(arr).toEqual(['Previous', '1', '2', '3', '...', '500', 'Next']) 60 | }) 61 | 62 | it('resets the active page when data changes', async () => { 63 | const wrapper = createWrapper({ dsData: users }) 64 | const newUsers = clone(users).slice(0, 200) 65 | 66 | await wrapper.setProps({ dsData: newUsers }) 67 | expect(wrapper.findAll('li.page-item')[1].classes()).toContain('active') 68 | }) 69 | 70 | it('correctly calculates the number of pages', async () => { 71 | const wrapper = createWrapper({ dsData: users }) 72 | const newUsers = clone(users).slice(0, 301) 73 | 74 | await wrapper.setProps({ dsData: newUsers }) 75 | expect(wrapper.vm.dsPagecount).toBe(31) 76 | }) 77 | 78 | it('correctly filters the search data', async () => { 79 | const wrapper = createWrapper({ dsData: users }) 80 | 81 | await wrapper.vm.search('tristique.net') 82 | expect(wrapper.vm.dsRows.length).toBe(4) 83 | }) 84 | 85 | it('displays items depending on show entries', async () => { 86 | const wrapper = createWrapper({ dsData: users }) 87 | 88 | await wrapper.vm.showEntries(5) 89 | expect(wrapper.vm.dsRows.length).toBe(5) 90 | await wrapper.vm.showEntries(25) 91 | expect(wrapper.vm.dsRows.length).toBe(25) 92 | }) 93 | 94 | it('sets the active page to the last one if previous pages number was higher than the current one', async () => { 95 | const wrapper = createWrapper({ dsData: users }) 96 | 97 | await wrapper.vm.showEntries(5) 98 | await wrapper.vm.setActive(1000) 99 | await wrapper.vm.showEntries(100) 100 | await waitNT(wrapper.vm) 101 | expect(wrapper.vm.dsPage).toBe(50) 102 | }) 103 | 104 | it('correctly sets the active page', async () => { 105 | const wrapper = createWrapper({ dsData: users }) 106 | 107 | await wrapper 108 | .findAll('a') 109 | .filter((node) => node.text().match(/Next/))[0] 110 | .trigger('click') 111 | expect(wrapper.findAll('li.page-item')[2].classes()).toContain('active') 112 | }) 113 | 114 | it('sorts the dataset using the dsSortBy prop', async () => { 115 | const wrapper = createWrapper({ dsData: users }) 116 | 117 | await wrapper.setProps({ dsSortby: ['name'] }) 118 | expect(wrapper.find('.items > div').text()).toBe('873 - Aaron Brock') 119 | await wrapper.setProps({ dsSortby: ['-name'] }) 120 | expect(wrapper.find('.items > div').text()).toBe('2299 - Zorita Rose') 121 | }) 122 | 123 | it('filters the dataset based on the dsFilterFields prop', async () => { 124 | const wrapper = createWrapper({ dsData: users }) 125 | 126 | await wrapper.setProps({ dsFilterFields: { onlineStatus: 'Active' } }) 127 | expect(wrapper.find('.items > div').text()).toBe('1 - Whoopi David') 128 | await wrapper.setProps({ dsFilterFields: { onlineStatus: 'Away' } }) 129 | expect(wrapper.find('.items > div').text()).toBe('2 - Peter Mendez') 130 | }) 131 | 132 | it('searches the dataset only in properties defined in the dsSearchIn prop', async () => { 133 | const wrapper = createWrapper({ dsData: users }) 134 | 135 | await wrapper.setProps({ dsSearchIn: ['name', 'favoriteColor'] }) 136 | await wrapper.vm.search('tristique.net') 137 | expect(wrapper.find('.items > div').text()).toBe('No results found') 138 | await wrapper.vm.search('Burke Kelley') 139 | expect(wrapper.find('.items > div').text()).toBe('4188 - Burke Kelley') 140 | expect(wrapper.findAll('.items > div').length).toBe(1) 141 | }) 142 | 143 | it('updates data correctly when initialized with an empty array', async () => { 144 | const wrapper = createWrapper() 145 | const newUsers = clone(users).slice(0, 5) 146 | 147 | expect(wrapper.vm.dsData.length).toBe(0) 148 | await wrapper.setProps({ dsData: newUsers }) 149 | expect(wrapper.findAll('.items > div')[0].text()).toBe('0 - Harper Nolan') 150 | }) 151 | 152 | it('updates data correctly when initialized with a non-empty array', async () => { 153 | const users1 = clone(users).slice(0, 5) 154 | const users2 = clone(users).slice(6, 10) 155 | const wrapper = createWrapper({ dsData: users1 }) 156 | 157 | expect(wrapper.findAll('.items > div')[0].text()).toBe('0 - Harper Nolan') 158 | await wrapper.setProps({ dsData: users2 }) 159 | expect(wrapper.findAll('.items > div')[0].text()).toBe('0 - Aimee Stephens') 160 | }) 161 | 162 | it('emits an event when the filtered results update', async () => { 163 | const wrapper = createWrapper({ dsData: users }) 164 | 165 | expect(wrapper.emitted()['update:dsData'][0][0]).toHaveLength(5000) 166 | 167 | await wrapper.vm.search('tristique.net') 168 | 169 | expect(wrapper.emitted()['update:dsData'][1][0]).toHaveLength(4) 170 | }) 171 | 172 | it('updates when a new object is pushed or deleted', async () => { 173 | const Container = { 174 | template: ` 175 | 176 | 177 | 180 | 183 | 184 | 185 | `, 186 | components: { Dataset, DatasetItem }, 187 | setup() { 188 | const datasetUsers = ref([]) 189 | 190 | const addOne = () => { 191 | const oneUser = { name: 'George' } 192 | 193 | datasetUsers.value.unshift(oneUser) 194 | } 195 | 196 | const removeOne = () => { 197 | datasetUsers.value.splice(0, 1) 198 | } 199 | 200 | return { 201 | datasetUsers, 202 | addOne, 203 | removeOne, 204 | } 205 | }, 206 | } 207 | 208 | const wrapper = mount(Container) 209 | 210 | wrapper.vm.addOne() 211 | 212 | await nextTick() 213 | 214 | expect(wrapper.find('.name').text()).toBe('George') 215 | 216 | wrapper.vm.removeOne() 217 | 218 | await nextTick() 219 | 220 | expect(wrapper.find('p.text-center').text()).toBe('No results found') 221 | }) 222 | }) 223 | -------------------------------------------------------------------------------- /tests/unit/DatasetInfo.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { ref } from 'vue' 3 | import DatasetInfo from '@/DatasetInfo.vue' 4 | import datasetI18n from '@/i18n/en.js' 5 | 6 | describe('DatasetInfo', () => { 7 | let wrapper = null 8 | 9 | function wrapperWithProvide(provideOpts) { 10 | const wrapper = mount(DatasetInfo, { 11 | global: { 12 | provide: { 13 | datasetI18n: ref(datasetI18n), 14 | ...provideOpts, 15 | }, 16 | }, 17 | }) 18 | 19 | return wrapper 20 | } 21 | 22 | afterEach(function () { 23 | wrapper.unmount() 24 | }) 25 | 26 | it('renders a div element', () => { 27 | wrapper = wrapperWithProvide({ 28 | dsResultsNumber: ref(1), 29 | dsFrom: ref(0), 30 | dsTo: ref(0), 31 | }) 32 | const div = wrapper.find('div') 33 | 34 | expect(div.exists()).toBe(true) 35 | }) 36 | 37 | it('shows the correct number of the "showing" label when results number is zero', () => { 38 | wrapper = wrapperWithProvide({ 39 | dsResultsNumber: ref(0), 40 | dsFrom: ref(0), 41 | dsTo: ref(0), 42 | }) 43 | expect(wrapper.vm.showing).toBe(0) 44 | }) 45 | 46 | it('shows the correct number of "showing" label when results number is not zero', () => { 47 | wrapper = wrapperWithProvide({ 48 | dsResultsNumber: ref(10), 49 | dsFrom: ref(0), 50 | dsTo: ref(0), 51 | }) 52 | expect(wrapper.vm.showing).toBe(1) 53 | }) 54 | 55 | it('shows the correct number of the "to" label when to number is greater or equals to the results number', () => { 56 | wrapper = wrapperWithProvide({ 57 | dsResultsNumber: ref(3), 58 | dsFrom: ref(1), 59 | dsTo: ref(4), 60 | }) 61 | expect(wrapper.vm.showingTo).toBe(3) 62 | }) 63 | 64 | it('shows the correct number of the "to" label when to number less than the results number', () => { 65 | wrapper = wrapperWithProvide({ 66 | dsResultsNumber: ref(3), 67 | dsFrom: ref(1), 68 | dsTo: ref(2), 69 | }) 70 | expect(wrapper.vm.showingTo).toBe(2) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /tests/unit/DatasetItem.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { ref } from 'vue' 3 | import DatasetItem from '@/DatasetItem.vue' 4 | 5 | describe('DatasetItem', () => { 6 | let wrapper = null 7 | 8 | function wrapperWithProvide(provideOpts) { 9 | const wrapper = mount(DatasetItem, { 10 | global: { 11 | provide: { 12 | dsData: ref([ 13 | { 14 | age: 20, 15 | name: 'Jessie Casey', 16 | email: 'jessie.casey@flyboyz.biz', 17 | }, 18 | { 19 | age: 26, 20 | name: 'Solomon Stanley', 21 | email: 'solomon.stanley@tetak.net', 22 | }, 23 | ]), 24 | dsRows: ref([0, 1]), 25 | dsFrom: ref(0), 26 | dsTo: ref(10), 27 | ...provideOpts, 28 | }, 29 | }, 30 | slots: { 31 | default: ` 32 | 39 | `, 40 | noDataFound: `

No results found

`, 41 | }, 42 | }) 43 | 44 | return wrapper 45 | } 46 | 47 | afterEach(function () { 48 | wrapper.unmount() 49 | }) 50 | 51 | it('renders correctly', () => { 52 | wrapper = wrapperWithProvide() 53 | expect(wrapper.element).toMatchSnapshot() 54 | }) 55 | 56 | it('renders divs based on passed props', () => { 57 | wrapper = wrapperWithProvide() 58 | expect(wrapper.findAll('div.result').length).toBe(2) 59 | }) 60 | 61 | it('calculates the correct indexes', () => { 62 | wrapper = wrapperWithProvide() 63 | expect(wrapper.vm.indexes).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 64 | }) 65 | 66 | it('does not render any results when dsRows is empty', () => { 67 | wrapper = wrapperWithProvide({ 68 | dsRows: ref([]), 69 | }) 70 | expect(wrapper.findAll('div.result').length).toBe(0) 71 | }) 72 | 73 | it('renders the noDataFound slot when dsRows is empty', () => { 74 | wrapper = wrapperWithProvide({ 75 | dsRows: ref([]), 76 | }) 77 | expect(wrapper.find('p').text()).toBe('No results found') 78 | }) 79 | 80 | it('renders divs after data changed', () => { 81 | wrapper = wrapperWithProvide({ 82 | dsData: ref([ 83 | { 84 | age: 17, 85 | name: 'John Doe', 86 | email: 'john.doe@flyboyz.biz', 87 | }, 88 | ]), 89 | dsRows: ref([0]), 90 | }) 91 | expect(wrapper.findAll('div.result').length).toBe(1) 92 | expect(wrapper.element).toMatchSnapshot() 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /tests/unit/DatasetPager.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import { ref } from 'vue' 3 | import DatasetPager from '@/DatasetPager.vue' 4 | import datasetI18n from '@/i18n/en.js' 5 | 6 | const mockSetActive = vi.fn() 7 | 8 | const isButtonDisabled = function (el) { 9 | return el.tabIndex === -1 && el.hasAttribute('aria-disabled') === true 10 | } 11 | 12 | const isButtonEnabled = function (el) { 13 | return el.hasAttribute('tabIndex') === false && el.hasAttribute('aria-disabled') === false 14 | } 15 | 16 | describe('DatasetPager', () => { 17 | let wrapper = null 18 | 19 | function wrapperWithProvide(provideOpts = {}) { 20 | const wrapper = shallowMount(DatasetPager, { 21 | global: { 22 | provide: { 23 | datasetI18n: ref(datasetI18n), 24 | setActive: function (value) { 25 | mockSetActive(value) 26 | }, 27 | dsPages: ref([1, 2, 3]), 28 | dsPagecount: ref(0), 29 | dsPage: ref(1), 30 | ...provideOpts, 31 | }, 32 | }, 33 | }) 34 | 35 | return wrapper 36 | } 37 | 38 | afterEach(function () { 39 | wrapper.unmount() 40 | }) 41 | 42 | it('renders a ul element', () => { 43 | wrapper = wrapperWithProvide() 44 | const ul = wrapper.find('ul') 45 | 46 | expect(ul.exists()).toBe(true) 47 | }) 48 | 49 | it('disables the previous button on first page', () => { 50 | wrapper = wrapperWithProvide({ 51 | dsPage: ref(1), 52 | }) 53 | const previousButton = wrapper.findAll('a')[0].element 54 | 55 | expect(isButtonDisabled(previousButton)).toBe(true) 56 | }) 57 | 58 | it('disables the previous button when there is only one page', () => { 59 | wrapper = wrapperWithProvide({ 60 | dsPagecount: ref(1), 61 | }) 62 | const previousButton = wrapper.findAll('a')[0].element 63 | 64 | expect(isButtonDisabled(previousButton)).toBe(true) 65 | }) 66 | 67 | it('enables the previous button', () => { 68 | wrapper = wrapperWithProvide({ 69 | dsPage: ref(2), 70 | dsPagecount: ref(3), 71 | }) 72 | const previousButton = wrapper.findAll('a')[0].element 73 | 74 | expect(isButtonEnabled(previousButton)).toBe(true) 75 | }) 76 | 77 | it('disables the next button on last page', () => { 78 | wrapper = wrapperWithProvide({ 79 | dsPage: ref(4), 80 | dsPagecount: ref(4), 81 | }) 82 | const buttons = wrapper.findAll('a') 83 | const nextButton = buttons[buttons.length - 1].element 84 | 85 | expect(isButtonDisabled(nextButton)).toBe(true) 86 | }) 87 | 88 | it('disables the next button when there is only one page', () => { 89 | wrapper = wrapperWithProvide({ 90 | dsPagecount: ref(1), 91 | }) 92 | const buttons = wrapper.findAll('a') 93 | const nextButton = buttons[buttons.length - 1].element 94 | 95 | expect(isButtonDisabled(nextButton)).toBe(true) 96 | }) 97 | 98 | it('disables the previous and next buttons when there are no pages', () => { 99 | wrapper = wrapperWithProvide({ 100 | dsPagecount: ref(0), 101 | }) 102 | const buttons = wrapper.findAll('a') 103 | const previousButton = buttons[0].element 104 | const nextButton = buttons[buttons.length - 1].element 105 | 106 | expect(isButtonDisabled(previousButton)).toBe(true) 107 | expect(isButtonDisabled(nextButton)).toBe(true) 108 | }) 109 | 110 | it('enables the next button', () => { 111 | wrapper = wrapperWithProvide({ 112 | dsPage: ref(2), 113 | dsPagecount: ref(3), 114 | }) 115 | const buttons = wrapper.findAll('a') 116 | const nextButton = buttons[buttons.length - 1].element 117 | 118 | expect(isButtonEnabled(nextButton)).toBe(true) 119 | }) 120 | 121 | it('makes the normal page button active', () => { 122 | wrapper = wrapperWithProvide({ 123 | dsPage: ref(1), 124 | dsPagecount: ref(3), 125 | }) 126 | const li = wrapper.findAll('li')[1] 127 | 128 | expect(li.classes()).toContain('active') 129 | }) 130 | 131 | it('makes the ... page button disabled', () => { 132 | wrapper = wrapperWithProvide({ 133 | dsPages: ref([1, '...', 4, 5, 6]), 134 | dsPage: ref(6), 135 | dsPagecount: ref(6), 136 | }) 137 | const li = wrapper.findAll('li')[2] 138 | 139 | expect(li.classes()).toContain('disabled') 140 | expect(li.find('span').exists()).toBe(true) 141 | }) 142 | 143 | it('sends the correct active page number on previous button click', () => { 144 | mockSetActive.mockClear() 145 | wrapper = wrapperWithProvide({ 146 | dsPages: ref([1, '...', 4, 5, 6]), 147 | dsPage: ref(6), 148 | dsPagecount: ref(6), 149 | }) 150 | const previousButton = wrapper.findAll('a')[0] 151 | 152 | previousButton.trigger('click') 153 | expect(mockSetActive.mock.calls[0][0]).toBe(5) 154 | }) 155 | 156 | it('sends the correct active page number on next button click', () => { 157 | mockSetActive.mockClear() 158 | wrapper = wrapperWithProvide({ 159 | dsPages: ref([1, '...', 4, 5, 6]), 160 | dsPage: ref(5), 161 | dsPagecount: ref(6), 162 | }) 163 | const buttons = wrapper.findAll('a') 164 | const nextButton = buttons[buttons.length - 1] 165 | 166 | nextButton.trigger('click') 167 | expect(mockSetActive.mock.calls[0][0]).toBe(6) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /tests/unit/DatasetSearch.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import DatasetSearch from '@/DatasetSearch.vue' 3 | 4 | const mockSearch = vi.fn() 5 | 6 | describe('DatasetSearch', () => { 7 | beforeEach(() => { 8 | vi.useFakeTimers() 9 | }) 10 | 11 | afterEach(() => { 12 | vi.useRealTimers() 13 | }) 14 | 15 | const wrapper = shallowMount(DatasetSearch, { 16 | global: { 17 | provide: { 18 | search: function (value) { 19 | mockSearch(value) 20 | }, 21 | }, 22 | }, 23 | }) 24 | 25 | it('renders an input element', () => { 26 | const inputText = wrapper.find('input.form-control') 27 | 28 | expect(inputText.exists()).toBe(true) 29 | }) 30 | 31 | it('passes the correct value to the injected search method', async () => { 32 | mockSearch.mockClear() 33 | const inputText = wrapper.find('input.form-control') 34 | 35 | await inputText.setValue('test') 36 | 37 | vi.advanceTimersByTime(1000) 38 | 39 | expect(mockSearch.mock.calls[0][0]).toBe('test') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/unit/DatasetShow.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import { ref } from 'vue' 3 | import DatasetShow from '@/DatasetShow.vue' 4 | import datasetI18n from '@/i18n/en.js' 5 | 6 | const mockShowEntries = vi.fn() 7 | 8 | describe('DatasetShow', () => { 9 | const wrapper = shallowMount(DatasetShow, { 10 | global: { 11 | provide: { 12 | datasetI18n: ref(datasetI18n), 13 | showEntries: function (value) { 14 | mockShowEntries(value) 15 | }, 16 | }, 17 | }, 18 | }) 19 | 20 | it('renders a select element', () => { 21 | const select = wrapper.find('select.form-control') 22 | 23 | expect(select.exists()).toBe(true) 24 | }) 25 | 26 | it('passes the correct value to the injected search method', () => { 27 | mockShowEntries.mockClear() 28 | const select = wrapper.find('select.form-control') 29 | 30 | select.setValue('25') 31 | expect(mockShowEntries.mock.calls[0][0]).toBe(25) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/DatasetItem.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`DatasetItem > renders correctly 1`] = ` 4 |
5 | 6 | 7 |
10 |

11 | Jessie Casey 12 |

13 |

14 | 0 15 |

16 |

17 | 0 18 |

19 |
20 | 21 | 22 |
25 |

26 | Solomon Stanley 27 |

28 |

29 | 1 30 |

31 |

32 | 1 33 |

34 |
35 | 36 | 37 | 38 |
39 | `; 40 | 41 | exports[`DatasetItem > renders divs after data changed 1`] = ` 42 |
43 | 44 | 45 |
48 |

49 | John Doe 50 |

51 |

52 | 0 53 |

54 |

55 | 0 56 |

57 |
58 | 59 | 60 | 61 |
62 | `; 63 | -------------------------------------------------------------------------------- /tests/unit/helpers/createPagingRange.spec.js: -------------------------------------------------------------------------------- 1 | import { createPagingRange, MORE_PAGES } from '@/helpers' 2 | 3 | describe('createPagingRange', () => { 4 | it('should return the correct range for 3 pages when current page is 1', () => { 5 | // createPagingRange(nrOfPages, currentPage); 6 | const res = createPagingRange(3, 1) 7 | 8 | expect(res).toStrictEqual([1, 2, 3]) 9 | }) 10 | 11 | it('should return the correct range for 10 pages when current page is 9', () => { 12 | const res = createPagingRange(10, 9) 13 | 14 | expect(res).toStrictEqual([1, MORE_PAGES, 7, 8, 9, 10]) 15 | }) 16 | 17 | it('should return the correct range for 20 pages when current page is 3', () => { 18 | const res = createPagingRange(20, 3) 19 | 20 | expect(res).toStrictEqual([1, 2, 3, 4, 5, MORE_PAGES, 20]) 21 | }) 22 | 23 | it('should return the correct range for 10 pages when current page is 10', () => { 24 | const res = createPagingRange(10, 10) 25 | 26 | expect(res).toStrictEqual([1, MORE_PAGES, 8, 9, 10]) 27 | }) 28 | 29 | it('should return the correct range for 100 pages when current page is 5', () => { 30 | const res = createPagingRange(100, 5) 31 | 32 | expect(res).toStrictEqual([1, 2, 3, 4, 5, 6, 7, MORE_PAGES, 100]) 33 | }) 34 | 35 | it('should return the correct range for 100 pages when current page is 1', () => { 36 | const res = createPagingRange(100, 1) 37 | 38 | expect(res).toStrictEqual([1, 2, 3, MORE_PAGES, 100]) 39 | }) 40 | 41 | it('should return the correct range for 100 pages when current page is 8', () => { 42 | const res = createPagingRange(100, 8) 43 | 44 | expect(res).toStrictEqual([1, MORE_PAGES, 6, 7, 8, 9, 10, MORE_PAGES, 100]) 45 | }) 46 | 47 | it('should return the correct range for 100 pages when current page is 96', () => { 48 | const res = createPagingRange(100, 96) 49 | 50 | expect(res).toStrictEqual([1, MORE_PAGES, 94, 95, 96, 97, 98, 99, 100]) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/unit/helpers/debounce.spec.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/helpers' 2 | 3 | beforeEach(() => { 4 | vi.clearAllMocks() 5 | vi.useRealTimers() 6 | }) 7 | 8 | describe('debounce', () => { 9 | it('calls the function immediately if theres no wait', () => { 10 | const func = vi.fn() 11 | 12 | const debounced = debounce(func, 0, true) 13 | 14 | debounced() 15 | 16 | expect(func).toHaveBeenCalled() 17 | }) 18 | 19 | it('calls the function only once for a wait time period', () => { 20 | vi.useFakeTimers() 21 | const func = vi.fn() 22 | 23 | const debounced = debounce(func, 100, false) 24 | 25 | debounced() 26 | debounced() 27 | debounced() 28 | debounced() 29 | 30 | vi.advanceTimersByTime(300) 31 | 32 | expect(func).toHaveBeenCalledTimes(1) 33 | }) 34 | 35 | it('calls the function immediately only once for a wait time period', () => { 36 | vi.useFakeTimers() 37 | const func = vi.fn() 38 | 39 | const debounced = debounce(func, 100, true) 40 | 41 | debounced() 42 | debounced() 43 | debounced() 44 | debounced() 45 | 46 | vi.advanceTimersByTime(300) 47 | 48 | expect(func).toHaveBeenCalledTimes(1) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /tests/unit/helpers/fieldFilter.spec.js: -------------------------------------------------------------------------------- 1 | import { fieldFilter } from '@/helpers' 2 | import { data } from './testData.js' 3 | 4 | let filterData = [] 5 | 6 | beforeEach(() => { 7 | filterData = JSON.parse(JSON.stringify(data)) 8 | }) 9 | 10 | describe('fieldFilter', () => { 11 | it('filters the given array by first name "Gawain"', () => { 12 | const res = fieldFilter(filterData, { firstName: 'Gawain' }) 13 | 14 | expect(res.length).toBe(2) 15 | }) 16 | 17 | it('filters the given array by first names that start with "Bo"', () => { 18 | const filterFunction = (value) => value.startsWith('Bo') 19 | const res = fieldFilter(filterData, { firstName: filterFunction }) 20 | 21 | expect(res.length).toBe(1) 22 | }) 23 | 24 | it('includes items with empty filter key', () => { 25 | const res = fieldFilter(filterData, { firstName: '' }) 26 | 27 | expect(res.length).toBe(4) 28 | }) 29 | 30 | it('filters the given array by first names that start with "Ga" and last names that start with "Arr"', () => { 31 | const filterFunction = (value, row) => value.startsWith('Ga') && row.lastName.startsWith('Arr') 32 | const res = fieldFilter(filterData, { firstName: filterFunction }) 33 | 34 | expect(res.length).toBe(1) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /tests/unit/helpers/fieldSorter.spec.js: -------------------------------------------------------------------------------- 1 | import { fieldSorter } from '@/helpers' 2 | import { data } from './testData.js' 3 | 4 | function isoDateToDate(isoDate) { 5 | const isoDateParts = isoDate.split('-').map((o) => Number(o)) 6 | const res = new Date(new Date(isoDateParts[0], isoDateParts[1] - 1, isoDateParts[2])) 7 | 8 | return res 9 | } 10 | 11 | let sortData = [] 12 | 13 | beforeEach(() => { 14 | sortData = JSON.parse(JSON.stringify(data)) 15 | }) 16 | 17 | describe('fieldSorter', () => { 18 | it('sorts the given array by the firstName column - asc', () => { 19 | sortData.sort(fieldSorter(['firstName'])) 20 | expect(sortData[0].value.firstName).toBe('Bob') 21 | expect(sortData[3].value.firstName).toBe('Mile') 22 | }) 23 | 24 | it('sorts the given array by the firstName column - desc', () => { 25 | sortData.sort(fieldSorter(['-firstName'])) 26 | expect(sortData[0].value.firstName).toBe('Mile') 27 | expect(sortData[3].value.firstName).toBe('Bob') 28 | }) 29 | 30 | it('sorts the given array by the birthdate column - asc', () => { 31 | sortData = sortData.map((o) => { 32 | o.value.birthdate = isoDateToDate(o.value.birthdate) 33 | 34 | return o 35 | }) 36 | sortData.sort(fieldSorter(['birthdate'])) 37 | expect(sortData[0].value.birthdate.getFullYear()).toBe(1972) 38 | expect(sortData[3].value.birthdate.getFullYear()).toBe(2004) 39 | }) 40 | 41 | it('sorts the given array by the birthdate column - desc', () => { 42 | sortData = sortData.map((o) => { 43 | o.value.birthdate = isoDateToDate(o.value.birthdate) 44 | 45 | return o 46 | }) 47 | sortData.sort(fieldSorter(['-birthdate'])) 48 | expect(sortData[0].value.birthdate.getFullYear()).toBe(2004) 49 | expect(sortData[3].value.birthdate.getFullYear()).toBe(1972) 50 | }) 51 | 52 | it('sorts the given array by the balance column - asc', () => { 53 | sortData.sort(fieldSorter(['balance'])) 54 | expect(sortData[0].value.balance).toBe(3855) 55 | expect(sortData[3].value.balance).toBe(64949) 56 | }) 57 | 58 | it('sorts the given array by the balance column - desc', () => { 59 | sortData.sort(fieldSorter(['-balance'])) 60 | expect(sortData[0].value.balance).toBe(64949) 61 | expect(sortData[3].value.balance).toBe(3855) 62 | }) 63 | 64 | it('sorts the given array by first name asc and last name asc', () => { 65 | sortData.sort(fieldSorter(['firstName', 'lastName'])) 66 | expect(sortData[1].value.lastName).toBe('Arraway') 67 | expect(sortData[2].value.lastName).toBe('Bumpsty') 68 | }) 69 | 70 | it('sorts the given array by first name asc and last name desc', () => { 71 | sortData.sort(fieldSorter(['firstName', '-lastName'])) 72 | expect(sortData[1].value.lastName).toBe('Bumpsty') 73 | expect(sortData[2].value.lastName).toBe('Arraway') 74 | }) 75 | 76 | it('sorts the given array by the birthdate column using sortAs - asc', () => { 77 | sortData.sort(fieldSorter(['birthdate'], { birthdate: isoDateToDate })) 78 | expect(sortData[0].value.birthdate).toBe('1972-09-29') 79 | expect(sortData[3].value.birthdate).toBe('2004-02-11') 80 | }) 81 | 82 | it('sorts the given array by the birthdate column using sortAs - desc', () => { 83 | sortData.sort(fieldSorter(['-birthdate'], { birthdate: isoDateToDate })) 84 | expect(sortData[0].value.birthdate).toBe('2004-02-11') 85 | expect(sortData[3].value.birthdate).toBe('1972-09-29') 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /tests/unit/helpers/findAny.spec.js: -------------------------------------------------------------------------------- 1 | import { findAny } from '@/helpers' 2 | import { data } from './testData.js' 3 | 4 | let findData = [] 5 | 6 | beforeEach(() => { 7 | findData = JSON.parse(JSON.stringify(data)) 8 | }) 9 | 10 | describe('findAny', () => { 11 | it('finds string inside a designated object property', () => { 12 | const res = findAny(['email'], undefined, findData[0].value, 'discuz') 13 | 14 | expect(res).toBe(true) 15 | }) 16 | 17 | it('does not find string in non-designated object property', () => { 18 | const res = findAny(['firstName', 'lastName'], undefined, findData[0].value, 'discuz') 19 | 20 | expect(res).toBe(false) 21 | }) 22 | 23 | it('does find number when search value is a string', () => { 24 | const res = findAny(['balance'], undefined, findData[0].value, '39317') 25 | 26 | expect(res).toBe(true) 27 | }) 28 | 29 | it('does find number when search value is a number', () => { 30 | const res = findAny(['balance'], undefined, findData[0].value, 39317) 31 | 32 | expect(res).toBe(true) 33 | }) 34 | 35 | it('uses the predicate function', () => { 36 | const searchAs = { 37 | birthdate: vi.fn((value, searchStr, rowData) => { 38 | const parts = searchStr.split('.') 39 | const isoDate = `${parts[2]}-${parts[1]}-${parts[0]}` 40 | 41 | return isoDate === value 42 | }), 43 | } 44 | const res = findAny(['birthdate'], searchAs, findData[0].value, '09.03.1980') 45 | 46 | expect(res).toBe(true) 47 | expect(searchAs.birthdate).toHaveBeenCalledWith('1980-03-09', '09.03.1980', findData[0].value) 48 | }) 49 | 50 | it(`returns false in case there's no match using the predicate function`, () => { 51 | const searchAs = { 52 | firstName: vi.fn((value, searchStr, rowData) => { 53 | return searchStr.toLowerCase() === value.toLowerCase() 54 | }), 55 | } 56 | const res = findAny(['firstName', 'balance'], searchAs, findData[0].value, 'Bob') 57 | 58 | expect(res).toBe(false) 59 | }) 60 | 61 | it(`performs a simple search in case searchAs does not contain a function`, () => { 62 | const searchAs = { 63 | firstName: 'string', 64 | balance: 20, 65 | } 66 | const res = findAny(['firstName', 'balance'], searchAs, findData[0].value, 'Gawain') 67 | 68 | expect(res).toBe(true) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /tests/unit/helpers/isEmptyObject.spec.js: -------------------------------------------------------------------------------- 1 | import { isEmptyObject } from '@/helpers' 2 | 3 | describe('isEmptyObject', () => { 4 | it('should return true when an object is empty', () => { 5 | const object = {} 6 | const res = isEmptyObject(object) 7 | 8 | expect(res).toBe(true) 9 | }) 10 | 11 | it('should return false when an object is not empty', () => { 12 | const object = { 13 | test: 'value', 14 | } 15 | const res = isEmptyObject(object) 16 | 17 | expect(res).toBe(false) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tests/unit/helpers/testData.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | index: 0, 4 | value: { 5 | balance: 39317, 6 | birthdate: '1980-03-09', 7 | name: 'Gawain Bumpsty', 8 | firstName: 'Gawain', 9 | lastName: 'Bumpsty', 10 | company: 'Quaxo', 11 | email: 'gcumpsty0@discuz.net', 12 | phone: '985-353-3917', 13 | address: '4669 Northland Circle', 14 | favoriteAnimal: 'Deer, roe', 15 | }, 16 | }, 17 | { 18 | index: 1, 19 | value: { 20 | balance: 64949, 21 | birthdate: '2004-02-11', 22 | name: 'Mile Fulloway', 23 | firstName: 'Mile', 24 | lastName: 'Fulloway', 25 | company: 'Flashspan', 26 | email: 'mfulloway2@whitehouse.gov', 27 | phone: '225-853-2332', 28 | address: '683 Lakeland Crossing', 29 | favoriteAnimal: 'Starling, cape', 30 | }, 31 | }, 32 | { 33 | index: 2, 34 | value: { 35 | balance: 3855, 36 | birthdate: '1974-05-14', 37 | name: 'Bob Kubiak', 38 | firstName: 'Bob', 39 | lastName: 'Kubiak', 40 | company: 'Youspan', 41 | email: 'gkubiak3@tripadvisor.com', 42 | phone: '235-467-8727', 43 | address: '157 Fisk Center', 44 | favoriteAnimal: 'Fairy penguin', 45 | }, 46 | }, 47 | { 48 | index: 3, 49 | value: { 50 | balance: 21192, 51 | birthdate: '1972-09-29', 52 | name: 'Gawain Arraway', 53 | firstName: 'Gawain', 54 | lastName: 'Arraway', 55 | company: 'Shuffledrive', 56 | email: 'eharraway1@wordpress.com', 57 | phone: '217-861-5367', 58 | address: '1 Meadow Valley Circle', 59 | favoriteAnimal: 'African clawless otter', 60 | }, 61 | }, 62 | ] 63 | 64 | export { data } 65 | -------------------------------------------------------------------------------- /tests/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | import Dataset from '@/Dataset.vue' 2 | import DatasetInfo from '@/DatasetInfo.vue' 3 | import DatasetItem from '@/DatasetItem.vue' 4 | import DatasetPager from '@/DatasetPager.vue' 5 | import DatasetSearch from '@/DatasetSearch.vue' 6 | import DatasetShow from '@/DatasetShow.vue' 7 | import * as index from '@/index.js' 8 | 9 | describe('index.js exports', () => { 10 | it('should export Dataset', () => { 11 | expect(index.Dataset).toBe(Dataset) 12 | }) 13 | 14 | it('should export DatasetInfo', () => { 15 | expect(index.DatasetInfo).toBe(DatasetInfo) 16 | }) 17 | 18 | it('should export DatasetItem', () => { 19 | expect(index.DatasetItem).toBe(DatasetItem) 20 | }) 21 | 22 | it('should export DatasetPager', () => { 23 | expect(index.DatasetPager).toBe(DatasetPager) 24 | }) 25 | 26 | it('should export DatasetSearch', () => { 27 | expect(index.DatasetSearch).toBe(DatasetSearch) 28 | }) 29 | 30 | it('should export DatasetShow', () => { 31 | expect(index.DatasetShow).toBe(DatasetShow) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 1 | export const clone = (obj) => JSON.parse(JSON.stringify(obj || {})) 2 | export const waitNT = (ctx) => new Promise((resolve) => ctx.$nextTick(resolve)) 3 | export const waitRAF = () => new Promise((resolve) => requestAnimationFrame(resolve)) 4 | export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 5 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | import { createHtmlPlugin } from 'vite-plugin-html' 5 | 6 | export default defineConfig({ 7 | publicDir: 'public-vite', 8 | plugins: [ 9 | vue(), 10 | createHtmlPlugin({ 11 | minify: true, 12 | inject: { 13 | data: { 14 | title: 'vue-dataset playground', 15 | description: 'Playground for vue-dataset Vue.js 3', 16 | }, 17 | }, 18 | }), 19 | ], 20 | resolve: { 21 | alias: { 22 | '@': resolve(__dirname, './src'), 23 | '@playground': resolve(__dirname, './playground'), 24 | '@root': resolve(__dirname, './'), 25 | '~bootstrap': 'bootstrap', 26 | }, 27 | }, 28 | rollupInputOptions: { 29 | input: resolve(__dirname, '/playground/main.js'), // custom main 30 | }, 31 | css: { 32 | preprocessorOptions: { 33 | scss: { 34 | additionalData: `@import "./playground/scss/variables";`, 35 | }, 36 | }, 37 | }, 38 | build: { 39 | lib: { 40 | entry: resolve(__dirname, 'src/index.js'), 41 | name: 'VueDataset', 42 | fileName: (format) => `vue-dataset.${format}.js`, 43 | }, 44 | rollupOptions: { 45 | external: ['vue'], 46 | output: { 47 | globals: { 48 | vue: 'Vue', 49 | }, 50 | }, 51 | }, 52 | }, 53 | test: { 54 | globals: true, 55 | // Clear the mocks call count before each test so that we don't have to call vi.clearAllMocks manually - https://vitest.dev/config/#clearmocks 56 | clearMocks: true, 57 | environment: 'jsdom', 58 | reporters: ['default'], 59 | coverage: { 60 | reporter: ['text', ['json-summary', { file: 'coverage-summary.json' }]], 61 | include: ['src/**'], 62 | }, 63 | }, 64 | }) 65 | --------------------------------------------------------------------------------