ngUpgrade & Migration

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this chapter we will be upgrading Socially from Angular1 to Angular2 using Angular's Upgrade Adapter. Why upgrading? Because Angular2 is the next major version of the framework and it will surely be the version we want to go with when building web apps in the future, and we can enjoy many of its benefits like better performance, server-side rendering, more powerful templating, better ecosystem and more. We will also be transforming our JavaScript code into TypeScript inspired by Angular2 team's recommendation.

Switching to TypeScript

First, we will remove the JavaScript compiler, whos belonging Meteor package is pbastowski:angular-babel:

meteor remove pbastowski:angular-babel

Although it is possible to just replace the old JavaScript compiler with a TypeScript compiler, we would like to reserve ng-annotate's compiler as well, since the updating process is gonna be done gradually and our app is gonna be a hybrid of Angular1 and Angular2. Thus, we're gonna install the following package:

meteor add mys:typescript-ng-annotate

Once our app is fully migrated to Angular2, we will replace mys:typescript-ng-annotate with barbatus:typescript, a package which will provide you with a pure TypeScript compiler. TypeScript compiler also relies on declerations, which can be installed via package manager called typings. Further informations about typings and how to install it can be found here. As for now just add the following configuration files:

23.3 Add typings configuration files .gitignore
1
2
3
4
node_modules/
typings/
.idea
npm-debug.log
23.3 Add typings configuration files typings.d.ts
1
2
3
4
declare module '*.html' {
  const template: string;
  export default template;
}
23.3 Add typings configuration files typings.json
1
2
3
4
5
6
7
8
9
10
11
{
  "name": "angular2-meteor-base",
  "version": false,
  "dependencies": {
    "chai-spies": "registry:npm/chai-spies#0.7.1+20160614064916"
  },
  "globalDependencies": {
    "meteor": "github:meteor-typings/meteor/1.3#955b89a4e2af892d1736bc570b490a97e860d5b7",
    "node": "registry:env/node#6.0.0+20161019193037"
  }
}

And run the following command which will install the declarations defined in the configuration files we've just added:

$ typings install

By now if will you run the app you will receive warning notifications by the TypeScript compiler. This is caused due to our app not being fully migrated to TypeScript. Once you finish the upgrading process and your app is purely based on Angular2 with proper declarations you shall receive no warnings.

Now that the compiler is ready we will start by switching our entry file into TypeScript:

23.4 Rename main file and make few changes to support typescript client/main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import * as angular from 'angular';
 
import { Meteor } from 'meteor/meteor';
 
import { registerAuth } from '../imports/ui/components/auth/auth';
import { registerLogin } from '../imports/ui/components/login/login';
import { registerNavigation } from '../imports/ui/components/navigation/navigation';
import { registerPartiesList } from '../imports/ui/components/partiesList/partiesList';
import { registerPartiesMap } from '../imports/ui/components/partiesMap/partiesMap';
import { registerPartiesSort } from '../imports/ui/components/partiesSort/partiesSort';
import { registerPartyAdd } from '../imports/ui/components/partyAdd/partyAdd';
import { registerPartyAddButton } from '../imports/ui/components/partyAddButton/partyAddButton';
import { registerPartyCreator } from '../imports/ui/components/partyCreator/partyCreator';
import { registerPartyDetails } from '../imports/ui/components/partyDetails/partyDetails';
import { registerPartyImage } from '../imports/ui/components/partyImage/partyImage';
import { registerPartyMap } from '../imports/ui/components/partyMap/partyMap';
import { registerPartyRemove } from '../imports/ui/components/partyRemove/partyRemove';
import { registerPartyRsvp } from '../imports/ui/components/partyRsvp/partyRsvp';
import { registerPartyRsvpsList } from '../imports/ui/components/partyRsvpsList/partyRsvpsList';
import { registerPartyUninvited } from '../imports/ui/components/partyUninvited/partyUninvited';
import { registerPartyUpload } from '../imports/ui/components/partyUpload/partyUpload';
import { registerPassword } from '../imports/ui/components/password/password';
import { registerRegister } from '../imports/ui/components/register/register';
import { registerSocially, SociallyNg1Module } from '../imports/ui/components/socially/socially';
 
