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:
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:
ngIf
.ngFor
.ngClass
.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.
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:
isConnected
, getter for userEmail
)So let's do it:
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:
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
.
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:
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 actualMongo.Collection
that wrapped.
Now let's implement the stub methods we created earlier, starting with isConnected
:
34
35
36
37
38
39
40
}
isConnected() {
return Meteor.status().connected;
}
get userEmail() {
And the getter userEmail
:
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
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:
1
2
3
4
5
6
7
import {Component} from "@angular/core";
@Component({
template: 'Join!'
})
export class JoinComponent {
}
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:
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:
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:
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:
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:
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:
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:
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!