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