diff --git a/tests/acceptance/document-title-test.js b/tests/acceptance/document-title-test.js
index 441465e..f896d3d 100644
--- a/tests/acceptance/document-title-test.js
+++ b/tests/acceptance/document-title-test.js
@@ -82,3 +82,53 @@ test('title updates when you switch routes', function(assert) {
assert.equal(document.title, 'Ember is omakase - Posts - My Blog');
});
});
+
+test('titleTokens are observed', function(assert) {
+ assert.expect(2);
+
+ visit('/followers');
+
+ andThen(function() {
+ assert.equal(document.title, 'Followers: 99');
+
+ click('.test-follow-button');
+
+ andThen(function() {
+ assert.equal(document.title, 'Followers: 100',
+ '`titleToken` is observed');
+ });
+ });
+});
+
+test('titles are observed', function(assert) {
+ assert.expect(2);
+
+ visit('/donations/a');
+
+ andThen(function() {
+ assert.equal(document.title, 'Donations: 999');
+
+ click('.test-donate-button');
+
+ andThen(function() {
+ assert.equal(document.title, 'Donations: 1000',
+ '`title` is observed');
+ });
+ });
+});
+
+test('title updates when you switch routes with computed properties', function(assert) {
+ assert.expect(2);
+
+ visit('/donations/a');
+
+ andThen(function() {
+ assert.equal(document.title, 'Donations: 999');
+ });
+
+ click('.test-change-bound-route');
+
+ andThen(function() {
+ assert.equal(document.title, 'Donations: 0');
+ });
+});
diff --git a/tests/dummy/app/controllers/followers.js b/tests/dummy/app/controllers/followers.js
new file mode 100644
index 0000000..cab254d
--- /dev/null
+++ b/tests/dummy/app/controllers/followers.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ followers: 99,
+
+ actions: {
+ follow: function(){
+ this.incrementProperty('followers');
+ }
+ }
+});
diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js
index b8b9e6c..fdd4782 100644
--- a/tests/dummy/app/router.js
+++ b/tests/dummy/app/router.js
@@ -10,6 +10,8 @@ Router.map(function() {
this.route('about');
this.route('team');
this.route('friendship-status', function() {});
+ this.route('followers');
+ this.route('donations', { path: 'donations/:donations' });
});
export default Router;
diff --git a/tests/dummy/app/routes/donations.js b/tests/dummy/app/routes/donations.js
new file mode 100644
index 0000000..4a0e22d
--- /dev/null
+++ b/tests/dummy/app/routes/donations.js
@@ -0,0 +1,25 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model: function(param) {
+ if (param.donations === 'b') {
+ return Ember.Object.create({
+ donations: 0
+ });
+ } else {
+ return Ember.Object.create({
+ donations: 999
+ });
+ }
+ },
+
+ title: Ember.computed('volatileController.model.donations', function() {
+ return 'Donations: ' + this.get('volatileController.model.donations');
+ }),
+
+ actions: {
+ donate: function() {
+ this.get('volatileController.model').incrementProperty('donations');
+ }
+ }
+});
diff --git a/tests/dummy/app/routes/followers.js b/tests/dummy/app/routes/followers.js
new file mode 100644
index 0000000..e26a455
--- /dev/null
+++ b/tests/dummy/app/routes/followers.js
@@ -0,0 +1,9 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ titleToken: Ember.computed.alias('volatileController.followers'),
+
+ title: function(tokens) {
+ return 'Followers: ' + tokens[0];
+ }
+});
diff --git a/tests/dummy/app/templates/donations.hbs b/tests/dummy/app/templates/donations.hbs
new file mode 100644
index 0000000..9e9b87b
--- /dev/null
+++ b/tests/dummy/app/templates/donations.hbs
@@ -0,0 +1,2 @@
+
+{{#link-to 'donations' 'b' class="test-change-bound-route"}}Steal The Money{{/link-to}}
diff --git a/tests/dummy/app/templates/followers.hbs b/tests/dummy/app/templates/followers.hbs
new file mode 100644
index 0000000..2b1d6c7
--- /dev/null
+++ b/tests/dummy/app/templates/followers.hbs
@@ -0,0 +1 @@
+
diff --git a/vendor/document-title/document-title.js b/vendor/document-title/document-title.js
index 3a42fe3..acd468f 100644
--- a/vendor/document-title/document-title.js
+++ b/vendor/document-title/document-title.js
@@ -1,4 +1,5 @@
var get = Ember.get;
+var getOwner = Ember.getOwner;
var routeProps = {
// `titleToken` can either be a static string or a function
@@ -11,7 +12,14 @@ var routeProps = {
// that will be the document title. The `collectTitleTokens` action
// stops bubbling once a route is encountered that has a `title`
// defined.
- title: null
+ title: null,
+
+ // 'volatileController' must be used instead of `controller` when creating
+ // a computed property for `title` or `titleToken` because `controller`
+ // is not observable.
+ volatileController: Ember.computed(function() {
+ return this.get('controller');
+ }).volatile()
};
/*
@@ -35,33 +43,13 @@ var mergedActionPropertyName = (function() {
})();
routeProps[mergedActionPropertyName] = {
- collectTitleTokens: function(tokens) {
- var titleToken = get(this, 'titleToken');
- if (typeof titleToken === 'function') {
- titleToken = titleToken.call(this, get(this, 'currentModel'));
- }
-
- if (Ember.isArray(titleToken)) {
- tokens.unshift.apply(this, titleToken);
- } else if (titleToken) {
- tokens.unshift(titleToken);
- }
+ collectTitleTokens: function(routes) {
+ routes.push(this);
- // If `title` exists, it signals the end of the
- // token-collection, and the title is decided right here.
+ // If `title` exists, it signals the end of the token-collection.
var title = get(this, 'title');
if (title) {
- var finalTitle;
- if (typeof title === 'function') {
- finalTitle = title.call(this, tokens);
- } else {
- // Tokens aren't even considered... a string
- // title just sledgehammer overwrites any children tokens.
- finalTitle = title;
- }
-
- // Stubbable fn that sets document.title
- this.router.setTitle(finalTitle);
+ this.router.setTitleRoutes(routes);
} else {
// Continue bubbling.
return true;
@@ -72,15 +60,87 @@ routeProps[mergedActionPropertyName] = {
Ember.Route.reopen(routeProps);
Ember.Router.reopen({
- updateTitle: Ember.on('didTransition', function() {
- this.send('collectTitleTokens', []);
+
+ titleTokenRoutes: Ember.A(),
+
+ titleRoute: null,
+
+ updateTitleRoutes: Ember.on('didTransition', function() {
+ this.send('collectTitleTokens', Ember.A());
}),
+ setTitleRoutes: function(routes) {
+ // title observer
+ var newTitleRoute = routes[routes.length - 1];
+ if (this.titleRoute !== newTitleRoute) {
+ if (this.titleRoute) {
+ this.titleRoute.removeObserver('title', this, 'titleChanged');
+ }
+ this.titleRoute = newTitleRoute;
+ newTitleRoute.addObserver('title', this, 'titleChanged');
+ }
+
+ // titleToken observers
+ var removedRoutes = Ember.A(), keptRoutes = Ember.A();
+ this.titleTokenRoutes.forEach(function(route){
+ (routes.contains(route) ? keptRoutes : removedRoutes).push(route);
+ });
+
+ this.titleTokenRoutes = routes;
+
+ removedRoutes.forEach(function (route) {
+ route.removeObserver('titleToken', this, 'titleChanged');
+ }, this);
+
+ routes.forEach(function (route) {
+ if (!keptRoutes.contains(route)) {
+ route.addObserver('titleToken', this, 'titleChanged');
+ }
+ }, this);
+
+ // update title
+ this.computeTitle();
+ },
+
+ titleChanged: function() {
+ Ember.run.scheduleOnce('render', this, 'computeTitle');
+ },
+
+ computeTitle: function() {
+ var title = get(this.titleRoute, 'title');
+
+ if (typeof title === 'function') {
+ var tokens = Ember.A();
+ this.titleTokenRoutes.forEach(function (route) {
+ var titleToken = get(route, 'titleToken');
+ if (typeof titleToken === 'function') {
+ titleToken = titleToken.call(route, get(route, 'currentModel'));
+ }
+
+ if (Ember.isArray(titleToken)) {
+ tokens.unshift.apply(route, titleToken);
+ } else if (titleToken) {
+ tokens.unshift(titleToken);
+ }
+ }, this);
+
+ title = title.call(this.titleRoute, tokens);
+ }
+
+ // Stubbable fn that sets document.title
+ this.setTitle(title);
+ },
+
setTitle: function(title) {
- var renderer = this.container.lookup('renderer:-dom');
+ var container = getOwner ? getOwner(this) : this.container;
+ var renderer = container.lookup('renderer:-dom');
+ var domForAppWithGlimmer2 = container.lookup('service:-document');
- if (renderer) {
+ if (renderer && renderer._dom) {
Ember.set(renderer, '_dom.document.title', title);
+ } else if (domForAppWithGlimmer2) {
+ // Glimmer 2 has a different renderer
+ Ember.set(domForAppWithGlimmer2, 'title', title);
} else {
document.title = title;
}