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