Welcome to the Angular Upgrade App. This is a quick tutorial that teaches you how to upgrade your existing Angular 1 app to Angular 2 step by step.
The app is written in ES5 using Angular 1.5.5 and we are upgrading to Angular 2 (rc1) with the Angular 2 code written in TypeScript.
-
Fork and clone this repo
-
Run
npm i
-
Run
npm start
(Runs the TypeScript compiler, watches for changes, starts the server, and launches the browser)
Exercise 1: Bootstrap the app via the UpgradeAdapter
Exercise 2: Create an ng2 component for the list items and use it within the ng-repeat
Exercise 3: Partly upgrade the ContactDetailComponent to ng2
Exercise 5: upgrade provider to use ng1 services in ng2
Exercise 6: refactor ContactsService to ng2 service and register with ng1
## Exercise 1: Bootstrap the app via the UpgradeAdapterThe goal of this first exercise is to simply throw in Angular 2 and bootstrap the existing Angular 1 app via the UpdateAdapter
provided by Angular 2.
- Adjust the
<head>
section of theindex.html
to include Angular 2 and SystemJS. Let's not get distracted by this. Simply replace your entire list of<script>
tags with this one:
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/angular/angular.js"></script>
<script src="node_modules/angular-route/angular-route.js"></script>
<script src="app/contacts-service/contacts-service.es5.js"></script>
<script src="app/clone-service/clone-service.es5.js"></script>
<script src="app/zippy-component/zippy-component.es5.js"></script>
<script src="app/contact-header-component/contact-header-component.es5.js"></script>
<script src="app/contacts-list-component/contacts-list-component.es5.js"></script>
<script src="app/contact-detail-component/contact-detail-component.es5.js"></script>
<script src="app/contact-editor-component/contact-editor-component.es5.js"></script>
<script src="app/contacts-app.es5.js"></script>
<script>
System.config({packages: {app: {format: 'register',defaultExtension: 'js'}}});
System.config({
map: {
'rxjs': 'node_modules/rxjs',
'@angular': 'node_modules/@angular',
'app': 'app'
},
packages: {
'app': {
main: 'main.js',
defaultExtension: 'js'
},
'@angular/core': {
main: 'index.js',
defaultExtension: 'js'
},
'@angular/compiler': {
main: 'index.js',
defaultExtension: 'js'
},
'@angular/common': {
main: 'index.js',
defaultExtension: 'js'
},
'@angular/platform-browser': {
main: 'index.js',
defaultExtension: 'js'
},
'@angular/platform-browser-dynamic': {
main: 'index.js',
defaultExtension: 'js'
},
'@angular/upgrade': {
main: 'index.js',
defaultExtension: 'js'
},
'rxjs': {
defaultExtension: 'js'
}
}
});
System.import('app/main').then(null, console.error.bind(console));
</script>
- Remove the
ng-app
attribute. Manual bootstrapping is mandatory for the upgrade - Create
app/upgrade-adapter.ts
and import theUpgradeAdapter
from@angular/upgrade
; - Export an instance of the
UpgradeAdapter
fromupgrade-adapter.ts
(export const upgradeAdapter = new UpgradeAdapter();
) - Create
app/main.ts
, import the instance of theUpgradeAdapter
and use it to manually bootstrap the app (upgradeAdapter.bootstrap(document.body, ['contacts-app']);
).
In this exercise we want to partly replace the contacts-list-component
with a new component written in TypeScript using Angular 2.
The goal is not to replace it entirely (just yet) but instead only replace what is repeated within ng-repeat
. So by the end of this exercise we'll have a good old Angular 1 ng-repeat
stamping out new Angular 2 components!
- Create a
Contact
model for a more idiomatic TypeScript usage. Create a new fileapp/models/contact.ts
with the following contents.
interface Address {
street: string;
city: string;
zip: string;
country: string;
}
export interface Contact {
id: number;
name: string;
email: string;
phone: string;
birthday: string;
website: string;
image: string;
address: Address;
}
- Create a
ContactsListItemComponent
-
Create a new file
app/contacts-list-item-component/contacts-list-item-component.ts
with the following contentsimport {Component, Input} from '@angular/core'; import {Contact} from '../models/contact'; @Component({ selector: 'contacts-list-item-component', templateUrl: 'app/contacts-list-item-component/contacts-list-item-component.html' }) export class ContactsListItemComponent { @Input() contact: Contact; }
-
Create a new file
app/contacts-list-item-component/contacts-list-item-component.html
with the following contents<a href="#/contact/{{contact.id}}"> <img [src]="contact.image" alt="" class="circle"> <span class="title">{{contact.name}}</span> </a>
- Create a new file
app/downgrades.ts
. We'll use it to expose Angular 2 components as Angular 1 directives.
-
Import the
UpgradeAdapter
instance -
Import the
ContactsListItemComponent
which we have just created -
Create a new Angular 1 module and register the downgraded component as directive
angular.module('contacts-list-item-component', []) .directive('contactsListItemComponent', upgradeAdapter.downgradeNg2Component(ContactsListItemComponent));
- In
main.ts
addimport './downgrades';
- Add the
contacts-list-item-component
as a module dependency in thecontacts-list-component
- Change the template of the
contacts-list-component
to repeat over our new ng2 component
<ul class="collection">
<li class="collection-item avatar" ng-repeat="contact in $ctrl.contacts">
<contacts-list-item-component [contact]="contact"></contacts-list-item-component>
</li>
</ul>
In this exercise we want to start upgrading the ContactDetailComponent
to Angular 2. This component has dependencies against services and also uses yet another Angular 1 component in its template, the zippy-component
.
So, let's break this down in more manageable steps to not get lost. Let's ignore the zippy-component
for now. Let's also ignore the services for now and instead keep a thin layer of Angular 1 around our new Angular 2 component that deals with the services and pass the Contact
instance through an @Input
-property as we used before. We will come back to these issues later.
- Create a
ContactDetailComponent
-
Create file
app/contact-detail-component/contact-detail-component.ts
with the following contentsimport {Component, Input} from '@angular/core'; import {Contact} from '../models/contact'; @Component({ selector: 'contact-detail-component', templateUrl: 'app/contact-detail-component/contact-detail-component.html' }) export class ContactDetailComponent { @Input() contact: Contact; }
-
Change the
contact-detail-component.html
to match the expected Angular 2 syntax.<div class="row"> <div class="col s12 m7"> <div class="card"> <div class="card-image"> <img src="{{contact?.image}}"> <span class="card-title">{{contact?.name}}</span> </div> <div class="card-content grey-text text-darken-4"> <div class="row"> <span class="col s6"><i class="material-icons prefix">email</i> Email:</span> <span class="col s6">{{contact?.email || '-'}}</span> </div> <div class="row"> <span class="col s6"><i class="material-icons prefix">phone</i> Phone:</span> <span class="col s6">{{contact?.phone || '-'}}</span> </div> <div class="row"> <span class="col s6"><i class="material-icons prefix">cake</i> Birthday:</span> <span class="col s6">{{contact?.birthday || '-'}}</span> </div> <div class="row"> <span class="col s6"><i class="material-icons prefix">public</i> Website:</span> <span class="col s6">{{contact?.website || '-'}}</span> </div> <zippy-component title="{{zippyCaption}}" toggle="toggleCaption(closed)"> <fieldset> <legend><i class="material-icons prefix">location_city</i> Address</legend> <div class="row"> <span class="col s6">Street:</span> <span class="col s6">{{contact?.address?.street || '-'}}</span> </div> <div class="row"> <span class="col s6">Zipcode:</span> <span class="col s6">{{contact?.address?.zip || '-'}}</span> </div> <div class="row"> <span class="col s6">City:</span> <span class="col s6">{{contact?.address?.city || '-'}}</span> </div> <div class="row"> <span class="col s6">Country:</span> <span class="col s6">{{contact?.address?.country || '-'}}</span> </div> </fieldset> </zippy-component> </div> <div class="card-action"> <a class="btn" href="#/">Go Back</a> <a class="btn" href="#/contact/{{contact?.id}}/edit">Edit</a> </div> </div> </div> </div>
- Downgrade the component in
downgrades.ts
but make sure to add it to the existing Angular 1 module instead of creating a new one. Don't forget that you have to import the component at the top of the file.
angular.module('contact-detail-component')
.directive('contactDetailComponent', upgradeAdapter.downgradeNg2Component(ContactDetailComponent));
- Change the
contact-detail-component.es5.js
file to remove the actual component but keep the routing and write an inline controller to retrieve the data and pass it through the property binding to our new Angular 2 component.
angular
.module('contact-detail-component', ['ngRoute', 'contacts-service', 'zippy-component'])
.config(function ($routeProvider) {
$routeProvider.when('/contact/:id', {
controller: function (contactsService, $routeParams) {
this.contact = contactsService.getContact($routeParams.id);
},
controllerAs: '$ctrl',
template: '<contact-detail-component [contact]="$ctrl.contact"><contact-detail-component>'
});
});
With the last exercise we left our ContactDetailComponent
partly broken because the template uses a zippy-component
which is written in Angular 1 but doesn't work out of the box within our Angular 2 template any longer.
Let's fix that by upgrading the ng1 component so that it can be used in Angular 2.
- Use the
UpgradeAdapter
instance to upgrade the zippy and use it in our newContactDetailComponent
import {Component, Input} from '@angular/core';
import {Contact} from '../models/contact';
import {upgradeAdapter} from '../upgrade-adapter';
const ZippyComponent = upgradeAdapter.upgradeNg1Component('zippyComponent');
@Component({
selector: 'contact-detail-component',
templateUrl: 'app/contact-detail-component/contact-detail-component.html',
directives: [ZippyComponent]
})
export class ContactDetailComponent {
@Input() contact: Contact;
zippyCaption: string;
constructor () {
this.toggleCaption(false);
}
toggleCaption (closed: boolean) {
this.zippyCaption = closed ? 'Show address' : 'Hide address';
}
}
- Change the syntax in the template that uses the zippy to the Angular 2 syntax.
<zippy-component title="{{zippyCaption}}" (toggle)="toggleCaption($event.closed)">
When we ported over the contact-detail-component
to Angular 2 we cheated a little to work around the fact that this component actually wants some services injected. Let's address these issues and upgrade the Angular 1 provider so that we can inject the Angular 1 services in our Angular 2 component.
- Upgrade the provider for
$routeParams
andcontactsService
through the already injectedUpgradeAdapter
instance.
upgradeAdapter.upgradeNg1Provider('$routeParams');
upgradeAdapter.upgradeNg1Provider('contactsService');
- Inject the services and retrieve the contact through it.
constructor (@Inject('$routeParams') $routeParams: any,
@Inject('contactsService') contactsService: any) {
this.contact = contactsService.getContact($routeParams.id);
this.toggleCaption(false);
}
At this point we want to rewrite the ContactsService
as an Angular 2 service. But since we are still using the service from different Angular 1 components we need to make sure that it continues to be available in both worlds. In other words, this time we want to use an Angular 2 service from within Angular 1.
- Delete the old
contacts-service.es5.js
file and create a fileapp/contacts-service/contacts-service.ts
with the following content.
```ts
import {Injectable} from '@angular/core';
@Injectable()
export class ContactsService {
private CONTACT_DATA = [
{
id: 0,
name: 'Christoph Burgdorf',
email: 'christoph@thoughtram.io',
phone: '+49 000 1111',
birthday: '1984-01-02',
website: 'thoughtram.io',
image: '/assets/images/0.jpg',
address: {
street: 'thoughtram road 1',
zip: '65222',
city: 'Hanover',
country: 'Germany'
}
},
{
id: 1,
name: 'Pascal Precht',
email: 'pascal@thoughtram.io',
phone: '+49 000 222',
birthday: '1991-03-31',
website: 'thoughtram.io',
image: '/assets/images/1.jpg',
address: {
street: 'thoughtram road 1',
zip: '65222',
city: 'Hanover',
country: 'Germany'
}
},
{
id: 2,
name: 'Nicole Hansen',
email: 'who@car.es',
phone: '+49 000 333',
birthday: '1981-03-31',
website: '',
image: '/assets/images/3.jpg',
address: {
street: 'Who Cares Street 42',
zip: '65222',
city: 'Sun Funcisco',
country: 'United States'
}
},
{
id: 3,
name: 'Zoe Moore',
email: 'zoe@moore.com',
phone: '+49 000 000',
birthday: '1990-02-18',
website: '',
image: '/assets/images/4.jpg',
address: {
street: '3745 denny street',
zip: '86337',
city: 'Ballinasloe',
country: 'United States'
}
},
{
id: 4,
name: 'Diane Hale',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/5.jpg',
address: {
street: '1459 tara street',
zip: '18371',
city: 'Bray',
country: 'United States'
}
},
{
id: 5,
name: 'Diana Ellis',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/6.jpg',
address: {
street: '6503 tara street',
zip: '43378',
city: 'Dungarvan',
country: 'United States'
}
},
{
id: 6,
name: 'Barry Ford',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/7.jpg',
address: {
street: '6554 park lane',
zip: '43378',
city: 'Rush',
country: 'United States'
}
},
{
id: 7,
name: 'Ella Grant',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/8.jpg',
address: {
street: '2749 church road',
zip: '87125',
city: 'Clonakilty',
country: 'United States'
}
},
{
id: 8,
name: 'Brent Mason',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/9.jpg',
address: {
street: '8436 tara street',
zip: '59949',
city: 'Dundalk',
country: 'United States'
}
},
{
id: 9,
name: 'Sam Thomas',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/10.jpg',
address: {
street: '2523 park road',
zip: '59949',
city: 'Drogheda',
country: 'United States'
}
},
{
id: 10,
name: 'Vicky Roberts',
email: '',
phone: '',
birthday: '',
website: '',
image: '/assets/images/11.jpg',
address: {
street: '9791 grafton street',
zip: '30165',
city: 'Galway',
country: 'London'
}
}
];
getContacts () {
return this.CONTACT_DATA;
}
getContact (id: string) {
return this.CONTACT_DATA.find(contact => contact.id.toString() === id);
}
}
```
- Import the new
ContactsService
in theContactDetailComponent
and change the constructor accordingly.
```ts
constructor (@Inject('$routeParams') $routeParams: any,
contactsService: ContactsService) {
this.contact = contactsService.getContact($routeParams.id);
this.toggleCaption(false);
}
```
-
Remove the call to
upgradeNg1Provider
in theContactDetailComponent
-
In
downgrades.ts
usedowngradeNg2Provider
to make the service available for Angular 1
```ts
angular.module('contacts-service', [])
.service('contactsService', upgradeAdapter.downgradeNg2Provider(ContactsService));
```
- In
main.ts
import theContactService
and register it as a provider for Angular 2.
```ts
upgradeAdapter.addProvider(ContactsService);
```