├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bili.config.js ├── example ├── App.vue └── main.js ├── package.json ├── src ├── LoadableMixin.ts ├── callWithHooks.ts ├── loadable.ts ├── mapLoadableMethods.ts └── vue-loadable.ts ├── test ├── LoadableMixin.ts ├── loadable.ts └── vue-loadable.ts ├── tsconfig.json ├── types ├── LoadableMixin.d.ts ├── callWithHooks.d.ts ├── loadable.d.ts ├── mapLoadableMethods.d.ts └── vue-loadable.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Settings for editors and IDEs. 2 | # References at https://editorconfig.org/. 3 | 4 | root = true 5 | 6 | # Settings for any file. 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Treat as text when is possible and ensure UNIX line-endings. 2 | * text=auto eol=lf 3 | 4 | # Ignore differences on Yarn's lockfile. 5 | # Since version 1.0, Yarn automatically handle merge conflicts. 6 | yarn.lock -diff 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Finder settings files (Mac). 2 | .DS_Store 3 | 4 | # Node.js modules. 5 | node_modules/ 6 | 7 | # NPM's lockfile. 8 | # We're using Yarn and it provides it's own lockfile. 9 | npm-lockfile.json 10 | 11 | # Log files. 12 | *.log 13 | *.log.* 14 | 15 | # Distribution sources. 16 | dist/ 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vitor Luiz Cavalcanti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `vue-loadable` 2 | 3 | [![Build Status](https://travis-ci.org/VitorLuizC/vue-loadable.svg?branch=master)](https://travis-ci.org/VitorLuizC/vue-loadable) 4 | [![License](https://badgen.net/github/license/VitorLuizC/vue-loadable)](./LICENSE) 5 | [![Library minified size](https://badgen.net/bundlephobia/min/vue-loadable)](https://bundlephobia.com/result?p=vue-loadable) 6 | [![Library minified + gzipped size](https://badgen.net/bundlephobia/minzip/vue-loadable)](https://bundlephobia.com/result?p=vue-loadable) 7 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FVitorLuizC%2Fvue-loadable.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FVitorLuizC%2Fvue-loadable?ref=badge_shield) 8 | 9 | `vue-loadable` improves your loading state flow by providing methods and helpers to manage it. 10 | 11 | ```html 12 | 18 | 19 | 74 | 75 | 76 | 82 | ``` 83 | 84 | ## Installation on Vue 85 | 86 | `vue-loadable` need to be installed to enable loadable methods, `loadable` decorator and `mapLoadableMethods` helper. 87 | 88 | To install globally, just pass default exported object as argment to `Vue.use`. 89 | 90 | ```js 91 | import Vue from 'vue'; 92 | import Loadable from 'vue-loadable'; 93 | 94 | Vue.use(Loadable); 95 | ``` 96 | 97 | You can install it locally instead with `LoadableMixin` mixin. 98 | 99 | ```vue 100 | 107 | ``` 108 | 109 | ## API 110 | 111 | - **`loadable`** decorates a function to change loading state during its execution. It sets the state as loading when function inits and unsets when it throws an error, when it resolves or when it returns a value. 112 | 113 | > Second argument is the loading state name, is `"unknown"` when it's not defined. 114 | 115 | ```js 116 | Vue.component('SignInForm', { 117 | methods: { 118 | signIn: loadable(async function(name) { 119 | // ... 120 | }, 'signIn'), 121 | }, 122 | 123 | async mounted() { 124 | this.$isLoading('signIn'); 125 | //=> false 126 | 127 | const promise = this.signIn('Vitor'); 128 | 129 | this.$isLoading('signIn'); 130 | //=> true 131 | 132 | await promise; 133 | 134 | this.$isLoading('signIn'); 135 | //=> false 136 | }, 137 | }); 138 | ``` 139 | 140 | > It passes down the function arguments, rejects the errors and resolves the returned value. 141 | > 142 | > ```ts 143 | > async function confirmUsername(username: string): Promise { 144 | > // ... 145 | > } 146 | > 147 | > export default { 148 | > methods: { 149 | > // Returns a function with same signature, but handling loading states. 150 | > confirm: loadable(confirmUsername, 'confirmation'), 151 | > }, 152 | > async mounted(): Promise { 153 | > try { 154 | > const isConfirmed = await this.confirm('VitorLuizC'); 155 | > this.$router.push(isConfirmed ? '/checkout' : '/confirmation'); 156 | > } catch (error) { 157 | > new Rollbar.Error(error).send(); 158 | > } 159 | > }, 160 | > }; 161 | > ``` 162 | 163 |
164 | TypeScript type definitions. 165 | 166 |
167 | 168 | ```ts 169 | type Method = 170 | | ((...args: any[]) => any) 171 | | ((this: Vue, ...args: any[]) => any); 172 | 173 | type LoadableMethod = ( 174 | this: Vue, 175 | ...args: Parameters 176 | ) => ReturnType extends Promise 177 | ? ReturnType 178 | : Promise>; 179 | 180 | const loadable: ( 181 | method: T, 182 | state?: string, 183 | ) => LoadableMethod; 184 | ``` 185 | 186 |
187 | 188 | - **`mapLoadableMethods`** maps methods into loadable ones that triggers loading states, it works pretty well with Vuex. 189 | 190 | > It uses method's names as loading state name. 191 | 192 | ```vue 193 | 201 | 202 | 49 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex, { Store } from 'vuex'; 3 | import App from './App.vue' 4 | import Loadable from '../src/vue-loadable' 5 | 6 | Vue.use(Vuex) 7 | 8 | Vue.use(Loadable) 9 | 10 | const store = new Store({ 11 | actions: { 12 | async w () { 13 | await new Promise((resolve) => setTimeout(resolve, 3000)) 14 | } 15 | } 16 | }) 17 | 18 | new Vue({ 19 | el: '#app', 20 | store, 21 | render: (λ) => λ(App) 22 | }) 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-loadable", 3 | "version": "0.2.0", 4 | "description": "Improves your loading state flow by providing methods and helpers to manage it.", 5 | "cdn": "dist/vue-loadable.umd.js", 6 | "types": "types/vue-loadable.d.ts", 7 | "main": "dist/vue-loadable.js", 8 | "unpkg": "dist/vue-loadable.umd.js", 9 | "module": "dist/vue-loadable.esm.js", 10 | "jsdelivr": "dist/vue-loadable.umd.js", 11 | "umd:main": "dist/vue-loadable.umd.js", 12 | "files": [ 13 | "dist/", 14 | "types/" 15 | ], 16 | "scripts": { 17 | "build": "bili", 18 | "test": "ava", 19 | "serve": "poi --serve example/main.js", 20 | "prepare": "yarn test && yarn build" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/VitorLuizC/vue-loadable.git" 25 | }, 26 | "keywords": [ 27 | "vue", 28 | "vue-mixin", 29 | "loadable", 30 | "load", 31 | "vue-loadable" 32 | ], 33 | "author": { 34 | "url": "https://vitorluizc.github.io/", 35 | "name": "Vitor Cavalcanti", 36 | "email": "vitorluizc@outlook.com" 37 | }, 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/VitorLuizC/vue-loadable/issues" 41 | }, 42 | "homepage": "https://github.com/VitorLuizC/vue-loadable#readme", 43 | "devDependencies": { 44 | "ava": "^2.4.0", 45 | "bili": "^4.8.1", 46 | "poi": "^12.7.3", 47 | "rollup-plugin-typescript2": "^0.24.3", 48 | "ts-node": "^8.4.1", 49 | "typescript": "^3.6.4", 50 | "vue": "^2.6.10", 51 | "vue-template-compiler": "^2.6.10", 52 | "vuex": "^3.1.1" 53 | }, 54 | "ava": { 55 | "require": [ 56 | "ts-node/register" 57 | ], 58 | "extensions": [ 59 | "ts" 60 | ], 61 | "compileEnhancements": false 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/LoadableMixin.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | /** 4 | * A mixin which adds loading states and helpers to Vue components. 5 | * @example ```js 6 | * Vue.component('SignUpForm', { 7 | * mixins: [ LoadableMixin ], 8 | * ..., 9 | * mounted () { 10 | * if (this.$isLoadingAny()) 11 | * console.log('Loading...'); 12 | * } 13 | * })``` 14 | */ 15 | const LoadableMixin = Vue.extend({ 16 | data() { 17 | return { 18 | LOADING_STATES: Object.create(null) as Record, 19 | }; 20 | }, 21 | methods: { 22 | $isLoading(state = 'unknown') { 23 | const value = this.LOADING_STATES[state]; 24 | return !!value && value > 0; 25 | }, 26 | 27 | $isLoadingAny() { 28 | return Object.keys(this.LOADING_STATES).some(this.$isLoading); 29 | }, 30 | 31 | $setLoading(state = 'unknown') { 32 | const value = this.LOADING_STATES[state]; 33 | this.$set(this.LOADING_STATES, state, value ? value + 1 : 1); 34 | }, 35 | 36 | $unsetLoading(state = 'unknown') { 37 | const value = this.LOADING_STATES[state]; 38 | this.$set(this.LOADING_STATES, state, value ? value - 1 : 0); 39 | }, 40 | }, 41 | }); 42 | 43 | export default LoadableMixin; 44 | -------------------------------------------------------------------------------- /src/callWithHooks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Call function and execute its hooks. Executes `onDone` when its done and 3 | * `onError` when it throws an error. 4 | * @param call 5 | * @param onDone 6 | * @param onError 7 | */ 8 | const callWithHooks = ( 9 | call: () => T | Promise, 10 | onDone: () => void, 11 | onError: () => void = onDone, 12 | ): Promise => { 13 | const handleError = (error: unknown) => { 14 | onError(); 15 | return Promise.reject(error); 16 | }; 17 | 18 | try { 19 | return Promise.resolve(call()) 20 | .then((value: T) => { 21 | onDone(); 22 | return Promise.resolve(value); 23 | }) 24 | .catch(handleError); 25 | } catch (error) { 26 | return handleError(error); 27 | } 28 | }; 29 | 30 | export default callWithHooks; 31 | -------------------------------------------------------------------------------- /src/loadable.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import callWithHooks from './callWithHooks'; 3 | 4 | /** 5 | * An union of any function and functions that have access to `this` 6 | * (Vue instance). 7 | */ 8 | export type Method = 9 | | ((...args: any[]) => any) 10 | | ((this: Vue, ...args: any[]) => any); 11 | 12 | /** 13 | * A Higher-order type to trasnform a method into loadable method that have 14 | * access to `this` (Vue instance) and returns a Promise. 15 | */ 16 | export type LoadableMethod = ( 17 | this: Vue, 18 | ...args: Parameters 19 | ) => ReturnType extends Promise 20 | ? ReturnType 21 | : Promise>; 22 | 23 | /** 24 | * Decorate a method to causes loading states changes during its execution. It 25 | * sets state as loading when function is init and unsets on throws an error or 26 | * resolve/return. 27 | * @example 28 | * Vue.component('SignInForm', { 29 | * methods: { 30 | * signIn: loadable(async function ({ email, password }) { 31 | * // ... 32 | * }, 'signIn') 33 | * } 34 | * }); 35 | * @param method - A method, commonly async, which causes loading state changes. 36 | * @param [state] - Loading state name. It's "unknown" if not defined. 37 | */ 38 | const loadable = (method: T, state: string = 'unknown') => 39 | function() { 40 | this.$setLoading(state); 41 | 42 | return callWithHooks( 43 | () => method.apply(this, arguments as any), 44 | () => this.$unsetLoading(state), 45 | ); 46 | } as LoadableMethod; 47 | 48 | export default loadable; 49 | -------------------------------------------------------------------------------- /src/mapLoadableMethods.ts: -------------------------------------------------------------------------------- 1 | import loadable, { Method, LoadableMethod } from './loadable'; 2 | 3 | /** 4 | * Type of an object whose keys are `string` and the values are methods. 5 | */ 6 | export type Methods = Record; 7 | 8 | /** 9 | * A Higher-order type to transform methods into loadable methods. It keeps keys 10 | * as-is, but values have access to `this` (Vue instance) and returns a Promise. 11 | */ 12 | export type LoadableMethods = { 13 | [K in keyof T]: LoadableMethod; 14 | }; 15 | 16 | /** 17 | * Maps an object, whose keys are `string` and the values are methods, to 18 | * loadable methods that triggers loading states. It uses property's keys as 19 | * loading state names. 20 | * @example 21 | * Vue.component('SignInForm', { 22 | * ..., 23 | * methods: { 24 | * onClick() { 25 | * if (this.$isLoading('signIn') || this.$isLoading('signUp')) 26 | * return; 27 | * // ... 28 | * }, 29 | * ...mapLoadableMethods( 30 | * mapActions('authentication', [ 31 | * 'signIn', 32 | * 'signUp' 33 | * ]) 34 | * ) 35 | * } 36 | * }); 37 | */ 38 | const mapLoadableMethods = ( 39 | methods: T, 40 | ): LoadableMethods => { 41 | const names = Object.keys(methods) as (string & keyof T)[]; 42 | 43 | return names.reduce( 44 | (loadableMethods, name) => { 45 | loadableMethods[name] = loadable(methods[name], name); 46 | return loadableMethods; 47 | }, 48 | Object.create(null) as LoadableMethods, 49 | ); 50 | }; 51 | 52 | export default mapLoadableMethods; 53 | -------------------------------------------------------------------------------- /src/vue-loadable.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import LoadableMixin from './LoadableMixin'; 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | /** 7 | * Check if a state is loading. 8 | * @param [state] - Loading state name. 9 | */ 10 | $isLoading(state?: string): boolean; 11 | 12 | /** 13 | * Check if any state is loading. 14 | */ 15 | $isLoadingAny(): boolean; 16 | 17 | /** 18 | * Set state as loading. 19 | * @param [state] - Loading state name. 20 | */ 21 | $setLoading(state?: string): void; 22 | 23 | /** 24 | * Unset state as loading. 25 | * @param [state] - Loading state name. 26 | */ 27 | $unsetLoading(state?: string): void; 28 | } 29 | } 30 | 31 | export { LoadableMixin }; 32 | 33 | export { default as loadable, Method, LoadableMethod } from './loadable'; 34 | 35 | export { 36 | default as mapLoadableMethods, 37 | Methods, 38 | LoadableMethods, 39 | } from './mapLoadableMethods'; 40 | 41 | /** 42 | * Installs LoadableMixin globally. 43 | * @example ```js 44 | * Vue.use(install)``` 45 | * @param Vue - The Vue constructor. 46 | */ 47 | export function install(Vue: VueConstructor): void { 48 | Vue.mixin(LoadableMixin); 49 | } 50 | 51 | export default { install }; 52 | -------------------------------------------------------------------------------- /test/LoadableMixin.ts: -------------------------------------------------------------------------------- 1 | import { LoadableMixin } from '../src/vue-loadable'; 2 | import test from 'ava'; 3 | 4 | const createComponent = () => new ( 5 | LoadableMixin.extend({}) 6 | ); 7 | 8 | // ..:: $isLoading method tests ::.. 9 | 10 | test('$isLoading: it returns false when no state is loading', (context) => { 11 | const Component = createComponent(); 12 | 13 | context.false(Component.$isLoading('state')); 14 | context.false(Component.$isLoading()); // 'unknown' state as default. 15 | }); 16 | 17 | // ..:: $isLoadingAny method tests ::.. 18 | 19 | test('$isLoadingAny: it returns false when no state is loading', (context) => { 20 | const Component = createComponent(); 21 | 22 | context.false(Component.$isLoadingAny()); 23 | }); 24 | 25 | test('$isLoadingAny: it checks if any state is loading', (context) => { 26 | const Component = createComponent(); 27 | 28 | context.false(Component.$isLoading('x')); // State 'x' and 'y' aren't loading 29 | context.false(Component.$isLoading('y')); // so any need to be false. 30 | context.false(Component.$isLoadingAny()); 31 | 32 | Component.$setLoading('x'); 33 | Component.$setLoading('y'); 34 | 35 | context.true(Component.$isLoading('x')); // State 'x' and 'y' are loading 36 | context.true(Component.$isLoading('y')); // so any need to be true. 37 | context.true(Component.$isLoadingAny()); 38 | 39 | Component.$unsetLoading('x'); 40 | 41 | context.false(Component.$isLoading('x')); // State 'y' is loading but not 'x' 42 | context.true(Component.$isLoading('y')); // so any need to be true. 43 | context.true(Component.$isLoadingAny()); 44 | }); 45 | 46 | // ..:: $setLoading method tests ::.. 47 | 48 | test('$setLoading: set state as loading', (context) => { 49 | const Component = createComponent(); 50 | 51 | context.false(Component.$isLoading('A')); 52 | context.false(Component.$isLoading()); 53 | 54 | Component.$setLoading('A'); 55 | Component.$setLoading(); // 'unknown' state as default. 56 | 57 | context.true(Component.$isLoading('A')); 58 | context.true(Component.$isLoading()); 59 | }); 60 | 61 | test('$setLoading: setted states are accumulative, can be setted N times', (context) => { 62 | const Component = createComponent(); 63 | 64 | context.false(Component.$isLoading('A')); 65 | 66 | Component.$setLoading('A'); 67 | Component.$setLoading('A'); 68 | 69 | context.true(Component.$isLoading('A')); 70 | 71 | Component.$unsetLoading('A'); 72 | 73 | context.true(Component.$isLoading('A')); // Still loading because it 74 | // accumulates 2 loading states. 75 | Component.$unsetLoading('A'); 76 | 77 | context.false(Component.$isLoading('A')); 78 | }); 79 | 80 | // ..:: $unsetLoading method tests ::.. 81 | 82 | test('$unsetLoading: unset state as loading', (context) => { 83 | const Component = createComponent(); 84 | 85 | Component.$setLoading('A'); 86 | Component.$setLoading(); 87 | 88 | context.true(Component.$isLoading('A')); 89 | context.true(Component.$isLoading()); 90 | 91 | Component.$unsetLoading('A'); 92 | Component.$unsetLoading(); // 'unknown' state as default. 93 | 94 | context.false(Component.$isLoading('A')); 95 | context.false(Component.$isLoading()); 96 | }); 97 | 98 | test('$unsetLoading: unset can\'t accumulate negative states', (context) => { 99 | const Component = createComponent(); 100 | 101 | Component.$setLoading('A'); 102 | 103 | context.true(Component.$isLoading('A')); 104 | 105 | Component.$unsetLoading('A'); 106 | Component.$unsetLoading('A'); 107 | 108 | context.false(Component.$isLoading('A')); 109 | 110 | Component.$setLoading('A'); 111 | 112 | context.true(Component.$isLoading('A')); // It changes because it can't 113 | }); // accumulate negative loading states 114 | -------------------------------------------------------------------------------- /test/loadable.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { loadable, LoadableMixin } from '../src/vue-loadable'; 3 | 4 | test('loadable: preserve function arguments and returned value, but in a promise', async (context) => { 5 | const Component = new ( 6 | LoadableMixin.extend({ 7 | methods: { 8 | // The function arguments are preserved, but returned value is wrapped 9 | // in a Promise. 10 | getParams: loadable((...args: any[]) => args, 'arguments') 11 | } 12 | }) 13 | ); 14 | 15 | const params = ['Vitor', 22, 'Samanta', 1.66, false, [1,2,3]]; 16 | 17 | context.deepEqual(params, await Component.getParams(...params)); 18 | }); 19 | 20 | const sleep = (time: number): Promise => { 21 | return new Promise((resolve) => setTimeout(resolve, time)); 22 | }; 23 | 24 | test('loadable: set loading on init and unset on resolve or reject', async (context) => { 25 | const Component = new ( 26 | LoadableMixin.extend({ 27 | methods: { 28 | x: loadable(function (): Promise { // This returns a Promise 29 | return sleep(1000) // just to show it works same 30 | .then(() => 1); // way as async/await fns. 31 | }, 'x'), 32 | y: loadable(async function () { 33 | await sleep(1000); 34 | throw new Error('Y'); 35 | }, 'y') 36 | } 37 | }) 38 | ); 39 | 40 | context.false(Component.$isLoading('x')); 41 | 42 | const promiseX = Component.x(); 43 | 44 | context.true(Component.$isLoading('x')); 45 | 46 | context.is(await promiseX, 1); 47 | context.false(Component.$isLoading('x')); 48 | 49 | context.false(Component.$isLoading('y')); 50 | 51 | const promiseY = Component.y(); 52 | 53 | context.true(Component.$isLoading('y')); 54 | 55 | await context.throwsAsync(() => promiseY, Error, 'Y'); 56 | context.false(Component.$isLoading('y')); 57 | }); 58 | -------------------------------------------------------------------------------- /test/vue-loadable.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import test from 'ava'; 3 | import Loadable, { install, LoadableMixin, loadable } from '../src/vue-loadable'; 4 | 5 | // ..:: API tests ::.. 6 | 7 | test('API: it default exports an object with install function', (context) => { 8 | context.truthy(Loadable); 9 | context.is(typeof Loadable, 'object'); 10 | context.is(typeof Loadable.install, 'function'); 11 | }); 12 | 13 | test('API: it named export install function, same as Loadable one', (context) => { 14 | context.is(typeof install, 'function'); 15 | context.is(install, Loadable.install); 16 | }); 17 | 18 | test('API: it named export LoadableMixin', (context) => { 19 | context.truthy(LoadableMixin); 20 | 21 | const methods = (LoadableMixin as any).options.methods; 22 | 23 | context.is(typeof methods.$isLoading, 'function'); 24 | context.is(typeof methods.$isLoadingAny, 'function'); 25 | context.is(typeof methods.$setLoading, 'function'); 26 | context.is(typeof methods.$unsetLoading, 'function'); 27 | 28 | const state = (LoadableMixin as any).options.data(); 29 | 30 | context.truthy(state.LOADING_STATES); 31 | context.is(typeof state.LOADING_STATES, 'object'); 32 | }); 33 | 34 | test('API: it named export loadable decorator', (context) => { 35 | context.is(typeof loadable, 'function'); 36 | }); 37 | 38 | // ..:: Installation tests ::.. 39 | 40 | test('Install: Vue components can extend LoadableMixin', (context) => { 41 | const SignInForm = Vue.component('SignInForm', { 42 | mixins: [ LoadableMixin ] 43 | }); 44 | 45 | const methods = (SignInForm as any).options.methods; 46 | 47 | context.is(typeof methods.$isLoading, 'function'); 48 | context.is(typeof methods.$isLoadingAny, 'function'); 49 | context.is(typeof methods.$setLoading, 'function'); 50 | context.is(typeof methods.$unsetLoading, 'function'); 51 | 52 | const state = (SignInForm as any).options.data(); 53 | 54 | context.truthy(state.LOADING_STATES); 55 | context.is(typeof state.LOADING_STATES, 'object'); 56 | }); 57 | 58 | test('Install: Loadable usage on Vue install globally (on every components)', (context) => { 59 | Vue.use(Loadable); 60 | 61 | const SignUpForm = Vue.component('SignUpForm', {}); 62 | 63 | const methods = (SignUpForm as any).options.methods; 64 | 65 | context.is(typeof methods.$isLoading, 'function'); 66 | context.is(typeof methods.$isLoadingAny, 'function'); 67 | context.is(typeof methods.$setLoading, 'function'); 68 | context.is(typeof methods.$unsetLoading, 'function'); 69 | 70 | const state = (SignUpForm as any).options.data(); 71 | 72 | context.truthy(state.LOADING_STATES); 73 | context.is(typeof state.LOADING_STATES, 'object'); 74 | }); 75 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "esnext", 5 | "lib": ["esnext", "dom"], 6 | 7 | // Module configuration. 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | 11 | // Declaration configuration. 12 | "declaration": true, 13 | "declarationDir": "types/" 14 | }, 15 | "exclude": [ 16 | "test/", 17 | "types/" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /types/LoadableMixin.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | /** 3 | * A mixin which adds loading states and helpers to Vue components. 4 | * @example ```js 5 | * Vue.component('SignUpForm', { 6 | * mixins: [ LoadableMixin ], 7 | * ..., 8 | * mounted () { 9 | * if (this.$isLoadingAny()) 10 | * console.log('Loading...'); 11 | * } 12 | * })``` 13 | */ 14 | declare const LoadableMixin: import("vue").VueConstructor<{ 15 | LOADING_STATES: Record; 16 | } & { 17 | $isLoading(state?: string): boolean; 18 | $isLoadingAny(): boolean; 19 | $setLoading(state?: string): void; 20 | $unsetLoading(state?: string): void; 21 | } & Record & Vue>; 22 | export default LoadableMixin; 23 | -------------------------------------------------------------------------------- /types/callWithHooks.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Call function and execute its hooks. Executes `onDone` when its done and 3 | * `onError` when it throws an error. 4 | * @param call 5 | * @param onDone 6 | * @param onError 7 | */ 8 | declare const callWithHooks: (call: () => T | Promise, onDone: () => void, onError?: () => void) => Promise; 9 | export default callWithHooks; 10 | -------------------------------------------------------------------------------- /types/loadable.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | /** 3 | * An union of any function and functions that have access to `this` 4 | * (Vue instance). 5 | */ 6 | export declare type Method = ((...args: any[]) => any) | ((this: Vue, ...args: any[]) => any); 7 | /** 8 | * A Higher-order type to trasnform a method into loadable method that have 9 | * access to `this` (Vue instance) and returns a Promise. 10 | */ 11 | export declare type LoadableMethod = (this: Vue, ...args: Parameters) => ReturnType extends Promise ? ReturnType : Promise>; 12 | /** 13 | * Decorate a method to causes loading states changes during its execution. It 14 | * sets state as loading when function is init and unsets on throws an error or 15 | * resolve/return. 16 | * @example 17 | * Vue.component('SignInForm', { 18 | * methods: { 19 | * signIn: loadable(async function ({ email, password }) { 20 | * // ... 21 | * }, 'signIn') 22 | * } 23 | * }); 24 | * @param method - A method, commonly async, which causes loading state changes. 25 | * @param [state] - Loading state name. It's "unknown" if not defined. 26 | */ 27 | declare const loadable: (method: T, state?: string) => LoadableMethod; 28 | export default loadable; 29 | -------------------------------------------------------------------------------- /types/mapLoadableMethods.d.ts: -------------------------------------------------------------------------------- 1 | import { Method, LoadableMethod } from './loadable'; 2 | /** 3 | * Type of an object whose keys are `string` and the values are methods. 4 | */ 5 | export declare type Methods = Record; 6 | /** 7 | * A Higher-order type to transform methods into loadable methods. It keeps keys 8 | * as-is, but values have access to `this` (Vue instance) and returns a Promise. 9 | */ 10 | export declare type LoadableMethods = { 11 | [K in keyof T]: LoadableMethod; 12 | }; 13 | /** 14 | * Maps an object, whose keys are `string` and the values are methods, to 15 | * loadable methods that triggers loading states. It uses property's keys as 16 | * loading state names. 17 | * @example 18 | * Vue.component('SignInForm', { 19 | * ..., 20 | * methods: { 21 | * onClick() { 22 | * if (this.$isLoading('signIn') || this.$isLoading('signUp')) 23 | * return; 24 | * // ... 25 | * }, 26 | * ...mapLoadableMethods( 27 | * mapActions('authentication', [ 28 | * 'signIn', 29 | * 'signUp' 30 | * ]) 31 | * ) 32 | * } 33 | * }); 34 | */ 35 | declare const mapLoadableMethods: >(methods: T) => LoadableMethods; 36 | export default mapLoadableMethods; 37 | -------------------------------------------------------------------------------- /types/vue-loadable.d.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import LoadableMixin from './LoadableMixin'; 3 | declare module 'vue/types/vue' { 4 | interface Vue { 5 | /** 6 | * Check if a state is loading. 7 | * @param [state] - Loading state name. 8 | */ 9 | $isLoading(state?: string): boolean; 10 | /** 11 | * Check if any state is loading. 12 | */ 13 | $isLoadingAny(): boolean; 14 | /** 15 | * Set state as loading. 16 | * @param [state] - Loading state name. 17 | */ 18 | $setLoading(state?: string): void; 19 | /** 20 | * Unset state as loading. 21 | * @param [state] - Loading state name. 22 | */ 23 | $unsetLoading(state?: string): void; 24 | } 25 | } 26 | export { LoadableMixin }; 27 | export { default as loadable, Method, LoadableMethod } from './loadable'; 28 | export { default as mapLoadableMethods, Methods, LoadableMethods, } from './mapLoadableMethods'; 29 | /** 30 | * Installs LoadableMixin globally. 31 | * @example ```js 32 | * Vue.use(install)``` 33 | * @param Vue - The Vue constructor. 34 | */ 35 | export declare function install(Vue: VueConstructor): void; 36 | declare const _default: { 37 | install: typeof install; 38 | }; 39 | export default _default; 40 | --------------------------------------------------------------------------------