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

Migrate away from ember-cli-head for <title> updates #168

Merged
merged 2 commits into from
Sep 23, 2020
Merged
Changes from all 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
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -8,17 +8,15 @@ This addon provides a helper for changing the title of the page you're on.
ember install ember-page-title
```

Add `{{head-layout}}` to your application's `application.hbs` template.

<details>
<summary>Fastboot vs Non-Fastboot Notes</summary>

### Compatibility

* Ember.js v3.12 or above
* Ember CLI v2.13 or above
* Node.js v10 or above

<details>
<summary>Fastboot vs Non-Fastboot Notes</summary>

#### Post Install Cleanup

As of v3.0.0 this addon maintains the page title by using the `<title>` tag in your document's `<head>`. This is necessary for [FastBoot](https://github.com/tildeio/ember-cli-fastboot) compatibility.
@@ -60,21 +58,17 @@ module.exports = function (environment) {
};
```

### Fastboot

When working with other addons that use `ember-cli-head`, you'll need to create a custom `head.hbs` file that exposes the `<title>` tag properly:

```hbs
<title>{{model.title}}</title>
```

This file is added automatically if you use `ember install`. This is for all the folks using ember-cli-head addons like ember-cli-meta-tags.

### Deprecations

- Since **v5.2.2**: The `{{title}}` helper has been deprecated, use `{{page-title}}` instead, it has the same API. The
`{{title}}` helper was an AST transform and will be removed in the next major release.

### Upgrading notes for 5.x to 6.x

`ember-page-title` no longer requires the usage of `ember-cli-head`.

Please remove `{{head-layout}}` from your application's `application.hbs` route template.

### Upgrading notes for 3.x to 4.x

From 4.x onward, you need to have `{{head-layout}}` within your application's `application.hbs` template. Without this, you will not see a page title appear.
56 changes: 20 additions & 36 deletions addon/helpers/page-title.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { scheduleOnce } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Helper from '@ember/component/helper';
import { set } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import { assign } from '@ember/polyfills';
import { getOwner } from '@ember/application';

function updateTitle(tokens) {
set(this, 'title', tokens.toString());
}

/**
`{{page-title}}` is used to communicate with
@@ -17,13 +10,15 @@ function updateTitle(tokens) {
@method page-title
*/
export default Helper.extend({
pageTitleList: service(),
headData: service(),
tokens: service('page-title-list'),

get tokenId() {
return guidFor(this);
},

init() {
this._super();
let tokens = this.pageTitleList;
tokens.push({ id: guidFor(this) });
this._super(...arguments);
this.tokens.push({ id: this.tokenId });
},

compute(params, _hash) {
@@ -35,33 +30,22 @@ export default Helper.extend({
_hash._deprecate
);
}
let tokens = this.pageTitleList;
let hash = assign({}, _hash);
hash.id = guidFor(this);
hash.title = params.join('');
tokens.push(hash);
scheduleOnce('afterRender', this.headData, updateTitle, tokens);
let hash = assign(
{},
_hash,
{
id: this.tokenId,
title: params.join('')
}
);

this.tokens.push(hash);
this.tokens.scheduleTitleUpdate();
return '';
},

destroy() {
let tokens = this.pageTitleList;
let id = guidFor(this);
tokens.remove(id);

let router = getOwner(this).lookup('router:main');
let routes = router._routerMicrolib || router.router;
let { activeTransition } = routes || {};
let headData = this.headData;
if (activeTransition) {
activeTransition.promise.finally(function () {
if (headData.isDestroyed) {
return;
}
scheduleOnce('afterRender', headData, updateTitle, tokens);
});
} else {
scheduleOnce('afterRender', headData, updateTitle, tokens);
}
this.tokens.remove(this.tokenId);
this.tokens.scheduleTitleUpdate();
},
});
43 changes: 35 additions & 8 deletions addon/services/page-title-list.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { A } from '@ember/array';
import { getOwner } from '@ember/application';
import { inject as service } from '@ember/service';
import { scheduleOnce } from '@ember/runloop';
import Service from '@ember/service';
import { set, computed } from '@ember/object';
import { copy } from 'ember-copy';
import { capitalize } from '@ember/string';
import { isPresent } from '@ember/utils';

