Currently, we use the data property of a route to have it fetch the data before it's resolved. This is how angular 1.x (and ui-router) have done it.
However, this makes the routes file extremely bloated, and splits the component's code into two disparate locations, making it hard to manage it.
Both Angular 2.0 and Aurelia have learned from this mistake and have changed it so that data is handled by the component. Their router shares a common heritage, so they both use the activate hook.
It would be really nice if we could move the data and lifecycle hooks to the components.
In addition to data fetching, it probably makes sense to move the entire route activation lifecycle into the component as well. (which is also what Angular2 / Aurelia routers are doing)
Proposed API:
// inside a component
module.exports = {
route: {
// every hook function gets a transition object as the only argument.
// - {Route} transition.from
// - {Route} transition.to
// - {Function} transition.next
// - {Function} transition.reject
// three options:
// 1. return a boolean
// 2. return a promise that resolves to a boolean
// 3. explicitly call transition.next() or reject()
beforeActivate(transition) {
if (transition.from.path === '/about') {
transition.reject('Not allowed!')
} else {
transition.next()
}
},
// for async data loading.
// sets the component's "$loading" meta property to true when called,
// and sets it to false when resolved.
// two options:
// 1. return a promise
// 2. explicitly call transition.next() or reject(reason)
activate(transition) {
var params = {
id: transition.to.params.messageId
}
// "this" is available
// callback based
this.messages.get(params, function (err, message) {
if (err) {
transition.reject(err)
} else {
transition.next({
message: message
})
}
})
// or promise based (with ES6 sugar)
return this.messages
.get(params)
.then(message => ({ message }))
},
// same deal with beforeActicate
beforeDeactivate(transition) {
// ...
},
// for doing cleanups
deactivate(transition) {
// ...
}
}
}
Lifecycle of the hooks follow the diagram here (at bottom of the page): https://angular.github.io/router/lifecycle
Yep. That's more or less what I had in mind.
Also, should there be a way to set a page title for any given route? Would be used as both the value of the entry in the browser's history as well as the document.title value.
Implemented. See example/components/inbox/index.vue & example/components/inbox/message.vue.
@yyx990803 what happens with the data in the returned promise?
Vue.component('user', {
template: '...',
route: {
activate(transition) {
const id = transition.to.params.userId
return this.$http.get('/users/' + id).then(({ data: user }) => user);
},
},
});
How can I now access the user object from within the component, either to set it as part of its data, or in some method?
You can do one of the following:
$set on the component. So in the promise callback:js
return this.$http
.get('/users/' + id)
.then(({ data: user }) => ({ user }))
The resolved value is { user: user }, so the router will call this.$set('user', user) for you.
js
return this.$http
.get('/users/' + id)
.then(({ data: user }) => {
this.user = user
})
So what's the lifecycle here?
Is data called before route.activate? When is compiled called? What about ready?
By default, data and compiled are called before activate, because the component is already created and available as this inside the hook. ready is called after activate and its exact call moment depends on the component transition mode.
You can also use waitForActivate: true in the component's route config - this will defer the creation and insertion of the component until the activate hook is resolved.
This is all nice and dandy if you only need a single thing.
If you need multiple sets of data, this becomes quite clumsy:
route: {
activate(transition) {
const id = transition.to.params.userId
const user = userService.get(id).then(user => {
this.user = user;
});
const posts = postsService.getForUser(id).then(posts => {
this.posts = posts;
});
// Not sure what would happen if the promise returns an array, so returning null here
return Promise.all([user, posts]).then(() => { return null; });
},
}
What if we could add a data property to the route to resolve multiple promises, like Angular 1.x's resolve property?
route: {
data: {
user: transition => userService.get(transition.to.params.userId),
posts: transition => postsService.getForUser(transition.to.params.userId),
}
}
And for bonus points, if someone wants to use a function, let them use that as well:
route: {
data(transition) {
const id = transition.to.params.userId;
return {
user: userService.get(id),
posts: postsService.getForUser(id),
};
}
}
In both cases, the router would then wait for all promises to complete, and then set the appropriate data properties and transition to the route.
Some syntax sugar could be nice, but you can use the current version like this:
route: {
activate(transition) {
const id = transition.to.params.userId
return Promise.all([
userService.get(id),
postsService.getForUser(id)
]).then(([user, post]) => ({user, post}))
}
}
or
route: {
activate(transition) {
const id = transition.to.params.userId
return Promise.all([
userService.get(id),
postsService.getForUser(id)
]).then(([user, post]) => {
this.user = user
this.post = post
})
}
}
@yyx990803 Shouldn't the context of the component be available in the activate hook?
Here's some pseudo-code of what I'm trying to do:
{
route: {
activate() {
return this.getStuff();
},
},
data() {
stuff: {},
},
methods: {
getStuff(page = 1) {
return globalGetStuff({ page }).then(stuff => {
this.stuff = stuff;
});
},
onPageChange(page) {
this.getStuff(page);
},
},
};
But the activate hook is not called with the component's context. It's null:
Uncaught TypeError: Cannot read property 'getStuff' of null
this is now available in activate as of 0.5.0
:+1:
Most helpful comment
@yyx990803 Shouldn't the context of the component be available in the
activatehook?Here's some pseudo-code of what I'm trying to do:
But the
activatehook is not called with the component's context. It'snull: