Skip to content
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

RouteInfo Metadata #398

Merged
merged 22 commits into from
Nov 30, 2018
Merged
Changes from 2 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
305 changes: 305 additions & 0 deletions text/0000-RouteInfo-Metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
- Start Date: 2018-11-02
- RFC PR: (leave this empty)
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
- Ember Issue: (leave this empty)

# RouteInfo MetaData

## Summary

The RFC introduces the ability to associate application specific metadata with it's corresponding `RouteInfo` object. This also adds a `metadata` field to the `RouteInfo` which will be the return value of `buildRouteInfoMetadata` for it's corresponding `Route`.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

```js
// app/route/profile.js
import Route from '@ember/routing/route';
import { inject } from '@ember/service';
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
export default Route.extend({
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
user: inject('user'),
buildRouteInfoMetadata() {
return {
trackingKey: 'page_profile',
user: {
id: this.user.id,
type: this.user.type
}
}
}
// ...
});
```

```js
// 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 just 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.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

This metadata could be used for more pratical things like updating the `document.title`. 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 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.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

See the [appendix](#appendix-a) for examples.

## Detailed design

### `buildRouteInfoMetadata`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Route#buildRouteInfoMetadata, right? As in, we are adding a hook method to Route.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to name that something a bit shorter? i.e. getRouteMetadata or something like that? I imagine you're aiming to keep from clobbering userland methods that already exist, but it'd sure be nice to not have to type that whole thing on a regular basis ... 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it’s not going to be super super common and verbosity also helps keep it clear what’s going on while also avoiding clobbering existing methods....


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

```ts
interface Route {
// ... existing public API
buildRouteInfoMetadata(): unknown
}
```

#### Runtime Semantics

- **Always** called before the `beforeModel` hook is called
- **Maybe** be called more than once during a transition e.g. aborts, redirects.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

### `RouteInfo.metadata`

The `metadata` optional field on the `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`.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

```ts
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 encoporate an example on how these APIs could integrate with services like Google Analytics.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

## Drawbacks

This adds an additional hook that is called during route activation, expands the surface area of the `Route` class. While this is true there is currently know good way to associate application specicific metadata with a route transition.
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

## Alternatives

There are numerous alternative to the proposal:

### `setRouteMetaData`
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

This API would be similar to `setComponentManager` and `setModifierManager`. For example:

```js
// app/route/profile.js
import Route, { setRouteMetaData } from '@ember/routing/route';
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

export default Route.extend({

init() {
this._super(...arguments);
setRouteMetaData(this, {
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
trackingKey: 'page_profile',
profile: {
viewing: this.userId,
locale: this.userLocale
}
});
}
// ...
});
```

You would then use the a `RouteInfo` to lookup the value:


```js
// app/services/analytics.js
import { getRouteMetaData } from '@ember/routing/route';
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
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);
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
let { trackingKey: toKey } = getRouteMetaData(to);
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
ga.sendEvent('pageView', {
from: fromKey,
to: toKey,
timestamp: Date.now(),
})
})
},
// ...
});
```

This could work but there are two things that are confusing here:

1. What happens if you call `setRouteMetaData` mutliple times. Do you clobber the existing metadata? Do you merge it?
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
2. It is very odd that you would use a `RouteInfo` to access the metadata when you set it on the `Route`.

### `Route.metadata`

This would add a special field to the `Route` class that would be copied off on to the `RouteInfo`. For example:

```js
// app/route/profile.js
import Route, { setRouteMetaData } from '@ember/routing/route';
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

export default Route.extend({
metadata: {
trackingKey: 'page_profile',
profile: {
viewing: this.userId,
locale: this.userLocale
}
}
// ...
});
```

The value would then be populated on `RouteInfo.metadata`.


```js
// app/services/analytics.js
import { getRouteMetaData } from '@ember/routing/route';
chadhietala marked this conversation as resolved.
Show resolved Hide resolved
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:

1. What happens to the this data if you subclass it? Do you merge or clobber the field?
2. This is a generic property name and may conflict in existing applications

### Return MetaData From `activate`
chadhietala marked this conversation as resolved.
Show resolved Hide resolved

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

```js
// 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
}
}
}
// ...
});
```

```js
// 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

```js
// 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'
}
}
// ...
});
```

```js
// 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;
});
},
// ...
});
```