├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── babel.config.js ├── build └── rollup.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── release.config.js ├── src ├── useDatatableUrlSync.ts └── utils │ ├── VDUSTypes.ts │ ├── helpers.ts │ └── listPaginatedTools.ts ├── tests └── unit │ └── helpers.spec.ts ├── tsconfig.json ├── vue.config.js ├── vue2-example ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── logo.png │ │ └── logo.svg │ ├── components │ │ ├── HelloWorld.vue │ │ └── data.js │ ├── main.ts │ ├── plugins │ │ └── vuetify.js │ ├── router │ │ └── index.js │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ └── views │ │ ├── About.vue │ │ └── Home.vue ├── tsconfig.json └── vue.config.js ├── vue2.7-example ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── logo.png │ │ └── logo.svg │ ├── components │ │ ├── HelloWorld.vue │ │ └── data.js │ ├── main.ts │ ├── plugins │ │ └── vuetify.js │ ├── router │ │ ├── composable.js │ │ └── index.js │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ └── views │ │ ├── About.vue │ │ └── Home.vue ├── tsconfig.json └── vue.config.js └── vue3-example ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── README.md ├── components.d.ts ├── env.d.ts ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── assets │ ├── data │ │ └── data.js │ ├── logo.png │ └── logo.svg ├── components │ ├── BaseOrdering.vue │ ├── README.md │ ├── SimpleDatatable.vue │ └── VuetifyDatatable.vue ├── main.ts ├── pages │ ├── README.md │ ├── index.vue │ ├── multiple-vuetify.vue │ └── simple.vue ├── plugins │ ├── README.md │ ├── index.ts │ └── vuetify.ts ├── router │ └── index.ts └── styles │ ├── README.md │ └── settings.scss ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── typed-router.d.ts ├── types └── vue-datatable-url-sync.d.ts └── vite.config.mts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true 6 | }, 7 | 8 | extends: [ 9 | //"plugin:vue/vue3-essential", 10 | "plugin:vue/vue3-recommended", 11 | "eslint:recommended", 12 | "@vue/typescript" 13 | ], 14 | 15 | parserOptions: { 16 | parser: '@typescript-eslint/parser' 17 | }, 18 | 19 | plugins: [ 20 | "typescript" 21 | ], 22 | 23 | rules: { 24 | 'vue/no-unused-vars': 'off', 25 | "@typescript-eslint/no-unused-vars": [ 26 | "error" 27 | ] 28 | }, 29 | 30 | overrides: [ 31 | { 32 | files: [ 33 | '**/__tests__/*.{j,t}s?(x)', 34 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 35 | ], 36 | env: { 37 | jest: true 38 | } 39 | } 40 | ] 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, next ] 6 | pull_request: 7 | branches: [ main, next ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '20.11.1' 20 | 21 | - name: Install dependencies 22 | run: npm install 23 | 24 | - name: Run unit tests 25 | run: npm run test:unit 26 | 27 | release: 28 | # This job will only run on pushes to main or next, and after the test job completes successfully. 29 | if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next' 30 | runs-on: ubuntu-latest 31 | needs: test 32 | 33 | env: 34 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Set up Node.js 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: '20.11.1' 45 | 46 | - name: Install dependencies 47 | run: npm install 48 | 49 | - name: Build project 50 | run: npm run build 51 | 52 | - name: Semantic release 53 | run: npx semantic-release 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.7: 2 | - Add updateFormFunction to user configurations 3 | 4 | ### 1.0.6: 5 | - Modify type and add doc to readme -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 socotec-io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-datatable-url-sync 2 | Synchronize your datatable filter, ordering and options with the query params. In Vue2 and Vue3 with the composition api ! 3 | 4 | # Goal and explanation 5 | 6 | Have you ever as an user filtered or navigated in a datable, open an item of the datable, go back to the datable and see you loose all your filter and pagination and have to rewrite them again ? 7 | If yes was it frustrating ? 8 | If yes then this lib is here to help your user never feel that again. Even if you reload the page or share the link with others user. 9 | 10 | 11 | Now there is already some other lib even in pure Vanilla to sync the query parameters with a javascript object. But you have to rewrite all the logic with the datatable options (page, pageSize, ordering) and even compare the difference between two state. 12 | 13 | By separating the filters and the datable options vue-datatable-url-sync automatise all the desired behavior for working with datatable. 14 | 15 | Example of usage with a vuetify datatable (It can work with any datatable): 16 | 17 | https://user-images.githubusercontent.com/11883225/114200483-8b1aee80-9955-11eb-9bd5-e16762476baf.mp4 18 | 19 | 20 | # Installation: 21 | 22 | ``` 23 | npm install vue-datatable-url-sync 24 | ``` 25 | 26 | # Usage 27 | 28 | [For full example see this file for vue 3](https://github.com/socotecio/vue-datatable-url-sync/blob/main/vue3-example/src/components/HelloWorld.vue) 29 | 30 | [For full example see this file for vue 2](https://github.com/socotecio/vue-datatable-url-sync/blob/main/vue2-example/src/components/HelloWorld.vue) 31 | ### 1 - Import the lib 32 | 33 | ```js 34 | import useDatatableUrlSync from 'vue-datatable-url-sync'; 35 | ``` 36 | 37 | ### 2 - Define your setup function and the data you use for your datatable 38 | 39 | ```js 40 | setup (props, ctx) { 41 | const form = ref({ 42 | search: "" 43 | }) 44 | const options = ref({ 45 | page: 1, 46 | page_size: 10, 47 | ordering: [] 48 | }) 49 | const items = ref([]) 50 | } 51 | ``` 52 | 53 | ### 3 - Define the function called each time the url query change (in the setup fucntion) 54 | 55 | ```js 56 | const fetchDatas = (queryParams, queryAsObject) => { 57 | console.log(queryParams, queryAsObject) 58 | } 59 | ``` 60 | 61 | ### 4 - Use it 62 | 63 | ```js 64 | // Vue 3 65 | useDatatableUrlSync(useRoute(), useRouter(), form, fetchDatas, options) 66 | // Vue 2 67 | useDatatableUrlSync(ctx.root.$route, ctx.root.$router, form, fetchDatas, options) 68 | ``` 69 | 70 | ### 5 Return your data to use it in the datatable 71 | 72 | ```js 73 | return { 74 | form, 75 | options, 76 | items 77 | } 78 | ``` 79 | 80 | 81 | # useDatatableUrlSync params 82 | 83 | Prototype: 84 | ```js 85 | function useDatatableUrlSync(route: any, router: any, form: GenericDictionnary, fetchDatas: Function, options: GenericDictionnary, formSchema?: VDUSFormSchema, initializeForm?: Function, configurations?: VDUSConfiguration) 86 | ``` 87 | 88 | | Params | Description | 89 | | --------- | ----------------------------------------------------------------- | 90 | | route | The route element from vue router. As it differe from Vue2 and Vue3 we can't import it automatically and need to have the instance in parameter | 91 | | router | The route instance from vue router. As it differe from Vue2 and Vue3 we can't import it automatically and need to have the instance in parameter | 92 | | form | A simple object used to filter the form. This object is synchronized with the url. When it change it reset the page attribute of the options variable | 93 | | fetchDatas | Callback function called when options or form changed or after a reload with the correct parameter to send to the backend for backend pagination or custom actions. This function take 2 parameter: queryParams, queryAsObject that are the data transformed by VDUS to match your backend criteria in string or object format. | 94 | | options | The options used for the datatable. It follow a strict pattern. See [VDUSDatatableOptions](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L15) type for more informations. If your server use other query identifier use formSchema to change their name before fetchDatas been called | 95 | | formSchema | Optional. The object that allow you to customize the defaut value, the type and the names send to the backend. See [VDUSFormSchema](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L11) type for the structure and [the documentation section](#formschema) to understand how to use it | 96 | | initializeForm | Optional. A callback function called at the component creation to allow developer to adapt behavior depending of the query params in the url if needed. Usefull if to value are non-compatible because user change it manually. | 97 | | configurations | Optional. Object that allow to personnalise the behavior of VDUS in specific case. See [VDUSConfiguration](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L21) type and [the documentation section](#configurations) to understand how to use it | 98 | 99 | 100 | # useDatatableUrlSync returned values 101 | 102 | | Key name | Description | 103 | | --------- | ----------------------------------------------------------------- | 104 | | loading | Boolean value to be able to wait in the template that the data is correctly setted from the url and the data retrieve or filtered in fetchDatas especially if your fetchDatas is asynchronous | 105 | | vuetifyOptions | The datatable options on the vuetify format to be able to use it directly in the template without the need to transform it | 106 | 107 | 108 | # Configurations 109 | 110 | Configurations object allow you to personnalize the behavior of vue-datatable-url-sync. All the configurations are optionals 111 | 112 | | Key name | Default | Description | 113 | | --------- | ------------ | ----------------------------------------------------------------- | 114 | | prefix | "" | Prefix all the params by a string only in the url to allow you have multiple instance of VDUS in the same html page | 115 | | debounceTime | 0 | Allow you to specify a debounce time before sending request to your server. This is usefull when you have only text field but can bring lag feeling when using checkbox or select. You can also use debounce directly in your component to personalize this behavior | 116 | | serveurDefaultPageSize | 10 | The default value for you backend pagination to be sure to send it if the default value in front is different that the one in your back | 117 | | extraQueryParams | {} | Put variable in the url and send them to your back even if not in your form | 118 | | updateFormFunction | null | Use a custom function to update form instead of direct assignation. Usefull when form is a props and you want to emit an event because form is not mutable. | 119 | 120 | # FormSchema 121 | 122 | The parameter formSchema allow you to adapt the default behavior of vue-datatable-url-sync for each parameter (for the form AND for the options). 123 | With it you can specify the type of the params, the default value and the query param name to send to the server. 124 | 125 | [See the type to understand better](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L11) 126 | 127 | Here is the description of the type for the configuration of each params ([ParamSchema](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L5)) 128 | 129 | | Key name | Description | 130 | | --------- | ----------------------------------------------------------------- | 131 | | name | The name to send to the backend server | 132 | | default | The default value. It prevent default value to show in the url | 133 | | type | The type of the params to cast it correctly into your form or your options | 134 | 135 | 136 | # VDUS Types 137 | 138 | | Type name | Description | 139 | | ----------- | ------------------------------------------------------------------- | 140 | | [GenericDictionnary](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L1) | Generic object that accept any key and any value | 141 | | [VDUSParamSchema](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L5) | Object describing how to handle query param when reading them from url or before sending them to the backend server | 142 | | [VDUSFormSchema](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L11) | Object describing how to handle the form and options object passed as parameter of useDatatableUrlSync | 143 | | [VDUSDatatableOptions](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L15) | Object describing the params accepted in the datatable options | 144 | | [VDUSConfiguration](https://github.com/socotecio/vue-datatable-url-sync/blob/main/src/utils/VDUSTypes.ts#L21) | Configuration object for vue datatable url sync | 145 | 146 | 147 | # Local dev 148 | 149 | You can use vue3-example to test local dev. You just have to go to vue3-example/src/components/HelloWorld.vue and uncomment the relative import of VDUS. 150 | Do not forget to launch `npm install` in both root directory AND vue3-example directory 151 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [['@babel/plugin-transform-runtime', { useESModules: true }]] 6 | } 7 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/camelcase */ 2 | /* eslint-disable @typescript-eslint/no-use-before-define */ 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | import path from 'path'; 5 | 6 | import ts from '@rollup/plugin-typescript'; 7 | import replace from '@rollup/plugin-replace'; 8 | import resolve from '@rollup/plugin-node-resolve'; 9 | import commonjs from '@rollup/plugin-commonjs'; 10 | import { babel } from '@rollup/plugin-babel'; 11 | 12 | const pkg = require('../package.json'); 13 | const name = pkg.name; 14 | 15 | function getAuthors(pkg) { 16 | const { contributors, author } = pkg; 17 | 18 | const authors = new Set(); 19 | if (contributors && contributors) 20 | contributors.forEach(contributor => { 21 | authors.add(contributor.name); 22 | }); 23 | if (author) authors.add(author.name); 24 | 25 | return Array.from(authors).join(', '); 26 | } 27 | 28 | const banner = `/*! 29 | * ${pkg.name} v${pkg.version} 30 | * (c) ${new Date().getFullYear()} ${getAuthors(pkg)} 31 | * @license MIT 32 | */`; 33 | 34 | const outputConfigs = { 35 | // each file name has the format: `dist/${name}.${format}.js` 36 | // format being a key of this object 37 | 'esm-bundler': { 38 | file: pkg.module, 39 | format: `es`, 40 | }, 41 | cjs: { 42 | file: pkg.main, 43 | format: `cjs`, 44 | }, 45 | 'global-vue-3': { 46 | file: pkg.unpkg.replace('VERSION', '3'), 47 | format: `iife`, 48 | }, 49 | 'global-vue-2': { 50 | file: pkg.unpkg.replace('VERSION', '2'), 51 | format: `iife`, 52 | }, 53 | esm: { 54 | file: pkg.browser, 55 | format: `es`, 56 | }, 57 | }; 58 | 59 | const allFormats = Object.keys(outputConfigs); 60 | const packageFormats = allFormats; 61 | const packageConfigs = packageFormats.map(format => 62 | createConfig(format, outputConfigs[format]), 63 | ); 64 | 65 | // only add the production ready if we are bundling the options 66 | packageFormats.forEach(format => { 67 | if (format === 'cjs') { 68 | packageConfigs.push(createProductionConfig(format)); 69 | } else if (format.startsWith('global')) { 70 | packageConfigs.push(createMinifiedConfig(format)); 71 | } 72 | }); 73 | 74 | export default packageConfigs; 75 | 76 | function createConfig(format, output, plugins = []) { 77 | if (!output) { 78 | console.log(require('chalk').yellow(`invalid format: "${format}"`)); 79 | process.exit(1); 80 | } 81 | 82 | output.sourcemap = !!process.env.SOURCE_MAP; 83 | output.banner = banner; 84 | output.externalLiveBindings = false; 85 | output.globals = { 'vue-demi': 'VueDemi' }; 86 | 87 | const isProductionBuild = /\.prod\.js$/.test(output.file); 88 | const isGlobalBuild = format.startsWith('global'); 89 | const isRawESMBuild = format === 'esm'; 90 | const isNodeBuild = format === 'cjs'; 91 | const isBundlerESMBuild = /esm-bundler/.test(format); 92 | 93 | if (isGlobalBuild) output.name = "VueDatatableUrlSync"; 94 | 95 | 96 | const tsPlugin = ts({ 97 | tsconfig: path.resolve(__dirname, '../tsconfig.json'), 98 | }); 99 | 100 | const external = ['vue-demi']; 101 | 102 | const nodePlugins = [resolve(), commonjs()]; 103 | 104 | return { 105 | //input: `src/useDatatableUrlSync.ts`, 106 | input: path.resolve(__dirname, '../src/useDatatableUrlSync.ts'), 107 | // Global and Browser ESM builds inlines everything so that they can be 108 | // used alone. 109 | external, 110 | plugins: [ 111 | tsPlugin, 112 | createReplacePlugin( 113 | isProductionBuild, 114 | isBundlerESMBuild, 115 | // isBrowserBuild? 116 | isGlobalBuild || isRawESMBuild || isBundlerESMBuild, 117 | isGlobalBuild, 118 | isNodeBuild, 119 | ), 120 | ...nodePlugins, 121 | // Babel plugin need to be placed after commonjs plugin 122 | babel({ 123 | exclude: 'node_modules/**', 124 | extensions: ['.js', '.ts'], 125 | babelHelpers: 'runtime' 126 | }), 127 | ...plugins, 128 | ], 129 | output, 130 | // onwarn: (msg, warn) => { 131 | // if (!/Circular/.test(msg)) { 132 | // warn(msg) 133 | // } 134 | // }, 135 | }; 136 | } 137 | 138 | function createReplacePlugin( 139 | isProduction, 140 | isBundlerESMBuild, 141 | isBrowserBuild, 142 | isGlobalBuild, 143 | isNodeBuild, 144 | ) { 145 | const replacements = { 146 | __COMMIT__: `"${process.env.COMMIT}"`, 147 | __VERSION__: `"${pkg.version}"`, 148 | __DEV__: isBundlerESMBuild 149 | ? // preserve to be handled by bundlers 150 | `(process.env.NODE_ENV !== 'production')` 151 | : // hard coded dev/prod builds 152 | !isProduction, 153 | // this is only used during tests 154 | __TEST__: isBundlerESMBuild ? `(process.env.NODE_ENV === 'test')` : false, 155 | // If the build is expected to run directly in the browser (global / esm builds) 156 | __BROWSER__: isBrowserBuild, 157 | // is targeting bundlers? 158 | __BUNDLER__: isBundlerESMBuild, 159 | __GLOBAL__: isGlobalBuild, 160 | // is targeting Node (SSR)? 161 | __NODE_JS__: isNodeBuild, 162 | preventAssignment: true 163 | }; 164 | // allow inline overrides like 165 | //__RUNTIME_COMPILE__=true yarn build 166 | Object.keys(replacements).forEach(key => { 167 | if (key in process.env) { 168 | replacements[key] = process.env[key]; 169 | } 170 | }); 171 | return replace(replacements); 172 | } 173 | 174 | function createProductionConfig(format) { 175 | return createConfig(format, { 176 | file: `dist/${name}.${format}.prod.js`, 177 | format: outputConfigs[format].format, 178 | }); 179 | } 180 | 181 | function createMinifiedConfig(format) { 182 | const terser = require('@rollup/plugin-terser'); 183 | return createConfig( 184 | format, 185 | { 186 | file: `dist/${name}.${format}.prod.js`, 187 | format: outputConfigs[format].format, 188 | }, 189 | [ 190 | terser({ 191 | module: /^esm/.test(format), 192 | compress: { 193 | ecma: 2015, 194 | pure_getters: true, 195 | }, 196 | }), 197 | ], 198 | ); 199 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-datatable-url-sync", 3 | "version": "0.1.0", 4 | "private": false, 5 | "author": "Adrien Montagu (https://github.com/amontagu)", 6 | "scripts": { 7 | "build": "rollup -c build/rollup.config.js", 8 | "test:unit": "vue-cli-service test:unit", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "main": "dist/vue-datatable-url-sync.cjs.js", 12 | "module": "dist/vue-datatable-url-sync.esm-bundler.js", 13 | "browser": "dist/vue-datatable-url-sync.esm.js", 14 | "unpkg": "dist/vue-datatable-url-sync-VERSION.global.js", 15 | "files": [ 16 | "dist/*", 17 | "src/*" 18 | ], 19 | "dependencies": { 20 | "lodash.clonedeep": "^4.5.0", 21 | "lodash.debounce": "^4.0.8", 22 | "lodash.isequal": "^4.5.0", 23 | "vue-demi": "^0.13.11" 24 | }, 25 | "devDependencies": { 26 | "@babel/plugin-transform-runtime": "^7.13.10", 27 | "@rollup/plugin-babel": "^5.3.0", 28 | "@rollup/plugin-commonjs": "^17.1.0", 29 | "@rollup/plugin-node-resolve": "^11.2.0", 30 | "@rollup/plugin-replace": "^2.4.1", 31 | "@rollup/plugin-terser": "^0.4.0", 32 | "@rollup/plugin-typescript": "^11.0.0", 33 | "@types/jest": "^27.0.0", 34 | "@types/lodash.clonedeep": "^4.5.6", 35 | "@types/lodash.debounce": "^4.0.6", 36 | "@types/lodash.isequal": "^4.5.5", 37 | "@typescript-eslint/eslint-plugin": "^5.4.0", 38 | "@typescript-eslint/parser": "^5.4.0", 39 | "@vue/cli-plugin-babel": "^5.0.8", 40 | "@vue/cli-plugin-eslint": "^5.0.8", 41 | "@vue/cli-plugin-typescript": "^5.0.8", 42 | "@vue/cli-plugin-unit-jest": "^5.0.8", 43 | "@vue/cli-service": "^5.0.8", 44 | "@vue/compiler-sfc": "^3.0.0", 45 | "@vue/eslint-config-typescript": "^9.1.0", 46 | "@vue/test-utils": "^2.3.2", 47 | "@vue/vue3-jest": "^27.0.0", 48 | "babel-eslint": "^10.1.0", 49 | "eslint": "^7.32.0", 50 | "eslint-plugin-typescript": "^0.14.0", 51 | "eslint-plugin-vue": "^8.0.3", 52 | "rollup": "^2.39.1", 53 | "rollup-plugin-vue": "^6.0.0", 54 | "ts-jest": "^27.1.5", 55 | "typescript": "~4.5.5", 56 | "vue": "^3.0.0", 57 | "vue-router": "^4.0.3" 58 | }, 59 | "peerDependencies": { 60 | "@vue/composition-api": "^1.0.0-rc.2", 61 | "vue": "^2.6.0 || >=3.0.0-rc.0", 62 | "vue-router": "^3.0.0 || ^4.0.0" 63 | }, 64 | "browserslist": [ 65 | "> 1%", 66 | "last 2 versions", 67 | "not dead" 68 | ], 69 | "jsdelivr": "dist/vue-datatable-url-sync.global.js", 70 | "peerDependenciesMeta": { 71 | "@vue/composition-api": { 72 | "optional": true 73 | } 74 | }, 75 | "types": "dist/vue-datatable-url-sync.d.ts" 76 | } 77 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ["main", "next"] 3 | } -------------------------------------------------------------------------------- /src/useDatatableUrlSync.ts: -------------------------------------------------------------------------------- 1 | import debounce from "lodash.debounce"; 2 | import { 3 | readFormAndOptionsFromLocalQuery, 4 | generateQueryFromObject, 5 | convertParamIfTypeInSchema, 6 | getRemovedKeyBetweenTwoObject, 7 | getDefaultValueForParam 8 | } from "./utils/listPaginatedTools"; 9 | import { 10 | getSortsArrayFromOrdering, 11 | getOrderingFromSortArray 12 | } from "./utils/helpers"; 13 | import cloneDeep from "lodash.clonedeep"; 14 | import isEqual from "lodash.isequal"; 15 | 16 | import { ref, watch, nextTick, computed, Ref } from 'vue-demi' 17 | import {GenericDictionnary, VDUSConfiguration, VuetifyOptions, VDUSFormSchema, VDUSDatatableOptions} from "./utils/VDUSTypes" 18 | 19 | /* 20 | DOC here on params and return value 21 | */ 22 | export default function useDatatableUrlSync(route: any, router: any, form: Ref, fetchDatas: Function, options: Ref, formSchema?: VDUSFormSchema, initializeForm?: Function, configurations?: VDUSConfiguration) { 23 | 24 | // ----------------------------- DEFAULTING PARAMS ------------------------------ 25 | configurations = { 26 | // use a prefix to differentiate possible same name if two component use the same mixin 27 | prefix: "", 28 | // Time of included debounce function 29 | debounceTime: 0, 30 | // The default value of the serveur to be sur to put it in the request event if not shown in url because default value 31 | serveurDefaultPageSize: 10, 32 | // This mean to be overrided to add query params that are fixed and should not appear in url 33 | extraQueryParams: {}, 34 | // this is used when you have special behavior to update your form like emit event instead of direct assignation 35 | updateFormFunction: null, 36 | ...configurations || {} 37 | } 38 | options.value = { 39 | page: 1, 40 | page_size: configurations.serveurDefaultPageSize, 41 | ordering: [], 42 | ...options.value 43 | } 44 | // This is just a convenient shortcut. You should speicify the page_size default in formSchema. But as it is already passed in options in the first call we can guess it. As you may pass a non default value on the first call you still can override the formSchema for more complex use case 45 | const frontDefaultPageSize = options.value.page_size ?? configurations.serveurDefaultPageSize 46 | formSchema = { 47 | page: { type: "integer", default: 1 }, 48 | page_size: { type: "integer", default: frontDefaultPageSize }, 49 | ordering: { type: "arrayString", default: [] }, 50 | ...formSchema || {} 51 | } 52 | 53 | // ----------------------------- DATA ------------------------------ 54 | let ignoredQueryParams: GenericDictionnary = {}; 55 | let localQuery: GenericDictionnary = {}; 56 | const loading = ref(false); 57 | let disableRouterWatch = false; 58 | 59 | const debounceSearch = debounce((isFilter:boolean) => { 60 | localQuery = triggerSearchIfNeeded(isFilter, getDatas); 61 | }, configurations?.debounceTime || 0); 62 | 63 | // ----------------------------- COMPUTED --------------------------- 64 | 65 | const vuetifyOptions = computed({ 66 | get: (): VuetifyOptions => { 67 | const vuetifyOptions: VuetifyOptions = { 68 | page: 1, 69 | itemsPerPage: 10, 70 | sortBy: [], 71 | sortDesc: [], 72 | groupBy: [], 73 | groupDesc: [], 74 | multiSort: false, 75 | mustSort: false 76 | }; 77 | 78 | vuetifyOptions.page = options.value.page ?? getDefaultValueForParam("page", formSchema); 79 | vuetifyOptions.itemsPerPage = options.value.page_size ?? getDefaultValueForParam("page_size", formSchema); 80 | 81 | const ordering: Array = 82 | Array.isArray(options.value.ordering) && 83 | options.value.ordering.length > 0 84 | ? options.value.ordering 85 | : getDefaultValueForParam("ordering", formSchema); 86 | 87 | const { sortBy, sortDesc } = getSortsArrayFromOrdering(ordering); 88 | 89 | vuetifyOptions.sortBy = sortBy; 90 | vuetifyOptions.sortDesc = sortDesc; 91 | 92 | return vuetifyOptions; 93 | }, 94 | set: (newOptions: VuetifyOptions) => { 95 | // As we do not define options by default, to avoid reload from other component we doesn't want, we need to set data because they are not reactive 96 | options.value.page = newOptions.page; 97 | options.value.page_size = newOptions.itemsPerPage; 98 | options.value.ordering = getOrderingFromSortArray(newOptions.sortBy, newOptions.sortDesc) 99 | } 100 | }) 101 | 102 | // ----------------------------- WATCH ------------------------------ 103 | watch(form, () => { 104 | debounceSearch(true); 105 | }, { deep: true }) 106 | 107 | watch(options, () => { 108 | nextTick(() => { 109 | localQuery = triggerSearchIfNeeded(false, getDatas); 110 | }); 111 | }, { deep: true }) 112 | 113 | 114 | watch(() => route.query, () => { 115 | if (disableRouterWatch) { 116 | disableRouterWatch = false; 117 | return; 118 | } 119 | initializeFromRouter(false); 120 | }, { deep: true }) 121 | 122 | 123 | // ----------------------------- METHODS ------------------------------ 124 | 125 | /* 126 | This function compare the current form and options to the current query and call triggerFunction if some change are detected 127 | isFilter is true if it's a filter attribute taht changed. This allow us to reset the page number to 1 128 | This allow to fetch data in back end and apply the filter in the url query 129 | */ 130 | const triggerSearchIfNeeded = (isFilter: boolean, triggerFunction: Function) => { 131 | const newLocalQuery: GenericDictionnary = { 132 | ...generateQueryFromObject(form.value, formSchema, true), 133 | ...generateQueryFromObject(options.value, formSchema, true) 134 | }; 135 | 136 | // To keep for debug please 137 | // console.log( 138 | // "triggerSearchIfNeeded", 139 | // JSON.stringify(localQuery), 140 | // JSON.stringify(newLocalQuery), 141 | // options.value?.name 142 | // ); 143 | 144 | // Do nothing if nothing change 145 | if (isEqual(localQuery, newLocalQuery)) { 146 | return localQuery; 147 | } 148 | 149 | // If filter change we need to go back to first page 150 | // This is not ideal because options has a watcher on it too so triggerSearchIfNeeded is called twice even if comparaison avoid 2 call to the server. 151 | // Other solutions would be to check if options.page > 1 in form watcher and if true only change options.page to 1 and let options watcher do the job else call this function 152 | // Choose this method because the code is shared here and not duplicated in watcher. Can be refactored if used vue Composition API 153 | if (newLocalQuery.page && isFilter) { 154 | delete newLocalQuery.page; 155 | options.value.page = 1; 156 | } 157 | disableRouterWatch = true; 158 | 159 | // As we have a local copy of the query without the prefix to help readability and developper experience 160 | // We need to set back the prefix before pushing 161 | const newLocalQueryWithPrefix: GenericDictionnary = {}; 162 | Object.entries(newLocalQuery).forEach(([key, value]) => { 163 | newLocalQueryWithPrefix[configurations?.prefix + key] = value; 164 | }); 165 | router.push({ 166 | query: { ...newLocalQueryWithPrefix, ...ignoredQueryParams } 167 | }); 168 | 169 | triggerFunction(); 170 | return cloneDeep(newLocalQuery); 171 | } 172 | 173 | /* DOC 174 | */ 175 | const getDatas = async () => { 176 | try { 177 | loading.value = true; 178 | 179 | const queryAsObject: GenericDictionnary = { 180 | ...generateQueryFromObject(form.value, formSchema, false), 181 | ...generateQueryFromObject(options.value, formSchema, false), 182 | ...configurations?.extraQueryParams || {} 183 | }; 184 | 185 | // Even if value is default for page number if it's not the server default we need to add it 186 | if ( 187 | options.value.page_size && 188 | options.value.page_size !== configurations?.serveurDefaultPageSize 189 | ) { 190 | queryAsObject.page_size = options.value.page_size; 191 | } 192 | 193 | if (fetchDatas) { 194 | const queryParams = new URLSearchParams(queryAsObject).toString(); 195 | await fetchDatas(queryParams, queryAsObject); 196 | } 197 | } finally { 198 | loading.value = false; 199 | } 200 | } 201 | 202 | /* 203 | Doc 204 | */ 205 | const getLocalCopyOfQueryUrl = () => { 206 | const newLocalQuery: GenericDictionnary = {}; 207 | Object.keys(route.query).forEach(key => { 208 | // We remove the prefix for the local copy to be agnostic of it in the code 209 | const keyWithoutPrefix = key.replace(configurations?.prefix || "", ""); 210 | 211 | // typeof undefined is important 212 | if ( 213 | key.startsWith(configurations?.prefix || "") && 214 | (typeof form.value[keyWithoutPrefix] !== "undefined" || 215 | typeof (options.value as GenericDictionnary)[keyWithoutPrefix] !== "undefined") 216 | ) { 217 | newLocalQuery[keyWithoutPrefix] = convertParamIfTypeInSchema( 218 | route.query, 219 | keyWithoutPrefix, 220 | formSchema, 221 | configurations?.prefix 222 | ); 223 | } else { 224 | // we save them in extra Query to continue to use them 225 | // We do not want to change it as it is ignored so we don't remove prefix 226 | ignoredQueryParams[key] = route.query[key]; 227 | } 228 | }); 229 | return newLocalQuery; 230 | } 231 | 232 | /* 233 | Doc 234 | */ 235 | const updateFormIfNeeded = (newForm: GenericDictionnary) => { 236 | newForm = { ...form.value, ...newForm }; 237 | if (isEqual(form.value, newForm)) { 238 | return false; 239 | } 240 | // Let user update his form as he want to. 241 | if (configurations?.updateFormFunction) { 242 | configurations?.updateFormFunction(newForm) 243 | } else { 244 | form.value = newForm; 245 | } 246 | return true; 247 | } 248 | 249 | /* 250 | Doc 251 | */ 252 | const updateOptionsIfNeeded = (newOptions: VDUSDatatableOptions) => { 253 | newOptions = { ...options.value, ...newOptions }; 254 | if (isEqual(options.value, newOptions)) { 255 | return false; 256 | } 257 | 258 | options.value = newOptions; 259 | return true; 260 | } 261 | 262 | /* 263 | DOC 264 | */ 265 | const initializeFromRouter = (created: boolean) => { 266 | // Need to select only elements that are in this.form to avoid multiple instance reload between them 267 | ignoredQueryParams = {}; 268 | 269 | // localQuery is the memory of the query in url to know when reload data. 270 | const newLocalQuery = getLocalCopyOfQueryUrl(); 271 | 272 | //To keep for debug please 273 | // console.log( 274 | // "initializeFromRouter", 275 | // JSON.stringify(localQuery), 276 | // JSON.stringify(newLocalQuery), 277 | // options.value.name 278 | // ); 279 | 280 | // route.query can change of instance if we route push in router children. SO we juste assure that we do not fetch data again if the current query object is the same than the new 281 | // If it's the created call we get datas as initialization because if route.query is empty it will not trigger call 282 | if (isEqual(localQuery, newLocalQuery)) { 283 | if (created) { 284 | if(initializeForm){ 285 | initializeForm(); 286 | } 287 | getDatas(); 288 | } 289 | return; 290 | } 291 | 292 | const removedParams: Array = getRemovedKeyBetweenTwoObject( 293 | localQuery, 294 | newLocalQuery 295 | ); 296 | 297 | localQuery = newLocalQuery; 298 | 299 | const { newForm, newOptions } = readFormAndOptionsFromLocalQuery( 300 | localQuery, 301 | form.value, 302 | options.value, 303 | formSchema, 304 | removedParams 305 | ); 306 | 307 | const formUpdated = updateFormIfNeeded(newForm); 308 | 309 | // If the form is updated because an other component pushed a new value that we was watching 310 | // And we was not on the first page we need to go back to the first page 311 | const currentPageNumber: number = options.value.page || 1; 312 | if (formUpdated && currentPageNumber > 1) { 313 | options.value.page = 1; 314 | } 315 | 316 | updateOptionsIfNeeded(newOptions); 317 | 318 | if(initializeForm) { 319 | initializeForm(); 320 | } 321 | 322 | getDatas() 323 | } 324 | 325 | // ---------------------------------- LIFECYCLE CREATED ---------------------------------------- 326 | // launch all the function needed at initialization 327 | initializeFromRouter(true) 328 | 329 | return { 330 | vuetifyOptions, 331 | loading 332 | } 333 | } -------------------------------------------------------------------------------- /src/utils/VDUSTypes.ts: -------------------------------------------------------------------------------- 1 | type GenericDictionnary = { 2 | [key: string]: any; 3 | } 4 | 5 | type VDUSParamSchema = { 6 | name?: string; 7 | default?: any; 8 | type?: "string" | "boolean" | "integer" | "arrayInt" | "arrayString" | "nullBoolean"; 9 | } 10 | 11 | type VDUSFormSchema = { 12 | [key: string]: VDUSParamSchema; 13 | } 14 | 15 | type VDUSDatatableOptions = { 16 | page?: number; 17 | page_size?: number; 18 | ordering?: Array; 19 | } 20 | 21 | type VDUSConfiguration = { 22 | prefix?: string; 23 | debounceTime?: number; 24 | serveurDefaultPageSize?: number; 25 | extraQueryParams?: GenericDictionnary; 26 | updateFormFunction?: Function | null; 27 | } 28 | 29 | type VuetifySortArraysObject = { 30 | sortBy: Array; // depending if vuetify 2 or 3 31 | sortDesc: Array; // not existing in vuetify 3 32 | } 33 | 34 | type VuetifyOptions = { 35 | page: number; 36 | itemsPerPage: number; 37 | sortBy: Array; // depending if vuetify 2 or 3 38 | sortDesc: Array; // not existing in vuetify 3 39 | groupBy: Array; 40 | groupDesc: Array; 41 | multiSort: boolean; 42 | mustSort: boolean; 43 | } 44 | 45 | export {GenericDictionnary, VDUSConfiguration, VuetifySortArraysObject, VuetifyOptions, VDUSParamSchema, VDUSFormSchema, VDUSDatatableOptions} -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import {VuetifySortArraysObject} from "./VDUSTypes"; 2 | import { isVue2 } from 'vue-demi' 3 | 4 | export const elementToArrayOfInt = (element: any): Array => { 5 | return ["number", "string"].includes(typeof element) 6 | ? [extractIntegerValue(element)] 7 | : element.map((item: any) => extractIntegerValue(item)); 8 | }; 9 | 10 | export const elementToArrayOfString = (element: any): Array => { 11 | return element ? (typeof element === "string" ? [element] : element) : []; 12 | }; 13 | 14 | export const extractBooleanValue = (value: any, defaultValue:boolean|null = true): boolean|null => { 15 | return value ? value.toString() === "true" : defaultValue; 16 | }; 17 | 18 | export const extractNullBooleanValue = (value: any, defaultValue:boolean|null = true): boolean|null => { 19 | if(value === null) { 20 | return null 21 | } 22 | if(value.toString() === "true") { 23 | return true 24 | } 25 | if(value.toString() === "false") { 26 | return false 27 | } 28 | if(["null", ""].includes(value.toString())) { 29 | return null 30 | } 31 | return defaultValue 32 | }; 33 | 34 | 35 | export const extractIntegerValue = (value: any, defaultValue = 0): number => { 36 | const parsed = parseInt(value); 37 | return isNaN(parsed) ? defaultValue : parsed; 38 | }; 39 | 40 | export const getSortsArrayFromOrdering = (ordering: Array|null): VuetifySortArraysObject => { 41 | 42 | if (!ordering) { 43 | return { sortBy: [], sortDesc: [] }; 44 | } 45 | const sortBy: Array = []; 46 | const sortDesc: Array = []; 47 | 48 | ordering.forEach(orderItem => { 49 | let isDesc = false; 50 | if (orderItem.startsWith("-")) { 51 | orderItem = orderItem.replace("-", ""); 52 | isDesc = true; 53 | } 54 | if(isVue2) { 55 | sortBy.push(orderItem); 56 | sortDesc.push(isDesc); 57 | } else { 58 | sortBy.push({key: orderItem, order: isDesc ? "desc" : "asc"}); 59 | } 60 | }); 61 | 62 | return { sortBy, sortDesc }; 63 | } 64 | 65 | export const getOrderingFromSortArray = (sortBy: Array, sortDesc: Array): Array => { 66 | let ordering: Array = []; 67 | if(isVue2) { 68 | (sortBy as string[]).forEach((orderItem: string, index: number) => { 69 | let isDesc = true; 70 | if (sortDesc.length > index) { 71 | isDesc = sortDesc[index]; 72 | } 73 | ordering.push(`${isDesc ? "-" : ""}${orderItem}`); 74 | }); 75 | } else { 76 | // Vue 3 scenario: sortBy is Array<{ key: string; order: 'asc' | 'desc' }> 77 | ordering = (sortBy as Array<{ key: string; order: 'asc' | 'desc' }>).reduce( 78 | (acc: string[], item) => { 79 | acc.push(`${item.order === 'desc' ? '-' : ''}${item.key}`); 80 | return acc; // Return the accumulator array, not push result 81 | }, 82 | [] 83 | ); 84 | } 85 | return ordering; 86 | } -------------------------------------------------------------------------------- /src/utils/listPaginatedTools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elementToArrayOfInt, 3 | elementToArrayOfString, 4 | extractBooleanValue, 5 | extractNullBooleanValue, 6 | extractIntegerValue 7 | } from "./helpers"; 8 | import isEqual from "lodash.isequal"; 9 | import {GenericDictionnary, VDUSDatatableOptions, VDUSFormSchema} from "./VDUSTypes" 10 | 11 | const getDefaultValueForParam = (param: string, schema?: VDUSFormSchema): any => { 12 | if (schema?.[param]) { 13 | // if there is a defautl value we change the condition to is non equality 14 | if (schema[param].default) { 15 | // TODO default value for array need to be stringify ? 16 | return schema[param].default; 17 | } 18 | // Else if we have a type we try to match the default for the type 19 | else if (schema[param].type === "boolean") { 20 | // Default for boolean is false 21 | return false; 22 | } else if (["arrayInt", "arrayString"].includes(schema[param].type as string)) { 23 | // Default for array is empty array or first element null or empty 24 | return []; 25 | } 26 | } 27 | return null; 28 | } 29 | 30 | const isValueDefault = (value: any, param: string, schema?: VDUSFormSchema): boolean => { 31 | // Default is string 32 | let isValueDefault: boolean = value === ""; 33 | 34 | if (schema?.[param]) { 35 | // if there is a defautl value we change the condition to is non equality 36 | if (typeof(schema[param].default) !== "undefined") { 37 | // We have a special case for nullBoolean because we can have null value that is not the default 38 | if(schema[param]?.type === "nullBoolean") { 39 | return schema[param].default === extractNullBooleanValue(value, schema[param].default) 40 | } 41 | // TODO default value for array need to be stringify ? 42 | if (Array.isArray(value)) { 43 | isValueDefault = isEqual(value, schema[param].default) || !value.length; 44 | } else { 45 | isValueDefault = value === schema[param].default; 46 | } 47 | } 48 | // Else if we have a type we try to match the default for the type 49 | else if (schema[param].type === "boolean") { 50 | // Default for boolean is false 51 | isValueDefault = value === false; 52 | } else if (schema[param].type === "nullBoolean") { 53 | // Default for null boolean is null. 54 | isValueDefault = value === ''; 55 | } else if (["arrayInt", "arrayString"].includes(schema[param].type as string)) { 56 | // Default for array is empty array or first element null or empty 57 | isValueDefault = 58 | value.length === 0 || value[0] === null || value[0] === ""; 59 | } 60 | } 61 | 62 | // We always check if value not null AND the specific condition 63 | return value === null || isValueDefault; 64 | } 65 | 66 | /* 67 | This function take a object in parameter that is often a form of filtering field 68 | all this field are filtered before being used to be transformed as a query url 69 | if localName is true it will no replace the param key with the real used for backend query 70 | if localName is false the name will be replaced by the correct one sended to backend 71 | */ 72 | const generateQueryFromObject = (object: GenericDictionnary, schema?: VDUSFormSchema, localName = true): GenericDictionnary => { 73 | const queryUrl: GenericDictionnary = {}; 74 | for (const [key, value] of Object.entries(object)) { 75 | // We do not want to send a default value 76 | if (isValueDefault(value, key, schema)) { 77 | continue; 78 | } 79 | 80 | // by default the quey key is the same that the form key 81 | let queryKey = key; 82 | let queryValue = value; 83 | // But this can be overrided if name attribute is defined in the param schema 84 | if (!localName && schema?.[key]?.name) { 85 | queryKey = (schema[key].name as string); // typescript error because .name can be undefined but if check it before 86 | } 87 | 88 | // As ordering key is a special key where other key can be specified inside its value we need to check for each ordernig key if there is a different server name 89 | if (key === 'ordering' && !localName && value && Array.isArray(value)) { 90 | queryValue = value.map((orderItem: string) => { 91 | let prefix = "" 92 | // If we have a desc order we need to remove the - to match with the schema 93 | if (orderItem.startsWith("-")) { 94 | orderItem = orderItem.replace("-", ""); 95 | prefix = "-" 96 | } 97 | // Look if we have a specific server name for this attribute in the schema. 98 | if(schema?.[orderItem]?.name) { 99 | orderItem = (schema[orderItem].name as string) 100 | } 101 | // Do not forget to add the prefix to have the correct ordering 102 | return `${prefix}${orderItem}`; 103 | }) 104 | } 105 | queryUrl[queryKey] = queryValue; 106 | } 107 | return queryUrl; 108 | } 109 | 110 | const convertParamIfTypeInSchema = (query: GenericDictionnary, param: string, schema?: VDUSFormSchema, prefix = ""): any => { 111 | if (!schema?.[param]?.type) { 112 | return query[prefix + param]; 113 | } 114 | if (schema[param].type === "boolean") { 115 | return extractBooleanValue(query[prefix + param]); 116 | } 117 | if (schema[param].type === "nullBoolean") { 118 | return extractNullBooleanValue(query[prefix + param], schema[param].default); 119 | } 120 | if (schema[param].type === "integer") { 121 | return extractIntegerValue(query[prefix + param]); 122 | } 123 | if (schema[param].type === "arrayInt") { 124 | return elementToArrayOfInt(query[prefix + param]); 125 | } 126 | if (schema[param].type === "arrayString") { 127 | return elementToArrayOfString(query[prefix + param]); 128 | } 129 | 130 | return query[prefix + param]; 131 | } 132 | 133 | /* 134 | Transform query parameter from vue router to two javascript objects representing the filtering form and the options 135 | */ 136 | const readFormAndOptionsFromLocalQuery = ( 137 | query: GenericDictionnary, 138 | form: GenericDictionnary, 139 | options: VDUSDatatableOptions, 140 | schema?: VDUSFormSchema, 141 | removedParam: Array = [] 142 | ): {newOptions: VDUSDatatableOptions; newForm: GenericDictionnary} => { 143 | 144 | const newOptions: VDUSDatatableOptions = {}; 145 | const newForm: GenericDictionnary = {}; 146 | 147 | for (const param in query) { 148 | if (typeof form[param] !== "undefined") { 149 | newForm[param] = convertParamIfTypeInSchema(query, param, schema); 150 | } else if (typeof (options as GenericDictionnary)[param] !== "undefined") { 151 | (newOptions as GenericDictionnary)[param] = convertParamIfTypeInSchema(query, param, schema); 152 | } 153 | } 154 | // This allow to reset to default deleted param by other component 155 | removedParam.forEach(param => { 156 | if (typeof form[param] !== "undefined") { 157 | newForm[param] = getDefaultValueForParam(param, schema); 158 | } 159 | }); 160 | removedParam.forEach(param => { 161 | if (typeof (options as GenericDictionnary)[param] !== "undefined") { 162 | (newOptions as GenericDictionnary)[param] = getDefaultValueForParam(param, schema); 163 | } 164 | }); 165 | return { newOptions, newForm }; 166 | } 167 | 168 | const getRemovedKeyBetweenTwoObject = (originalObject: GenericDictionnary, newObject: GenericDictionnary): Array => { 169 | const originalObjectKeys: Array = Object.keys(originalObject); 170 | const newObjectKeys: Array = Object.keys(newObject); 171 | return originalObjectKeys.filter( 172 | originalKey => !newObjectKeys.includes(originalKey) 173 | ); 174 | } 175 | 176 | export { 177 | generateQueryFromObject, 178 | readFormAndOptionsFromLocalQuery, 179 | convertParamIfTypeInSchema, 180 | getRemovedKeyBetweenTwoObject, 181 | getDefaultValueForParam 182 | }; 183 | -------------------------------------------------------------------------------- /tests/unit/helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import {elementToArrayOfInt, elementToArrayOfString, extractBooleanValue, extractIntegerValue, getSortsArrayFromOrdering, getOrderingFromSortArray} from "@/utils/helpers" 2 | 3 | describe('helpers.ts', () => { 4 | describe('elementToArrayOfInt', () => { 5 | 6 | it('transform a string to array of int', () => { 7 | const element = '1'; 8 | const returnValue = elementToArrayOfInt(element); 9 | 10 | expect(returnValue).toEqual([1]) 11 | }) 12 | 13 | it('transform a int to array of int', () => { 14 | const element = 1; 15 | const returnValue = elementToArrayOfInt(element); 16 | 17 | expect(returnValue).toEqual([1]) 18 | }) 19 | 20 | it('transform an array of string to array of int', () => { 21 | const element = ["1", "2", "3"]; 22 | const returnValue = elementToArrayOfInt(element); 23 | 24 | expect(returnValue).toEqual([1, 2, 3]) 25 | }) 26 | 27 | it('transform a string not parsable to 0', () => { 28 | const element = 'test'; 29 | const returnValue = elementToArrayOfInt(element); 30 | 31 | expect(returnValue).toEqual([0]) 32 | }) 33 | 34 | }) 35 | 36 | describe('elementToArrayOfString', () => { 37 | 38 | it('transform a string to array of string', () => { 39 | const element = '1'; 40 | const returnValue = elementToArrayOfString(element); 41 | 42 | expect(returnValue).toEqual(["1"]) 43 | }) 44 | 45 | it('transform an array of string to array of string', () => { 46 | const element = ["1", "2", "3"]; 47 | const returnValue = elementToArrayOfString(element); 48 | 49 | expect(returnValue).toEqual(["1", "2", "3"]) 50 | }) 51 | 52 | }) 53 | 54 | describe('extractBooleanValue', () => { 55 | 56 | it('transform a true string to true boolean', () => { 57 | const element = 'true'; 58 | const returnValue = extractBooleanValue(element); 59 | 60 | expect(returnValue).toEqual(true) 61 | }) 62 | 63 | it('transform a false of string to false boolean', () => { 64 | const element = "false"; 65 | const returnValue = extractBooleanValue(element); 66 | 67 | expect(returnValue).toEqual(false) 68 | }) 69 | 70 | it('transform a non boolean string to false', () => { 71 | const element = "falsee"; 72 | const returnValue = extractBooleanValue(element, null); 73 | 74 | expect(returnValue).toEqual(false) 75 | }) 76 | 77 | it('transform a null value to default value', () => { 78 | const element = null; 79 | let returnValue = extractBooleanValue(element, true); 80 | expect(returnValue).toEqual(true) 81 | 82 | returnValue = extractBooleanValue(element, false); 83 | expect(returnValue).toEqual(false) 84 | 85 | returnValue = extractBooleanValue(element, null); 86 | expect(returnValue).toEqual(null) 87 | }) 88 | 89 | }) 90 | 91 | describe('extractIntegerValue', () => { 92 | 93 | it('transform a number string to int', () => { 94 | const element = '10'; 95 | const returnValue = extractIntegerValue(element); 96 | 97 | expect(returnValue).toEqual(10) 98 | }) 99 | 100 | it('transform a wrong number string to default value', () => { 101 | const element = "a10"; 102 | const returnValue = extractIntegerValue(element, 20); 103 | 104 | expect(returnValue).toEqual(20) 105 | }) 106 | 107 | }) 108 | 109 | describe('getSortsArrayFromOrdering', () => { 110 | 111 | it("return default VuetifySortArraysObject if no value", () => { 112 | const element = null; 113 | const returnValue = getSortsArrayFromOrdering(element); 114 | 115 | expect(returnValue).toEqual({ sortBy: [], sortDesc: [] }) 116 | }) 117 | 118 | it("return correct VuetifySortArraysObject if positive ordering", () => { 119 | const element = ["ordering_field"]; 120 | const returnValue = getSortsArrayFromOrdering(element); 121 | 122 | expect(returnValue).toEqual({ sortBy: [{key: "ordering_field", order: "asc"}], sortDesc: [] }) 123 | }) 124 | 125 | it("return correct VuetifySortArraysObject if negative ordering", () => { 126 | const element = ["-ordering_field"]; 127 | const returnValue = getSortsArrayFromOrdering(element); 128 | 129 | expect(returnValue).toEqual({ sortBy: [{key: "ordering_field", order: "desc"}], sortDesc: [] }) 130 | }) 131 | }) 132 | 133 | describe('getOrderingFromSortArray', () => { 134 | 135 | it("return correct ordering if positive ordering", () => { 136 | const sortBy: { key: string; order: 'asc' | 'desc'; }[] = [{key: "ordering_field", order: "asc"}]; 137 | const sortDesc: boolean[] = []; 138 | const returnValue = getOrderingFromSortArray(sortBy, sortDesc); 139 | 140 | expect(returnValue).toEqual(["ordering_field"]) 141 | }) 142 | 143 | it("return correct ordering if negative ordering", () => { 144 | const sortBy: { key: string; order: 'asc' | 'desc'; }[] = [{key: "ordering_field", order: "desc"}]; 145 | const sortDesc: boolean[] = []; 146 | const returnValue = getOrderingFromSortArray(sortBy, sortDesc); 147 | 148 | expect(returnValue).toEqual(["-ordering_field"]) 149 | }) 150 | }) 151 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "jest" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | port: 9000, 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /vue2-example/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /vue2-example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | parser: '@typescript-eslint/parser' 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | "@typescript-eslint/camelcase": "off", 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "@typescript-eslint/no-use-before-define": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vue2-example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /vue2-example/README.md: -------------------------------------------------------------------------------- 1 | # vue2-example 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /vue2-example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue2-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@vue/composition-api": "^1.0.0-rc.5", 12 | "core-js": "^3.6.5", 13 | "vue": "^2.6.11", 14 | "vue-datatable-url-sync": "^1.1.1", 15 | "vue-router": "^3.5.1", 16 | "vue2-helpers": "^1.1.7", 17 | "vuetify": "^2.4.0" 18 | }, 19 | "devDependencies": { 20 | "@typescript-eslint/eslint-plugin": "^2.33.0", 21 | "@typescript-eslint/parser": "^2.33.0", 22 | "@vue/cli-plugin-babel": "~4.5.0", 23 | "@vue/cli-plugin-eslint": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-typescript": "^4.5.12", 26 | "@vue/cli-service": "~4.5.0", 27 | "@vue/eslint-config-typescript": "^5.0.2", 28 | "babel-eslint": "^10.1.0", 29 | "eslint": "^6.7.2", 30 | "eslint-plugin-vue": "^6.2.2", 31 | "sass": "^1.32.0", 32 | "sass-loader": "^10.0.0", 33 | "typescript": "~4.1.5", 34 | "vue-cli-plugin-vuetify": "~2.3.1", 35 | "vue-template-compiler": "^2.6.11", 36 | "vuetify-loader": "^1.7.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vue2-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socotecio/vue-datatable-url-sync/8c1e9b93d5c55ab04606c1d9dd250ed4eb62640a/vue2-example/public/favicon.ico -------------------------------------------------------------------------------- /vue2-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /vue2-example/src/App.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 61 | -------------------------------------------------------------------------------- /vue2-example/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socotecio/vue-datatable-url-sync/8c1e9b93d5c55ab04606c1d9dd250ed4eb62640a/vue2-example/src/assets/logo.png -------------------------------------------------------------------------------- /vue2-example/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /vue2-example/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 77 | -------------------------------------------------------------------------------- /vue2-example/src/components/data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: "1", 4 | title: "test1" 5 | }, 6 | { 7 | id: "2", 8 | title: "test2" 9 | }, 10 | { 11 | id: "3", 12 | title: "test3" 13 | }, 14 | { 15 | id: "4", 16 | title: "test4" 17 | }, 18 | { 19 | id: "5", 20 | title: "test5" 21 | }, 22 | { 23 | id: "6", 24 | title: "test6" 25 | }, 26 | { 27 | id: "7", 28 | title: "test7" 29 | }, 30 | { 31 | id: "8", 32 | title: "test8" 33 | }, 34 | { 35 | id: "9", 36 | title: "test9" 37 | }, 38 | { 39 | id: "10", 40 | title: "test10" 41 | }, 42 | { 43 | id: "11", 44 | title: "test11" 45 | }, 46 | { 47 | id: "12", 48 | title: "test12" 49 | }, 50 | { 51 | id: "13", 52 | title: "test13" 53 | }, 54 | ] -------------------------------------------------------------------------------- /vue2-example/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import vuetify from './plugins/vuetify'; 5 | import VueCompositionAPI from '@vue/composition-api' 6 | 7 | Vue.use(VueCompositionAPI) 8 | 9 | Vue.config.productionTip = false 10 | 11 | new Vue({ 12 | router, 13 | vuetify, 14 | render: h => h(App) 15 | }).$mount('#app') 16 | -------------------------------------------------------------------------------- /vue2-example/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /vue2-example/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: Home 12 | }, 13 | { 14 | path: '/about', 15 | name: 'About', 16 | // route level code-splitting 17 | // this generates a separate chunk (about.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') 20 | } 21 | ] 22 | 23 | const router = new VueRouter({ 24 | mode: 'history', 25 | base: process.env.BASE_URL, 26 | routes 27 | }) 28 | 29 | export default router 30 | -------------------------------------------------------------------------------- /vue2-example/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vue2-example/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-datatable-url-sync'; 2 | 3 | declare module '*.vue' { 4 | import Vue from 'vue' 5 | export default Vue 6 | } 7 | -------------------------------------------------------------------------------- /vue2-example/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /vue2-example/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /vue2-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "vuetify", 18 | "vue-datatable-url-sync", 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ], 31 | "typeRoots": [ 32 | "./node_modules/@types", 33 | "./node_modules/vuetify/types", 34 | "./node_modules/vue-datatable-url-sync/dist" 35 | ], 36 | }, 37 | "include": [ 38 | "src/**/*.ts", 39 | "src/**/*.tsx", 40 | "src/**/*.vue", 41 | "tests/**/*.ts", 42 | "tests/**/*.tsx" 43 | ], 44 | "exclude": [ 45 | "node_modules" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /vue2-example/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue2.7-example/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /vue2.7-example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | parser: '@typescript-eslint/parser' 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | "@typescript-eslint/camelcase": "off", 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "@typescript-eslint/no-use-before-define": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vue2.7-example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /vue2.7-example/README.md: -------------------------------------------------------------------------------- 1 | # vue2-example 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /vue2.7-example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue2.7-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue2-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.7.8", 13 | "vue-datatable-url-sync": "^2.0.1", 14 | "vue-router": "^3.5.4", 15 | "vuetify": "^2.6.7" 16 | }, 17 | "devDependencies": { 18 | "@typescript-eslint/eslint-plugin": "^5.4.0", 19 | "@typescript-eslint/parser": "^5.4.0", 20 | "@vue/cli-plugin-babel": "^5.0.8", 21 | "@vue/cli-plugin-eslint": "^5.0.8", 22 | "@vue/cli-plugin-router": "^5.0.8", 23 | "@vue/cli-plugin-typescript": "^5.0.8", 24 | "@vue/cli-service": "^5.0.8", 25 | "@vue/eslint-config-typescript": "^9.1.0", 26 | "babel-eslint": "^10.1.0", 27 | "eslint": "^7.32.0", 28 | "eslint-plugin-vue": "^8.0.3", 29 | "sass": "^1.32.0", 30 | "sass-loader": "^10.0.0", 31 | "typescript": "~4.5.5", 32 | "vue-cli-plugin-vuetify": "^2.5.1", 33 | "vuetify-loader": "^1.9.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vue2.7-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socotecio/vue-datatable-url-sync/8c1e9b93d5c55ab04606c1d9dd250ed4eb62640a/vue2.7-example/public/favicon.ico -------------------------------------------------------------------------------- /vue2.7-example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /vue2.7-example/src/App.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 61 | -------------------------------------------------------------------------------- /vue2.7-example/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socotecio/vue-datatable-url-sync/8c1e9b93d5c55ab04606c1d9dd250ed4eb62640a/vue2.7-example/src/assets/logo.png -------------------------------------------------------------------------------- /vue2.7-example/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /vue2.7-example/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 80 | -------------------------------------------------------------------------------- /vue2.7-example/src/components/data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: "1", 4 | title: "test1" 5 | }, 6 | { 7 | id: "2", 8 | title: "test2" 9 | }, 10 | { 11 | id: "3", 12 | title: "test3" 13 | }, 14 | { 15 | id: "4", 16 | title: "test4" 17 | }, 18 | { 19 | id: "5", 20 | title: "test5" 21 | }, 22 | { 23 | id: "6", 24 | title: "test6" 25 | }, 26 | { 27 | id: "7", 28 | title: "test7" 29 | }, 30 | { 31 | id: "8", 32 | title: "test8" 33 | }, 34 | { 35 | id: "9", 36 | title: "test9" 37 | }, 38 | { 39 | id: "10", 40 | title: "test10" 41 | }, 42 | { 43 | id: "11", 44 | title: "test11" 45 | }, 46 | { 47 | id: "12", 48 | title: "test12" 49 | }, 50 | { 51 | id: "13", 52 | title: "test13" 53 | }, 54 | ] -------------------------------------------------------------------------------- /vue2.7-example/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import vuetify from './plugins/vuetify'; 5 | 6 | Vue.config.productionTip = false 7 | 8 | new Vue({ 9 | router, 10 | vuetify, 11 | render: h => h(App) 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /vue2.7-example/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib/framework'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | }); 8 | -------------------------------------------------------------------------------- /vue2.7-example/src/router/composable.js: -------------------------------------------------------------------------------- 1 | // INFO - AM - 25/07/2022 - Hack for vue router wainting for integration: 2 | 3 | // https://github.com/vuejs/vue-router/issues/3760 4 | // https://github.com/vuejs/vue-router/pull/3763 5 | 6 | 7 | import { getCurrentInstance, reactive, watchEffect } from 'vue' 8 | 9 | /** 10 | * Returns the current route location. Equivalent to using `$route` inside 11 | * templates. 12 | */ 13 | export function useRoute () { 14 | const instance = getCurrentInstance() 15 | if (!instance) { 16 | throw new Error('getCurrentInstance() returned null. useRoute() must be called at the top of a setup function') 17 | } 18 | const route = reactive(Object.assign({}, instance.proxy.$root.$route)) 19 | watchEffect(() => { 20 | Object.assign(route, instance.proxy.$root.$route) 21 | }) 22 | 23 | return route 24 | } 25 | 26 | /** 27 | * Returns the router instance. Equivalent to using `$router` inside 28 | * templates. 29 | */ 30 | export function useRouter () { 31 | const instance = getCurrentInstance() 32 | if (!instance) { 33 | throw new Error( 34 | 'getCurrentInstance() returned null. useRouter() must be called at the top of a setup function', 35 | ) 36 | } 37 | const router = instance.proxy.$root.$router 38 | watchEffect(() => { 39 | if (router) { 40 | Object.assign(router, instance.proxy.$root.$router) 41 | } 42 | }) 43 | return router 44 | } -------------------------------------------------------------------------------- /vue2.7-example/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: Home 12 | }, 13 | { 14 | path: '/about', 15 | name: 'About', 16 | // route level code-splitting 17 | // this generates a separate chunk (about.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') 20 | } 21 | ] 22 | 23 | const router = new VueRouter({ 24 | mode: 'history', 25 | base: process.env.BASE_URL, 26 | routes 27 | }) 28 | 29 | export default router 30 | -------------------------------------------------------------------------------- /vue2.7-example/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vue2.7-example/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-datatable-url-sync'; 2 | 3 | declare module '*.vue' { 4 | import Vue from 'vue' 5 | export default Vue 6 | } 7 | -------------------------------------------------------------------------------- /vue2.7-example/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /vue2.7-example/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /vue2.7-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "vuetify", 18 | "vue-datatable-url-sync", 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ], 31 | "typeRoots": [ 32 | "./node_modules/@types", 33 | "./node_modules/vuetify/types", 34 | "./node_modules/vue-datatable-url-sync/dist" 35 | ], 36 | }, 37 | "include": [ 38 | "src/**/*.ts", 39 | "src/**/*.tsx", 40 | "src/**/*.vue", 41 | "tests/**/*.ts", 42 | "tests/**/*.tsx" 43 | ], 44 | "exclude": [ 45 | "node_modules" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /vue2.7-example/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue3-example/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /vue3-example/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /vue3-example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 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 | -------------------------------------------------------------------------------- /vue3-example/README.md: -------------------------------------------------------------------------------- 1 | # Vuetify (Default) 2 | 3 | This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch. 4 | 5 | ## ❗️ Important Links 6 | 7 | - 📄 [Docs](https://vuetifyjs.com/) 8 | - 🚨 [Issues](https://issues.vuetifyjs.com/) 9 | - 🏬 [Store](https://store.vuetifyjs.com/) 10 | - 🎮 [Playground](https://play.vuetifyjs.com/) 11 | - 💬 [Discord](https://community.vuetifyjs.com) 12 | 13 | ## 💿 Install 14 | 15 | Set up your project using your preferred package manager. Use the corresponding command to install the dependencies: 16 | 17 | | Package Manager | Command | 18 | |---------------------------------------------------------------|----------------| 19 | | [yarn](https://yarnpkg.com/getting-started) | `yarn install` | 20 | | [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` | 21 | | [pnpm](https://pnpm.io/installation) | `pnpm install` | 22 | | [bun](https://bun.sh/#getting-started) | `bun install` | 23 | 24 | After completing the installation, your environment is ready for Vuetify development. 25 | 26 | ## ✨ Features 27 | 28 | - 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/) 29 | - 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue. 30 | - 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) 31 | - 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/) 32 | - ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/) 33 | - 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) 34 | - 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc) 35 | 36 | These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable. 37 | 38 | ## 💡 Usage 39 | 40 | This section covers how to start the development server and build your project for production. 41 | 42 | ### Starting the Development Server 43 | 44 | To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000): 45 | 46 | ```bash 47 | yarn dev 48 | ``` 49 | 50 | (Repeat for npm, pnpm, and bun with respective commands.) 51 | 52 | > Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script. 53 | 54 | ### Building for Production 55 | 56 | To build your project for production, use: 57 | 58 | ```bash 59 | yarn build 60 | ``` 61 | 62 | (Repeat for npm, pnpm, and bun with respective commands.) 63 | 64 | Once the build process is completed, your application will be ready for deployment in a production environment. 65 | 66 | ## 💪 Support Vuetify Development 67 | 68 | This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider: 69 | 70 | - [Requesting Enterprise Support](https://support.vuetifyjs.com/) 71 | - [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship) 72 | - [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship) 73 | - [Supporting the team on Open Collective](https://opencollective.com/vuetify) 74 | - [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify) 75 | - [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify) 76 | - [Making a one-time donation with Paypal](https://paypal.me/vuetify) 77 | 78 | ## 📑 License 79 | [MIT](http://opensource.org/licenses/MIT) 80 | 81 | Copyright (c) 2016-present Vuetify, LLC 82 | -------------------------------------------------------------------------------- /vue3-example/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | BaseOrdering: typeof import('./src/components/BaseOrdering.vue')['default'] 11 | HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] 12 | RouterLink: typeof import('vue-router')['RouterLink'] 13 | RouterView: typeof import('vue-router')['RouterView'] 14 | SimpleDatatable: typeof import('./src/components/SimpleDatatable.vue')['default'] 15 | VuetifyDatatable: typeof import('./src/components/VuetifyDatatable.vue')['default'] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /vue3-example/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /vue3-example/eslint.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * .eslint.js 3 | * 4 | * ESLint configuration file. 5 | */ 6 | 7 | import pluginVue from 'eslint-plugin-vue' 8 | import vueTsEslintConfig from '@vue/eslint-config-typescript' 9 | 10 | export default [ 11 | { 12 | name: 'app/files-to-lint', 13 | files: ['**/*.{ts,mts,tsx,vue}'], 14 | }, 15 | 16 | { 17 | name: 'app/files-to-ignore', 18 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], 19 | }, 20 | 21 | ...pluginVue.configs['flat/recommended'], 22 | ...vueTsEslintConfig(), 23 | 24 | { 25 | rules: { 26 | "@typescript-eslint/no-explicit-any": "off", 27 | '@typescript-eslint/no-unused-expressions': [ 28 | 'error', 29 | { 30 | allowShortCircuit: true, 31 | allowTernary: true, 32 | }, 33 | ], 34 | 'vue/multi-word-component-names': 'off', 35 | } 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /vue3-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to Vuetify 3 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /vue3-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-example", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force", 12 | "lint": "eslint . --fix" 13 | }, 14 | "dependencies": { 15 | "@mdi/font": "7.4.47", 16 | "core-js": "^3.37.1", 17 | "lodash.debounce": "^4.0.8", 18 | "lodash.isequal": "^4.5.0", 19 | "roboto-fontface": "*", 20 | "vue": "^3.4.31", 21 | "vue-datatable-url-sync": "^2.1.1", 22 | "vue-demi": "^0.14.10", 23 | "vuetify": "^3.6.14" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.14.0", 27 | "@tsconfig/node22": "^22.0.0", 28 | "@types/node": "^22.9.0", 29 | "@vitejs/plugin-vue": "^5.1.4", 30 | "@vue/eslint-config-typescript": "^14.1.3", 31 | "@vue/tsconfig": "^0.5.1", 32 | "eslint": "^9.14.0", 33 | "eslint-plugin-vue": "^9.30.0", 34 | "npm-run-all2": "^7.0.1", 35 | "sass": "1.77.8", 36 | "sass-embedded": "^1.77.8", 37 | "typescript": "~5.6.3", 38 | "unplugin-fonts": "^1.1.1", 39 | "unplugin-vue-components": "^0.27.2", 40 | "unplugin-vue-router": "^0.10.0", 41 | "vite": "^5.4.10", 42 | "vite-plugin-vuetify": "^2.0.3", 43 | "vue-router": "^4.4.0", 44 | "vue-tsc": "^2.1.10" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vue3-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socotecio/vue-datatable-url-sync/8c1e9b93d5c55ab04606c1d9dd250ed4eb62640a/vue3-example/public/favicon.ico -------------------------------------------------------------------------------- /vue3-example/src/App.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 40 | -------------------------------------------------------------------------------- /vue3-example/src/assets/data/data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: "1", 4 | title: "test1", 5 | is_answered: true 6 | }, 7 | { 8 | id: "2", 9 | title: "test2", 10 | is_answered: true 11 | }, 12 | { 13 | id: "3", 14 | title: "test3", 15 | is_answered: false 16 | }, 17 | { 18 | id: "4", 19 | title: "test4", 20 | is_answered: true 21 | }, 22 | { 23 | id: "5", 24 | title: "test5", 25 | is_answered: false 26 | }, 27 | { 28 | id: "6", 29 | title: "test6", 30 | is_answered: true 31 | }, 32 | { 33 | id: "7", 34 | title: "test7", 35 | is_answered: true 36 | }, 37 | { 38 | id: "8", 39 | title: "test8", 40 | is_answered: true 41 | }, 42 | { 43 | id: "9", 44 | title: "test9", 45 | is_answered: false 46 | }, 47 | { 48 | id: "10", 49 | title: "test10", 50 | is_answered: true 51 | }, 52 | { 53 | id: "11", 54 | title: "test11", 55 | is_answered: true 56 | }, 57 | { 58 | id: "12", 59 | title: "test12", 60 | is_answered: true 61 | }, 62 | { 63 | id: "13", 64 | title: "test13", 65 | is_answered: true 66 | }, 67 | ] -------------------------------------------------------------------------------- /vue3-example/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/socotecio/vue-datatable-url-sync/8c1e9b93d5c55ab04606c1d9dd250ed4eb62640a/vue3-example/src/assets/logo.png -------------------------------------------------------------------------------- /vue3-example/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /vue3-example/src/components/BaseOrdering.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 60 | -------------------------------------------------------------------------------- /vue3-example/src/components/README.md: -------------------------------------------------------------------------------- 1 | # Components 2 | 3 | Vue template files in this folder are automatically imported. 4 | 5 | ## 🚀 Usage 6 | 7 | Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it. 8 | 9 | The following example assumes a component located at `src/components/MyComponent.vue`: 10 | 11 | ```vue 12 | 17 | 18 | 21 | ``` 22 | 23 | When your template is rendered, the component's import will automatically be inlined, which renders to this: 24 | 25 | ```vue 26 | 31 | 32 | 35 | ``` 36 | -------------------------------------------------------------------------------- /vue3-example/src/components/SimpleDatatable.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 125 | 126 | 155 | -------------------------------------------------------------------------------- /vue3-example/src/components/VuetifyDatatable.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 132 | -------------------------------------------------------------------------------- /vue3-example/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * main.ts 3 | * 4 | * Bootstraps Vuetify and other plugins then mounts the App` 5 | */ 6 | 7 | // Plugins 8 | import { registerPlugins } from '@/plugins' 9 | 10 | // Components 11 | import App from './App.vue' 12 | 13 | // Composables 14 | import { createApp } from 'vue' 15 | 16 | const app = createApp(App) 17 | 18 | registerPlugins(app) 19 | 20 | app.mount('#app') 21 | -------------------------------------------------------------------------------- /vue3-example/src/pages/README.md: -------------------------------------------------------------------------------- 1 | # Pages 2 | 3 | Vue components created in this folder will automatically be converted to navigatable routes. 4 | 5 | Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository. 6 | -------------------------------------------------------------------------------- /vue3-example/src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /vue3-example/src/pages/multiple-vuetify.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /vue3-example/src/pages/simple.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 123 | 124 | 125 | 141 | 142 | -------------------------------------------------------------------------------- /vue3-example/src/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally. 4 | -------------------------------------------------------------------------------- /vue3-example/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/index.ts 3 | * 4 | * Automatically included in `./src/main.ts` 5 | */ 6 | 7 | // Plugins 8 | import vuetify from './vuetify' 9 | import router from '../router' 10 | 11 | // Types 12 | import type { App } from 'vue' 13 | 14 | export function registerPlugins (app: App) { 15 | app 16 | .use(vuetify) 17 | .use(router) 18 | } 19 | -------------------------------------------------------------------------------- /vue3-example/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/vuetify.ts 3 | * 4 | * Framework documentation: https://vuetifyjs.com` 5 | */ 6 | 7 | // Styles 8 | import '@mdi/font/css/materialdesignicons.css' 9 | import 'vuetify/styles' 10 | 11 | // Composables 12 | import { createVuetify } from 'vuetify' 13 | 14 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 15 | export default createVuetify({ 16 | theme: {}, 17 | }) 18 | -------------------------------------------------------------------------------- /vue3-example/src/router/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * router/index.ts 3 | * 4 | * Automatic routes for `./src/pages/*.vue` 5 | */ 6 | 7 | // Composables 8 | import { createRouter, createWebHistory } from 'vue-router/auto' 9 | import { routes } from 'vue-router/auto-routes' 10 | 11 | console.log(routes) 12 | 13 | const router = createRouter({ 14 | history: createWebHistory(import.meta.env.BASE_URL), 15 | routes, 16 | }) 17 | 18 | // Workaround for https://github.com/vitejs/vite/issues/11804 19 | router.onError((err, to) => { 20 | if (err?.message?.includes?.('Failed to fetch dynamically imported module')) { 21 | if (!localStorage.getItem('vuetify:dynamic-reload')) { 22 | console.log('Reloading page to fix dynamic import error') 23 | localStorage.setItem('vuetify:dynamic-reload', 'true') 24 | location.assign(to.fullPath) 25 | } else { 26 | console.error('Dynamic import error, reloading page did not fix it', err) 27 | } 28 | } else { 29 | console.error(err) 30 | } 31 | }) 32 | 33 | router.isReady().then(() => { 34 | localStorage.removeItem('vuetify:dynamic-reload') 35 | }) 36 | 37 | export default router 38 | -------------------------------------------------------------------------------- /vue3-example/src/styles/README.md: -------------------------------------------------------------------------------- 1 | # Styles 2 | 3 | This directory is for configuring the styles of the application. 4 | -------------------------------------------------------------------------------- /vue3-example/src/styles/settings.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * src/styles/settings.scss 3 | * 4 | * Configures SASS variables and Vuetify overwrites 5 | */ 6 | 7 | // https://vuetifyjs.com/features/sass-variables/` 8 | // @use 'vuetify/settings' with ( 9 | // $color-pack: false 10 | // ); 11 | -------------------------------------------------------------------------------- /vue3-example/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | }, 13 | "typeRoots": ["./types", "./node_modules/@types"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vue3-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | // ... 13 | "typeRoots": ["./types", "./node_modules/@types"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vue3-example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"], 18 | "typeRoots": ["./types", "./node_modules/@types"] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vue3-example/typed-router.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️ 5 | // It's recommended to commit this file. 6 | // Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry. 7 | 8 | declare module 'vue-router/auto-routes' { 9 | import type { 10 | RouteRecordInfo, 11 | ParamValue, 12 | ParamValueOneOrMore, 13 | ParamValueZeroOrMore, 14 | ParamValueZeroOrOne, 15 | } from 'vue-router' 16 | 17 | /** 18 | * Route name map generated by unplugin-vue-router 19 | */ 20 | export interface RouteNamedMap { 21 | '/': RouteRecordInfo<'/', '/', Record, Record>, 22 | '/multiple-vuetify': RouteRecordInfo<'/multiple-vuetify', '/multiple-vuetify', Record, Record>, 23 | '/simple': RouteRecordInfo<'/simple', '/simple', Record, Record>, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vue3-example/types/vue-datatable-url-sync.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-datatable-url-sync' { 2 | // Place minimal type declarations here if you know the shape of the module's exports 3 | // For now, you can declare a fallback type for the default export if you're not sure: 4 | const useDatatableUrlSync: (...args: any[]) => any; 5 | export default useDatatableUrlSync; 6 | } -------------------------------------------------------------------------------- /vue3-example/vite.config.mts: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import Components from 'unplugin-vue-components/vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 5 | import ViteFonts from 'unplugin-fonts/vite' 6 | import VueRouter from 'unplugin-vue-router/vite' 7 | 8 | // Utilities 9 | import { defineConfig } from 'vite' 10 | import { fileURLToPath, URL } from 'node:url' 11 | 12 | // https://vitejs.dev/config/ 13 | export default defineConfig({ 14 | plugins: [ 15 | VueRouter(), 16 | Vue({ 17 | template: { transformAssetUrls }, 18 | }), 19 | // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme 20 | Vuetify({ 21 | autoImport: true, 22 | styles: { 23 | configFile: 'src/styles/settings.scss', 24 | }, 25 | }), 26 | Components(), 27 | ViteFonts({ 28 | google: { 29 | families: [ { 30 | name: 'Roboto', 31 | styles: 'wght@100;300;400;500;700;900', 32 | }], 33 | }, 34 | }), 35 | ], 36 | define: { 'process.env': {} }, 37 | resolve: { 38 | alias: { 39 | '@': fileURLToPath(new URL('./src', import.meta.url)), 40 | }, 41 | extensions: [ 42 | '.js', 43 | '.json', 44 | '.jsx', 45 | '.mjs', 46 | '.ts', 47 | '.tsx', 48 | '.vue', 49 | ], 50 | }, 51 | server: { 52 | port: 3000, 53 | }, 54 | css: { 55 | preprocessorOptions: { 56 | sass: { 57 | api: 'modern-compiler', 58 | }, 59 | }, 60 | }, 61 | }) 62 | --------------------------------------------------------------------------------