Skip to content

Commit 2903498

Browse files
authored
Merge pull request rust-lang#398 from emberjs/route-info-meta-data
RouteInfo Metadata
2 parents 1a6513f + 17b6ee2 commit 2903498

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed

text/0000-RouteInfo-Metadata.md

+308
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
- Start Date: 2018-11-02
2+
- RFC PR: https://github.com/emberjs/rfcs/pull/398
3+
- Ember Issue: (leave this empty)
4+
5+
# RouteInfo MetaData
6+
7+
## Summary
8+
9+
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`.
10+
11+
```js
12+
// app/route/profile.js
13+
import Route from '@ember/routing/route';
14+
import { inject as service } from '@ember/service';
15+
16+
export default Route.extend({
17+
user: inject('user'),
18+
buildRouteInfoMetadata() {
19+
return {
20+
trackingKey: 'page_profile',
21+
user: {
22+
id: this.user.id,
23+
type: this.user.type
24+
}
25+
}
26+
}
27+
// ...
28+
});
29+
```
30+
31+
```js
32+
// app/services/analytics.js
33+
import Service, { inject } from '@ember/service';
34+
35+
export default Service.extend({
36+
router: inject('router'),
37+
init() {
38+
this._super(...arguments);
39+
this.router.on('routeDidUpdate', (transition) => {
40+
let { to, from } = transition;
41+
let fromMeta = from.metadata;
42+
let toMeta = to.metadata;
43+
ga.sendEvent('pageView', {
44+
from: fromMeta,
45+
to: toMeta,
46+
timestamp: Date.now(),
47+
})
48+
})
49+
},
50+
// ...
51+
});
52+
```
53+
54+
## Motivation
55+
56+
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.
57+
58+
This metadata could be used for more pratical things like updating the `document.title`.
59+
Currently, addons like [Ember CLI Head](https://github.com/ronco/ember-cli-head) and [Ember CLI Document Title](https://github.com/kimroen/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.
60+
61+
See the [appendix](#appendix-a) for examples.
62+
63+
## Detailed design
64+
65+
### `buildRouteInfoMetadata`
66+
67+
This optional hook is intended to be used as a way of letting the routing system know about any metadata associated with the route.
68+
69+
#### `Route` Interface Extension
70+
71+
```ts
72+
interface Route {
73+
// ... existing public API
74+
buildRouteInfoMetadata(): unknown
75+
}
76+
```
77+
78+
#### Runtime Semantics
79+
80+
- **Always** called before the `beforeModel` hook is called
81+
- **Maybe** called more than once during a transition e.g. aborts, redirects.
82+
83+
### `RouteInfo.metadata`
84+
85+
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`.
86+
87+
```ts
88+
interface RouteInfo {
89+
// ... existing public API
90+
metadata: Maybe<unknown>;
91+
}
92+
```
93+
94+
This field will also be added to `RouteInfoWithAttributes` as it is just a super-set of `RouteInfo`.
95+
96+
97+
## How we teach this
98+
99+
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`.
100+
101+
The guides and tutorial should be updated to incorporate an example on how these APIs could integrate with services like Google Analytics.
102+
103+
## Drawbacks
104+
105+
This adds an additional hook that is called during route activation, expanding the surface area of the `Route` class.
106+
While this is true, there is currently no good way to associate application-specific metadata with a route transition.
107+
108+
## Alternatives
109+
110+
There are numerous alternative to the proposal:
111+
112+
### `setRouteMetadata`
113+
114+
This API would be similar to `setComponentManager` and `setModifierManager`. For example:
115+
116+
```js
117+
// app/route/profile.js
118+
import Route, { setRouteMetadata } from '@ember/routing/route';
119+
120+
export default Route.extend({
121+
122+
init() {
123+
this._super(...arguments);
124+
setRouteMetadata(this, {
125+
trackingKey: 'page_profile',
126+
profile: {
127+
viewing: this.userId,
128+
locale: this.userLocale
129+
}
130+
});
131+
}
132+
// ...
133+
});
134+
```
135+
136+
You would then use the a `RouteInfo` to lookup the value:
137+
138+
139+
```js
140+
// app/services/analytics.js
141+
import { getRouteMetadata } from '@ember/routing/route';
142+
import Service, { inject } from '@ember/service';
143+
export default Service.extend({
144+
router: inject('router'),
145+
init() {
146+
this._super(...arguments);
147+
this.router.on('routeDidUpdate', (transition) => {
148+
let { to, from } = transition;
149+
let { trackingKey: fromKey } = getRouteMetadata(from);
150+
let { trackingKey: toKey } = getRouteMetadata(to);
151+
ga.sendEvent('pageView', {
152+
from: fromKey,
153+
to: toKey,
154+
timestamp: Date.now(),
155+
})
156+
})
157+
},
158+
// ...
159+
});
160+
```
161+
162+
This could work but there are two things that are confusing here:
163+
164+
1. What happens if you call `setRouteMetadata` mutliple times. Do you clobber the existing metadata? Do you merge it?
165+
2. It is very odd that you would use a `RouteInfo` to access the metadata when you set it on the `Route`.
166+
167+
### `Route.metadata`
168+
169+
This would add a special field to the `Route` class that would be copied off on to the `RouteInfo`. For example:
170+
171+
```js
172+
// app/route/profile.js
173+
import Route, { setRouteMetadata } from '@ember/routing/route';
174+
175+
export default Route.extend({
176+
metadata: {
177+
trackingKey: 'page_profile',
178+
profile: {
179+
viewing: this.userId,
180+
locale: this.userLocale
181+
}
182+
}
183+
// ...
184+
});
185+
```
186+
187+
The value would then be populated on `RouteInfo.metadata`.
188+
189+
190+
```js
191+
// app/services/analytics.js
192+
import { getRouteMetadata } from '@ember/routing/route';
193+
import Service, { inject } from '@ember/service';
194+
export default Service.extend({
195+
router: inject('router'),
196+
init() {
197+
this._super(...arguments);
198+
this.router.on('routeDidUpdate', (transition) => {
199+
let { to, from } = transition;
200+
let fromMeta = from.metadata;
201+
let toMeta = to.metadata;
202+
ga.sendEvent('pageView', {
203+
from: fromKey,
204+
to: toKey,
205+
timestamp: Date.now(),
206+
})
207+
})
208+
},
209+
// ...
210+
});
211+
```
212+
213+
This could work but there are two things that are problematic here:
214+
215+
1. What happens to the this data if you subclass it? Do you merge or clobber the field?
216+
2. This is a generic property name and may conflict in existing applications
217+
218+
### Return Metadata From `activate`
219+
220+
Today `activate` does not get called when the dynamic segments of the `Route` change, making it not well fit for this use case.
221+
222+
## Unresolved questions
223+
224+
TBD?
225+
226+
227+
### Apendix A
228+
229+
Tracking example
230+
231+
```js
232+
// app/route/profile.js
233+
import Route from '@ember/routing/route';
234+
import { inject } from '@ember/service';
235+
export default Route.extend({
236+
user: inject('user'),
237+
buildRouteInfoMetadata() {
238+
return {
239+
trackingKey: 'page_profile',
240+
user: {
241+
id: this.user.id,
242+
type: this.user.type
243+
}
244+
}
245+
}
246+
// ...
247+
});
248+
```
249+
250+
```js
251+
// app/services/analytics.js
252+
import Service, { inject } from '@ember/service';
253+
254+
export default Service.extend({
255+
router: inject('router'),
256+
init() {
257+
this._super(...arguments);
258+
this.router.on('routeDidUpdate', (transition) => {
259+
let { to, from } = transition;
260+
let fromMeta = from.metadata;
261+
let toMeta = to.metadata;
262+
ga.sendEvent('pageView', {
263+
from: fromMeta,
264+
to: toMeta,
265+
timestamp: Date.now(),
266+
})
267+
})
268+
},
269+
// ...
270+
});
271+
```
272+
273+
274+
### Appendix B
275+
276+
Updating document.title
277+
278+
```js
279+
// app/route/profile.js
280+
import Route from '@ember/routing/route';
281+
import { inject } from '@ember/service';
282+
export default Route.extend({
283+
user: inject('user'),
284+
buildRouteInfoMetadata() {
285+
return {
286+
title: 'My Cool WebPage'
287+
}
288+
}
289+
// ...
290+
});
291+
```
292+
293+
```js
294+
// app/router.js
295+
import Router from '@ember/routing/router';
296+
297+
// ...
298+
export default Router.extend({
299+
init() {
300+
this._super(...arguments);
301+
this.on('routeDidUpdate', (transition) => {
302+
let { title } = transition.metadata;
303+
document.title = title;
304+
});
305+
},
306+
// ...
307+
});
308+
```

0 commit comments

Comments
 (0)