Document.title integration into Ember


#1

Hey all,

I’ve submitted a pull request for Ember to have integration to handle the document.title of the app (see: https://github.com/emberjs/ember.js/pull/3689). I’ll describe in short the goals that I have and how to implement various patterns using the approach that I’ve created in this post. Any feedback on this proposal is very welcome, as I’m fairly sure that I’ve overlooked a fair bit of use cases.

The app that I’m currently working on is mostly forms that, upon saving the form, I’d like to update the title of the document to keep the page of the title as fresh as possible. Some apps use the show / edit pattern; ours doesn’t. The approach taken for managing the title of the page doesn’t care which approach you take, and allows you to configure various bits about the title and to also completely override the title implementation.

For a basic app like discuss, the implementation would be as follows:

var Discuss = Ember.Application.create();

Discuss.Router = Ember.Router.extend();

Discuss.Router.map(function () {
  this.resource('list');
  this.resource('topic');
});

Discuss.ApplicationRoute = Ember.Route.extend({
  title: Ember.computed.oneWay('controller.name') // The name of the forum
});

Discuss.TopicRoute = Ember.Route.extend({
  title: Ember.computed.oneWay('controller.title') // The name of the topic
});

This requires only that the associated controller has the property that the title needs on it. If data from another controller is needed, it should be provided via the needs property on the controller associated with the route.

For a less trivial example of a REST-like application, I’m going to use Ravelry’s title design.

var Ravelry = Ember.Application.create();

Ravelry.Router = Ember.Router.extend({
  title: function () {
    var tokens  = this.get('titleTokens'),
        title   = tokens.unshift(),
        subject = tokens.join(" "),
        divider = ": ";

    if (subject === "") {
      subject = "a knit and crochet community";
      divider = " - ";
    }

    return title + divider + subject;
  }.property('titleTokens')
});

Ravelry.Router.map(function () {
  this.resource('user', function () {
    this.route('stash');
    this.route('projects');
    this.route('queue');
    this.route('favorites');
  });
}):

Ravelry.ApplicationRoute = Ember.Route.extend({
  title: "Ravelry"
});

Ravelry.UserRoute = Ember.Route.extend({
  title: function () {
    return this.get('controller.name') + "'s";
  }.property('controller.name')
});

Ravelry.UserIndexRoute = Ember.Route.extend({
  title: "Profile"
});

Ravelry.UserProjectsRoute = Ember.Route.extend({
  title: "Projects"
});

Ravelry.UserStashRoute = Ember.Route.extend({
  title: "Stash"
});

Ravelry.UserQueueRoute = Ember.Route.extend({
  title: "Queue"
});

Ravelry.UserFavoritesRoute = Ember.Route.extend({
  title: "Favorites"
});

Upon visiting / in this application, the title would be: Ravelry - a knit and crochet community. Visiting a user’s page, you would see Ravelry: purrrl's Profile; their stash Ravelry: purrrl's Stash and so on…

If you have a more complex use case and would like titles to be custom per route, then the following recipe would do quite well:

App.Router = Ember.Router.extend({
  title: Ember.computed.oneWay('titleTokens.lastObject')
});

This would make the title dependent on the title of your route, allowing fine-grained control over your route.

Another useful recipe to consider is notifications using the document.title (for IM applications, for example.):

App.Router = Ember.Router.extend({
  notification: null,

  shouldShowNotification: false,

  title: function () {
    if (this.get('shouldShowNotification')) {
      return this.get('notification');
    } else {
      return this._super();
    }
  }.property('titleTokens', 'shouldShowNotification')
});

App.ApplicationRoute = Ember.Route.extend({
  actions: {
    notify: function (message) {
      var self = this.get('router');
      self.set('notification', message);
      if (message) {
        this._timer = setInterval(function () {
          self.toggleProperty('shouldShowNotification');
        }, 500);
        self.set('shouldShowNotification', true);
      } else {
        self.set('shouldShowNotification', false);
        clearInterval(this._timer);
      }
    }
  }
});

Using this pattern, you can send an action called notify with a message and it will flash every half second until you call notify with a falsy value.

Any more patterns would be welcome, and please poke any holes you can in the proposal :D.

Cheers~


Script[type="text/x-content-handlebars"] && contentFor
#2

I’ve been waiting for this feature for some time. I do have a use case that is not (at least by my quick look at the source) be covered by the PR.

Say I have a deeply nested route:

App.Router.map(function() {
  this.resource("LevelA", function() {
    this.resource("LevelB", function() {
      this.resource("LevelC", function() {
        this.route("LevelD");
      });
    });
  });
});

Then, my routes and titles look like:

App.ApplicationRoute = Ember.Route.extend({
  title : "My Product"
});

App.LevelARoute = Ember.Route.extend({
  title : "Level A"
});

App.LevelAIndexRoute = Ember.Route.extend({
  title : "A Index"
});

App.LevelBRoute = Ember.Route.extend({
  title : "Level B"
});

App.LevelBIndexRoute = Ember.Route.extend({
  title : "B Index"
});

App.LevelCRoute = Ember.Route.extend({
  title : "Level C"
});

App.LevelCIndexRoute = Ember.Route.extend({
  title : "C Index"
});

App.LevelCLevelDRoute = Ember.Route.extend({
  title : "Level D"
});

I would presume that were I to enter LevelC.LevelD then the title of my document would be: My Product - Level A - Level B - Level C - Level D. For my purposes, I wouldn’t necessarily want that level of detail for my document title…instead, I might want the control for it to include the Application’s Title and the current Resource/Route only.

In that case, I’d end up with:

Index => My Product

LevelA => My Product - Level A - A Index

LevelB => My Product - Level B - B Index

LevelC.LevelD => My Product - Level C - Level D

I suppose that one way that this could be done is to include an alwaysShowTitle option on the route that, when false, will only be included if it is a currently active Resource or Route.

App.LevelARoute = Ember.Route.extend({
  alwaysShowTitle : false,
  title : "Level A"
});

Anyway…food for thought.

Cheers!


#3

The recipe for what you propose is the following:

App.Router = Ember.Router.extend({
  title: function () {
    var tokens = Ember.copy(this.get('titleTokens')),
        parts = [tokens.shift()];
    parts.pushObjects(tokens.slice(-2));
    return parts.without(undefined).join(this.get('titleDivider'));
  }.property('titleTokens')
});

I think you could simplify the recipe further, but it should do the trick.

EDIT: this was wrong; now fixed


#4

BUMP

Would love some feedback on some of the recent changes to the proposed API.

Check out a bunch of examples of the proposed API here:


#5

I like this! I think it’d cover any use case I’d have.


#6

Been following the progress on this the past couple days! Glad to see it’s pretty much ready to go.

I’ve been using an old gist version of this code in my app for a little while and I haven’t run into any edge cases or bugs.

In any case – I definitely prefer title and titleToken to titleFormat, so I’m glad to see that won out. Also glad ya’ll decided to punt on the <meta> stuff since the SEO problem is mostly orthogonal to updating the title for the purposes of real users.

:thumbsup:


#7

this looks great, if i wasn’t using the ember-rails gem i’d have pulled the PR into our app already.

:+1:


#8

I would love to see this feature continue to push forward, I think it’s a use case that is common to many applications and could be extended to include breadcrumbs. There has been additional breadcrumb interest expressed on the PR.

As Tim noted, this would require the titleTokens to be available to the controller. Perhaps all of the titles should be located on the controller rather than the route.?. Just a thought. And also just to note, the breadcrumb would also need access to the related routes path for the link. More thought is probably needed for breadcrumb support but the basic idea would be:

<ol class="breadcrumb">
    {{#each titleTokens}}
        <li>{{#link-to 'route.path.somehow'}}{{title}}{{/link-to}}</li>
    {{/each}}
</ol>