vue-waiting
v0.0.10
Published
Vue Plugin for Global Loader Management
Downloads
330
Maintainers
Readme
vue-waiting helps to manage multiple loading states on the page without any conflict. It's based on a very simple idea that manages an array (or Vuex store optionally) with multiple loading states. The built-in loader component listens its registered loader and immediately become loading state.
⏩Quick Start
Note:
This is a copy of work from VueWait, just add more features, and support typescript.
1. Install:
yarn add vue-waiting
2. Require:
import VueWaiting from 'vue-waiting'
Vue.use(VueWaiting)
new Vue({
// your vue config
waiting: new VueWaiting(),
})
3. Use in Your Components
<template>
<v-waiting for="my list is to load">
<template slot="waiting">
<div>
<img src="loading.gif" alt="loading"/>
Loading the list...
</div>
</template>
<ul>
<li v-for="item in myList">{{ item }}</li>
</ul>
</v-waiting>
</template>
<script>
export default {
data() {
return {
myList: []
}
},
async created() {
// start waiting
this.$waiting.start('my list is to load');
this.myList = await fetch('/my-list-url');
// stop waiting
this.$waiting.end('my list is to load');
},
};
</script>
vue-waiting has more abilities to make the management easier, please read the complete documentation.
▶️Detailed Start
📦 Requirements
- Vue.js (v2.0.0+) < 3.x
🚀 Power Supplies
- Vuex, optionally (v2.0.0+)
🔧 Installation
via CLI:
$ yarn add vue-waiting
# or if you using npm
$ npm install vue-waiting
📖 Usage
import VueWaiting from 'vue-waiting'
Vue.use(VueWaiting) // add VueWaiting as Vue plugin
Then you should register waiting
property (VueWaiting
instance) to the Vue instance:
new Vue({
el: '#app',
store,
waiting: new VueWaiting({
// Defaults values are following:
useVuex: true, // Uses Vuex to manage waiting state
vuexModuleName: 'waiting', // Vuex module name
registerComponent: true, // Registers `v-waiting` component
componentName: 'v-waiting', // <v-waiting> component name, you can set `my-loader` etc.
registerDirective: true, // Registers `v-waiting` directive
directiveName: 'waiting', // <span v-waiting /> directive name, you can set `my-loader` etc.
}),
});
♻️ Usage with Vuex
Simply set useVuex
parameter to true
and optionally override
vuexModuleName
import VueWaiting from 'vue-waiting'
Vue.use(Vuex)
Vue.use(VueWaiting) // add VueWaiting as Vue plugin
Then you should register VueWaiting
module:
new Vue({
el: '#app',
store,
waiting: new VueWaiting({
useVuex: true, // You must pass this option `true` to use Vuex
vuexModuleName: 'vuex-example-module' // It's optional, `waiting` by default.
}),
});
Now VueWaiting
will use Vuex
store for data management which can be traced in Vue DevTools > Vuex
♻️ Usage with Nuxt.js
Add vue-waiting/nuxt
to modules section of nuxt.config.js
export default {
modules: [
// Simple usage
'vue-waiting/nuxt',
// Optionally passing options in module configuration
['vue-waiting/nuxt', { useVuex: true }]
],
// Optionally passing options in module top level configuration
waiting: { useVuex: true }
}
🔁 VueWaiting
Options
You can use this options for customize VueWaiting behavior.
| Option Name | Type | Default | Description |
| ----------- | ---- | ------- | ----------- |
| accessorName
| String
| "$waiting"
| You can change this value to rename the accessor. E.g. if you rename this to $w
, your VueWaiting
methods will be accessible by $w.waits(..)
etc. |
| useVuex
| Boolean
| false
| Use this value for enabling integration with Vuex
store. When this value is true VueWaiting
will store data in Vuex
store and all changes to this data will be made by dispatching actions to store |
| vuexModuleName
| String
| "waiting"
| Name for Vuex
store if useVuex
set to true, otherwise not used. |
| registerComponent
| Boolean
| true
| Registers v-waiting
component. |
| componentName
| String
| "v-waiting"
| Changes v-waiting
component name. |
| registerDirective
| Boolean
| true
| Registers v-waiting
directive. |
| directiveName
| String
| "v-waiting"
| Changes v-waiting
directive name. |
If you change the property name, typescript will not understand your custom property. so you have to manually add below to your
shims-vue.d.ts
at the root of your project.
VueJs standalone
// shims-vue.d.ts
import VueWaiting from 'vue-waiting';
declare module 'vue/types/vue' {
interface Vue {
$custmProperty: VueWaiting;
$custmProperty: VueWaiting;
}
}
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
customProperty?: VueWaiting;
customProperty?: VueWaiting;
}
}
Working with NuxtJs
// shims-vue.d.ts
import VueWaiting from 'vue-waiting';
declare module '@nuxt/types' {
interface Context {
$customProperty: VueWaiting;
$customProperty: VueWaiting;
}
interface NuxtAppOptions {
$customProperty: VueWaiting;
$customProperty: VueWaiting;
}
}
🌈 Global Template Helpers
vue-waiting provides some helpers to you to use in your templates. All features can be obtained from $waiting property in Vue components.
.any
Returns boolean value if any loader exists in page.
<template>
<progress-bar v-if="$waiting.any">Please waiting...</progress-bar>
</template>
.is(loader String | Matcher)
or .waiting(loader String | Matcher)
Returns boolean value if given loader exists in page.
<template>
<progress-bar v-if="$waiting.is('creating user')">Creating User...</progress-bar>
</template>
You can use waiting
alias instead of is
.
<template>
<div v-if="$waiting.waiting('fetching users')">
Fetching users...
</div>
</template>
Also you can use matcher to make it more flexible:
Please see matcher library to see how to use matchers.
<template>
<progress-bar v-if="$waiting.is('creating.*')">Creating something...</progress-bar>
</template>
.is(loaders Array<String | Matcher>)
or .waiting(loaders Array<String | Matcher>)
Returns boolean value if some of given loaders exists in page.
<template>
<progress-bar v-if="$waiting.is(['creating user', 'page loading'])">Creating User...</progress-bar>
</template>
.start(loader String)
Starts the given loader.
<template>
<button @click="$waiting.start('creating user')">Create User</button>
</template>
.end(loader String)
Stops the given loader.
<template>
<button @click="$waiting.end('creating user')">Cancel</button>
</template>
.progress(loader String, current [, total = 100])
Sets the progress of the given loader.
<template>
<progress min="0" max="100" :value="$waiting.percent('downloading')"/>
<button @click="$waiting.progress('downloading', 10)">Set progress to 10</button>
<button @click="$waiting.progress('downloading', 50)">Set progress to 50</button>
<button @click="$waiting.progress('downloading', 50, 200)">Set progress to 50 of 200 (25%)</button>
</template>
Completing the Progress
To complete the progress, current
value should be set bigger than 100
. If you total
is given, current
must be
bigger than total
.
<button @click="$waiting.progress('downloading', 101)">Set as downloaded (101 of 100)</button>
or
<button @click="$waiting.progress('downloading', 5, 6)">Set as downloaded (6 of 5)</button>
.percent(loader String)
Returns the percentage of the given loader.
<template>
<progress min="0" max="100" :value="$waiting.percent('downloading')"/>
</template>
🏹 Directives
You can use directives to make your template cleaner.
v-waiting:visible='"loader name"'
Shows if the given loader is loading.
<template>
<progress-bar v-waiting:visible='"creating user"'>Creating User...</progress-bar>
</template>
v-waiting:hidden='"loader name"'
or v-waiting:visible.not='"loader name"'
Hides if the given loader is loading.
<template>
<main v-waiting:hidden='"creating *"'>Some Content</main>
</template>
v-waiting:disabled='"loader name"'
Sets disabled="disabled"
attribute to element if the given loader is loading.
<template>
<input v-waiting:disabled="'*'" placeholder="Username"/>
<input v-waiting:disabled="'*'" placeholder="Password"/>
</template>
v-waiting:enabled='"loader name"'
or v-waiting:disabled.not='"loader name"'
Removes disabled="disabled"
attribute to element if the given loader is loading.
<template>
<button v-waiting:enabled='"creating user"'>Abort Request</button>
</template>
v-waiting:click.start='"loader name"'
Starts given loader on click.
<template>
<button v-waiting:click.start='"create user"'>Start loader</button>
</template>
v-waiting:click.end='"loader name"'
Ends given loader on click.
<template>
<button v-waiting:click.end='"create user"'>End loader</button>
</template>
v-waiting:toggle='"loader name"'
Toggles given loader on click.
<template>
<button v-waiting:toggle='"flip flop"'>Toggles the loader</button>
</template>
v-waiting:click.progress='["loader name", 80]'
Sets the progress of given loader on click.
<template>
<button v-waiting:click.progress='["downloading", 80]'>Set the "downloading" loader to 80</button>
</template>
🔌 Loading Action and Getter Mappers
vue-waiting provides mapWaitingActions
and mapWaitingGetters
mapper to be used with your Vuex stores.
Let's assume you have a store and async actions called createUser
and updateUser
. It will call the methods you
map and will start loaders while action is resolved.
import {mapWaitingActions, mapWaitingGetters} from 'vue-waiting'
export default {
methods: {
...mapWaitingActions('users', {
getUsers: 'loading users',
createUser: 'creating user',
updateUser: 'updating user',
}),
},
computed: {
...mapWaitingGetters({
somethingWithUsers: [
'loading users',
'creating user',
'updating user',
],
deletingUser: 'deleting user',
}),
}
}
// ...
You can also map action to custom method and customize loader name like in example below:
import {mapWaitingActions, mapWaitingGetters} from 'vue-waiting'
// ...
export default {
methods: {
...mapWaitingActions('users', {
getUsers: { action: 'getUsers', loader: 'loading users' },
createUser: { action: 'createUser', loader: 'creating user' },
createSuperUser: { action: 'createUser', loader: 'creating super user' },
}),
},
}
// ...
There is also possibility to use array as a second argument to mapWaitingActions:
// ...
export default {
methods: {
...mapWaitingActions('users', [
'getUsers',
{ method: 'createUser', action: 'createUser', loader: 'creating user' },
{ method: 'createSuperUser', action: 'createUser', loader: 'creating super user' },
]),
},
}
// ...
☢️Advanced Getters and Actions Usage
The Vuex module name is
waiting
by default. If you've changed on config, you should get it byrootGetters['<vuex module name>/is']
orrootGetters['<vuex module name>/any']
.
You can access vue-waiting
's Vuex getters using rootGetters
in Vuex.
export default {
getters: {
cartOperationInProgress(state, getters, rootState, rootGetters) {
return rootGetters['waiting/is']('cart.*');
}
},
}
And you can start and end loaders using waiting
actions. You must pass root: true
option to the dispatch
method.
export default {
actions: {
async addItemToCart({ dispatch }, item) {
dispatch('waiting/start', 'cart.addItem', { root: true });
await CartService.addItem(item);
dispatch('waiting/end', 'cart.addItem', { root: true });
}
},
}
waitFor(loader String, func Function [,forceSync = false])
Decorator that wraps function, will trigger a loading and will end loader after the original function (func
argument)
is finished.
By default waitFor
return async function, if you want to wrap default sync function pass true
in last argument
Example using with async function
import {waitFor} from 'vue-waiting';
// ...
export default {
methods: {
fetchDataFromApi: waitFor('fetch data', async function () {
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// do work here
await sleep(3000);
// simulate some api call
this.fetchResponse = Math.random()
})
}
}
// ...
See also examples/wrap-example
💧 Using v-waiting
Component
If you disable registerComponent
option then import and add v-waiting
into components
import vLoading from 'vue-waiting/src/components/v-waiting.vue'
export default {
components: {
'v-waiting': vLoading
}
}
In template, you should wrap your content with v-waiting
component to show loading on it.
<template>
<v-waiting for='fetching data'>
<template slot='waiting'>
This will be shown when "fetching data" loader starts.
</template>
This will be shown when "fetching data" loader ends.
</v-waiting>
</template>
Better example for a button
with loading state:
<template>
<button :disabled='$waiting.is("creating user")'>
<v-waiting for='creating user'>
<template slot='waiting'>Creating User...</template>
Create User
</v-waiting>
</button>
</template>
🔁 Transitions
You can use transitions with v-waiting
component.
Just pass <transition>
props and listeners to the v-waiting
with transition
prop.
<template>
<v-waiting for="users"
transition="fade"
mode="out-in"
:duration="1000"
enter-active-class="enter-active"
@leave='someAwesomeFinish()'
>
<template slot="waiting">
<p>Loading...</p>
</template>
My content
</v-waiting>
</template>
⚡️ Making Reusable Loader Components
With reusable loader components, you will be able to use custom loader components as example below. This will allow you to create better user loading experience.
In this example above, the tab gets data from back-end, and the table loads data from back-end at the same time. With vue-waiting, you will be able to manage these two seperated loading processes easily:
<template>
<div>
<v-waiting for="fetching tabs">
<template slot="waiting">
<b-tabs>
<template slot="tabs">
<b-nav-item active="active" disabled>
<v-icon name="circle-o-notch" spin="spin"/>
</b-nav-item>
</template>
</b-tabs>
</template>
<b-tabs>
<template slot="tabs">
<b-nav-item v-for="tab in tabs">{{ tab.name }}</b-nav-item>
</template>
</b-tabs>
</v-waiting>
<v-waiting for="fetching data">
<table-gradient-spinner slot="waiting"/>
<table>
<tr v-for="row in data">
<!-- ...-->
</tr>
</table>
</v-waiting>
</div>
</template>
You may want to design your own reusable loader for your project. You better create a wrapper component
called my-waiter
:
<!-- MySpinner.vue -->
<i18n>
kh:
loading: កំពុងទាញ...
en:
loading: Loading...
</i18n>
<template>
<div class="loading-spinner">
<v-icon name="refresh" spin="spin"/>
<span>{{ $t('loading') }}</span>
</div>
</template>
<style scoped lang="scss">
.loading-spinner {
opacity: 0.5;
margin: 50px auto;
text-align: center;
.fa-icon {
vertical-align: middle;
margin-right: 10px;
}
}
</style>
Now you can use your spinner everywhere using slot='waiting'
attribute:
<template>
<v-waiting for="fetching data">
<my-waiter slot="waiting"/>
<div>
<p>My main content after fetching data...</p>
</div>
</v-waiting>
</template>
📦 Using with external spinner libraries
You can use vue-waiting
with another spinner libraries
like epic-spinners or other libraries. You just need to
add slot="waiting"
to the component and Vue handles rest of the work.
First register the component,
import { OrbitSpinner } from 'epic-spinners';
Vue.component('orbit-spinner', OrbitSpinner);
Then use it in your as a v-waiting
's waiting
slot.
<template>
<v-waiting for='something to load'>
<orbit-spinner
slot='waiting'
:animation-duration="1500"
:size="64"
:color="'#ff1d5e'"
/>
</v-waiting>
</template>
... and done!
For other libraries you can use, please see Loaders section of ** vuejs/awesome-vue**.
🚌 Run example
Use npm run vuex:dev
, npm run vue:dev
or npm run wrap:dev
or npm run transition:dev
commands. for running examples locally.
✔ Testing components
You can test components using vue-waiting
but it requires the configuration. Let's take a basic component for
instance:
<template>
<v-waiting for="loading">
<Spinner slot="waiting"/>
<ul class="suggestions">
<li v-for="suggestion in suggestions">{{ suggestion.name }}</li>
</ul>
</v-waiting>
</template>
<script>
export default {
data() {
return {
suggestions: [{ name: 'Suggest 1' }]
}
},
}
</script>
const localVue = createLocalVue();
localVue.use(Vuex); // optionally when you use Vuex integration
it('uses vue-waiting component', () => {
const wrapper = shallowMount(Suggestions, {localVue});
expect(wrapper.find('.suggestions').exists()).toBeTruthy();
});
vue-test-utils
will replace v-waiting
component with an empty div
, making it difficult to test correctly.
First, make your local Vue instance use vue-waiting
,
const localVue = createLocalVue();
localVue.use(Vuex); // optionally when you use Vuex integration
localVue.use(VueWaiting);
Then inject the waiting
property using VueWaiting
constructor,
it('uses vue-waiting component', () => {
const wrapper = shallowMount(SuggestedAddresses, {
localVue,
waiting: new VueWaiting()
});
expect(wrapper.find('.suggestions').exists()).toBeTruthy(); // it works!
});
🎯 Contributors
- Chantouch Sek, (writer)
🔑 License
MIT © Chantouch Sek