registerAuth();
registerLogin();
registerNavigation();
registerPartiesList();
registerPartiesMap();
registerPartiesSort();
registerPartyAdd();
registerPartyAddButton();
registerPartyCreator();
registerPartyDetails();
registerPartyImage();
registerPartyMap();
registerPartyRemove();
registerPartyRsvp();
registerPartyRsvpsList();
registerPartyUninvited();
registerPartyUpload();
registerPassword();
registerRegister();
registerSocially();
 
function onReady() {
  angular.bootstrap(document, [
    SociallyNg1Module.name
  ]);
}
 
if (Meteor.isCordova) {
  angular.element(document).on('deviceready', onReady);
} else {
  angular.element(document).ready(onReady);
}

Not only we changed the extension of the file, but we also made some adjustments to its content. All Angular1 modules registrations are now manually invoked, due to dependency on the upgrade who's gonna take part further in this tutorial. In addition, the way we import the angular library have changed.

Before:

import angular from 'angular';

After:

import * as angular from 'angular';

Now we gonna go through a component transformation. We will start with the Socially component since it is the root component of our app. We will first change the way we import Angular-related libraries, just like demonstrated above:

23.5 Default imports of libraries inside Socially component imports/ui/components/socially/socially.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import * as angular from 'angular';
import * as angularMeteor from 'angular-meteor';
import * as ngMaterial from 'angular-material';
import * as ngSanitize from 'angular-sanitize';
import * as uiRouter from 'angular-ui-router';
import 'ionic-sdk/release/js/ionic';
import 'ionic-sdk/release/js/ionic-angular';
import 'ionic-sdk/release/css/ionic.css';
 
import { Meteor } from 'meteor/meteor';
 
import webTemplate from './web.html';
import mobileTemplate from './mobile.html';
import { name as PartiesList } from '../partiesList/partiesList';
import { name as PartyDetails } from '../partyDetails/partyDetails';
import { name as Navigation } from '../navigation/navigation';
import { name as Auth } from '../auth/auth';
 
class Socially {}
 
const name = 'socially';
const template = Meteor.isCordova ? mobileTemplate : webTemplate;
 
// create a module
export default angular.module(name, [
  angularMeteor,
  ngMaterial,
  ngSanitize,
  uiRouter,
  PartiesList,
  PartyDetails,
  Navigation,
  Auth,
  'accounts.ui',
  'ionic'
]).component(name, {
  template,
  controllerAs: name,
  controller: Socially
})
  .config(config)
  .run(run);
 
function config($locationProvider, $urlRouterProvider, $qProvider, $mdIconProvider) {
  'ngInject';
 
  $locationProvider.html5Mode(true);
 
  $urlRouterProvider.otherwise('/parties');
 
  $qProvider.errorOnUnhandledRejections(false);
 
  const iconPath =  '/packages/planettraining_material-design-icons/bower_components/material-design-icons/sprites/svg-sprite/';
 
  $mdIconProvider
    .iconSet('social',
      iconPath + 'svg-sprite-social.svg')
    .iconSet('action',
      iconPath + 'svg-sprite-action.svg')
    .iconSet('communication',
      iconPath + 'svg-sprite-communication.svg')
    .iconSet('content',
      iconPath + 'svg-sprite-content.svg')
    .iconSet('toggle',
      iconPath + 'svg-sprite-toggle.svg')
    .iconSet('navigation',
      iconPath + 'svg-sprite-navigation.svg')
    .iconSet('image',
      iconPath + 'svg-sprite-image.svg');
}
 
function run($rootScope, $state) {
  'ngInject';
 
  $rootScope.$on('$stateChangeError',
    (event, toState, toParams, fromState, fromParams, error) => {
      if (error === 'AUTH_REQUIRED') {
        $state.go('parties');
      }
    }
  );
}

Second, we will specify the exported module as an Angular1 module, since we're dealing with two module systems which come both from Angular1 and Angular2, and we will implement a registration which is called from the entry file like we showed earlier:

23.6 Default imports of components imports/ui/components/socially/socially.ts
11
12
13
14
15
16
17
18
19
20
 
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 
import webTemplate from './web.html';
import mobileTemplate from './mobile.html';
import { PartiesListNg1Module } from '../partiesList/partiesList';
import { PartyDetailsNg1Module } from '../partyDetails/partyDetails';
import { NavigationNg1Module } from '../navigation/navigation';
import { AuthNg1Module } from '../auth/auth';
 
