├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── LICENSE
├── README.md
├── config
├── webpack.config.base.js
├── webpack.config.browser.js
├── webpack.config.common.js
└── webpack.config.dev.js
├── dist
├── vue-async-operations.browser.js
└── vue-async-operations.common.js
├── index.js
├── package-lock.json
├── package.json
└── src
├── async-operations.js
├── index.js
└── mix.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["es2015", { "modules": false }],
4 | "stage-0"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
8 | extends: 'standard',
9 | // required to lint *.vue files
10 | plugins: [
11 | 'html'
12 | ],
13 | env: {
14 | browser: true,
15 | },
16 | // add your custom rules here
17 | 'rules': {
18 | // allow paren-less arrow functions
19 | 'arrow-parens': 0,
20 | // allow async-await
21 | 'generator-star-spacing': 0,
22 | // allow debugger during development
23 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
24 | // trailing comma
25 | 'comma-dangle': ['error', 'always-multiline'],
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Devstark
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-async-operations
2 |
3 | [ ](https://www.npmjs.com/package/vue-async-operations)
4 | [](https://vuejs.org/)
5 |
6 | > Managing async operations statuses in your Vue components
7 |
8 | ### Install
9 |
10 | ```bash
11 | npm install vue-async-operations
12 | ```
13 |
14 | ### Basic usage
15 | ```js
16 | import Vue from 'vue'
17 | import VueAsyncOperations from 'vue-async-operations'
18 |
19 | Vue.use(VueAsyncOperations)
20 | ```
21 |
22 | Then, in your component options, provide an `asyncOperations` object where each key is a name of an async operation and the value is a function that returns `Promise`:
23 |
24 | ```js
25 | //...
26 | asyncOperations: {
27 | someAsyncStuff () {
28 | // return Promise
29 | // ☝️vm instance is binded to function as `this` context
30 | }
31 | }
32 | //...
33 | ```
34 |
35 | Or you can link operation to some method in component:
36 |
37 | ```js
38 | //...
39 | asyncOperations: {
40 | someAsyncStuff: 'someMethodName'
41 | }
42 | //...
43 | ```
44 |
45 | Then, trigger an operation in your component e.g. in `created` hook:
46 |
47 | ```js
48 | //...
49 | created () {
50 | this.$async.someAsyncStuff.$perform()
51 | }
52 | //...
53 | ```
54 |
55 | And use operation performing state in the template:
56 |
57 | ```html
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | ```
76 |
77 | ### Several operations in one
78 |
79 | The function that defines an operation may return an array of promises, e.g.:
80 |
81 | ```js
82 | //...
83 | asyncOperations: {
84 | someCompositAsyncStuff () {
85 | return [
86 | this.someVuexAction(),
87 | this.someAnotherVuexAction()
88 | ]
89 | }
90 | }
91 | //...
92 | ```
93 |
94 | This way, the operation state handler, that placed under the hood of this plugin, will operates via `Promise.all([])` so reactive states of operation will be changed only when last promise will be resolved or rejected.
95 |
96 | Also, you can define as much separate operations as you need:
97 |
98 | ```js
99 | //...
100 | asyncOperations: {
101 | asyncStuff1 () { //... },
102 | asyncStuff2 () { //... },
103 | asyncStuff3 () { //... },
104 | ...
105 | }
106 | //...
107 | ```
108 |
109 | ### Passing args
110 | You can pass arguments to `$perform()` method and receive them in operation function:
111 | ```js
112 | //...
113 | asyncOperations: {
114 | // some operation defined as function
115 | stuff1 (a, b, c) {
116 | console.log(a, b, c) // 1, 2, 3
117 | },
118 | // another operation defined as link to some method of a component
119 | stuff2: 'loadStuff2'
120 | },
121 | methods: {
122 | loadStuff2 (d, e, f) {
123 | console.log(d, e, f) // 4, 5, 6
124 | }
125 | },
126 | created () {
127 | this.$async.stuff1.$perform(1, 2, 3)
128 | this.$async.stuff2.$perform(4, 5, 6)
129 | }
130 | //...
131 | ```
132 |
133 | ### Handle operation result
134 |
135 | The `.$perform()` method of an operation returns `Promise` and passes the result of the original promise into `resolve` and `reject` methods:
136 |
137 | ```js
138 | //...
139 | created () {
140 | this.$async.someAsyncStuff.$perform().then(
141 | result => { // handle a result },
142 | err => { // handle an error },
143 | finally => { // handle the finish of an operation }
144 | )
145 | }
146 | //...
147 | ```
148 |
149 | If your operation returns an array of promises, the result will contain an array of results in the order they were defined in the original array
150 |
151 | Also, you can handle the result directly in operation function:
152 |
153 | ```js
154 | //...
155 | asyncOperations: {
156 | someOperation: 'someMethod'
157 | },
158 | methods: {
159 | someMethod () {
160 | this.$api.someApiCall().then(
161 | result => { // handle the result },
162 | error => { // handle an error }
163 | )
164 | }
165 | },
166 | created () {
167 | this.$async.someOperation.$perform()
168 | }
169 | //...
170 | ```
171 |
172 | ### Operations composing
173 |
174 | If you have several async operations that are leading to some common result and you need to track their reactive statuses separately but also you wanna have an aggregated reactive status for whole batch, you can compose your operations the following way:
175 |
176 | ```js
177 | //...
178 | asyncOperations: {
179 | allAsyncStuff: {
180 | asyncStuff1 () {},
181 | asyncStuff2 () {}
182 | }
183 | }
184 | //...
185 | ```
186 |
187 | Then, use separate and aggregated reactive statuses
188 |
189 | ```html
190 |
191 | Some stuff is still loading...
192 | All stuff loaded
193 |
194 | Stuff 1 is loaded
195 |
196 | Stuff 2 is loaded
197 |
198 | ```
199 |
200 | ### Performing composed operations with passing args
201 |
202 | ```js
203 | //...
204 | asyncOperations: {
205 | allAsyncStuff: {
206 | asyncStuff1 (a, b, c) {
207 | console.log(a, b, c) // 1, 2, 3
208 | },
209 | asyncStuff2 ({a, b, c}) {
210 | console.log(a, b, c) // 4, 5, 6
211 | }
212 | }
213 | },
214 | created () {
215 | this.$async.allAsyncStuff.$perform({
216 | asyncStuff1: [1, 2, 3], // pass args to `asyncStuff1`
217 | asyncStuff2: {a: 4, b: 5, c: 6} // pass args to `asyncStuff2`
218 | })
219 | }
220 | //...
221 | ```
222 |
223 | ### Operation states
224 |
225 | - `$pending`
226 | - `$resolved`
227 | - `$rejected`
228 | - `$err`
229 |
230 | ### Operation methods
231 |
232 | - `$perform`
233 |
234 | ### Plugin options
235 |
236 | You can customize some stuff:
237 |
238 | ```js
239 | Vue.use(VueAsyncOperations, {
240 | mixinPrefix,
241 | dataPropName,
242 | computedPropName,
243 | componentOptionName
244 | })
245 | ```
246 |
247 | - `mixinPrefix` - plugin adds to your application a global mixin which injects property with operations states into the components `data` and according to [official vuejs style guide](https://vuejs.org/v2/style-guide/#Private-property-names-essential) this property is prefixed, but you can change this prefix if it's necessary for some reason
248 |
249 | - `dataPropName` - actually the name of the prop mentioned above
250 |
251 | - `computedPropName` - the name of the computed prop you use for getting acces to operations for getting its states and calling `$perform()` method
252 |
253 | - `componentOptionName` - the name of the component option where you define operations
254 |
255 | Plugin defaults are:
256 | ```
257 | {
258 | mixinPrefix: 'vueAsyncOps_',
259 | dataPropName: 'async',
260 | computedPropName: '$async',
261 | componentOptionName: 'asyncOperations'
262 | }
263 | ```
264 |
--------------------------------------------------------------------------------
/config/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
3 |
4 | var outputFile = 'vue-async-operations'
5 | var globalName = 'VueAsyncOperations'
6 |
7 | var config = require('../package.json')
8 |
9 | module.exports = {
10 | entry: './src/index.js',
11 | module: {
12 | rules: [
13 | {
14 | enforce: 'pre',
15 | test: /\.(js|vue)$/,
16 | loader: 'eslint-loader',
17 | exclude: /node_modules/,
18 | },
19 | {
20 | test: /.js$/,
21 | use: 'babel-loader',
22 | },
23 | {
24 | test: /\.vue$/,
25 | loader: 'vue-loader',
26 | options: {
27 | loaders: {
28 | css: ExtractTextPlugin.extract('css-loader'),
29 | },
30 | },
31 | },
32 | ],
33 | },
34 | plugins: [
35 | new webpack.DefinePlugin({
36 | 'VERSION': JSON.stringify(config.version),
37 | }),
38 | new ExtractTextPlugin(outputFile + '.css'),
39 | ],
40 | }
41 |
--------------------------------------------------------------------------------
/config/webpack.config.browser.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var merge = require('webpack-merge')
3 | var base = require('./webpack.config.base')
4 | var path = require('path')
5 |
6 | var outputFile = 'vue-async-operations'
7 | var globalName = 'VueAsyncOperations'
8 |
9 | module.exports = merge(base, {
10 | output: {
11 | path: path.resolve(__dirname, '../dist'),
12 | filename: outputFile + '.browser.js',
13 | library: globalName,
14 | libraryTarget: 'umd',
15 | },
16 | externals: {
17 | // Put external libraries like lodash here
18 | // With their global name
19 | // Example: 'lodash': '_'
20 | },
21 | plugins: [
22 | new webpack.optimize.UglifyJsPlugin({
23 | compress: {
24 | warnings: true,
25 | },
26 | mangle: false,
27 | }),
28 | ],
29 | })
30 |
--------------------------------------------------------------------------------
/config/webpack.config.common.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack')
2 | var merge = require('webpack-merge')
3 | var base = require('./webpack.config.base')
4 | var path = require('path')
5 |
6 | var outputFile = 'vue-async-operations'
7 | var globalName = 'VueAsyncOperations'
8 |
9 | module.exports = merge(base, {
10 | output: {
11 | path: path.resolve(__dirname, '../dist'),
12 | filename: outputFile + '.common.js',
13 | libraryTarget: 'commonjs2',
14 | },
15 | target: 'node',
16 | externals: {
17 | // Put external libraries like lodash here
18 | // With their package name
19 | // Example: 'lodash': 'lodash'
20 | },
21 | plugins: [
22 | new webpack.optimize.UglifyJsPlugin({
23 | compress: {
24 | warnings: true,
25 | },
26 | mangle: false,
27 | }),
28 | ],
29 | })
30 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var base = require('./webpack.config.base')
3 | var path = require('path')
4 |
5 | var outputFile = 'vue-async-operations'
6 | var globalName = 'VueAsyncOperations'
7 |
8 | module.exports = merge(base, {
9 | output: {
10 | path: path.resolve(__dirname, '../dist'),
11 | filename: outputFile + '.common.js',
12 | library: globalName,
13 | libraryTarget: 'umd',
14 | },
15 | devtool: 'eval-source-map',
16 | })
17 |
--------------------------------------------------------------------------------
/dist/vue-async-operations.browser.js:
--------------------------------------------------------------------------------
1 | !function(root,factory){"object"==typeof exports&&"object"==typeof module?module.exports=factory():"function"==typeof define&&define.amd?define([],factory):"object"==typeof exports?exports.VueAsyncOperations=factory():root.VueAsyncOperations=factory()}(this,function(){return function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={i:moduleId,l:!1,exports:{}};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.l=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.i=function(value){return value},__webpack_require__.d=function(exports,name,getter){__webpack_require__.o(exports,name)||Object.defineProperty(exports,name,{configurable:!1,enumerable:!0,get:getter})},__webpack_require__.n=function(module){var getter=module&&module.__esModule?function(){return module.default}:function(){return module};return __webpack_require__.d(getter,"a",getter),getter},__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=3)}([function(module,__webpack_exports__,__webpack_require__){"use strict";function _toConsumableArray(arr){if(Array.isArray(arr)){for(var i=0,arr2=Array(arr.length);i1&&void 0!==arguments[1]?arguments[1]:{},mergedOptions=_extends({},defaultOptions,options);__WEBPACK_IMPORTED_MODULE_0__async_operations_js__.a.init(mergedOptions),Vue.mixin(__WEBPACK_IMPORTED_MODULE_1__mix_js__.a)}__webpack_exports__.install=install;var __WEBPACK_IMPORTED_MODULE_0__async_operations_js__=__webpack_require__(0),__WEBPACK_IMPORTED_MODULE_1__mix_js__=__webpack_require__(1),_extends=Object.assign||function(target){for(var i=1;i1&&void 0!==arguments[1]?arguments[1]:{},mergedOptions=_extends({},defaultOptions,options);__WEBPACK_IMPORTED_MODULE_0__async_operations_js__.a.init(mergedOptions),Vue.mixin(__WEBPACK_IMPORTED_MODULE_1__mix_js__.a)}Object.defineProperty(__webpack_exports__,"__esModule",{value:!0}),__webpack_exports__.install=install;var __WEBPACK_IMPORTED_MODULE_0__async_operations_js__=__webpack_require__(0),__WEBPACK_IMPORTED_MODULE_1__mix_js__=__webpack_require__(1),_extends=Object.assign||function(target){for(var i=1;i {
2 | return path.split('.').reduce((prev, curr) => {
3 | return prev ? prev[curr] : null
4 | }, target)
5 | }
6 |
7 | export default {
8 | cfg: null,
9 |
10 | init (options) {
11 | this.cfg = {
12 | ...options,
13 | dataPropName: options.mixinPrefix + options.dataPropName,
14 | }
15 | },
16 |
17 | onCreated (vm) {
18 | if (!vm.$options[this.cfg.componentOptionName]) return
19 | this.buildTree(vm, vm.$options[this.cfg.componentOptionName], this.cfg.dataPropName)
20 | },
21 |
22 | buildTree (vm, nodes, target) {
23 | const entries = Object.entries(nodes)
24 | entries.forEach(([key, value]) => {
25 | this.addNode(vm, target, key, value)
26 | })
27 | },
28 |
29 | addNode (vm, target, key, value) {
30 | if (['string', 'function'].includes(typeof value)) this.addOperation(vm, target, key)
31 | if (typeof value === 'object') this.addBatch(vm, target, key, value)
32 | },
33 |
34 | addBatch (vm, path, key, obj) {
35 | const batch = this.createNode(vm, 'batch', path, key)
36 | let target = resolvePath(path, vm.$data)
37 | vm.$set(target, key, batch)
38 | const childrenPath = path + '.' + key
39 | this.buildTree(vm, obj, childrenPath)
40 | },
41 |
42 | addOperation (vm, path, key) {
43 | const single = this.createNode(vm, 'single', path, key)
44 | let target = resolvePath(path, vm.$data)
45 | vm.$set(target, key, single)
46 | },
47 |
48 | /**
49 | * Create data node for an operation or a batch of operations
50 | * @param {String} type
51 | * @param {String} path
52 | * @param {String} key
53 | */
54 | createNode (vm, type, path, key) {
55 | return {
56 | $type: type,
57 | $pending: null,
58 | $resolved: null,
59 | $rejected: null,
60 | $err: null,
61 | $perform: (...args) => {
62 | const relPath = path.split('.').filter(el => el !== this.cfg.dataPropName).join('.')
63 | const nodePath = (relPath === '' ? '' : relPath + '.') + key
64 |
65 | switch (type) {
66 | case 'batch':
67 | return this.execBatch(vm, nodePath, args)
68 | case 'single':
69 | return this.execOperation(vm, nodePath, args)
70 | }
71 | },
72 | }
73 | },
74 |
75 | /**
76 | * Execute a batch of operations
77 | * @todo pass args to children
78 | * @param {String} batchPath
79 | * @param {Array} args
80 | */
81 | execBatch (vm, batchPath, args) {
82 | let state = resolvePath(batchPath, vm.$data[this.cfg.dataPropName])
83 | const batch = resolvePath(batchPath, vm.$options[this.cfg.componentOptionName])
84 |
85 | this.resetStates(vm, state)
86 | return new Promise((resolve, reject) => {
87 | const arr = Object.entries(batch).map(([key, value]) => {
88 | const childPath = batchPath + '.' + key
89 | const child = resolvePath(childPath, vm.$data[this.cfg.dataPropName])
90 | const allArgs = [...args][0]
91 | const operationArgs = !allArgs ? undefined : allArgs[key]
92 | return child.$perform(operationArgs)
93 | })
94 | Promise.all(arr).then(
95 | result => this.handleResolve(vm, state, result, resolve),
96 | err => this.handleReject(vm, state, err, reject)
97 | )
98 | })
99 | },
100 |
101 | /**
102 | * Execute single async operation
103 | * @param {String} funcPath
104 | * @param {Array} args
105 | */
106 | execOperation (vm, funcPath, args) {
107 | let state = resolvePath(funcPath, vm.$data[this.cfg.dataPropName])
108 | const func = resolvePath(funcPath, vm.$options[this.cfg.componentOptionName])
109 |
110 | this.resetStates(vm, state)
111 | return new Promise((resolve, reject) => {
112 | let result
113 | if (typeof func === 'string') result = vm[func](...args)
114 | if (typeof func === 'function') {
115 | result = func.call(vm, ...args)
116 | }
117 |
118 | if (Array.isArray(result)) {
119 | return Promise.all(result).then(
120 | res => this.handleResolve(vm, state, res, resolve),
121 | err => this.handleReject(vm, state, err, reject)
122 | )
123 | }
124 |
125 | if (!result.then) return this.handleResolve(vm, state, result, resolve)
126 |
127 | result.then(
128 | res => this.handleResolve(vm, state, res, resolve),
129 | err => this.handleReject(vm, state, err, reject)
130 | )
131 | })
132 | },
133 |
134 | resetStates (vm, state) {
135 | vm.$set(state, '$err', null)
136 | vm.$set(state, '$rejected', false)
137 | vm.$set(state, '$resolved', false)
138 | vm.$set(state, '$pending', true)
139 | },
140 |
141 | handleResolve (vm, state, result, resolve) {
142 | resolve(result)
143 | vm.$set(state, '$pending', false)
144 | vm.$set(state, '$resolved', true)
145 | },
146 |
147 | handleReject (vm, state, err, reject) {
148 | reject(err)
149 | vm.$set(state, '$pending', false)
150 | vm.$set(state, '$rejected', true)
151 | vm.$set(state, '$err', err)
152 | },
153 | }
154 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import asyncOperations from './async-operations.js'
2 | import mixin from './mix.js'
3 |
4 | const defaultOptions = {
5 | mixinPrefix: 'vueAsyncOps_',
6 | dataPropName: 'async',
7 | computedPropName: '$async',
8 | componentOptionName: 'asyncOperations',
9 | }
10 |
11 | export function install (Vue, options = {}) {
12 | const mergedOptions = {...defaultOptions, ...options}
13 | asyncOperations.init(mergedOptions)
14 | Vue.mixin(mixin)
15 | }
16 |
17 | /* -- Plugin definition & Auto-install -- */
18 | /* You shouldn't have to modify the code below */
19 |
20 | // Plugin
21 | const plugin = {
22 | /* eslint-disable no-undef */
23 | version: VERSION,
24 | install,
25 | }
26 |
27 | export default plugin
28 |
29 | // Auto-install
30 | let GlobalVue = null
31 | if (typeof window !== 'undefined') {
32 | GlobalVue = window.Vue
33 | } else if (typeof global !== 'undefined') {
34 | GlobalVue = global.Vue
35 | }
36 | if (GlobalVue) {
37 | GlobalVue.use(plugin)
38 | }
39 |
--------------------------------------------------------------------------------
/src/mix.js:
--------------------------------------------------------------------------------
1 | import aops from './async-operations.js'
2 | export default {
3 | data () {
4 | return {
5 | [aops.cfg.dataPropName]: {},
6 | }
7 | },
8 | computed: {
9 | $async () {
10 | return this[aops.cfg.dataPropName]
11 | },
12 | },
13 | created () {
14 | aops.onCreated(this)
15 | },
16 | }
17 |
--------------------------------------------------------------------------------