let isFastBoot = typeof FastBoot !== 'undefined';

/**
@class page-title-list
@extends Ember.Service
*/
export default Service.extend({
document: service('-document'),

init() {
this._super();
set(this, 'tokens', A());
set(this, 'length', 0);
this._removeExistingTitleTag();

let config = getOwner(this).resolveRegistration('config:environment');
@@ -112,7 +116,6 @@ export default Service.extend({
let tokens = copy(this.tokens);
tokens.push(token);
set(this, 'tokens', A(tokens));
set(this, 'length', this.length + 1);
},

remove(id) {
@@ -131,7 +134,6 @@ export default Service.extend({
let tokens = A(copy(this.tokens));
tokens.removeObject(token);
set(this, 'tokens', A(tokens));
set(this, 'length', this.length - 1);
},

visibleTokens: computed('tokens', {
@@ -190,6 +192,21 @@ export default Service.extend({
}
}),

scheduleTitleUpdate() {
let router = getOwner(this).lookup('router:main');
let { activeTransition } = router._routerMicrolib;
if (activeTransition) {
activeTransition.promise.finally(() => {
if (this.isDestroyed) {
return;
}
scheduleOnce('afterRender', this, this._updateTitle);
});
} else {
scheduleOnce('afterRender', this, this._updateTitle);
}
},

toString() {
let tokens = this.sortedTokens;
let title = [];
@@ -205,12 +222,26 @@ export default Service.extend({
return title.join('');
},

_updateTitle() {
const toBeTitle = this.toString();

if (isFastBoot) {
// in fastboot context "document" is instance of ember-fastboot/simple-dom document
let titleEl = this.document.createElement('title');
let titleContents = this.document.createTextNode(toBeTitle);
titleEl.appendChild(titleContents);
this.document.head.appendChild(titleEl);
} else {
this.document.title = toBeTitle;
}
},

/**
* Remove any existing title tags from the head.
* @private
*/
_removeExistingTitleTag() {
if (this._hasFastboot()) {
if (isFastBoot) {
return;
}

@@ -219,9 +250,5 @@ export default Service.extend({
let title = titles[i];
title.parentNode.removeChild(title);
}
},

_hasFastboot() {
return !!getOwner(this).lookup('service:fastboot');
}
});
6 changes: 0 additions & 6 deletions app/templates/head.hbs

This file was deleted.

10,808 changes: 6,056 additions & 4,752 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -28,8 +28,6 @@
},
"dependencies": {
"ember-cli-babel": "^7.21.0",
"ember-cli-head": "^1.0.0",
"ember-cli-htmlbars": "^5.2.0",
"ember-copy": "^1.0.0"
},
"devDependencies": {
@@ -43,7 +41,10 @@
"ember-cli": "~3.20.0",
"ember-cli-code-coverage": "1.0.0-beta.8",
"ember-cli-dependency-checker": "^3.2.0",
"ember-cli-fastboot": "^2.2.3",
"ember-cli-fastboot-testing": "^0.4.0",
"ember-cli-github-pages": "^0.2.0",
"ember-cli-htmlbars": "^5.2.0",
"ember-cli-inject-live-reload": "^2.0.2",
"ember-cli-sass": "^10.0.1",
"ember-cli-uglify": "^3.0.0",
32 changes: 13 additions & 19 deletions tests/acceptance/posts-test.js
Original file line number Diff line number Diff line change
@@ -1,95 +1,89 @@
import { click, find, waitUntil, visit } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { getPageTitle } from '../helpers/get-page-title';

module('Acceptance: title', function(hooks) {
setupApplicationTest(hooks);

// Testem appends progress to the title...
// and there's no way to stop this at the moment
function title() {
let element = document.querySelector('head title');
return element && element.innerText.trim().replace(/^\(\d+\/\d+\)/, '');
}

test('the default configuration works', async function (assert) {
assert.expect(1);
await visit('/posts');

assert.equal(title(), 'Posts | My App');
assert.equal(getPageTitle(), 'Posts | My App');
});

test('the replace attribute works', async function (assert) {
assert.expect(1);
await visit('/about');

assert.equal(title(), 'About My App');
assert.equal(getPageTitle(), 'About My App');
});

test('custom separators work', async function (assert) {
assert.expect(1);
await visit('/about/authors');

assert.equal(title(), 'Authors > About My App');
assert.equal(getPageTitle(), 'Authors > About My App');
});

test('custom separators are inherited', async function (assert) {
assert.expect(1);
await visit('/about/authors/profile');

assert.equal(title(), 'Profile > Authors > About My App');
assert.equal(getPageTitle(), 'Profile > Authors > About My App');
});

test('multiple components in a row work', async function (assert) {
assert.expect(1);
await visit('/posts/rails-is-omakase');

assert.equal(title(), 'Rails is Omakase | Posts | My App');
assert.equal(getPageTitle(), 'Rails is Omakase | Posts | My App');
});

test('the prepend=false declaration works', async function (assert) {
assert.expect(1);
await visit('/authors/tomster');

assert.equal(title(), 'My App | Authors < Tomster');
assert.equal(getPageTitle(), 'My App | Authors < Tomster');
});

test('replace nested in prepends work', async function (assert) {
assert.expect(1);
await visit('/hollywood');

assert.equal(title(), 'Hollywood ★ Stars everywhere');
assert.equal(getPageTitle(), 'Hollywood ★ Stars everywhere');
});

test('multitoken titles work', async function (assert) {
assert.expect(1);
await visit('/feeds/tomster');

assert.equal(title(), 'Tomster (@tomster)');
assert.equal(getPageTitle(), 'Tomster (@tomster)');
});

test('loading substates are not shown', async function (assert) {
assert.expect(3);
await visit('/feeds/tomster');
assert.equal(title(), 'Tomster (@tomster)');
assert.equal(getPageTitle(), 'Tomster (@tomster)');

await click('#zoey');
await waitUntil(() => {
return find('div[data-test-substate-loading]') === null;
});
assert.equal(title(), 'Zoey (@zoey)');
assert.equal(getPageTitle(), 'Zoey (@zoey)');

await click('#tomster');
await waitUntil(() => {
return find('div[data-test-substate-loading]') === null;
});
assert.equal(title(), 'Tomster (@tomster)');
assert.equal(getPageTitle(), 'Tomster (@tomster)');
});

test('front tokens work', async function (assert) {
assert.expect(1);
await visit('/reader');

assert.equal(title(), '(10) Reader | My App');
assert.equal(getPageTitle(), '(10) Reader | My App');
});
});
15 changes: 5 additions & 10 deletions tests/dummy/app/components/page-title-pane/component.js
Original file line number Diff line number Diff line change
@@ -8,23 +8,18 @@ export default Component.extend({

titleList: service(),

lastIndex: computed('titleList.{sortedTokens.length,tokens.length}', {
lastIndex: computed('titleList.{tokens.length}', {
get() {
return this.titleList.sortedTokens.length - 1;
return this.titleList.tokens.length - 1;
},
}),

actions: {
highlight(token) {
let sortedTokens = A(this.titleList.sortedTokens);
let tokens = A(this.titleList.tokens);
let wasActive = token.active;
this.titleList.tokens.setEach('active', false);
sortedTokens.setEach('active', false);

if (!wasActive) {
set(sortedTokens.findBy('id', token.id), 'active', true);
set(token, 'active', true);
}
tokens.setEach('active', false);
set(token, 'active', !wasActive);
},
},
});
Original file line number Diff line number Diff line change
@@ -7,10 +7,10 @@ export default Component.extend({
classNames: ['nested-template'],
classNameBindings: ['active:active'],

active: computed('titleList.{sortedTokens,tokens.@each.active}', 'token.id', {
active: computed('titleList.{tokens.@each.active}', 'token.id', {
get() {
let sortedTokens = A(this.titleList.sortedTokens);
let token = sortedTokens.findBy('id', this.token.id);
let tokens = A(this.titleList.tokens);
let token = tokens.findBy('id', this.token.id);
return token && token.active;
},
}),
2 changes: 0 additions & 2 deletions tests/dummy/app/templates/application.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{{head-layout}}

{{! template-lint-disable simple-unless }}
{{#unless (equals this.router.currentRouteName "index")}}
{{page-title "My App"}}
70 changes: 70 additions & 0 deletions tests/fastboot/title-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { module, test } from 'qunit';
import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
import { getPageTitle } from "../helpers/get-page-title"

module('FastBoot: title', function(hooks) {
setup(hooks);

test('the default configuration works', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/posts');

assert.equal(getPageTitle(htmlDocument), 'Posts | My App');
});

test('the replace attribute works', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/about');

assert.equal(getPageTitle(htmlDocument), 'About My App');
});

test('custom separators work', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/about/authors');

assert.equal(getPageTitle(htmlDocument), 'Authors > About My App');
});

test('custom separators are inherited', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/about/authors/profile');

assert.equal(getPageTitle(htmlDocument), 'Profile > Authors > About My App');
});

test('multiple components in a row work', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/posts/rails-is-omakase');

assert.equal(getPageTitle(htmlDocument), 'Rails is Omakase | Posts | My App');
});

test('the prepend=false declaration works', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/authors/tomster');

assert.equal(getPageTitle(htmlDocument), 'My App | Authors < Tomster');
});

test('replace nested in prepends work', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/hollywood');

assert.equal(getPageTitle(htmlDocument), 'Hollywood ★ Stars everywhere');
});

test('multitoken titles work', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/feeds/tomster');

assert.equal(getPageTitle(htmlDocument), 'Tomster (@tomster)');
});

test('front tokens work', async function (assert) {
assert.expect(1);
let { htmlDocument } = await visit('/reader');

assert.equal(getPageTitle(htmlDocument), '(10) Reader | My App');
});
});
10 changes: 10 additions & 0 deletions tests/helpers/get-page-title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Testem appends progress to the title...
Copy link
Member

Choose a reason for hiding this comment

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

Ugh, that sucks 😭

// and there's no way to stop this at the moment

export function getPageTitle(doc) {
// In Fastboot context we get 2 title elements if we don't remove one from app/index.html
// In real world applications, it is mandatory to remove <title> from app/index.html
// We are keeping both for sake for testing browser and fastboot scenarios
let element = [...(doc || window.document).querySelectorAll('head title')].pop();
return element && element.innerText.trim().replace(/^\(\d+\/\d+\)/, '');
}
8 changes: 4 additions & 4 deletions tests/unit/services/page-title-list-test.js
Original file line number Diff line number Diff line change
@@ -5,13 +5,13 @@ module('service:page-title-list', function(hooks) {
setupTest(hooks);

test('the list has no tokens by default', function (assert) {
assert.equal(this.owner.lookup('service:page-title-list').length, 0);
assert.equal(this.owner.lookup('service:page-title-list').tokens.length, 0);
});

test('calling `push` adds a token to the end of the list', function (assert) {
let list = this.owner.lookup('service:page-title-list');
list.push({ id: 1});
assert.equal(list.length, 1);
assert.equal(list.tokens.length, 1);
});

test('tokens have next and previous tokens', function (assert) {
@@ -24,7 +24,7 @@ module('service:page-title-list', function(hooks) {
list.push(second);
list.push(third);

assert.equal(list.length, 3);
assert.equal(list.tokens.length, 3);

assert.equal(first.previous, null);
assert.equal(first.next, second);
@@ -46,7 +46,7 @@ module('service:page-title-list', function(hooks) {
list.push(second);
list.push(third);
list.remove(2);
assert.equal(list.length, 2);
assert.equal(list.tokens.length, 2);

assert.equal(first.previous, null);
assert.equal(first.next, third);