This chapter will cover the following recipes:
Componentizing directives using the controllerAs encapsulation
Migrating an application to component directives
Implementing a basic component in AngularJS 1.5
Normalizing service types
Connecting Angular 1 and Angular 2 with UpgradeModule
Downgrading Angular 2 components to Angular 1 directives with downgradeComponent
Downgrading Angular 2 providers to Angular 1 services with downgradeInjectable
The introduction of Angular 2 into the Angular ecosystem will surely be interpreted and handled differently for all developers. Some will stick to their existing Angular 1 codebases, some will start brand new Angular 2 codebases, and some will do a gradual or partial transition.
It is recommended that you become familiar with the behavior of Angular 2 components before you dive into these recipes. This will help you frame mental models as you adapt your existing applications to be more compliant with the Angular 2 style.
One of the unusual conventions introduced in Angular 1 was the relationship between directives and the data they consumed. By default, directives used an inherited scope, which suited the needs of most developers just fine. While this was easy to use, it had the effect of introducing extra dependencies in the directives, and also the convention that directives often did not own the data they were consuming. Additionally, the data interpolated in the template was unclear in relation to where it was being assigned or owned.
Angular 2 utilizes components as the building blocks of the entire application. These components are class-based and are therefore in some ways at odds with the scope mechanisms of Angular 1. Transitioning to a controller-centric directive model is a large step towards compliance with the Angular 2 standards.
Note
The code, links, and a live example related to this recipe are available at http://ngcookbook.herokuapp.com/8194.
Suppose your application contains the following setup that involves the nested directives that share data using an isolate scope:
[index.html] <div ng-app="articleApp"> <article></article> </div> [app.js] angular.module('articleApp', []) .directive('article', function() { return { controller: function($scope) { $scope.articleData = { person: {firstName: 'Jake'}, title: 'Lesotho Yacht Club Membership Booms' }; }, template: ` <h1>{{articleData.title}}</h1> <attribution author="articleData.person.firstName"> </attribution> ` }; }) .directive('attribution', function() { return { scope: {author: '='}, template: `<p>Written by: {{author}}</p>` }; });
The goal is to refactor this setup so that templates can be explicit about where the data is coming from and so that the directives have ownership of this data:
[app.js] angular.module('articleApp', []) .directive('article', function() { return { controller: function() { this.person = {firstName: 'Jake'}; this.title = 'Lesotho Yacht Club Membership Booms'; }, controllerAs: 'articleCtrl', template: ` <h1>{{articleCtrl.title}}</h1> <attribution></attribution> ` }; }) .directive('attribution', function() { return { template: `<p>Written by: {{articleCtrl.author}}</p>` }; });
In this second implementation, anywhere you use the article data, you are certain of its origin. This is better, but the child directive is still referencing the parent controller, which isn't ideal since it is introducing an unneeded dependency. The attribution directive instance should be provided with the data, and it should instead interpolate from its own controller instance:
[app.js]
angular.module('articleApp', [])
.directive('article', function() {
return {
controller: function() {
this.person = {firstName: 'Jake'};
this.title = 'Lesotho Yacht Club Membership Booms';
},
controllerAs: 'articleCtrl',
template: `
<h1>{{articleCtrl.title}}</h1>
<attribution author="articleCtrl.person.firstName">
</attribution>
`
};
})
.directive('attribution', function() {
return {
controller: function() {},
controllerAs: 'attributionCtrl',
bindToController: {author: '='},
template: `<p>Written by: {{attributionCtrl.author}}</p>`
};
});
Much better! You provide the child directive with a stand-in controller and give it an alias in the attributionCtrl
template. It is implicitly bound to the controller instance via bindToController
in the same way you would accomplish a regular isolate scope; however, the binding is directly attributed to the controller object instead of the scope.
Now that you have introduced the notion of data ownership, suppose you want to modify your application data. What's more, you want different parts of your application to be able to modify it. A naïve implementation of this would be something as follows:
[app.js] angular.module('articleApp', []) .directive('attribution', function() { return { controller: function() { this.capitalize = function() { this.author = this.author.toUpperCase(); } }, controllerAs: 'attributionCtrl', bindToController: {author: '='}, template: ` <p ng-click="attributionCtrl.capitalize()"> Written by: {{attributionCtrl.author}} </p>` }; });
The desired behavior is for you to click on the author, and it will become capitalized. However, in this implementation, the article controller's data is modified in the attribution controller, which does not own it. It is preferable for the controller that owns the data to perform the actual modification and instead supply an interface that an outside entity—here, the attribution directive—could use:
[app.js] angular.module('articleApp', []) .directive('article', function() { return { controller: function() { this.person = {firstName: 'Jake'}; this.title = 'Lesotho Yacht Club Membership Booms'; this.capitalize = function() { this.person.firstName = this.person.firstName.toUpperCase(); }; }, controllerAs: 'articleCtrl', template: ` <h1>{{articleCtrl.title}}</h1> <attribution author="articleCtrl.person.firstName" upper-case-author="articleCtrl.capitalize()"> </attribution> ` }; }) .directive('attribution', function() { return { controller: function() {}, controllerAs: 'attributionCtrl', bindToController: { author: '=', upperCaseAuthor: '&' }, template: ` <p ng-click="attributionCtrl.upperCaseAuthor()"> Written by: {{attributionCtrl.author}} </p>` }; });
Vastly superior! You are still able to namespace within the click binding, but the owning directive controller is providing a method to outside entities instead of just giving them direct data access.
When a controller is specified in the directive definition object, one will be explicitly instantiated for each directive instance that is created. Thus, it is natural for this controller object to encapsulate the data that it owns and for it to be delegated the responsibility of passing its data to the members that require it.
The final implementation accomplishes several things:
Improved template namespacing: When you use the
$scope
properties that span multiple directives or nestings, you are creating a scenario where multiple entities can manipulate and read data without being able to concretely reason about where it is coming from or what is controlling it.Improved testability: If you look at each of the directives in the final implementation, you'll find they are not too difficult to test. The attribution directive has no dependencies other than what are explicitly passed to it.
Encapsulation: Introducing the notion of data ownership in your application affords you a much more robust structure, better reusability, and additional insight and control involving pieces of your application interacting.
Angular 2 style: Angular 2 uses the
@Input
and@Output
annotations on component definitions. Mirroring this style will make the process of transitioning to an application easier.
You will notice that $scope
has been made totally irrelevant in these examples. This is good as there is no notion of $scope
in Angular 2, which means you are heading towards having an upgradeable application. This is not to say that $scope
does not still have utility in an Angular 1 application, and surely, there are scenarios where this is unavoidable, like with $scope.$apply()
.
However, thinking about the application pieces in this component style will allow you to be more adequately prepared to adopt Angular 2 conventions.
Migrating an application to component directives demonstrates how to refactor Angular 1 to a component style
Implementing a basic component in AngularJS 1.5 details how to write an Angular 1 component
Normalizing service types gives instruction on how to align your Angular 1 service types for Angular 2 compatibility
In Angular 1, there are several built-in directives, including ngController
and ngInclude
, that developers tend to lean on when building applications. While not anti-patterns, using these features moves away from having a component-centric application.
All these directives are actually subsets of component functionality, and they can be entirely refactored out.
Note
The code, links, and a live example related to this recipe are available at http://ngcookbook.herokuapp.com/1008/.
Suppose your initial application is as follows:
[index.html] <div ng-app="articleApp"> <ng-include src="'/press_header.html'"></ng-include> <div ng-controller="articleCtrl as article"> <h1>{{article.title}}</h1> <p>Written by: {{article.author}}</p> </div> <script type="text/ng-template" id="/press_header.html"> <div ng-controller="headerCtrl as header"> <strong> Angular Chronicle - {{header.currentDate | date}} </strong> <hr /> </div> </script> </div> [app.js] angular.module('articleApp', []) .controller('articleCtrl', function() { this.title = 'Food Fight Erupts During Diplomatic Luncheon'; this.author = 'Jake'; }) .controller('headerCtrl', function() { this.currentDate = new Date(); });
Note
Note that this example application contains a large number of very common Angular 1 patterns; you can see the ngController
directives sprinkled throughout. Also, it uses an ngInclude
directive to incorporate a header. Keep in mind that these directives are not inappropriate for a well-formed Angular 1 application. However, you can do better, and this involves refactoring to a component-driven design.
Component-driven patterns don't need to be frightening in appearance. In this example (and for essentially all Angular 1 applications), you can do a component refactor while leaving the existing template largely intact.
Begin with the ngInclude
directive. Moving this to a component directive is simple—it becomes a directive with templateUrl
set to the template path:
[index.html] <div ng-app="articleApp"> <header></header> <div ng-controller="articleCtrl as article"> <h1>{{article.title}}</h1> <p>Written by: {{article.author}}</p> </div> <script type="text/ng-template" id="/press_header.html"> <div ng-controller="headerCtrl as header"> <strong> Angular Chronicle - {{header.currentDate | date}} </strong> <hr /> </div> </script> </div> [app.js] angular.module('articleApp', []) .controller('articleCtrl', function() { this.title = 'Food Fight Erupts During Diplomatic Luncheon'; this.author = 'Jake'; }) .controller('headerCtrl', function() { this.currentDate = new Date(); }) .directive('header', function() { return { templateUrl: '/press_header.html' }; });
Next, you can also refactor ngController
everywhere it appears. In this example, you find two extremely common appearances of ngController
. The first is at the head of the press_header.html
template, acting as the top-level controller for that template. Often, this results in needing a superfluous wrapper element just to house the ng-controller
attribute. The second is ngController
nested inside your primary application template, controlling some arbitrary portion of the DOM. Both of these can be refactored to component directives by reassigning ngController
to a directive controller:
[index.html] <div ng-app="articleApp"> <header></header> <article></article> </div> [app.js] angular.module('articleApp', []) .directive('header', function() { return { controller: function() { this.currentDate = new Date(); }, controllerAs: 'header', template: ` <strong> Angular Chronicle - {{header.currentDate | date}} </strong> <hr /> ` }; }) .directive('article', function() { return { controller: function() { this.title = 'Food Fight Erupts During Diplomatic Luncheon'; this.author = 'Jake'; }, controllerAs: 'article', template: ` <h1>{{article.title}}</h1> <p>Written by: {{article.author}}</p> ` }; });
Generally speaking, an application can be represented by a hierarchy of nested MVC components. ngInclude
and ngController
act as subsets of a component functionality, and so it makes sense that you are able to expand them into full component directives.
In the preceding example, the ultimate application structure is comprised of only components. Each component is delegated its own template, controller, and model (by virtue of the controller object itself). Sticklers will dispute whether or not Angular belongs to true MVC style, but in the context of component refactoring, this is irrelevant. Here, you have defined a structure that is completely modular, reusable, testable, abstractable, and easily maintainable. This is the style of Angular 2, and the value of this should be immediately apparent.
An alert developer will notice that no attention is paid to scope inheritance. This is a difficult problem to approach, mostly because many of the patterns in Angular 1 are designed for a mishmash between a scope and controllerAs
. Angular 2 is built around strict input and output between nested components; however, in Angular 1, scope is inherited by default, and nested directives, by default, have access to their encompassing controller objects.
Thus, to truly emulate an Angular 2 style, one must configure their application to explicitly pass data and methods to children, similar to the controllerAs
encapsulation recipe. However, this does not preclude direct data access to ancestral component directive controllers; it merely wags a finger at it since it adds additional dependencies.
Componentizing directives using controllerAs encapsulation shows you a superior method of organizing Angular 1 directives
Implementing a basic component in AngularJS 1.5 details how to write an Angular 1 component
Normalizing service types gives instruction on how to align your Angular 1 service types for Angular 2 compatibility
The 1.5 release of AngularJS introduced a new tool: the component. While it isn't exactly similar to the concept of the Angular 2 component, it does allow you to build directive-style pieces in an explicitly componentized fashion.
Note
The code, links, and a live example related to this recipe are available at http://ngcookbook.herokuapp.com/7756/.
Suppose your application had a directive defined as follows:
[index.html] <div ng-app="articleApp"> <article></article> </div> [app.js] angular.module('articleApp', []) .directive('article', function() { return { controller: function() { this.person = {firstName: 'Jake'}; this.title = 'Police Bust Illegal Snail Racing Ring'; this.capitalize = function() { this.person.firstName = this.person.firstName.toUpperCase(); }; }, controllerAs: 'articleCtrl', template: ` <h1>{{articleCtrl.title}}</h1> <attribution author="articleCtrl.person.firstName" upper-case-author="articleCtrl.capitalize()"> </attribution> ` }; }) .directive('attribution', function() { return { controller: function() {}, controllerAs: 'attributionCtrl', bindToController: { author: '=', upperCaseAuthor: '&' }, template: ` <p ng-click="attributionCtrl.upperCaseAuthor()"> Written by: {{attributionCtrl.author}} </p>` }; });
Since this application is already organized around the controllerAs
encapsulation, you can migrate it to use the component()
definition introduced in the Angular 1.5 release.
Components accept an object definition similar to a directive, but the object does not demand to be returned by a function—an object literal is all that is needed. Components utilize the bindings property in this object definition object in the same way that bindToController
works for directives. With this, you can easily introduce components in this application instead of directives:
[index.html] <div ng-app="articleApp"> <article></article> </div> [app.js] angular.module('articleApp', []) .component('article', { controller: function() { this.person = {firstName: 'Jake'}; this.title = ' Police Bust Illegal Snail Racing Ring '; this.capitalize = function() { this.person.firstName = this.person.firstName.toUpperCase(); }; }, controllerAs: 'articleCtrl', template: ` <h1>{{articleCtrl.title}}</h1> <attribution author="articleCtrl.person.firstName" upper-case-author="articleCtrl.capitalize()"> </attribution>` }) .component('attribution', { controller: function() {}, controllerAs: 'attributionCtrl', bindings: { author: '=', upperCaseAuthor: '&' }, template: ` <p ng-click="attributionCtrl.upperCaseAuthor()"> Written by: {{attributionCtrl.author}} </p> ` });
Notice that since your controller-centric data organization matches what a component definition expects, no template modifications are necessary. Components, by default, will utilize an isolate scope. What's more, they will not have access to the alias of the surrounding controller objects, something that cannot be said for component-style directives. This encapsulation is an important offering of the new component feature, as it has direct parity to how components operate in Angular 2.
Since you have now entirely isolated each individual component, there is only a single controller object to deal with in each template. Thus, Angular 1.5 automatically provides a convenient alias for the component's controller object, namely—$ctrl
. This is provided whether or not a controllerAs
alias is specified. Therefore, a further refactoring yields the following:
[index.html] <div ng-app="articleApp"> <article></article> </div> [app.js] angular.module('articleApp', []) .component('article', { controller: function() { this.person = {firstName: 'Jake'}; this.title = 'Police Bust Illegal Snail Racing Ring'; this.capitalize = function() { this.person.firstName = this.person.firstName.toUpperCase(); }; }, template: ` <h1>{{$ctrl.title}}</h1> <attribution author="$ctrl.person.firstName" upper-case-author="$ctrl.capitalize()"> </attribution> ` }) .component('attribution', { controller: function() {}, bindings: { author: '=', upperCaseAuthor: '&' }, template: ` <p ng-click="$ctrl.upperCaseAuthor()"> Written by: {{$ctrl.author}} </p> ` });
Componentizing directives using controllerAs encapsulation shows you a superior method of organizing Angular 1 directives
Migrating an application to component directives demonstrates how to refactor Angular 1 to a component style
Normalizing service types gives instruction on how to align your Angular 1 service types for Angular 2 compatibility
Angular 1 developers will be quite familiar with the factory/service/provider trifecta. In many ways, this has gone largely unaltered in Angular 2 conceptually. However, in the interest of upgrading an existing application, there is one thing that should be done to make the migration as easy as possible: eliminate factories and replace them with services.
Note
The code, links, and a live example related to this recipe are available at http://ngcookbook.herokuapp.com/5637/.
Suppose you had a simple application as follows:
[index.html] <div ng-app="articleApp"> <article></article> </div> [app.js] angular.module('articleApp', []) .factory('ArticleData', function() { var title = 'Incumbent Senator Ousted by Stalk of Broccoli'; return { getTitle: function() { return title; }, author: 'Jake' }; }) .component('article', { controller: function(ArticleData) { this.title = ArticleData.getTitle(); this.author = ArticleData.author; }, template: ` <h1>{{$ctrl.title}}</h1> <p>Written by: {{$ctrl.author}}</p> ` });
Angular 2 is class-based, and it includes its service types as well. The example here does not have a service type that is compatible with a class structure. So it must be converted. Thankfully, this is quite easy to do:
[index.html]
<div ng-app="articleApp">
<article></article>
</div>
[app.js]
angular.module('articleApp', [])
.service('ArticleData', function() {
var title = 'Incumbent Senator Ousted by Stalk of Broccoli';
this.getTitle = function() {
return title;
};
this.author = 'Jake';
})
.component('article', {
controller: function(ArticleData) {
this.title = ArticleData.getTitle();
this.author = ArticleData.author;
},
template: `
<h1>{{$ctrl.title}}</h1>
<p>Written by: {{$ctrl.author}}</p>
`
});
You still want to keep the notion of title
private, but you also want to maintain the API that the injected service type is providing. Services are defined by a function that acts as a constructor, and an instance created from this constructor is what is ultimately injected. Here, you are simply moving getTitle()
and author
to be defined on the this
keyword, which thereby makes it a property on all instances. Note that the use in the component and template does not change in any way, and it shouldn't.
The simplest to implement service types, Angular 1 factories were often used first by many developers, including myself. Some developers might take offense at the following claim, but I don't think there was ever a good reason for both factories and services to exist. Both have a high degree of redundancy, and if you dig down into the Angular source code, you will see that they essentially converge to the same methods.
Why services over factories then? The new world of JavaScript, ES6, and TypeScript is being built around classes. They are a far more elegant way of expressing and organizing logic. Angular 1 services are an implementation of prototype-based classes, which when used correctly function in essentially the same way as formal ES6/TypeScript classes. If you stop here, you will have modified your services to be more extensible and comprehensible. If you intend to upgrade, you will find that Angular 1 services will cleanly upgrade to Angular 2 services.
Componentizing directives using controllerAs encapsulation shows you a superior method for organizing Angular 1 directives
Migrating an application to component directives demonstrates how to refactor Angular 1 to a component style
Implementing a basic component in AngularJS 1.5 details how to write an Angular 1 component
Angular 2 comes with the ability to connect it to an existing Angular 1 application. This is obviously advantageous since this will allow you to utilize existing components and services in Angular 1 in tandem with Angular 2's components and services. UpgradeModule
is the tool that is supported by Angular teams to accomplish such a feat.
Note
The code, links, and a live example in relation to this recipe are available at http://ngcookbook.herokuapp.com/4137/.
Suppose you had a very simple Angular 1 application as follows:
[index.html] <!DOCTYPE html> <html> <head> <!-- Angular 1 scripts --> <script src="angular.js"></script> </head> <body> <div ng-app="hybridApp" ng-init="val='Angular 1 bootstrapped successfully!'"> {{val}} </div> </body> </html>
This application interpolates a value set in an Angular expression so you can visually confirm that the application has bootstrapped and is working.
Begin by declaring the top-level angular module inside its own file. Instead of using a script tag to fetch the angular module, require Angular 1, import it, and create the root Angular 1 module:
[ng1.module.ts] import 'angular' export const Ng1AppModule = angular.module('Ng1AppModule', []);
Angular 2 ships with an upgrade module out of the box, which is provided inside upgrade.js
. The two frameworks can be connected with UpgradeModule
.
Note
This recipe utilizes SystemJS and TypeScript, the specifications for which lie inside a very complicated config file. This is discussed in a later chapter, so don't worry about the specifics. For now, you are free to assume the following:
SystemJS is configured to compile TypeScript (
.ts
) files on the flySystemJS is able to resolve the
import
andexport
statements in TypeScript filesSystemJS is able to resolve Angular 1 and 2 library imports
Angular 2 requires a top-level module definition as part of its base configuration:
[app/ng2.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {UpgradeModule} from '@angular/upgrade/static'; import {RootComponent} from './root.component'; @NgModule({ imports: [ BrowserModule, UpgradeModule, ], bootstrap: [ RootComponent ], declarations: [ RootComponent ] }) export class Ng2AppModule { constructor(public upgrade: UpgradeModule){} } export class AppModule {}
Tip
The reason why this module definition exists this way isn't critical for understanding this recipe. Angular 2 modules are covered in Chapter 7, Services, Dependency Injection, and NgModule.
Create the root component of the Angular 2 application:
[app/root.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'root', template: ` <p>Angular 2 bootstrapped successfully!</p> ` }) export class RootComponent {}
Since Angular 2 will often bootstrap from a top-level file, create this file as main.ts
and bootstrap the Angular 2 module:
[main.ts] import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {Ng1AppModule} from './app/ng1.module'; import {Ng2AppModule} from './app/ng2.module'; platformBrowserDynamic() .bootstrapModule(Ng2AppModule);
Don't use an ng-app to bootstrap the Angular 1 application; instead, do this after you bootstrap Angular 2:
[main.ts]
import {platformBrowserDynamic}
from '@angular/platform-browser-dynamic';
import {Ng1AppModule} from './app/ng1.module';
import {Ng2AppModule} from './app/ng2.module';
platformBrowserDynamic()
.bootstrapModule(Ng2AppModule)
.then(ref => {
ref.instance.upgrade
.bootstrap(document.body, [Ng1AppModule.name]);
});
With this, you'll be able to remove Angular 1's JS script, the ng-app
directive, and add in the root element of the Angular 2 app:
[index.html] <!DOCTYPE html> <html> <head> <!-- Angular 2 scripts --> <script src="zone.js "></script> <script src="reflect-metadata.js"></script> <script src="system.js"></script> <script src="system-config.js"></script> </head> <body> <div ng-init="val='Angular 1 bootstrapped successfully!'"> {{val}} </div> <root></root> </body> </html>
Note
The new scripts listed here are dependencies of an Angular 2 application, but understanding what they're doing isn't critical for understanding this recipe. This is explained later in the book.
With all this, you should see your Angular 1 application template compile and the Angular 2 component render properly again. This means that you are successfully running Angular 1 and Angular 2 frameworks side by side.
Make no mistake, when you use UpgradeModule
, you create an Angular 1 and Angular 2 app on the same page and connect them together. This adapter instance will allow you to connect pieces from each framework and use them in harmony.
More specifically, this creates an Angular 1 application at the top level and allows you to uses pieces of an Angular 2 application inside it.
While useful for experimentation and upgrading purposes, this should not be a solution that any application should rely on in a production context. You have effectively doubled the framework payload size and introduced additional complexity in an existing application. Although Angular 2 is a far more performant framework, do not expect to have the same pristine results with the UpgradeModule
cross-pollination.
That said, as you will see in subsequent recipes, you can now use Angular 2 components in an Angular 1 application using the adapter translation methods.
Downgrading Angular 2 components to Angular 1 directives with downgradeComponent demonstrates how to use an Angular 2 component inside an Angular 1 application
Downgrade Angular 2 providers to Angular 1 services with downgradeInjectable, which demonstrates how to use an Angular 2 service inside an Angular 1 application
If you have followed the steps in Connecting Angular 1 and Angular 2 with UpgradeModule, you should now have a hybrid application that is capable of sharing different elements with the opposing framework.
Tip
If you are unfamiliar with Angular 2 components, it is recommended that you go through the components chapter before you proceed.
This recipe will allow you to fully utilize Angular 2 components inside an Angular 1 template.
Note
The code, links, and a live example in relation to this recipe are available at http://ngcookbook.herokuapp.com/1499/.
Suppose you had the following Angular 2 component that you wanted to use in an Angular 1 application:
[app/article.component.ts] import {Component, Input} from '@angular/core'; @Component({ selector: 'ng2-article', template: ` <h1>{{title}}</h1> <p>Written by: {{author}}</p> ` }) export class ArticleComponent { @Input() author:string title:string = 'Unicycle Jousting Recognized as Olympic Sport'; }
Begin by completing the Connecting Angular 1 and Angular 2 with UpgradeModule recipe.
Angular 1 has no comprehension of how to utilize Angular 2 components. The existing Angular 2 framework will dutifully render it if given the opportunity, but the definition itself must be connected to the Angular 1 framework so that it may be requested when needed.
Begin by adding the component declarations to the module definition; this is used to link the two frameworks:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {UpgradeModule} from '@angular/upgrade/static'; import {RootComponent} from './root.component'; import {ArticleComponent} from './article.component'; @NgModule({ imports: [ BrowserModule, UpgradeModule, ], declarations: [ RootComponent, ArticleComponent ], bootstrap: [ RootComponent ] }) export class Ng2AppModule { constructor(public upgrade: UpgradeModule){} }
This connects the component declaration to the Angular 2 context, but Angular 1 still has no concept of how to interface with it. For this, you'll need to use downgradeComponent()
to define the Angular 2 component as an Angular 1 directive. Give the Angular 1 directive a different HTML tag to render inside so you can be certain that it's Angular 1 doing the rendering and not Angular 2:
[main.ts] import {Component, Input} from '@angular/core'; import {downgradeComponent} from '@angular/upgrade/static'; import {Ng1AppModule} from './ng1.module'; @Component({ selector: 'ng2-article', template: ` <h1>{{title}}</h1> <p>Written by: {{author}}</p> ` }) export class ArticleComponent { @Input() author:string title:string = 'Unicycle Jousting Recognized as Olympic Sport'; } Ng1AppModule.directive( 'ng1Article', downgradeComponent({component: ArticleComponent}));
Finally, since this component has an input, you'll need to pass this value via a binding attribute. Even though the component is still being declared as an Angular 1 directive, you'll use the Angular 2 binding syntax:
[index.html]
<!DOCTYPE html>
<html>
<head>
<!-- Angular 2 scripts -->
<script src="zone.js "></script>
<script src="reflect-metadata.js"></script>
<script src="system.js"></script>
<script src="system-config.js"></script>
</head>
<body>
<div ng-init="authorName='Jake Hsu'">
<ng1-article [author]="authorName"></ng1-article>
</div>
<root></root>
</body>
</html>
The input and output must be explicitly declared at the time of conversion:
[app/article.component.ts]
import {Component, Input} from '@angular/core';
import {downgradeComponent} from '@angular/upgrade/static';
import {Ng1AppModule} from './ng1.module';
@Component({
selector: 'ng2-article',
template: `
<h1>{{title}}</h1>
<p>Written by: {{author}}</p>
`
})
export class ArticleComponent {
@Input() author:string
title:string = 'Unicycle Jousting Recognized as Olympic Sport';
}
Ng1AppModule.directive(
'ng1Article',
downgradeComponent({
component: ArticleComponent,
inputs: ['author']
}));
These are all the steps required. If done properly, you should see the component render along with the author's name being interpolated inside the Angular 2 component through Angular 1's ng-init
definition.
You are giving Angular 1 the ability to direct Angular 2 to a certain element in the DOM and say, "I need you to render here." Angular 2 still controls the component view and operation, and in every sense, the main thing we really care about is a full Angular 2 component adapted for use in an Angular 1 template.
If you have followed the steps in Connecting Angular 1 and Angular 2 with UpgradeModule, you should now have a hybrid application that is capable of sharing different elements with the opposing framework. If you are unfamiliar with Angular 2 providers, it is recommended that you go through the dependency injection chapter before you proceed.
Like with templated components, interchangeability is also offered to service types. It is possible to define a service type in Angular 2 and then inject it into an Angular 1 context.
Note
The code, links, and a live example in relation to this recipe are available at http://ngcookbook.herokuapp.com/2824/.
First, define the service you would like to inject into an Angular 1 component:
[app/article.service.ts] import {Injectable} from '@angular/core'; @Injectable() export class ArticleService { article:Object = { title: 'Research Shows Moon Not Actually Made of Cheese', author: 'Jake Hsu' }; }
Next, define the Angular 1 component that should inject it:
[app/article.component.ts] export const ng1Article = { template: ` <h1>{{article.title}}</h1> <p>{{article.author}}</p> `, controller: (ArticleService, $scope) => { $scope.article = ArticleService.article; } };
ArticleService
won't be injected yet though, since Angular 1 has no idea that this service exists. Doing this is very simple, however. First, you'll list the service provider in the Angular 2 module definition as you normally would:
[app/ng2.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {UpgradeModule} from '@angular/upgrade/static'; import {RootComponent} from './root.component'; import {ArticleService} from './article.service'; @NgModule({ imports: [ BrowserModule, UpgradeModule, ], declarations: [ RootComponent ], providers: [ ArticleService ], bootstrap: [ RootComponent ] }) export class Ng2AppModule { constructor(public upgrade: UpgradeModule){} }
Still, Angular 1 does not understand how to use the service.
In the same way you convert an Angular 2 component definition into an Angular 1 directive, convert an Angular 2 service into an Angular 1 factory. Use downgradeInjectable
and add the Angular 1 component and the converted service to the Angular 1 module definition:
[app/ng1.module.ts] import 'angular'; import {ng1Article} from './article.component'; import {ArticleService} from './article.service'; import {downgradeInjectable} from '@angular/upgrade/static'; export const Ng1AppModule = angular.module('Ng1AppModule', []) .component('ng1Article', ng1Article) .factory('ArticleService', downgradeInjectable(ArticleService));
That's all! You should be able to see the Angular 1 component render with the data passed from the Angular 2 service.