Skip to content

Add $asyncComputed status property #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,29 @@ new Vue({
}
```

You can trigger re-computation of an async computed property manually, e.g. to re-try if an error occured during evaluation. This should be avoided if you are able to achieve the same result using a watched property.

````js

new Vue({
asyncComputed: {
blogPosts: {
get () {
return Vue.http.get('/posts')
.then(response => response.data)
},
}
},
methods: {
refresh() {
// Triggers an immediate update of blogPosts
// Will work even if an update is in progress.
this.$asyncComputed.blogPosts.update();
}
}
}
````

### Conditional Recalculation

Using `watch` it is possible to run the computed property again but it will run regardless of the
Expand Down Expand Up @@ -323,7 +346,50 @@ new Vue({
}
```

## Error handling
## Computation status

For each async comptued property, an object is added to `$asyncComputed` that contains information about the current computation state of that object. This object contains the following properties:

```js
{
// Can be one of updating, success, error
state: 'updating',
// A boolean that is true while the property is updating.
updating: true,
// The property finished updating wihtout errors (the promise was resolved) and the current value is available.
success: false,
// The promise was rejected.
error: false,
// The raw error/exception with which the promise was rejected.
exception: null
}
```

It is meant to be used in your rendering code to display update / error information.

````js
new Vue({
asyncComputed: {
posts() {
return Vue.http.get('/posts')
.then(response => response.data)
}
}
}
}
// This will display a loading message every time the posts are updated:
// <div v-if="$asyncComputed.posts.updating"> (Re)loading posts </div>

// If you only want to display the message the first times the posts load, you can use the fact that the default value is null:
// <div v-if="$asyncComputed.posts.updating && posts === null"> Loading posts </div>

// You can display an error message if loading the posts failed.
// The vue-resources library passes the error response on to the rejection handler.
// It is therefore available in $asyncComputed.posts.exception
// <div v-else-if="$asyncComputed.posts.error"> Error while loading posts: $asyncComputed.posts.exception.statusText </div>
````

## Global error handling

By default, in case of a rejected promise in an async computed property, vue-async-computed will take care of logging the error for you.

Expand Down
40 changes: 35 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ const AsyncComputed = {
Vue.mixin({
beforeCreate () {
const optionData = this.$options.data
const asyncComputed = this.$options.asyncComputed || {}
this.$asyncComputed = {}

if (!Object.keys(asyncComputed).length) return

if (!this.$options.computed) this.$options.computed = {}

for (const key in this.$options.asyncComputed || {}) {
this.$options.computed[prefix + key] = getterFn(key, this.$options.asyncComputed[key])
for (const key in asyncComputed) {
const getter = getterFn(key, this.$options.asyncComputed[key])
this.$options.computed[prefix + key] = getter
}

this.$options.data = function vueAsyncComputedInjectedDataFn () {
Expand All @@ -34,7 +39,7 @@ const AsyncComputed = {
? optionData.call(this)
: optionData
) || {}
for (const key in this.$options.asyncComputed || {}) {
for (const key in asyncComputed) {
const item = this.$options.asyncComputed[key]
if (isComputedLazy(item)) {
initLazy(data, key)
Expand All @@ -59,7 +64,7 @@ const AsyncComputed = {

for (const key in this.$options.asyncComputed || {}) {
let promiseId = 0
this.$watch(prefix + key, newPromise => {
const watcher = newPromise => {
const thisPromise = ++promiseId

if (newPromise === DidNotUpdate) {
Expand All @@ -69,13 +74,17 @@ const AsyncComputed = {
if (!newPromise || !newPromise.then) {
newPromise = Promise.resolve(newPromise)
}
setAsyncState(this.$asyncComputed[key], 'updating')

newPromise.then(value => {
if (thisPromise !== promiseId) return
setAsyncState(this.$asyncComputed[key], 'success')
this[key] = value
}).catch(err => {
if (thisPromise !== promiseId) return

setAsyncState(this.$asyncComputed[key], 'error')
this.$asyncComputed[key].exception = err
if (pluginOptions.errorHandler === false) return

const handler = (pluginOptions.errorHandler === undefined)
Expand All @@ -88,13 +97,34 @@ const AsyncComputed = {
handler(err.stack)
}
})
}, { immediate: true })
}
this.$asyncComputed[key] = {
exception: null,
update: () => {
watcher(getterOnly(this.$options.asyncComputed[key])())
}
}
setAsyncState(this.$asyncComputed[key], 'updating')
this.$watch(prefix + key, watcher, { immediate: true })
}
}
})
}
}

function setAsyncState (stateObject, state) {
stateObject.state = state
stateObject.updating = state === 'updating'
stateObject.error = state === 'error'
stateObject.success = state === 'success'
}

function getterOnly (fn) {
if (typeof fn === 'function') return fn

return fn.get
}

function getterFn (key, fn) {
if (typeof fn === 'function') return fn

Expand Down
92 changes: 92 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,3 +716,95 @@ test("shouldUpdate works with lazy", t => {
})
})
})

test("$asyncComputed is empty if there are no async computed properties", t => {
t.plan(1)
const vm = new Vue({
})
t.deepEqual(vm.$asyncComputed, {})
})

test("$asyncComputed[name] is created for all async computed properties", t => {
t.plan(15)
const vm = new Vue({
asyncComputed: {
a () {
return Promise.resolve(1)
},
b () {
return Promise.resolve(2)
}
}
})
t.deepEqual(Object.keys(vm.$asyncComputed), ['a', 'b'])
t.equal(vm.$asyncComputed['a'].state, 'updating')
t.equal(vm.$asyncComputed['b'].state, 'updating')
t.equal(vm.$asyncComputed['a'].updating, true)
t.equal(vm.$asyncComputed['a'].success, false)
t.equal(vm.$asyncComputed['a'].error, false)
t.equal(vm.$asyncComputed['a'].exception, null)

Vue.nextTick(() => {
t.equal(vm.a, 1)
t.equal(vm.b, 2)
t.equal(vm.$asyncComputed['a'].state, 'success')
t.equal(vm.$asyncComputed['b'].state, 'success')
t.equal(vm.$asyncComputed['a'].updating, false)
t.equal(vm.$asyncComputed['a'].success, true)
t.equal(vm.$asyncComputed['a'].error, false)
t.equal(vm.$asyncComputed['a'].exception, null)
})
})

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')
}
}
})
t.equal(vm.$asyncComputed['a'].state, 'updating')
pluginOptions.errorHandler = stack => {
t.equal(vm.a, null)
t.equal(vm.$asyncComputed['a'].state, 'error')
t.equal(vm.$asyncComputed['a'].updating, false)
t.equal(vm.$asyncComputed['a'].success, false)
t.equal(vm.$asyncComputed['a'].error, true)
t.equal(vm.$asyncComputed['a'].exception, 'error-message')
pluginOptions.errorHandler = baseErrorCallback
}
})

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

Vue.nextTick(() => {
t.equal(vm.a, 1)
valueToReturn = 2
t.equal(vm.$asyncComputed['a'].state, 'success')
vm.$asyncComputed['a'].update()
t.equal(vm.$asyncComputed['a'].state, 'updating')

Vue.nextTick(() => {
t.equal(vm.a, 2)
valueToReturn = 3

Vue.nextTick(() => {
t.equal(vm.a, 2)
})
})
})
})