Multiple Process Loader Management forVueand (optionally)Vuex.
Read theMedium post "Managing Complex Waiting Experiences on Web UIs".
vue-waithelps to manage multiple loading states on the page without any conflict. It's based on avery simple ideathat manages an array (or Vuex store optionally) with multiple loading states. Thebuilt-in loader componentlistens its registered loader and immediately become loading state.
If you are atry and learndeveloper, you can start trying thevue-waitnow usingcodesandbox.io.
yarn add vue-wait
importVueWaitfrom'vue-wait'
Vue.use(VueWait)
newVue({
// your vue config
wait:newVueWait(),
})
import{createApp}from'vue'
import{createVueWait}from'vue-wait'
importAppfrom'./App.vue'
constVueWait=createVueWait()
createApp(App)// Create app with root component
.use(VueWait)// Register vue-wait
.mount('#app')
<template>
<v-waitfor="my list is to load">
<templateslot="waiting">
<div>
<imgsrc="loading.gif"/>
Loading the list...
</div>
</template>
<ul>
<liv-for="iteminmyList">{{ item }}</li>
</ul>
</v-wait>
</template>
<script>
exportdefault{
data() {
return{
myList:[]
}
},
asynccreated() {
//start waiting
this.$wait.start('my list is to load');
this.myList=awaitfetch('/my-list-url');
//stop waiting
this.$wait.end('my list is to load');
},
};
</script>
vue-wait has more abilities to make the management easier, please read the complete documentation.
- Vue.js(v2.0.0+)
- Vuex,optionally (v2.0.0+)
via CLI:
$ yarn add vue-wait
#or if you using npm
$ npm install vue-wait
via Vue UI:
importVueWaitfrom'vue-wait'
Vue.use(VueWait)// add VueWait as Vue plugin
Then you should registerwait
property (VueWait
instance) to the Vue instance:
newVue({
el:'#app',
store,
wait:newVueWait({
// Defaults values are following:
useVuex:false,// Uses Vuex to manage wait state
vuexModuleName:'wait',// Vuex module name
registerComponent:true,// Registers `v-wait` component
componentName:'v-wait',// <v-wait> component name, you can set `my-loader` etc.
registerDirective:true,// Registers `v-wait` directive
directiveName:'wait',// <span v-wait /> directive name, you can set `my-loader` etc.
}),
});
Simply setuseVuex
parameter totrue
and optionally override
vuexModuleName
importVueWaitfrom'vue-wait'
Vue.use(Vuex)
Vue.use(VueWait)// add VueWait as Vue plugin
Then you should registerVueWait
module:
newVue({
el:'#app',
store,
wait:newVueWait({
useVuex:true,// You must pass this option `true` to use Vuex
vuexModuleName:'vuex-example-module'// It's optional, `wait` by default.
}),
});
NowVueWait
will useVuex
store for data management which can be traced inVue DevTools > Vuex
Addvue-wait/nuxt
to modules section ofnuxt.config.js
{
modules:[
// Simple usage
'vue-wait/nuxt'
// Optionally passing options in module configuration
['vue-wait/nuxt',{useVuex:true}]
],
// Optionally passing options in module top level configuration
wait:{useVuex:true}
}
You can use this options for customize VueWait behavior.
Option Name | Type | Default | Description |
---|---|---|---|
accessorName |
String |
"$wait" |
You can change this value to rename the accessor. E.g. if you rename this to$w ,yourVueWait methods will be accessible by$w.waits(..) etc. |
useVuex |
Boolean |
false |
Use this value for enabling integration withVuex store. When this value is trueVueWait will store data inVuex store and all changes to this data will be made by dispatching actions to store |
vuexModuleName |
String |
"wait" |
Name forVuex store ifuseVuex set to true, otherwise not used. |
registerComponent |
Boolean |
true |
Registersv-wait component. |
componentName |
String |
"v-wait" |
Changesv-wait component name. |
registerDirective |
Boolean |
true |
Registersv-wait directive. |
directiveName |
String |
"v-wait" |
Changesv-wait directive name. |
vue-waitprovides some helpers to you to use in your templates. All features can be obtained from $wait property in Vue components.
Returns boolean value if any loader exists in page.
<template>
<progress-barv-if="$wait.any">Please wait...</progress-bar>
</template>
Returns boolean value if given loader exists in page.
<template>
<progress-barv-if="$wait.is('creating user')">Creating User...</progress-bar>
</template>
You can usewaiting
alias instead ofis
.
<template>
<divv-if="$wait.waiting('fetching users')">
Fetching users...
</div>
</template>
Also you can use matcher to make it more flexible:
Please seematcherlibrary to see how to use matchers.
<template>
<progress-barv-if="$wait.is('creating.*')">Creating something...</progress-bar>
</template>
Returns boolean value if some of given loaders exists in page.
<template>
<progress-barv-if="$wait.is(['creating user','page loading'])">Creating User...</progress-bar>
</template>
Starts the given loader.
<template>
<button@click="$wait.start('creating user')">Create User</button>
</template>
Stops the given loader.
<template>
<button@click="$wait.end('creating user')">Cancel</button>
</template>
Sets the progress of the given loader.
<template>
<progressmin="0"max="100":value="$wait.percent('downloading')"/>
<button@click="$wait.progress('downloading',10)">Set progress to 10</button>
<button@click="$wait.progress('downloading',50)">Set progress to 50</button>
<button@click="$wait.progress('downloading',50,200)">Set progress to 50 of 200 (25%)</button>
</template>
To complete the progress,current
value should be set bigger than100
.
If youtotal
is given,current
must be bigger thantotal
.
<button@click="$wait.progress('downloading',101)">Set as downloaded (101 of 100)</button>
or
<button@click="$wait.progress('downloading',5,6)">Set as downloaded (6 of 5)</button>
Returns the percentage of the given loader.
<template>
<progressmin="0"max="100":value="$wait.percent('downloading')"/>
</template>
You can use directives to make your template cleaner.
Shows if the given loader is loading.
<template>
<progress-barv-wait:visible='"creating user"'>Creating User...</progress-bar>
</template>
v-wait:hidden=' "loader name" '
orv-wait:visible.not=' "loader name" '
Hides if the given loader is loading.
<template>
<mainv-wait:hidden='"creating *"'>Some Content</main>
</template>
Setsdisabled= "disabled"
attribute to element if the given loader is loading.
<template>
<inputv-wait:disabled="'*'"placeholder="Username"/>
<inputv-wait:disabled="'*'"placeholder="Password"/>
</template>
Removesdisabled= "disabled"
attribute to element if the given loader is loading.
<template>
<buttonv-wait:enabled='"creating user"'>Abort Request</button>
</template>
Starts given loader on click.
<template>
<buttonv-wait:click.start='"create user"'>Start loader</button>
</template>
Ends given loader on click.
<template>
<buttonv-wait:click.end='"create user"'>End loader</button>
</template>
Toggles given loader on click.
<template>
<buttonv-wait:toggle='"flip flop"'>Toggles the loader</button>
</template>
Sets the progress of given loader on click.
<template>
<buttonv-wait:click.progress='["downloading",80]'>Set the "downloading" loader to 80</button>
</template>
vue-waitprovidesmapWaitingActions
andmapWaitingGetters
mapper to be used with your Vuex stores.
Let's assume you have a store and asyncactions calledcreateUser
andupdateUser
.
It will call the methods you map and will start loaders while action is resolved.
import{mapWaitingActions,mapWaitingGetters}from'vue-wait'
//...
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 mapactionto custom method and customize loader name like in example below:
import{mapWaitingActions,mapWaitingGetters}from'vue-wait'
//...
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:
//...
methods:{
...mapWaitingActions('users',[
'getUsers',
{method:'createUser',action:'createUser',loader:'creating user'},
{method:'createSuperUser',action:'createUser',loader:'creating super user'},
]),
},
//...
The Vuex module name is
wait
by default. If you've changed on config, you should get it byrootGetters['<vuex module name>/is']
orrootGetters['<vuex module name>/any']
.
You can accessvue-wait
's Vuex getters usingrootGetters
in Vuex.
getters:{
cartOperationInProgress(state,getters,rootState,rootGetters){
returnrootGetters['wait/is']('cart.*');
}
},
And you can start and end loaders usingwait
actions. You must passroot: true
option to thedispatch
method.
actions:{
asyncaddItemToCart({dispatch},item){
dispatch('wait/start','cart.addItem',{root:true});
awaitCartService.addItem(item);
dispatch('wait/end','cart.addItem',{root:true});
}
},
Decorator that wraps function, will trigger a loading and will end loader after the original function (func
argument) is finished.
By defaultwaitFor
return async function, if you want to wrap default sync function passtrue
in last argument
Example using with async function
import{waitFor}from'vue-wait';
...
methods:{
fetchDataFromApi:waitFor('fetch data',asyncfunction(){
functionsleep(ms){
returnnewPromise(resolve=>setTimeout(resolve,ms));
}
// do work here
awaitsleep(3000);
// simulate some api call
this.fetchResponse=Math.random()
})
}
...
See alsoexamples/wrap-example
If you disableregisterComponent
option then import and addv-wait
into components
importvLoadingfrom'vue-wait/src/components/v-wait.vue'
components:{
'v-wait':vLoading
}
In template, you should wrap your content withv-wait
component to show loading on it.
<v-waitfor='fetching data'>
<templateslot='waiting'>
This will be shown when "fetching data" loader starts.
</template>
This will be shown when "fetching data" loader ends.
</v-wait>
Better example for abutton
with loading state:
<button:disabled='$wait.is("creating user")'>
<v-waitfor='creating user'>
<templateslot='waiting'>Creating User...</template>
Create User
</v-wait>
</button>
You can use transitions withv-wait
component.
Just pass<transition>
props and listeners to thev-wait
withtransition
prop.
<v-waitfor="users"
transition="fade"
mode="out-in"
:duration="1000"
enter-active-class="enter-active"
@leave='someAwesomeFinish()'
>
<templateslot="waiting">
<p>Loading...</p>
</template>
My content
</v-wait>
With reusable loader components, you will be able to use custom loader components as example below. This will allow you to create betteruser loading experience.
In this example above, thetab gets data from back-end,and thetable loads data from back-end at the same time.Withvue-wait,you will be able to manage these two seperated loading processes easily:
<templatelang='pug'>
<div>
<v-waitfor="fetching tabs">
<templateslot="waiting">
<b-tabs>
<templateslot="tabs">
<b-nav-itemactive="active"disabled>
<v-iconname="circle-o-notch"spin="spin"/>
</b-nav-item>
</template>
</b-tabs>
</template>
<b-tabs>
<templateslot="tabs">
<b-nav-itemv-for="tab in tabs">{{ tab.name }}</b-nav-item>
</template>
</b-tabs>
</v-wait>
<v-waitfor="fetching data">
<table-gradient-spinnerslot="waiting"/>
<table>
<trv-for="row in data">
<!--...-->
</tr>
</table>
</v-wait>
</div>
</template>
You may want to design your own reusable loader for your project. You better create a wrapper component calledmy-waiter
:
<!--MySpinner.vue-->
<i18n>
tr:
loading: Yükleniyor...
en:
loading: Loading...
</i18n>
<template>
<divclass="loading-spinner">
<v-iconname="refresh"spin="spin"/>
<span>{{$t('loading') }}</span>
</div>
</template>
<stylescopedlang="scss">
.loading-spinner{
opacity:0.5;
margin:50pxauto;
text-align:center;
.fa-icon{
vertical-align:middle;
margin-right:10px;
}
}
</style>
Now you can use your spinner everywhere usingslot='waiting'
attribute:
<templatelang="pug">
<v-waitfor="fetching data">
<my-waiterslot="waiting"/>
<div>
<p>My main content after fetching data...</p>
</div>
</v-wait>
</template>
You can usevue-wait
with another spinner libraries likeepic-spinnersor other libraries. You just need to addslot= "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 av-wait
'swaiting
slot.
<v-waitfor='something to load'>
<orbit-spinner
slot='waiting'
:animation-duration="1500"
:size="64"
:color="'#ff1d5e'"
/>
</v-wait>
... and done!
For other libraries you can use, please seeLoaders section ofvuejs/awesome-vue.
Usenpm run dev-vuex
,npm run dev-vue
ornpm run dev-wrap
commands.
for running examples locally.
You can test components usingvue-wait
but it requires configuration. Let's take a basic component for instance:
<v-waitfor="loading">
<Spinnerslot="waiting"/>
<ulclass="suggestions">
<liv-for="suggestioninsuggestions">{{suggestion.Name}}</li>
</ul>
</v-wait>
constlocalVue=createLocalVue();
localVue.use(Vuex);// optionally when you use Vuex integration
it('uses vue-wait component',()=>{
constwrapper=shallowMount(Suggestions,{localVue});
expect(wrapper.find('.suggestions').exists()).toBe(true);
});
vue-test-utils
will replacev-wait
component with an emptydiv
,making it difficult to test correctly.
First, make your local Vue instance usevue-wait
,
constlocalVue=createLocalVue();
localVue.use(Vuex);// optionally when you use Vuex integration
localVue.use(VueWait);
Then inject thewait
property usingVueWait
constructor,
it('uses vue-wait component',()=>{
constwrapper=shallowMount(SuggestedAddresses,{
localVue,
wait:newVueWait()
});
expect(wrapper.find('.suggestions').exists()).toBe(true);// it works!
});
Install packages
$ yarn install
#or if you using npm
$ npm install
Bundle it
$ yarn bundle
#or if you using npm
$ npm run bundle
- Fatih Kadir Akın, (creator)
- Igor, (maintainer, made Vuex-free)
Sincevue-waitbased on a very simple idea, it can be implemented on other frameworks.
- react-wait:Multiple Process Loader Management for React.
- dom-wait:Multiple Process Loader Management for vanilla JavaScript.
MIT ©Fatih Kadir Akın