RouteInfo MetaData
Summary
The RFC introduces the ability to associate application specific metadata with its corresponding RouteInfo
object. This also adds a metadata
field to RouteInfo
, which will be the return value of buildRouteInfoMetadata
for its corresponding Route
.
// app/route/profile.js
import Route from '@ember/routing/route';
import { inject } from '@ember/service';
export default Route.extend({
user: inject('user'),
buildRouteInfoMetadata() {
return {
trackingKey: 'page_profile',
user: {
id: this.user.id,
type: this.user.type
}
}
}
// ...
});
// app/services/analytics.js
import Service, { inject } from '@ember/service';
export default Service.extend({
router: inject('router'),
init() {
this._super(...arguments);
this.router.on('routeDidUpdate', (transition) => {
let { to, from } = transition;
let fromMeta = from.metadata;
let toMeta = to.metadata;
ga.sendEvent('pageView', {
from: fromMeta,
to: toMeta,
timestamp: Date.now(),
})
})
},
// ...
});
Motivation
While the RouteInfo
object is sufficient in providing developers metadata about the Route
itself, it is not sufficient in layering on application specific metadata about the Route
. This metadata could be anything from a more domain-specific name for a Route
, e.g. profile_page
vs profile.index
, all the way to providing contextual data when the Route
was visited.
This metadata could be used for more pratical things like updating the document.title
.
Currently, addons like Ember CLI Head and Ember CLI Document Title require the user to supply special metadata fields on your Route
that will be used to update the title. This API would be a formalized place to place that metadata.
See the appendix for examples.
Detailed design
buildRouteInfoMetadata
This optional hook is intended to be used as a way of letting the routing system know about any metadata associated with the route.
Route
Interface Extension
interface Route {
// ... existing public API
buildRouteInfoMetadata(): unknown
}
Runtime Semantics
- Always called before the
beforeModel
hook is called - Maybe called more than once during a transition e.g. aborts, redirects.
RouteInfo.metadata
The metadata
optional field on RouteInfo
will be populated with the return value of buildRouteInfoMetadata
. If there is no metadata associated with the Route
, the metadata
field will be null
.
interface RouteInfo {
// ... existing public API
metadata: Maybe<unknown>;
}
This field will also be added to RouteInfoWithAttributes
as it is just a super-set of RouteInfo
.
How we teach this
We feel that this a low-level primitive that will allow existing tracking addons to encapsulate. That being said the concept here is pretty simple: What gets returned from buildRouteInfoMetadata
becomes the value of RouteInfo.metadata
for that Route
.
The guides and tutorial should be updated to incorporate an example on how these APIs could integrate with services like Google Analytics.
Drawbacks
This adds an additional hook that is called during route activation, expanding the surface area of the Route
class.
While this is true, there is currently no good way to associate application-specific metadata with a route transition.
Alternatives
There are numerous alternative to the proposal:
setRouteMetadata
This API would be similar to setComponentManager
and setModifierManager
. For example:
// app/route/profile.js
import Route, { setRouteMetadata } from '@ember/routing/route';
export default Route.extend({
init() {
this._super(...arguments);
setRouteMetadata(this, {
trackingKey: 'page_profile',
profile: {
viewing: this.userId,
locale: this.userLocale
}
});
}
// ...
});
You would then use the a RouteInfo
to lookup the value:
// app/services/analytics.js
import { getRouteMetadata } from '@ember/routing/route';
import Service, { inject } from '@ember/service';
export default Service.extend({
router: inject('router'),
init() {
this._super(...arguments);
this.router.on('routeDidUpdate', (transition) => {
let { to, from } = transition;
let { trackingKey: fromKey } = getRouteMetadata(from);
let { trackingKey: toKey } = getRouteMetadata(to);
ga.sendEvent('pageView', {
from: fromKey,
to: toKey,
timestamp: Date.now(),
})
})
},
// ...
});
This could work but there are two things that are confusing here:
- What happens if you call
setRouteMetadata
mutliple times. Do you clobber the existing metadata? Do you merge it? - It is very odd that you would use a
RouteInfo
to access the metadata when you set it on theRoute
.
Route.metadata
This would add a special field to the Route
class that would be copied off on to the RouteInfo
. For example:
// app/route/profile.js
import Route, { setRouteMetadata } from '@ember/routing/route';
export default Route.extend({
metadata: {
trackingKey: 'page_profile',
profile: {
viewing: this.userId,
locale: this.userLocale
}
}
// ...
});
The value would then be populated on RouteInfo.metadata
.
// app/services/analytics.js
import { getRouteMetadata } from '@ember/routing/route';
import Service, { inject } from '@ember/service';
export default Service.extend({
router: inject('router'),
init() {
this._super(...arguments);
this.router.on('routeDidUpdate', (transition) => {
let { to, from } = transition;
let fromMeta = from.metadata;
let toMeta = to.metadata;
ga.sendEvent('pageView', {
from: fromKey,
to: toKey,
timestamp: Date.now(),
})
})
},
// ...
});
This could work but there are two things that are problematic here:
- What happens to the this data if you subclass it? Do you merge or clobber the field?
- This is a generic property name and may conflict in existing applications
Return Metadata From activate
Today activate
does not get called when the dynamic segments of the Route
change, making it not well fit for this use case.
Unresolved questions
TBD?
Apendix A
Tracking example
// app/route/profile.js
import Route from '@ember/routing/route';
import { inject } from '@ember/service';
export default Route.extend({
user: inject('user'),
buildRouteInfoMetadata() {
return {
trackingKey: 'page_profile',
user: {
id: this.user.id,
type: this.user.type
}
}
}
// ...
});
// app/services/analytics.js
import Service, { inject } from '@ember/service';
export default Service.extend({
router: inject('router'),
init() {
this._super(...arguments);
this.router.on('routeDidUpdate', (transition) => {
let { to, from } = transition;
let fromMeta = from.metadata;
let toMeta = to.metadata;
ga.sendEvent('pageView', {
from: fromMeta,
to: toMeta,
timestamp: Date.now(),
})
})
},
// ...
});
Appendix B
Updating document.title
// app/route/profile.js
import Route from '@ember/routing/route';
import { inject } from '@ember/service';
export default Route.extend({
user: inject('user'),
buildRouteInfoMetadata() {
return {
title: 'My Cool WebPage'
}
}
// ...
});
// app/router.js
import Router from '@ember/routing/router';
// ...
export default Router.extend({
init() {
this._super(...arguments);
this.on('routeDidUpdate', (transition) => {
let { title } = transition.metadata;
document.title = title;
});
},
// ...
});