diff --git a/package.json b/package.json index 24f606a..8657b57 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "3.5.0", "description": "Async computed properties for Vue", "main": "dist/vue-async-computed.js", + "types": "src/index.d.ts", "files": [ "bin/", "dist/" diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..7dbcfec --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,39 @@ +import Vue, { PluginFunction } from "vue"; + +export interface IAsyncComputedOptions { + errorHandler?: (error: string[]) => void; + useRawError?: boolean; + default?: any; +} + +export default class AsyncComputed { + constructor(options?: IAsyncComputedOptions) + static install: PluginFunction; + static version: string; +} + +declare module "vue/types/options" { + interface ComputedOptions { + asynchronous?: (() => T | Promise) | { + get: (() => T | Promise); + default?: T; + lazy?: boolean; + }; + } +} + +interface IASyncComputedState { + state: "updating" | "success" | "error"; + updating: boolean; + success: boolean; + error: boolean; + exception: Error | null; + update: () => void; +} + +declare module "vue/types/vue" { + // tslint:disable-next-line:interface-name + interface Vue { + $asyncComputed: {[K: string]: IASyncComputedState }; + } +} diff --git a/src/index.js b/src/index.js index a216e20..9501ee2 100644 --- a/src/index.js +++ b/src/index.js @@ -20,46 +20,42 @@ const AsyncComputed = { Vue.mixin({ beforeCreate () { - const optionData = this.$options.data const asyncComputed = this.$options.asyncComputed || {} this.$asyncComputed = {} + for (const key in this.$options.computed) { + if (this.$options.computed[key].asynchronous) { + asyncComputed[key] = this.$options.computed[key].asynchronous + } + } + if (!Object.keys(asyncComputed).length) return + this.$options.asyncComputed = asyncComputed + if (!this.$options.computed) this.$options.computed = {} for (const key in asyncComputed) { const getter = getterFn(key, this.$options.asyncComputed[key]) this.$options.computed[prefix + key] = getter + const getset = makeLazyComputed(key) + this.$options.computed[key].get = getset.get + this.$options.computed[key].set = getset.set } - - this.$options.data = function vueAsyncComputedInjectedDataFn () { - const data = ( - (typeof optionData === 'function') - ? optionData.call(this) - : optionData - ) || {} - for (const key in asyncComputed) { - const item = this.$options.asyncComputed[key] - if (isComputedLazy(item)) { - initLazy(data, key) - this.$options.computed[key] = makeLazyComputed(key) - } else { - data[key] = null - } - } - return data + }, + data () { + let ret = {} + const asyncComputed = this.$options.asyncComputed || {} + for (const key in asyncComputed) { + initLazy(ret, key) } + return ret }, created () { for (const key in this.$options.asyncComputed || {}) { const item = this.$options.asyncComputed[key], value = generateDefault.call(this, item, pluginOptions) - if (isComputedLazy(item)) { - silentSetLazy(this, key, value) - } else { - this[key] = value - } + silentSetLazy(this, key, value) } for (const key in this.$options.asyncComputed || {}) { @@ -79,7 +75,7 @@ const AsyncComputed = { newPromise.then(value => { if (thisPromise !== promiseId) return setAsyncState(this.$asyncComputed[key], 'success') - this[key] = value + silentSetLazy(this, key, value) }).catch(err => { if (thisPromise !== promiseId) return diff --git a/test/index.js b/test/index.js index 35cfb35..b8805aa 100644 --- a/test/index.js +++ b/test/index.js @@ -7,7 +7,7 @@ let baseErrorCallback = () => { } const pluginOptions = { - errorHandler: msg => baseErrorCallback(msg), + errorHandler: msg => baseErrorCallback(), } Vue.use(AsyncComputed, pluginOptions) @@ -15,16 +15,20 @@ Vue.use(AsyncComputed, pluginOptions) test("Async computed values are computed", t => { t.plan(4) const vm = new Vue({ - asyncComputed: { - a () { - return new Promise(resolve => { - setTimeout(() => resolve('done'), 10) - }) + computed: { + a: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => resolve('done'), 10) + }) + }, }, - b () { - return new Promise(resolve => { - setTimeout(() => resolve(1337), 20) - }) + b: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => resolve(1337), 20) + }) + }, } } }) @@ -41,9 +45,11 @@ test("Async computed values are computed", t => { test("An async computed value which is an pre-resolved promise updates at the next tick", t => { t.plan(2) const vm = new Vue({ - asyncComputed: { - a () { - return Promise.resolve('done') + computed: { + a: { + asynchronous () { + return Promise.resolve('done') + }, } } }) @@ -54,18 +60,18 @@ test("An async computed value which is an pre-resolved promise updates at the ne test("Sync and async computed data work together", t => { t.plan(4) const vm = new Vue({ - asyncComputed: { - a () { - return new Promise(resolve => { - setTimeout(() => resolve('done'), 10) - }) - } - }, computed: { + a: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => resolve('done'), 10) + }) + }, + }, b () { return 0 } - } + }, }) t.equal(vm.a, null) @@ -80,17 +86,21 @@ test("Sync and async computed data work together", t => { test("Async values are properly recalculated", t => { t.plan(6) const vm = new Vue({ - asyncComputed: { - a () { - const data = this.x - return new Promise(resolve => { - setTimeout(() => resolve(data), 10) - }) + computed: { + a: { + asynchronous () { + const data = this.x + return new Promise(resolve => { + setTimeout(() => resolve(data), 10) + }) + }, }, - b () { - return new Promise(resolve => { - setTimeout(() => resolve('done'), 40) - }) + b: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => resolve('done'), 40) + }) + }, } }, data: { @@ -117,11 +127,13 @@ test("Async values are properly recalculated", t => { test("Old async values are properly invalidated", t => { t.plan(2) const vm = new Vue({ - asyncComputed: { - a () { - return new Promise(resolve => { - setTimeout(() => resolve(this.waitTime), this.waitTime) - }) + computed: { + a: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => resolve(this.waitTime), this.waitTime) + }) + }, } }, data: { @@ -157,9 +169,11 @@ test("Having only sync computed data still works", t => { test("Errors in computed properties are handled", t => { t.plan(3) const vm = new Vue({ - asyncComputed: { - a () { - return Promise.reject(new Error('error')) + computed: { + a: { + asynchronous () { + return Promise.reject(new Error('error')) + }, } } }) @@ -175,10 +189,12 @@ test("Errors in computed properties are handled, with useRawError", t => { pluginOptions.useRawError = true t.plan(3) const vm = new Vue({ - asyncComputed: { - a () { - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject('error') + computed: { + a: { + asynchronous () { + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject('error') + }, } } }) @@ -195,21 +211,29 @@ test("Multiple asyncComputed objects are handled the same as normal computed pro t.plan(3) const vm = new Vue({ mixins: [{ - asyncComputed: { - a () { - return Promise.resolve('mixin-a') + computed: { + a: { + asynchronous () { + return Promise.resolve('mixin-a') + }, }, - b () { - return Promise.resolve('mixin-b') + b: { + asynchronous () { + return Promise.resolve('mixin-b') + }, } } }], - asyncComputed: { - a () { - return Promise.resolve('vm-a') + computed: { + a: { + asynchronous () { + return Promise.resolve('vm-a') + }, }, - c () { - return Promise.resolve('vm-c') + c: { + asynchronous () { + return Promise.resolve('vm-c') + }, } } }) @@ -223,24 +247,28 @@ test("Multiple asyncComputed objects are handled the same as normal computed pro test("Async computed values can have defaults", t => { t.plan(6) const vm = new Vue({ - asyncComputed: { + computed: { x: { - default: false, - get () { - return Promise.resolve(true) + asynchronous: { + get () { + return Promise.resolve(true) + }, + default: false, } }, - y () { - return Promise.resolve(true) + y: { + asynchronous () { + return Promise.resolve(true) + }, }, z: { - get () { + asynchronous () { return Promise.resolve(true) - } + }, } } }) - t.equal(vm.x, false, 'x should default to true') + t.equal(vm.x, false, 'x should default to false') t.equal(vm.y, null, 'y doesn\'t have a default') t.equal(vm.z, null, 'z doesn\'t have a default despite being defined with an object') Vue.nextTick(() => { @@ -256,17 +284,21 @@ test("Default values can be functions", t => { data: { x: 1 }, - asyncComputed: { + computed: { y: { - default () { return 2 }, - get () { - return Promise.resolve(3) + asynchronous: { + default () { return 2 }, + get () { + return Promise.resolve(3) + }, } }, z: { - default () { return this.x }, - get () { - return Promise.resolve(4) + asynchronous: { + default () { return this.x }, + get () { + return Promise.resolve(4) + }, } } } @@ -285,12 +317,16 @@ test("Async computed values can be written to, and then will be properly overrid data: { x: 1 }, - asyncComputed: { - y () { - this.y = this.x + 1 - return new Promise(resolve => { - setTimeout(() => resolve(this.x), 10) - }) + computed: { + y: { + asynchronous: { + get () { + this.y = this.x + 1 + return new Promise(resolve => { + setTimeout(() => resolve(this.x), 10) + }) + }, + } } } }) @@ -319,14 +355,16 @@ test("Watchers rerun the computation when a value changes", t => { x: 0, y: 2, }, - asyncComputed: { + computed: { z: { - get () { - return Promise.resolve(i + this.y) - }, - watch () { - // eslint-disable-next-line no-unused-expressions - this.x + asynchronous: { + get () { + return Promise.resolve(i + this.y) + }, + watch () { + // eslint-disable-next-line no-unused-expressions + this.x + }, } } } @@ -358,13 +396,15 @@ test("shouldUpdate controls when to rerun the computation when a value changes", x: 0, y: 2, }, - asyncComputed: { + computed: { z: { - get () { - return Promise.resolve(i + this.y) - }, - shouldUpdate () { - return this.x % 2 === 0 + asynchronous: { + get () { + return Promise.resolve(i + this.y) + }, + shouldUpdate () { + return this.x % 2 === 0 + }, } } } @@ -413,17 +453,19 @@ test("Watchers trigger but shouldUpdate can still block their updates", t => { x: 0, y: 2, }, - asyncComputed: { + computed: { z: { - get () { - return Promise.resolve(i + this.y) - }, - watch () { - // eslint-disable-next-line no-unused-expressions - this.x - }, - shouldUpdate () { - return this.canUpdate + asynchronous: { + get () { + return Promise.resolve(i + this.y) + }, + watch () { + // eslint-disable-next-line no-unused-expressions + this.x + }, + shouldUpdate () { + return this.canUpdate + }, } } } @@ -466,9 +508,13 @@ test("The default default value can be set in the plugin options", t => { t.plan(2) pluginOptions.default = 53 const vm = new Vue({ - asyncComputed: { - x () { - return Promise.resolve(0) + computed: { + x: { + asynchronous: { + get () { + return Promise.resolve(0) + }, + } } } }) @@ -483,9 +529,13 @@ test("The default default value can be set to undefined in the plugin options", t.plan(2) pluginOptions.default = undefined const vm = new Vue({ - asyncComputed: { - x () { - return Promise.resolve(0) + computed: { + x: { + asynchronous: { + get () { + return Promise.resolve(0) + }, + } } } }) @@ -499,9 +549,11 @@ test("The default default value can be set to undefined in the plugin options", test("Handle an async computed value returning synchronously", t => { t.plan(2) const vm = new Vue({ - asyncComputed: { - x () { - return 1 + computed: { + x: { + asynchronous () { + return 1 + }, } } }) @@ -514,13 +566,18 @@ test("Handle an async computed value returning synchronously", t => { test("Work correctly with Vue.extend", t => { t.plan(2) const SubVue = Vue.extend({ - asyncComputed: { - x () { - return Promise.resolve(1) + computed: { + x: { + asynchronous: { + get () { + return Promise.resolve(1) + }, + }, } } }) const vm = new SubVue({}) + console.log(vm.$options.computed.x) t.equal(vm.x, null) Vue.nextTick(() => { @@ -533,12 +590,14 @@ test("Async computed values can be calculated lazily", t => { let called = false const vm = new Vue({ - asyncComputed: { + computed: { a: { - lazy: true, - get () { - called = true - return Promise.resolve(10) + asynchronous: { + lazy: true, + get () { + called = true + return Promise.resolve(10) + }, } } } @@ -564,12 +623,14 @@ test("Async computed values aren't lazy with { lazy: false }", t => { let called = false const vm = new Vue({ - asyncComputed: { + computed: { a: { - lazy: false, - get () { - called = true - return Promise.resolve(10) + asynchronous: { + lazy: false, + get () { + called = true + return Promise.resolve(10) + }, } } } @@ -588,13 +649,15 @@ test("Async computed values can be calculated lazily with a default", t => { let called = false const vm = new Vue({ - asyncComputed: { + computed: { a: { - lazy: true, - default: 3, - get () { - called = true - return Promise.resolve(4) + asynchronous: { + lazy: true, + default: 3, + get () { + called = true + return Promise.resolve(4) + }, } } } @@ -624,26 +687,28 @@ test("Underscore prefixes work (issue #33)", t => { }, _sync_b () { return 2 + }, + _async_a: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => { + resolve(this.sync_a) + this.a_complete = true + }, 10) + }) + }, + }, + async_b: { + asynchronous () { + return new Promise(resolve => { + setTimeout(() => resolve(this._sync_b), 10) + }) + }, } }, data () { return { a_complete: false } }, - asyncComputed: { - _async_a () { - return new Promise(resolve => { - setTimeout(() => { - resolve(this.sync_a) - this.a_complete = true - }, 10) - }) - }, - async_b () { - return new Promise(resolve => { - setTimeout(() => resolve(this._sync_b), 10) - }) - } - } }) t.equal(vm._async_a, null) t.equal(vm.async_b, null) @@ -667,23 +732,27 @@ test("shouldUpdate works with lazy", t => { x: true, y: false, }, - asyncComputed: { + computed: { b: { - lazy: true, - get () { - return Promise.resolve(this.a) - }, - shouldUpdate () { - return this.x + asynchronous: { + lazy: true, + get () { + return Promise.resolve(this.a) + }, + shouldUpdate () { + return this.x + }, } }, c: { - lazy: true, - get () { - return Promise.resolve(this.a) - }, - shouldUpdate () { - return this.y + asynchronous: { + lazy: true, + get () { + return Promise.resolve(this.a) + }, + shouldUpdate () { + return this.y + }, } } } @@ -727,12 +796,16 @@ test("$asyncComputed is empty if there are no async computed properties", t => { test("$asyncComputed[name] is created for all async computed properties", t => { t.plan(15) const vm = new Vue({ - asyncComputed: { - a () { - return Promise.resolve(1) + computed: { + a: { + asynchronous () { + return Promise.resolve(1) + }, }, - b () { - return Promise.resolve(2) + b: { + asynchronous () { + return Promise.resolve(2) + }, } } }) @@ -759,10 +832,12 @@ test("$asyncComputed[name] is created for all async computed properties", t => { test("$asyncComputed[name] handles errors and captures exceptions", t => { t.plan(7) const vm = new Vue({ - asyncComputed: { - a () { - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject('error-message') + computed: { + a: { + asynchronous () { + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject('error-message') + }, } } }) @@ -782,11 +857,13 @@ test("$asyncComputed[name].update triggers re-evaluation", t => { let valueToReturn = 1 t.plan(5) const vm = new Vue({ - asyncComputed: { - a () { - return new Promise(resolve => { - resolve(valueToReturn) - }) + computed: { + a: { + asynchronous () { + return new Promise(resolve => { + resolve(valueToReturn) + }) + }, } } })