Fork me on GitHub

Migrate Template into Component

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

So far we have Angular 2 application with Angular 2 Router, that wraps existing Blaze application and load it's Templates.

Our next step is about converting Blaze existing Template into Angular 2 Component.

In order to to so, we need to work top-down - because we can load Blaze existing Template from Angular 2 code.

So at the moment, the top most Blaze Template that loaded is the App_Body which contains the following:

  • Full layout
  • Dynamic load of child Blaze Templates
  • List of To-do lists
  • Other UI features such as login, sign-up and more

So let's start with the migration - our goal now is to migrate the App_Body so we can remove the Blaze files in the end of this step.

The replacement of this component as the main component will be our MainComponent.

Let's start with the HTML template - we will create a new file named client/imports/components/main-component.ng2.html and copy the contents of the App_Body template from the file (imports/ui/layouts/app-body.html).

Now let's start to make some modifications to make that file a valid Angular 2 template - we need to remove all the Blaze UI bindings, if, each and such.

We will replace them as follow:

  • Blaze if/else - will become ngIf.
  • Blaze each - will become ngFor.
  • Blaze classes bindings will become ngClass.
  • Dynamic load of Blaze Template will become router-outlet.

We will also remove all the router dynamic links, and put a placeholder for them at the moment - we will take care of them later.

10.1 Migrate the App_body template client/imports/main-component.ng2.html
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
<div id="container" [ngClass]="{'menu-open': menuOpen, 'cordova': isCordova}">
    <section id="menu">
        <div *ngIf="currentUser" class="btns-group-vertical">
            <a href="#" class="js-user-menu btn-secondary">
                <span *ngIf="userMenuOpen" class="icon-arrow-up"></span>
                <span *ngIf="!userMenuOpen" class="icon-arrow-down"></span>
 
                {{ userEmail }}
            </a>
            <a *ngIf="userMenuOpen" class="js-logout btn-secondary">Logout</a>
        </div>
        <div *ngIf="!currentUser" class="btns-group">
            <a href="SIGNIN_LINK" class="btn-secondary">Sign In</a>
            <a href="JOIN_LINK" class="btn-secondary">Join</a>
        </div>
 
        <div class="list-todos">
            <a class="js-new-list link-list-new">
                <span class="icon-plus"></span>
                New List
            </a>
 
            <a *ngFor="let list of lists | async" href="LIST_LINK" [title]="list.name" class="list-todo">
                <span *ngIf="list.userId" class="icon-lock"></span>
                <span *ngIf="list.incompleteCount" class="count-list">{{list.incompleteCount}}</span>
 
                {{list.name}}
            </a>
        </div>
    </section>
 
    <div *ngIf="!isConnected()" class="notifications">
        <div class="notification">
            <span class="icon-sync"></span>
            <div class="meta">
                <div class="title-notification">Trying to connect</div>
                <div class="description">There seems to be a connection issue</div>
            </div>
        </div>
    </div>
 
    <div class="content-overlay"></div>
 
    <div id="content-container">
        <router-outlet></router-outlet>
    </div>
</div>

Note that unlike Blaze, in Angular 2 we define events such click in the HTML - we will handle that later.

So now we have the HTML template - we need to add some code to the Angular 2 Component:

  • We need to use the new template.
  • We need to add stubs for the methods we use in the template (isConnected, getter for userEmail)

So let's do it:

10.2 Use the new template and added stub methods client/imports/main.component.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
import '/imports/startup/client';
import {Component, OnInit} from "@angular/core";
import {MeteorObservable} from "meteor-rxjs";
import {Meteor} from "meteor/meteor";
 
@Component({
  selector: 'app',
  templateUrl: '/client/imports/main-component.html'
})
export class MainComponent implements OnInit {
  private isCordova : boolean;
  private menuOpen : boolean = false;
  private userMenuOpen : boolean = false;
 
  constructor() {
    this.isCordova = Meteor.isCordova;
  }
 
  ngOnInit() {
    MeteorObservable.subscribe("lists.public").subscribe();
    MeteorObservable.subscribe("lists.private").subscribe();
  }
 
