Setting The Window / Document Title In Angular 2 Beta 9
CAUTION: The approaches outlined in this post are platform-specific (meaning, they reference the browser platform directly). This didn't sit right with me and _I re-examined this problem using a _platform-agnostic approach that should work in multiple rendering contexts.
With Angular 2, the entire concept of the DOM (Document Object Model) is abstracted away so that you can theoretically run your Angular 2 application in many different environments including, but not limited to, the browser, the server, and within Web Workers. While this is an interesting concept (and I am trying to reserve judgement), it means that we have to re-learn how to do many of the things that were mostly obvious in an AngularJS 1.x application. Things like setting the window / document title now needs to be done through a layer of abstraction. And, as I'm discovering, that's not exactly obvious in Angular 2 Beta 9.
Run this demo in my JavaScript Demos project on GitHub.
As you're reading through the Angular 2 documentation (as one often does), you may notice that Angular 2 actually provides a Title service. This is a service designed to do exactly what we want: set the HTML document title. But, if you try and inject that service into one of your Angular 2 components, you'll get the following bootstrap error:
EXCEPTION: No provider for Title!
As it turns out, while Angular 2 provides this Title service in code, it doesn't actually provide it in the default Browser platform providers. Which is why we get the above error. Fortunately, we can explicitly provide this service during the bootstrapping of our application.
But, the Title service isn't the only service that looks like it exposes a mechanism for setting the document title. The BrowserDomAdapter service also has methods for getting and setting the document title. But, much like the Title service, trying to inject it into one of your Angular 2 components throws the following error:
EXCEPTION: No provider for BrowserDomAdapter!
Just like the Title service, Angular 2 doesn't provide the BrowserDomAdapter in the default Browser platform providers. But, just like the Title service, we can tell Angular 2 to make this an injectable when bootstrapping our application.
If you look at the Angular 2 source code, you will see that a number of core services (like the Title service) make reference to a DOM object, which is an instance of the BrowserDomAdapter that Angular 2 instantiates during the platform initialization. However, this can only be used internally by the Angular 2 application due to the way it is exported.
It might look like this DOM object is available on:
ng.platform.common_dom.DOM
But, this value will always be null. The problem is that this service is exported at page load time but is not initialized until bootstrapping time. Of course, by then, the null value has already been exported and is never re-exported after the platform has been initialized. So, while it may be available internally, the instantiated BrowserDomAdapter is never exposed publicly. At least not that I could see.
Now, it looks like there is one more way to get at the document and that is to inject the DOCUMENT service. This service is the default document for the given platform; which, in our case, is the Browser document object.
Ok, so let's take a look at how this all fits together. In the following demo, I'm using the Title service, the BrowserDomAdapter service, and the DOCUMENT service all as a means to set the document / window title.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>
Setting The Window / Document Title In Angular 2 Beta 9
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body>
<h1>
Setting The Window / Document Title In Angular 2 Beta 9
</h1>
<my-app>
Loading...
</my-app>
<!-- Load demo scripts. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/es6-shim.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/Rx.umd.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/angular2-polyfills.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/angular2-all.umd.js"></script>
<!-- AlmondJS - minimal implementation of RequireJS. -->
<script type="text/javascript" src="../../vendor/angularjs-2-beta/9/almond.js"></script>
<script type="text/javascript">
// Defer bootstrapping until all of the components have been declared.
// --
// NOTE: Not all components have to be required here since they will be
// implicitly required by other components.
requirejs(
[ /* Using require() for better readability. */ ],
function run() {
ng.platform.browser.bootstrap(
require( "App" ),
[
// As turns out, neither the Title service nor the
// BrowserDomAdapter service is included in any of the browser /
// platform providers. As such, if we want to use either of these
// services in our Angular 2 application, we have to explicitly
// tell the bootstrapping mechanism where to find these tokens.
ng.platform.browser.Title,
ng.platform.browser.BrowserDomAdapter
// NOTE: From what I can see in the source code, you cannot use
// ng.platform.common_dom.DOM service directly because it is not
// set until the platform is initialized. And, by that point, the
// original NULL value has already been exported and the subsequent
// change is not "re-exported".
]
);
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I provide the root App component.
define(
"App",
function registerApp() {
// Configure the App component definition.
ng.core
.Component({
selector: "my-app",
template:
`
<h2>
Using Title Service
</h2>
<p>
<a (click)="setTitle( 1, 'Hello' )">Set Title One</a>
—
<a (click)="setTitle( 1, 'Goodbye' )">Set Title Two</a>
</p>
<h2>
Using BrowserDomAdapter Service
</h2>
<p>
<a (click)="setTitle( 2, 'Hello' )">Set Title One</a>
—
<a (click)="setTitle( 2, 'Goodbye' )">Set Title Two</a>
</p>
<h2>
Using Injected Document Reference
</h2>
<p>
<a (click)="setTitle( 3, 'Hello' )">Set Title One</a>
—
<a (click)="setTitle( 3, 'Goodbye' )">Set Title Two</a>
</p>
`
})
.Class({
constructor: AppController
})
;
AppController.parameters = [
new ng.core.Inject( ng.platform.browser.Title ),
new ng.core.Inject( ng.platform.browser.BrowserDomAdapter ),
new ng.core.Inject( ng.platform.browser.DOCUMENT )
];
return( AppController );
// I control the App component.
function AppController( documentTitle, browserDomAdapter, doc ) {
var vm = this;
// Expose the public methods.
vm.setTitle = setTitle;
// ---
// PUBLIC METHODS.
// ---
// I set the current document title using the desired service.
function setTitle( whichService, newTitle ) {
// Using title service.
if ( whichService === 1 ) {
// Internally, this service uses the root DOM adapter.
documentTitle.setTitle( "1: " + newTitle );
// Using browserDomAdapter service.
} else if ( whichService === 2 ) {
// Internally, this service just references the document
// object directly.
browserDomAdapter.setTitle( "2: " + newTitle );
// Using the injected document reference (which is the native
// default document object on the browser platform but may be
// different objects in different platforms).
} else {
doc.title = ( "3: " + newTitle );
}
}
}
}
);
</script>
</body>
</html>
As you can see, after telling the application bootstrap about the Title And BrowserDomAdapter services, I'm then able to inject them into my root component. I can then use each of the three injected services to set the window / document title:
I'm still learning all of this myself, so I hope that I'm not giving out (too much) misinformation here. Having to shift your mindset to think about the Document Object Model as an abstraction and not as an assumed constant of the browser is difficult. Right now, that's one of my biggest points of frustration with Angular 2. But, I assume it will all start to make sense eventually.
Want to use code from this post? Check out the license.
Reader Comments
Hold up, I'm having doubts.
After writing this, I'm not having my doubts about it. Each one of these injected services is an explicit reference to the *Browser* platform; which, is supposed to be abstracted away from us. I believe that I should be able to set the title without referencing the Browser. For example, what if I was rendering this on the server - would the browser platform be available?
@All,
Ok, I sat down and tried to revisit this problem, but using a platform-agnostic approach:
www.bennadel.com/blog/3051-setting-the-document-title-using-platform-agnostic-methods-in-angular-2-beta-9.htm
I was able to set the document title using the core Renderer and the DOCUMENT services. I think that is probably more "correct" than anything I outlined in this post.
Hi Ben,
I was talking to my team about how your examples are written in ES5/6 instead of TypeScript and the consensus is that, love it or hate it, you're swimming against a very strong current going the plain old JS way. Understand if your business requirement don't give you the option, but as long as you're teaching yourself to be an Angular 2 programmer we think you're actually hurting yourself by not diving into TS now.
Another disadvantage is your examples will not benefit the community at large, which would be a real shame.
The problem you are trying to solve here in this post is common, but once I got to the source code I'm like, "What is this?" I'm looking for the TypeScript and scratching my head.
I know it's a LOT to take in all at once once you switch to an IDE like WebStorm or IntelliJ (IntelliJ has deep ColdFusion support, BTW) that has excellent built-in TypeScript support, the switch is really easy. There are other benefits too, even if you don't go whole hog with TS, but it's something you want to consider as soon as possible.
Oh, an then there's the whole not having to write twice as much code as is necessary with all the non-TS Angular2 boilerplate.
If you're still in NYC I can meet you some afternoon and get you going. I'm sure once you drink the Kool Aid you will be glad you did!
Sincere wishes,
John
@John,
When I start writing "production" code, I will likely use TypeScript. For me, the big win in TypeScript is the way the types can document argument methods. Actually, that feels like main draw for me. Being able to look at a method signature and see what it expected and what it returns is super helpful.
That said, I don't think that the TypeScript and the ES5 versions are all that different. And, I have to push back against ES5 examples being helpful to the community. Look at it from my perspective - most of the examples I see is in TypeScript and all the documentation and source code is in TypeScript and I still find it all very helpful when it comes to understand the ES5 approach. The differences are not as large as you might at first think they are.
Also, let's not conflate TypeScript with "classes". I tend to use something like the revealing module pattern, in which public methods are exposed on the instance as opposed to defined on the prototype. This has nothing to do with ES5 vs. TypeScript but is more a pattern of development that allows for private methods and properties. There's no magical syntax in TypeScript that bridges that gap except for the "private" keyword, which doesn't actually do anything except provide some sort of compile time access check.
But, really, the main reason that I use ES5 is so that I can put all of the code in one file for the demo. This allows me to tell a "top down" story. If you look at all of my demos, the flow of code usually follows the dependency:
App -> Component -> Directive -> Service -> Data Access
Each "unknown" in one area of the code is usually followed by a definition of something else in the next chunk of code. I find ES5 makes this much easier than TypeScript, which doesn't allow classes to be "hoisted" (for what I consider odd reasoning).
Honestly, if I could figure out how to get all three:
* TypeScript.
* Single file of code.
* Proper syntax color coding (ie, not some Script tag with a non-JS type).
... I would totally do it. But, I haven't been able to figure that out.
But, like I said, ultimately, I'll probably be using TypeScript in production :D