class Socially {}
 
...some lines skipped...
const template = Meteor.isCordova ? mobileTemplate : webTemplate;
 
// create a module
export const SociallyNg1Module = angular.module(name, [
  angularMeteor,
  ngMaterial,
  ngSanitize,
  uiRouter,
  PartiesListNg1Module.name,
  PartyDetailsNg1Module.name,
  NavigationNg1Module.name,
  AuthNg1Module.name,
  'accounts.ui',
  'ionic'
]);
 
export function registerSocially() {
  SociallyNg1Module
    .component(name, {
      template,
      controllerAs: name,
      controller: Socially
    })
    .config(config)
    .run(run);
}
 
function config($locationProvider, $urlRouterProvider, $qProvider, $mdIconProvider) {
  'ngInject';

Now we gonna simply switch all the file extensions from .js to .ts and repeat the recent process for each component. This process is a pain so if you don't wanna deal with it just git-checkout the next step of this tutorial.

Once your'e done we shall change the way we import the underscore library otherwise our app might break:

23.8 Import underscore correctly inside Parties Methods imports/api/parties/methods.ts
1
2
3
4
import * as _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { Email } from 'meteor/email';

Upgrading to Angular2

Since we wanna use Angular2 we will have to install its essential packages:

23.9 Install angular2 modules package.json
6
7
8
9
10
11
12
13
14
15
16
17
18
 
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    "test:watch": "meteor test --driver-package sanjo:jasmine"
  },
  "dependencies": {
    "@angular/common": "^2.1.0",
    "@angular/compiler": "^2.1.0",
    "@angular/core": "^2.1.0",
    "@angular/forms": "^2.1.0",
    "@angular/platform-browser": "^2.1.0",
    "@angular/platform-browser-dynamic": "^2.1.0",
    "@angular/upgrade": "^2.1.0",
    "angular": "^1.5.3",
    "angular-animate": "^1.5.3",
    "angular-aria": "^1.5.3",
...some lines skipped...
    "angular-material": "^1.0.7",
    "angular-messages": "^1.5.3",
    "angular-meteor": "^1.3.9",
    "angular-sanitize": "^1.5.5",
    "angular-simple-logger": "^0.1.7",
    "angular-sortable-view": "0.0.15",
    "angular-ui-router": "^0.2.18",
    "angular-utils-pagination": "^0.11.1",
    "angular2-meteor": "^0.7.0",
    "angular2-meteor-polyfills": "^0.1.1",
    "es6-shim": "^0.35.1",
    "gm": "^1.22.0",
    "ionic-sdk": "^1.2.4",
    "meteor-node-stubs": "~0.2.0",
    "ng-file-upload": "^12.0.4",
    "ng-img-crop": "^0.2.0",
    "reflect-metadata": "^0.1.8",
    "rxjs": "^5.0.0-beta.12",
    "underscore": "^1.8.3",
    "zone.js": "^0.6.21"
  },
  "devDependencies": {
    "angular-mocks": "^1.5.3"

Raw commands are listed below:

$ npm install --save @angular/common
$ npm install --save @angular/compiler
$ npm install --save @angular/core
$ npm install --save @angular/forms
$ npm install --save @angular/platform-browser
$ npm install --save @angular/platform-browser-dynamic
$ npm install --save @angular/upgrade
$ npm install --save angular2-meteor
$ npm install --save angular2-meteor-polyfills
$ npm install --save es6-shim
$ npm install --save reflect-metadata
$ npm install --save rxjs
$ npm install --save underscore
$ npm install --save zone.js

angular2-meteor-polyfills will load rxjs, reflect-metadata and zone.js in their chronological order.

Now will start replacing our Angular1 components with Angular2 components, and it would be helpful if we could just do it gradually and not all at once, one component at a time. This is where the Upgrade Adapter kicks in. The adapter will give us the ability to downgrade Angular2 so they can be registered to Angular1 modules, and upgrade Angular1 components so they can be registered to Angular2 modules, thereby we can have a hybrid app. Let's get into business by initializing a new instance of the UpgradeAdapter and passing it as an argument to each module registration:

23.10 Create module with instance of UpgradeAdapter client/main.ts
1
2
3
4
5
 
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import * as angular from 'angular';
import { UpgradeAdapter } from [email protected]/upgrade';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
import { registerRegister } from '../imports/ui/components/register/register';
import { registerSocially, SociallyNg1Module } from '../imports/ui/components/socially/socially';
 
const adapter = new UpgradeAdapter();
 
registerAuth(adapter);
registerLogin(adapter);
registerNavigation(adapter);
registerPartiesList(adapter);
registerPartiesMap(adapter);
registerPartiesSort(adapter);
registerPartyAdd(adapter);
registerPartyAddButton(adapter);
registerPartyCreator(adapter);
registerPartyDetails(adapter);
registerPartyImage(adapter);
registerPartyMap(adapter);
registerPartyRemove(adapter);
registerPartyRsvp(adapter);
registerPartyRsvpsList(adapter);
registerPartyUninvited(adapter);
registerPartyUpload(adapter);
registerPassword(adapter);
registerRegister(adapter);
registerSocially(adapter);
 
function onReady() {
  angular.bootstrap(document, [

Note that you have to share the same instance of the adapter otherwise the application might not work as expected. Once we have our adapter we will create an Angular2 module which will represent our Angular2 app's module, and bootstrap our application using the adapter:

23.11 Bootstrap using UpgradeAdapter client/main.ts
1
2
3
4
5
6
7
8
 
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 
60
61
62
63
64
65
66
import 'angular2-meteor-polyfills/browser';
import * as angular from 'angular';
import { NgModule } from [email protected]/core';
import { BrowserModule } from [email protected]/platform-browser';
import { FormsModule } from [email protected]/forms';
import { UpgradeAdapter } from [email protected]/upgrade';
 
import { Meteor } from 'meteor/meteor';
...some lines skipped...
import { registerRegister } from '../imports/ui/components/register/register';
import { registerSocially, SociallyNg1Module } from '../imports/ui/components/socially/socially';
 
@NgModule({
  imports: [
    BrowserModule,
    FormsModule
  ]
})
class AppNg2Module {}
 
const adapter = new UpgradeAdapter(AppNg2Module);
 
registerAuth(adapter);
registerLogin(adapter);
...some lines skipped...
registerSocially(adapter);
 
function onReady() {
  adapter.bootstrap(document.body, [
    SociallyNg1Module.name
  ]);
}

Filters to Pipes

Angular1 filters have the same functionality as Angular2 pipes. Further details about the relation between filters pipes can be found in Angular's docs. Filters should be registered to Angular1 modules as normal and pipes should be registered to Angular2 modules, so even though they are almost identical they will have to be duplicated for now.

We will start with the displayName pipe. This is how it should look like, and after we create a pipe it should be declared our app's Angular2 module:

23.12 Create displayNamePipe based on displayNameFilter client/main.ts
27
28
29
30
31
32
33
34
35
36
37
38
import { registerPassword } from '../imports/ui/components/password/password';
import { registerRegister } from '../imports/ui/components/register/register';
import { registerSocially, SociallyNg1Module } from '../imports/ui/components/socially/socially';
import { DisplayNamePipe } from '../imports/ui/filters/displayNamePipe';
 
@NgModule({
  declarations: [
    DisplayNamePipe
  ],
  imports: [
    BrowserModule,
    FormsModule
23.12 Create displayNamePipe based on displayNameFilter imports/ui/filters/displayNamePipe.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Pipe } from [email protected]/core';
 
const name = 'displayName';
 
@Pipe({
  name
})
export class DisplayNamePipe {
  transform(user) {
    if (!user) {
      return '';
    }
 
    if (user.profile && user.profile.name) {
      return user.profile.name;
    }
 
    if (user.emails) {
      return user.emails[0].address;
    }
 
    return user;
  }
}

The key concepts of this convertion are:

  • import Pipe decorator.
  • change name to displayName.
  • move a function to be a class with transform method.
  • remove everything related to Angular1.

The same process should be applied for uninvited pipe as well:

23.13 Create UninvitedPipe based on UninvitedFilter client/main.ts
28
29
30
31
32
33
34
35
36
37
38
39
import { registerRegister } from '../imports/ui/components/register/register';
import { registerSocially, SociallyNg1Module } from '../imports/ui/components/socially/socially';
import { DisplayNamePipe } from '../imports/ui/filters/displayNamePipe';
import { UninvitedPipe } from '../imports/ui/filters/uninvitedPipe';
 
@NgModule({
  declarations: [
    DisplayNamePipe,
    UninvitedPipe
  ],
  imports: [
    BrowserModule,
23.13 Create UninvitedPipe based on UninvitedFilter imports/ui/filters/uninvitedPipe.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Pipe } from [email protected]/core';
import * as _ from 'underscore';
 
const name = 'uninvited';
 
@Pipe({
  name
})
export class UninvitedPipe {
  transform(users, party) {
    if (!party) {
      return false;
    }
 
    return users.filter((user) => {
      // if not the owner and not invited
      return user._id !== party.owner && !_.contains(party.invited, user._id);
    });
  }
}

We can't return false inside an ngFor directive so let's replace it with an empty array:

23.14 Return an empty array instead of false imports/ui/filters/uninvitedPipe.ts
9
10
11
12
13
14
15
export class UninvitedPipe {
  transform(users, party) {
    if (!party) {
      return [];
    }
 
    return users.filter((user) => {

Preparing Angular1 component for migration

We will focus on the PartyUninvited to start with. Let's decorate it with a Component decorator, And like the pipe creation process we will also have to declare a component whenever we create it:

23.15 Use Component decorator client/main.ts
22
23
24
25
26
27
28
 
33
34
35
36
37
38
39
40
import { registerPartyRemove } from '../imports/ui/components/partyRemove/partyRemove';
import { registerPartyRsvp } from '../imports/ui/components/partyRsvp/partyRsvp';
import { registerPartyRsvpsList } from '../imports/ui/components/partyRsvpsList/partyRsvpsList';
import { registerPartyUninvited, PartyUninvited } from '../imports/ui/components/partyUninvited/partyUninvited';
import { registerPartyUpload } from '../imports/ui/components/partyUpload/partyUpload';
import { registerPassword } from '../imports/ui/components/password/password';
import { registerRegister } from '../imports/ui/components/register/register';
...some lines skipped...
@NgModule({
  declarations: [
    DisplayNamePipe,
    UninvitedPipe,
    PartyUninvited
  ],
  imports: [
    BrowserModule,
1
2
3
4
5
6
 
8
9
10
11
12
13
14
15
16
17
18
import * as angular from 'angular';
import * as angularMeteor from 'angular-meteor';
import { Component } from [email protected]/core';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
import UninvitedFilter from '../../filters/uninvitedFilter';
import DisplayNameFilter from '../../filters/displayNameFilter';
 
@Component({
  template,
  selector: 'party-uninvited'
})
export class PartyUninvited {
  constructor($scope) {
    'ngInject';
 

Since this is an Angular2 component it relies on pipes rather than filters, so we can remove the filters' importations:

23.16 Use Pipes instead of Filters imports/ui/components/partyUninvited/partyUninvited.ts
5
6
7
8
9
10
 
40
41
42
43
44
45
46
import { Meteor } from 'meteor/meteor';
 
import template from './partyUninvited.html';
 
@Component({
  template,
...some lines skipped...
 
// create a module
export const PartyUninvitedNg1Module = angular.module(name, [
  angularMeteor
]);
 
export function registerPartyUninvited() {

Switching to angular2-meteor

Switching to angular2-meteor would be an integral part of the migration. We will extend our component by MeteorComponent so angular2-meteor's API would be available for use

1
2
3
4
5
6
7
 
11
12
13
14
15
16
17
18
19
20
import * as angular from 'angular';
import * as angularMeteor from 'angular-meteor';
import { Component } from [email protected]/core';
import { MeteorComponent } from 'angular2-meteor';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
  template,
  selector: 'party-uninvited'
})
export class PartyUninvited extends MeteorComponent {
  constructor($scope) {
    'ngInject';
    super();
 
    $scope.viewModel(this);
 

Once it's done we will replace angular-meteor's API with angular2-meteor's.

23.18 Switch to MeteorComponent API imports/ui/components/partyUninvited/partyUninvited.ts
12
13
14
15
16
17
18
19
20
21
22
  selector: 'party-uninvited'
})
export class PartyUninvited extends MeteorComponent {
  constructor() {
    super();
 
    this.autorun(() => {
      this.users = Meteor.users.find({}).fetch();
    });
  }
 

Now that we have our Angular2 component ready, we need to use it in our Angular1 module. The UpgradeAdapter's prototype contains a function called downgradeNg2Component which will transform an Angular2 component into an Angular1 directive, once it is transformed we can declare it in our Angular1 module:

23.19 Use downgraded Ng2 Component imports/ui/components/partyUninvited/partyUninvited.ts
40
41
42
43
44
45
46
  angularMeteor
]);
 
export function registerPartyUninvited(adapter) {
  PartyUninvitedNg1Module
    .directive(name, adapter.downgradeNg2Component(PartyUninvited))
}

Note that a component is a actually a directive so registering a directive achieves the same result.

The bindings of our Angular1 component can be translated to Angular2 using the Input decorator.

1
2
3
4
5
6
 
12
13
14
15
16
17
18
19
import * as angular from 'angular';
import * as angularMeteor from 'angular-meteor';
import { Component, Input } from [email protected]/core';
import { MeteorComponent } from 'angular2-meteor';
 
import { Meteor } from 'meteor/meteor';
...some lines skipped...
  selector: 'party-uninvited'
})
export class PartyUninvited extends MeteorComponent {
  @Input() party: any;
 
  constructor() {
    super();
 

We will no longer use prefixed variables inside our template since Angular2's template engine doesn't support it:

3
4
5
6
7
8
9
10
11
12
</h4>
 
<md-list>
  <md-list-item ng-repeat="user in users | uninvitedFilter:party" ng-click="invite(user)">
    <p>{{ user | displayNameFilter }}</p>
  </md-list-item>
  <md-list-item ng-if="(users | uninvitedFilter:party).length <= 0">
    Everyone are already invited.
  </md-list-item>
</md-list>

Our template uses Pipes instead of Filters, so we will make the following adjustments:

3
4
5
6
7
8
9
10
11
12
</h4>
 
<md-list>
  <md-list-item ng-repeat="user in users | uninvited:party" ng-click="invite(user)">
    <p>{{ user | displayName }}</p>
  </md-list-item>
  <md-list-item ng-if="(users | uninvited:party).length <= 0">
    Everyone are already invited.
  </md-list-item>
</md-list>

The ngFor directive of Angular2 is equivalent to Angular1's ng-for:

3
4
5
6
7
8
9
</h4>
 
<md-list>
  <md-list-item *ngFor="let user of users | uninvited:party" ng-click="invite(user)">
    <p>{{ user | displayName }}</p>
  </md-list-item>
  <md-list-item ng-if="(users | uninvited:party).length <= 0">

And ng-if directive is now ngIf:

6
7
8
9
10
11
12
  <md-list-item *ngFor="let user of users | uninvited:party" ng-click="invite(user)">
    <p>{{ user | displayName }}</p>
  </md-list-item>
  <md-list-item *ngIf="(users | uninvited:party).length <= 0">
    Everyone are already invited.
  </md-list-item>
</md-list>

Events are registered using (parenthesis) rather than an ng prefix:

3
4
5
6
7
8
9
</h4>
 
<md-list>
  <md-list-item *ngFor="let user of users | uninvited:party" (click)="invite(user)">
    <p>{{ user | displayName }}</p>
  </md-list-item>
  <md-list-item *ngIf="(users | uninvited:party).length <= 0">

Since PartyUninvited is a Angular2 component we have to change the way we're passing a value:

Bound attributes should be notated with [square brackets]:

24
25
26
27
28
    <party-map flex="50" location="partyDetails.party.location"></party-map>
  </div>
 
  <party-uninvited flex [party]="partyDetails.party" ng-show="partyDetails.canInvite()"></party-uninvited>
</div>

Now let's do the same for PartyDetails. First we will turn it into a component:

23.27 Implement Component decorator in PartyDetails client/main.ts
16
17
18
19
20
21
22
 
34
35
36
37
38
39
40
import { registerPartyAdd } from '../imports/ui/components/partyAdd/partyAdd';
import { registerPartyAddButton } from '../imports/ui/components/partyAddButton/partyAddButton';
import { registerPartyCreator } from '../imports/ui/components/partyCreator/partyCreator';
import { registerPartyDetails, PartyDetails } from '../imports/ui/components/partyDetails/partyDetails';
import { registerPartyImage } from '../imports/ui/components/partyImage/partyImage';
import { registerPartyMap } from '../imports/ui/components/partyMap/partyMap';
import { registerPartyRemove } from '../imports/ui/components/partyRemove/partyRemove';
...some lines skipped...
  declarations: [
    DisplayNamePipe,
    UninvitedPipe,
    PartyDetails,
    PartyUninvited
  ],
  imports: [
23.27 Implement Component decorator in PartyDetails imports/ui/components/partyDetails/partyDetails.ts
1
2
3
4
5
6
7
 
10
11
12
13
14
15
16
17
18
19
20
import * as angular from 'angular';
import * as angularMeteor from 'angular-meteor';
import * as uiRouter from 'angular-ui-router';
import { Component } from [email protected]/core';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
import { PartyUninvitedNg1Module } from '../partyUninvited/partyUninvited';
import { PartyMapNg1Module } from '../partyMap/partyMap';
 
@Component({
  template,
  selector: 'party-details'
})
export class PartyDetails {
  constructor($stateParams, $scope, $reactive) {
    'ngInject';
 

Since PartyDetails component is dependent on an Angular1 component which outside our project's scope, we will have to upgrade it using the upgradeNg1Component method:

23.28 Upgrade PartyMap to be used inside Ng2 Component client/main.ts
46
47
48
49
50
51
52
53
 
const adapter = new UpgradeAdapter(AppNg2Module);
 
adapter.upgradeNg1Component('partyMap');
 
registerAuth(adapter);
registerLogin(adapter);
registerNavigation(adapter);

Then again, we will extend the MeteorComponent:

2
3
4
5
6
7
8
 
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import * as angularMeteor from 'angular-meteor';
import * as uiRouter from 'angular-ui-router';
import { Component } from [email protected]/core';
import { MeteorComponent } from 'angular2-meteor';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
  template,
  selector: 'party-details'
})
export class PartyDetails extends MeteorComponent {
  partyId: string;
  party: Object = {};
  users: Object[];
  isLoggedIn: boolean;
 
  constructor() {
    super();
 
    this.subscribe('parties');
    this.subscribe('users');
 
    this.autorun(() => {
      this.party = Parties.findOne({
        _id: this.partyId
      });
 
      this.users = Meteor.users.find({}).fetch();
 
      this.isOwner = this.party && this.party.owner === Meteor.userId();
    }, true);
  }
 
  canInvite() {

And we will downgrade our newly created component so it can be loaded with Angular1's module system:

23.30 Downgrade PartyDetails to ng1 directive imports/ui/components/partyDetails/partyDetails.ts
76
77
78
79
80
81
82
83
84
  PartyMapNg1Module.name
]);
 
export function registerPartyDetails(adapter) {
  PartyDetailsNg1Module
    .directive(name, adapter.downgradeNg2Component(PartyDetails))
    .config(config);
}
 

Our router might be implemented using Angular1's router, but the template can be Angular2's:

23.31 Implement ng2 component inside ui-router template imports/ui/components/partyDetails/partyDetails.ts
87
88
89
90
91
92
93
94
95
96
97
98
 
  $stateProvider.state('partyDetails', {
    url: '/parties/:partyId',
    template: '<party-details [party-id]="partyDetailsRoute.partyId"></party-details>',
    controllerAs: 'partyDetailsRoute',
    controller: function($stateParams, $scope) {
      'ngInject';
      this.partyId = $stateParams.partyId;
    },
    resolve: {
      currentUser($q) {
        if (Meteor.userId() === null) {

Let's take care of the properties binding in the PartyDetails component:

1
2
3
4
5
6
7
 
16
17
18
19
20
21
22
23
import * as angular from 'angular';
import * as angularMeteor from 'angular-meteor';
import * as uiRouter from 'angular-ui-router';
import { Component, Input, Output } from [email protected]/core';
import { MeteorComponent } from 'angular2-meteor';
 
import { Meteor } from 'meteor/meteor';
...some lines skipped...
  selector: 'party-details'
})
export class PartyDetails extends MeteorComponent {
  @Input() partyId: string;
  @Output() party: Object = {};
  users: Object[];
  isLoggedIn: boolean;
 

Further information about the Output decorator can be found here.

Removing Angular1 support

Once a component is fully integrated with Angular2, everything related to Angular1 can be removed, in this case, the PartyUninvited component:

23.33 Remove ng1 from PartyUninvited client/main.ts
22
23
24
25
26
27
28
 
63
64
65
66
67
68
import { registerPartyRemove } from '../imports/ui/components/partyRemove/partyRemove';
import { registerPartyRsvp } from '../imports/ui/components/partyRsvp/partyRsvp';
import { registerPartyRsvpsList } from '../imports/ui/components/partyRsvpsList/partyRsvpsList';
import { PartyUninvited } from '../imports/ui/components/partyUninvited/partyUninvited';
import { registerPartyUpload } from '../imports/ui/components/partyUpload/partyUpload';
import { registerPassword } from '../imports/ui/components/password/password';
import { registerRegister } from '../imports/ui/components/register/register';
...some lines skipped...
registerPartyRemove(adapter);
registerPartyRsvp(adapter);
registerPartyRsvpsList(adapter);
registerPartyUpload(adapter);
registerPassword(adapter);
registerRegister(adapter);
23.33 Remove ng1 from PartyUninvited imports/ui/components/partyDetails/partyDetails.ts
8
9
10
11
12
13
 
71
72
73
74
75
76
 
import template from './partyDetails.html';
import { Parties } from '../../../api/parties';
import { PartyMapNg1Module } from '../partyMap/partyMap';
 
@Component({
...some lines skipped...
export const PartyDetailsNg1Module = angular.module(name, [
  angularMeteor,
  uiRouter,
  PartyMapNg1Module.name
]);
 
23.33 Remove ng1 from PartyUninvited imports/ui/components/partyUninvited/partyUninvited.ts
1
2
3
 
32
33
34
import { Component, Input } from [email protected]/core';
import { MeteorComponent } from 'angular2-meteor';
 
...some lines skipped...
    );
  }
}

Using Material2

Indeed, We can also move our app's design structure to Angular2 and reserve the API. Let's install the necessary packages

23.34 Install material2 package.json
10
11
12
13
14
15
16
17
    "@angular/compiler": "^2.1.0",
    "@angular/core": "^2.1.0",
    "@angular/forms": "^2.1.0",
    "@angular/http": "^2.1.0",
    "@angular/material": "^2.0.0-alpha.9-3",
    "@angular/platform-browser": "^2.1.0",
    "@angular/platform-browser-dynamic": "^2.1.0",
    "@angular/upgrade": "^2.1.0",

Raw commands are listed below:

$ npm install --save @angular/http
$ npm install --save @angular/material

@angular/http is just a peer dependency of @angular/material

We now have md-checkbox, md-button and md-input available for us. But will first need to declare those:

23.35 Import material2 directives client/main.ts
4
5
6
7
8
9
10
 
40
41
42
43
44
45
46
47
import { BrowserModule } from [email protected]/platform-browser';
import { FormsModule } from [email protected]/forms';
import { UpgradeAdapter } from [email protected]/upgrade';
import { MaterialModule } from [email protected]/material';
 
import { Meteor } from 'meteor/meteor';
 
...some lines skipped...
  ],
  imports: [
    BrowserModule,
    FormsModule,
    MaterialModule
  ]
})
class AppNg2Module {}

Now that material2 is fully registered we can safely replace PartyDetails template with Angular2's:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div *ngIf="party" layout="column" layout-padding>
  <div layout="column" layout-gt-sm="row"  layout-padding>
    <form flex="50" layout="column">
      <md-input [(ngModel)]="party.name" [disabled]="!isOwner" placeholder="Party name"></md-input>
 
      <md-input [(ngModel)]="party.description" [disabled]="!isOwner" placeholder="Description"></md-input>
 
      <div>
        <md-checkbox [checked]="party.public">
          Public Party?
        </md-checkbox>
      </div>
 
      <div>
        <md-button md-raised-button color="primary" (click)="save()">Save</md-button>
      </div>
    </form>
    <party-map flex="50" [location]="party.location"></party-map>
  </div>
 
  <party-uninvited flex [party]="party" *ngIf="canInvite()"></party-uninvited>
</div>

The upgrading process for Socially might take a while and can be done in many different ways, but hopefully you got the concept. If your'e not familiar with Angular2-Metoer the following tutorial might come in handy.