Fork me on GitHub

Migrate the Todo List

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

So let's continue! now we will migrate the list of todo, so far we use an existing Blaze Template called Lists_show_page inside a new Angular 2 Component called ListShowComponent.

First, let's modify the template, we use the same techniques we learned in the previous steps - we will use the existing template and just change the events, bindings and directives:

12.1 Migrate the list_show template client/imports/components/list-show.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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<div class="page lists-show">
    <nav class="js-title-nav" *ngIf="list">
        <form *ngIf="editing" class="js-edit-form list-edit-form">
            <input type="text" name="name" [value]="editModel.name">
            <div class="nav-group right">
                <a href="#" class="js-cancel nav-item">
                    <span class="icon-close js-cancel" title="Cancel"></span>
                </a>
            </div>
        </form>
        <div *ngIf="!editing">
            <div class="nav-group">
                <a href="#" class="js-menu nav-item">
                    <span class="icon-list-unordered" title="Show Menu"></span>
                </a>
            </div>
 
            <h1 class="js-edit-list title-page">
                <span class="title-wrapper">{{list.name}}</span>
                <span class="count-list">{{list.incompleteCount}}</span>
            </h1>
 
            <div class="nav-group right">
                <div class="nav-item options-mobile">
                    <select class="list-edit">
                        <option disabled selected>Select an action</option>
                        <option *ngIf="list.userId" value="public">Make Public</option>
                        <option *ngIf="!list.userId" value="private">Make Private</option>
                        <option value="delete">Delete</option>
                    </select>
                    <span class="icon-cog"></span>
                </div>
                <div class="options-web">
                    <a class="js-toggle-list-privacy nav-item">
                        <span *ngIf="list.userId" class="icon-lock" title="Make list public"></span>
                        <span *ngIf="!list.userId" class="icon-unlock" title="Make list private"></span>
                    </a>
                    <a class="js-delete-list nav-item">
                        <span class="icon-trash" title="Delete list"></span>
                    </a>
                </div>
            </div>
        </div>
 
        <form class="js-todo-new todo-new input-symbol">
            <input type="text" placeholder="Type to add new tasks">
            <span class="icon-add js-todo-add"></span>
        </form>
    </nav>
 
    <div class="content-scrollable list-items">
        <div *ngIf="todosReady">
            <div *ngFor="let todo of todos">
                <blaze-template name="Todos_item" [context]="getContextForItem(todo)"></blaze-template>
            </div>
            <div class="wrapper-message" *ngIf="!todos || todos.length == 0">
                <div class="title-message">No tasks here</div>
                <div class="subtitle-message">Add new tasks using the field above</div>
            </div>
        </div>
        <div *ngIf="!todosReady" class="wrapper-message">
            <div class="title-message">Loading tasks...</div>
        </div>
    </div>
</div>

And because we are using RxJS Observable as a wrapper for our data, we need to add async Pipe in our view, and let's migrate the Template code into a Component

12.2 Added logic for the list show component client/imports/components/list-show.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
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
import {Component, OnInit} from "@angular/core";
import {ActivatedRoute, Params} from "@angular/router";
import {MeteorObservable} from "meteor-rxjs";
import {Lists} from "../../../imports/api/lists/lists";
 
@Component({
    template: '<blaze-template *ngIf="templateContext" name="Lists_show_page" [context]="templateContext"></blaze-template>'
})
export class ListShowComponent implements OnInit {
    private list : any;
    private todosReady : boolean = false;
    private todos : Array<any>;
    private editing : boolean = false;
 
    constructor(private currentRoute: ActivatedRoute) {
 
...some lines skipped...
    ngOnInit() {
        this.currentRoute.params.subscribe((params: Params) => {
            const listId = params['_id'];
            MeteorObservable.subscribe('todos.inList', listId).subscribe();
 
            MeteorObservable.autorun().zone().subscribe(() => {
                if (listId && Lists.findOne(listId)) {
                    this.list = Lists.findOne(listId);
                    this.todosReady = true;
                    this.todos = this.list.todos();
                }
            })
        });
    }
 
    getContextForItem(todo) {
        return {
              todo: todo,
              editing: false,
              onEditingChange(editing) {
 
              },
        }
    }
}
12.2 Added logic for the list show component client/imports/components/list-show.ng2.html
50
51
52
53
54
55
56
 
    <div class="content-scrollable list-items">
        <div *ngIf="todosReady">
            <div *ngFor="let todo of todos | async">
                <blaze-template name="Todos_item" [context]="getContextForItem(todo)"></blaze-template>
            </div>
            <div class="wrapper-message" *ngIf="!todos || todos.length == 0">

At the moment, we will use the exiting Todo_item template to show the items - we will later migrate it too - so we just pass the required params using getContextForItem.

And now let's implement and migrate the code into the Component's class, and let's add the events in the view

12.3 Implemented the events in the UI client/imports/components/list-show.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
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import {Component, OnInit} from "@angular/core";
import {ActivatedRoute, Params, Router} from "@angular/router";
import {MeteorObservable} from "meteor-rxjs";
import {Lists} from "../../../imports/api/lists/lists";
import {
  updateName,
  makePublic,
  makePrivate,
  remove,
  insert,
} from '../../../imports/api/todos/methods';
import { displayError } from '../../../imports/ui/lib/errors';
import {Observable} from "rxjs";
 
@Component({
    templateUrl: '/client/imports/components/list-show.html'
})
export class ListShowComponent implements OnInit {
    private list : any;
    private todosReady : boolean = false;
    private todos : Observable<any>;
    private editing : boolean = false;
    private editModel : any;
    private newItemModel : string = '';
 