  isConnected() {
    return true;
  }
 
  get userEmail() {
    return "";
  }
}

We also commented the code that in charge of redirection to a list page, we will handle that later.

Now, because we are implementing a replacement for App_body, we need to load the inner Template instead of the App_Body, so let's change it:

10.3 Change the blaze template type that loaded client/imports/components/list-show.component.ts
2
3
4
5
6
7
8
 
16
17
18
19
20
21
import {ActivatedRoute, Params} from "@angular/router";
 
@Component({
    template: '<blaze-template *ngIf="templateContext" name="Lists_show_page" [context]="templateContext"></blaze-template>'
})
export class ListShowComponent implements OnInit {
    private templateContext: any;
...some lines skipped...
            const listId = params['_id'];
 
            this.templateContext = {
                _id: listId
            };
        });

Now, we need to provide the lists object to the view - this will be that lists of Todo lists.

We will use MeteorObservable again, and create the lists as an Observable of the data in the Cursor (we will change the implementation of the Collection soon to support RxJS).

Because our MongoDB selector is depend on the use connection, and we want to update it when the user log in/out, we need to wrap our query with Tracker.autorun.

10.4 Added the list using MeteorObservable client/imports/main.component.ts
1
2
3
4
5
6
7
8
9
 
13
14
15
16
17
18
19
 
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import '/imports/startup/client';
import {Component, OnInit} from "@angular/core";
import {Meteor} from "meteor/meteor";
import {MeteorObservable} from "meteor-rxjs";
import {Observable} from "rxjs";
import {Lists} from "../../imports/api/lists/lists";
 
@Component({
  selector: 'app',
...some lines skipped...
  private isCordova : boolean;
  private menuOpen : boolean = false;
  private userMenuOpen : boolean = false;
  private lists: Observable<any>;
 
  constructor() {
    this.isCordova = Meteor.isCordova;
...some lines skipped...
  ngOnInit() {
    MeteorObservable.subscribe("lists.public").subscribe();
    MeteorObservable.subscribe("lists.private").subscribe();
 
    MeteorObservable.autorun().zone().subscribe(() => {
      this.lists = Lists.find({
        $or: [
          {userId: {$exists: false}},
          {userId: Meteor.userId()},
        ]
      }).zone();
    });
  }
 
  isConnected() {

We used the zone() method in order to bind the async data fetch to the Angular 2 Zone of the current Component - so when the data changes - the Zone will trigger an update - and the view will update.

Now we need to change the Mongo.Collection creation in order to have a Collection with RxJS API - there are multiple ways of doing it - in this case, we just wrap the existing Collection:

10.5 Changes in the lists collection definition to expose RxJS API imports/api/lists/lists.js
1
2
3
4
5
6
 
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
 
70
71
72
73
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
import { Todos } from '../todos/todos.js';
import {MongoObservable} from "meteor-rxjs";
 
class ListsCollection extends Mongo.Collection {
  insert(list, callback) {
...some lines skipped...
  }
}
 
export const Lists = new MongoObservable.fromExisting(new ListsCollection("Lists"));
 
// Deny all client-side updates since we will be using methods to manage this collection
Lists.collection.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; },
});
 
let schema = new SimpleSchema({
  name: { type: String },
  incompleteCount: { type: Number, defaultValue: 0 },
  userId: { type: String, regEx: SimpleSchema.RegEx.Id, optional: true },
});
 
Lists.collection.attachSchema(schema);
 
// This represents the keys from Lists objects that should be published
// to the client. If we add secret properties to List objects, don't list
// them here to keep them private to the server.
Lists.collection.publicFields = {
  name: 1,
  incompleteCount: 1,
  userId: 1,
};
 
Lists.collection.helpers({
  // A list is considered to be private if it has a userId set
  isPrivate() {
    return !!this.userId;
...some lines skipped...
    return Todos.find({ listId: this._id }, { sort: { createdAt: -1 } });
  },
});
 

You can use the collection property of the instance in order to get the actual Mongo.Collection that wrapped.

Now let's implement the stub methods we created earlier, starting with isConnected:

10.6 Implemented isConnected client/imports/main.component.ts
34
35
36
37
38
39
40
  }
 
  isConnected() {
    return Meteor.status().connected;
  }
 
  get userEmail() {

And the getter userEmail:

10.7 Implemented emailLocalPart client/imports/main.component.ts
38
39
40
41
42
43
44
45
46
47
48
49
50
  }
 
  get userEmail() {
    if (Meteor.user()) {
      const email = Meteor.user().emails[0].address;
 
      return email.substring(0, email.indexOf('@'));
    }
    else {
      return "";
    }
  }
}

