Skip to content
/ vue-wait Public

Complex Loader and Progress Management for Vue/Vuex and Nuxt Applications

License

Notifications You must be signed in to change notification settings

f/vue-wait

Repository files navigation

Multiple Process Loader Management forVueand (optionally)Vuex.

Read theMedium post "Managing Complex Waiting Experiences on Web UIs".

npm version


vue-wait

Play with demo above.

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.

⏩Quick Start

If you are atry and learndeveloper, you can start trying thevue-waitnow usingcodesandbox.io.

Edit VueWait Sandbox

1. Install:

yarn add vue-wait

2. Require:

For Vue 2.x

importVueWaitfrom'vue-wait'

Vue.use(VueWait)

newVue({
// your vue config
wait:newVueWait(),
})

For Vue 3.x

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')

3. Use in Your Components

<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.

▶️Detailed Start

📦 Requirements

🚀 Power Supplies

  • Vuex,optionally (v2.0.0+)

🔧 Installation

via CLI:

$ yarn add vue-wait
#or if you using npm
$ npm install vue-wait

via Vue UI:

📖 Usage

importVueWaitfrom'vue-wait'

Vue.use(VueWait)// add VueWait as Vue plugin

Then you should registerwaitproperty (VueWaitinstance) 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.

}),
});

♻️ Usage with Vuex

Simply setuseVuexparameter totrueand optionally override vuexModuleName

importVueWaitfrom'vue-wait'

Vue.use(Vuex)
Vue.use(VueWait)// add VueWait as Vue plugin

Then you should registerVueWaitmodule:

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.
}),
});

NowVueWaitwill useVuexstore for data management which can be traced inVue DevTools > Vuex

♻️ Usage with Nuxt.js

Addvue-wait/nuxtto 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}
}

🔁VueWaitOptions

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,yourVueWaitmethods will be accessible by$w.waits(..)etc.
useVuex Boolean false Use this value for enabling integration withVuexstore. When this value is trueVueWaitwill store data inVuexstore and all changes to this data will be made by dispatching actions to store
vuexModuleName String "wait" Name forVuexstore ifuseVuexset to true, otherwise not used.
registerComponent Boolean true Registersv-waitcomponent.
componentName String "v-wait" Changesv-waitcomponent name.
registerDirective Boolean true Registersv-waitdirective.
directiveName String "v-wait" Changesv-waitdirective name.

🌈 Global Template Helpers

vue-waitprovides some helpers to you to use in your templates. All features can be obtained from $wait property in Vue components.

.any

Returns boolean value if any loader exists in page.

<template>
<progress-barv-if="$wait.any">Please wait...</progress-bar>
</template>

.is(loader String | Matcher)or.waiting(loader String | Matcher)

Returns boolean value if given loader exists in page.

<template>
<progress-barv-if="$wait.is('creating user')">Creating User...</progress-bar>
</template>

You can usewaitingalias 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>

.is(loaders Array<String | Matcher>)or.waiting(loaders Array<String | Matcher>)

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>

.start(loader String)

Starts the given loader.

<template>
<button@click="$wait.start('creating user')">Create User</button>
</template>

.end(loader String)

Stops the given loader.

<template>
<button@click="$wait.end('creating user')">Cancel</button>
</template>

.progress(loader String, current [, total = 100])

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>
Completing the Progress

To complete the progress,currentvalue should be set bigger than100. If youtotalis given,currentmust 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>

.percent(loader String)

Returns the percentage of the given loader.

<template>
<progressmin="0"max="100":value="$wait.percent('downloading')"/>
</template>

🏹 Directives

You can use directives to make your template cleaner.

v-wait:visible=' "loader name" '

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>

v-wait:disabled=' "loader name" '

Setsdisabled= "disabled"attribute to element if the given loader is loading.

<template>
<inputv-wait:disabled="'*'"placeholder="Username"/>
<inputv-wait:disabled="'*'"placeholder="Password"/>
</template>

v-wait:enabled=' "loader name" 'orv-wait:disabled.not=' "loader name" '

Removesdisabled= "disabled"attribute to element if the given loader is loading.

<template>
<buttonv-wait:enabled='"creating user"'>Abort Request</button>
</template>

v-wait:click.start=' "loader name" '

Starts given loader on click.

<template>
<buttonv-wait:click.start='"create user"'>Start loader</button>
</template>

v-wait:click.end=' "loader name" '

Ends given loader on click.

<template>
<buttonv-wait:click.end='"create user"'>End loader</button>
</template>

v-wait:toggle=' "loader name" '

Toggles given loader on click.

<template>
<buttonv-wait:toggle='"flip flop"'>Toggles the loader</button>
</template>

v-wait:click.progress='[ "loader name", 80]'

Sets the progress of given loader on click.

<template>
<buttonv-wait:click.progress='["downloading",80]'>Set the "downloading" loader to 80</button>
</template>

🔌 Loading Action and Getter Mappers

vue-waitprovidesmapWaitingActionsandmapWaitingGettersmapper to be used with your Vuex stores.

Let's assume you have a store and asyncactions calledcreateUserandupdateUser. 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'},
]),
},
//...

☢️Advanced Getters and Actions Usage

The Vuex module name iswaitby 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 usingrootGettersin Vuex.

getters:{
cartOperationInProgress(state,getters,rootState,rootGetters){
returnrootGetters['wait/is']('cart.*');
}
},

And you can start and end loaders usingwaitactions. You must passroot: trueoption to thedispatchmethod.

actions:{
asyncaddItemToCart({dispatch},item){
dispatch('wait/start','cart.addItem',{root:true});
awaitCartService.addItem(item);
dispatch('wait/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 (funcargument) is finished.

By defaultwaitForreturn async function, if you want to wrap default sync function passtruein 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

💧 Usingv-waitComponent

If you disableregisterComponentoption then import and addv-waitinto components

importvLoadingfrom'vue-wait/src/components/v-wait.vue'
components:{
'v-wait':vLoading
}

In template, you should wrap your content withv-waitcomponent 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 abuttonwith loading state:

<button:disabled='$wait.is("creating user")'>
<v-waitfor='creating user'>
<templateslot='waiting'>Creating User...</template>
Create User
</v-wait>
</button>

🔁 Transitions

You can use transitions withv-waitcomponent.

Just pass<transition>props and listeners to thev-waitwithtransitionprop.

<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>

⚡️ 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 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>

📦 Using with external spinner libraries

You can usevue-waitwith 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'swaitingslot.

<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.

🚌 Run example

Usenpm run dev-vuex,npm run dev-vueornpm run dev-wrapcommands. for running examples locally.

✔ Testing components

You can test components usingvue-waitbut 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-utilswill replacev-waitcomponent 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 thewaitproperty usingVueWaitconstructor,

it('uses vue-wait component',()=>{
constwrapper=shallowMount(SuggestedAddresses,{
localVue,
wait:newVueWait()
});
expect(wrapper.find('.suggestions').exists()).toBe(true);// it works!
});

For Development on vue-wait

Install packages

$ yarn install
#or if you using npm
$ npm install

Bundle it

$ yarn bundle
#or if you using npm
$ npm run bundle

🎯 Contributors

  • Fatih Kadir Akın, (creator)
  • Igor, (maintainer, made Vuex-free)

🔗 Other Implementations

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.

🔑 License

MIT ©Fatih Kadir Akın