    constructor(private currentRoute: ActivatedRoute, private router: Router) {
        this.editModel = {
            name: ''
        }
    }
 
    ngOnInit() {
        this.currentRoute.params.subscribe((params: Params) => {
            const listId = params['_id'];
            MeteorObservable.subscribe('todos.inList', listId).zone().subscribe();
 
            MeteorObservable.autorun().zone().subscribe(() => {
                if (listId && Lists.findOne(listId)) {
                    this.list = Lists.findOne(listId);
                    this.todosReady = true;
                    this.todos = this.list.todos().zone();
                }
            })
        });
    }
 
    deleteList() {
        const list = this.list;
        const message = `Are you sure you want to delete the list?`;
 
        if (confirm(message)) {
            remove.call({
                listId: list._id,
            }, displayError);
 
            this.router.navigate(['Home']);
 
            return true;
        }
 
        return false;
    }
 
    editList(toggle) {
        this.editModel.name = this.list.name;
        this.editing = toggle;
    }
 
    toggleListPrivacy() {
        const list = this.list;
 
        if (list.userId) {
            makePublic.call({ listId: list._id }, displayError);
        } else {
            makePrivate.call({ listId: list._id }, displayError);
        }
    }
 
    addNew() {
        if (this.newItemModel == '') {
            return;
        }
 
        insert.call({
            listId: this.list._id,
            text: this.newItemModel,
        }, displayError);
 
        this.newItemModel = '';
    }
 
    saveList() {
        if (this.editing) {
            updateName.call({
                listId: this.list._id,
                newName: this.editModel.name,
            }, displayError);
 
            this.editing = false;
        }
    }
 
    getContextForItem(todo) {
        return {
            todo: todo,
            editing: false,
            onEditingChange(editing) {
 
            },
        }
    }
}
12.3 Implemented the events in the UI client/imports/components/list-show.ng2.html
1
2
3
4
5
6
7
8
9
 
15
16
17
18
19
20
21
 
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<div class="page lists-show">
    <nav class="js-title-nav" *ngIf="list">
        <form *ngIf="editing" class="js-edit-form list-edit-form" (ngSubmit)="saveList()" #editForm="ngForm">
            <input type="text" [(ngModel)]="editModel.name" (blur)="saveList()" name="editNameInput">
            <div class="nav-group right">
                <a href="#" class="js-cancel nav-item" (click)="editList(false)">
                    <span class="icon-close js-cancel" title="Cancel"></span>
                </a>
            </div>
...some lines skipped...
                </a>
            </div>
 
            <h1 class="js-edit-list title-page" (click)="editList(true)">
                <span class="title-wrapper">{{list.name}}</span>
                <span class="count-list">{{list.incompleteCount}}</span>
            </h1>
...some lines skipped...
                    <span class="icon-cog"></span>
                </div>
                <div class="options-web">
                    <a class="js-toggle-list-privacy nav-item" (click)="toggleListPrivacy()">
                        <span *ngIf="list.userId" class="icon-lock" title="Make list public"></span>
                        <span *ngIf="!list.userId" class="icon-unlock" title="Make list private"></span>
                    </a>
                    <a class="js-delete-list nav-item" (click)="deleteList()">
                        <span class="icon-trash" title="Delete list"></span>
                    </a>
                </div>
            </div>
        </div>
 
        <form class="js-todo-new todo-new input-symbol" (ngSubmit)="addNew()" #newForm="ngForm">
            <input type="text" placeholder="Type to add new tasks" [(ngModel)]="newItemModel" name="newItemInput">
            <span class="icon-add js-todo-add"></span>
        </form>
    </nav>

And remember we wrapped the Collection? we need to do the same for Todo Collection:

12.4 Changes in the Todos collection imports/api/todos/todos.js
1
2
3
4
5
6
7
 
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 
57
58
59
60
61
62
63
64
65
66
67
68
 
78
79
80
81
82
83
84
import { Mongo } from 'meteor/mongo';
import { Factory } from 'meteor/factory';
import faker from 'faker';
import {MongoObservable} from "meteor-rxjs";
 
import incompleteCountDenormalizer from './incompleteCountDenormalizer.js';
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
...some lines skipped...
  }
}
 
export const Todos = new MongoObservable.fromExisting(new TodosCollection('Todos'));
 
// Deny all client-side updates since we will be using methods to manage this collection
Todos.collection.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; },
});
 
let schema = new SimpleSchema({
  listId: {
    type: String,
    regEx: SimpleSchema.RegEx.Id,
...some lines skipped...
  },
});
 
Todos.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.
Todos.collection.publicFields = {
  listId: 1,
  text: 1,
  createdAt: 1,
...some lines skipped...
  createdAt: () => new Date(),
});
 
Todos.collection.helpers({
  list() {
    return Lists.findOne(this.listId);
  },

That's it! we can now remove the old files of this Template (imports/ui/components/lists-show.html, imports/ui/components/lists-show.js, imports/ui/pages/lists-show-page.js, imports/ui/pages/lists-show-page.html), and we can removed the imports for those files from the routes file (imports/startup/client/routes.js).