Now let's keep implementing the missing logic in our MainComponent - starting with the router links and the active route indication:

10.8 Added router links and the active lists indication client/imports/main-component.ng2.html
10
11
12
13
14
15
16
17
 
20
21
22
23
24
25
26
            <a *ngIf="userMenuOpen" class="js-logout btn-secondary">Logout</a>
        </div>
        <div *ngIf="!currentUser" class="btns-group">
            <a routerLink="/signin" class="btn-secondary">Sign In</a>
            <a routerLink="/join" class="btn-secondary">Join</a>
        </div>
 
        <div class="list-todos">
...some lines skipped...
                New List
            </a>
 
            <a *ngFor="let list of lists | async" [routerLink]="['lists', list._id]" routerLinkActive="active" [title]="list.name" class="list-todo">
                <span *ngIf="list.userId" class="icon-lock"></span>
                <span *ngIf="list.incompleteCount" class="count-list">{{list.incompleteCount}}</span>
 

We used a new Routes that not yet implemented - signin and join - so let's create stubs for them:

10.9 Added stubs for signin/join components client/imports/components/join.component.ts
1
2
3
4
5
6
7
import {Component} from "@angular/core";
 
@Component({
  template: 'Join!'
})
export class JoinComponent {
}
10.9 Added stubs for signin/join components client/imports/components/signin.component.ts
1
2
3
4
5
6
7
import {Component} from "@angular/core";
 
@Component({
  template: 'Signin!'
})
export class SigninComponent {
}

And let's add them to the NgModule declaration:

10.10 Added join/signin component to the module declaration client/imports/app.module.ts
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {ListShowComponent} from "./components/list-show.component";
import {ListRedirectorComponent} from "./components/list-redirector.component";
import {Angular2BlazeTemplateModule} from "angular2-blaze-template";
import {JoinComponent} from "./components/join.component";
import {SigninComponent} from "./components/signin.component";
 
@NgModule({
    // Components, Pipes, Directive
    declarations: [
        MainComponent,
        ListShowComponent,
        ListRedirectorComponent,
        JoinComponent,
        SigninComponent
    ],
    // Entry Components
    entryComponents: [

And let's add the routes to the Router definition:

10.11 Added join/signin component to the router client/imports/app.routes.ts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Routes, RouterModule} from '@angular/router';
import {ListShowComponent} from "./components/list-show.component";
import {ListRedirectorComponent} from "./components/list-redirector.component";
import {JoinComponent} from "./components/join.component";
import {SigninComponent} from "./components/signin.component";
 
const appRoutes: Routes = [
    {path: '', component: ListRedirectorComponent},
    {path: 'lists/:_id', component: ListShowComponent},
    {path: 'join', component: JoinComponent},
    {path: 'signin', component: SigninComponent}
];
 
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

Let's implement addNewList action in our Component, which uses the existing logic from the old Blaze code:

10.12 Added addNewList method client/imports/main.component.ts
4
5
6
7
8
9
10
11
 
17
18
19
20
21
22
23
 
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import {MeteorObservable} from "meteor-rxjs";
import {Observable} from "rxjs";
import {Lists} from "../../imports/api/lists/lists";
import {insert} from "../../imports/api/lists/methods";
import {Router} from "@angular/router";
 
@Component({
  selector: 'app',
...some lines skipped...
  private userMenuOpen : boolean = false;
  private lists: Observable<any>;
 
  constructor(private router: Router) {
    this.isCordova = Meteor.isCordova;
  }
 
...some lines skipped...
      return "";
    }
  }
 
  addNewList() {
    const listId = insert.call((err) => {
      if (err) {
        this.router.navigate(['/']);
        alert('Could not create list.');
      }
    });
 
    this.router.navigate(['/lists', listId]);
  }
}

And let's bind the action in the view:

10.13 Added click action to add new list client/imports/main-component.ng2.html
15
16
17
18
19
20
21
        </div>
 
        <div class="list-todos">
            <a class="js-new-list link-list-new" (click)="addNewList()">
                <span class="icon-plus"></span>
                New List
            </a>

We need to make some changes in the implementation of the methods.js file, because we wrapped the collection with RxJS collection, so let's change the usage to use the actual Mongo.Collection object:

10.14 Changes in the method implementation imports/api/lists/methods.js
14
15
16
17
18
19
20
 
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 
49
50
51
52
53
54
55
 
58
59
60
61
62
63
64
 
71
72
73
74
75
76
77
 
81
82
83
84
85
86
87
 
91
92
93
94
95
96
97
 
106
107
108
109
110
111
112
  name: 'lists.insert',
  validate: new SimpleSchema({}).validator(),
  run() {
    return Lists.collection.insert({});
  },
});
 
...some lines skipped...
        'Must be logged in to make private lists.');
    }
 
    const list = Lists.collection.findOne(listId);
 
    if (list.isLastPublicList()) {
      throw new Meteor.Error('lists.makePrivate.lastPublicList',
        'Cannot make the last public list private.');
    }
 
    Lists.collection.update(listId, {
      $set: { userId: this.userId },
    });
  },
...some lines skipped...
        'Must be logged in.');
    }
 
    const list = Lists.collection.findOne(listId);
 
    if (!list.editableBy(this.userId)) {
      throw new Meteor.Error('lists.makePublic.accessDenied',
...some lines skipped...
 
    // XXX the security check above is not atomic, so in theory a race condition could
    // result in exposing private data
    Lists.collection.update(listId, {
      $unset: { userId: true },
    });
  },
...some lines skipped...
    newName: { type: String },
  }).validator(),
  run({ listId, newName }) {
    const list = Lists.collection.findOne(listId);
 
    if (!list.editableBy(this.userId)) {
      throw new Meteor.Error('lists.updateName.accessDenied',
...some lines skipped...
    // XXX the security check above is not atomic, so in theory a race condition could
    // result in exposing private data
 
    Lists.collection.update(listId, {
      $set: { name: newName },
    });
  },
...some lines skipped...
  name: 'lists.remove',
  validate: LIST_ID_ONLY,
  run({ listId }) {
    const list = Lists.collection.findOne(listId);
 
    if (!list.editableBy(this.userId)) {
      throw new Meteor.Error('lists.remove.accessDenied',
...some lines skipped...
        'Cannot delete the last public list.');
    }
 
    Lists.collection.remove(listId);
  },
});
 

Let's implement logout method:

10.15 Added logout logic client/imports/main.component.ts
60
61
62
63
64
65
66
67
68
 
    this.router.navigate(['/lists', listId]);
  }
 
  logout() {
    Meteor.logout();
    this.router.navigate(['/']);
  }
}

And bind the click event to the method:

10.16 Added logout event client/imports/main-component.ng2.html
7
8
9
10
11
12
13
 
                {{ userEmail }}
            </a>
            <a *ngIf="userMenuOpen" class="js-logout btn-secondary" (click)="logout()">Logout</a>
        </div>
        <div *ngIf="!currentUser" class="btns-group">
            <a routerLink="/signin" class="btn-secondary">Sign In</a>

The only missing thing at the moment is the currentUser field in this Component - we will add it in the next step.

Now we can remove the old Blaze Templates from the project (commit #6.17).

So at the moment, we have fully migrated Blaze Template and all the application features works as before!