Step 5 - Create the ToDo List Display

Let's start with the list of Todos to display.

We already have data in our Todo component from the last step where we created the static Javascript array of Todo items. Let's bind those to a list on the page.

Add the following below the header:

<div *ngFor="let todo of todos"
     class="todo-item">

    <div class="pull-left">
        <i class="fa fa-bookmark "
           style="cursor: pointer">
        </i>
    </div>

    <div class="pull-right">
        <i class="fa fa-remove" 
           style="color: darkred; cursor: pointer"></i>
    </div>

    <div class="todo-content">
        <div class="todo-header">
            {{ todo.title}}
        </div>
        <div>
            {{todo.description}}
        </div>
    </div>
</div>

This code relies on some CSS styling for various elements. I can add the CSS directly into this page, or I can create an external style sheet which is preferrable.

To do the latter let's create a new file todo.css and add:

.row li {
    margin-left: -10px;
}
#form1 input.ng-invalid.ng-touched, #form1 textarea.ng-invalid.ng-touched {
    background-color: #ffced1;
}
#form1 input.ng-valid.ng-touched, #form1 textarea.ng-valid.ng-touched {
    background-color: #baffc3;
}
.todo-item {
    padding: 10px;
    margin-left: 10px;
    border-bottom: 1px solid silver;
    transition: opacity 900ms ease-out;
}
.todo-header {
    font-weight: bold;
    font-size: 1.2em;
    color: #457196;
}
.todo-content {
    padding-left: 40px;
}
.todo-content .fa-check {
    color: green !important;
    font-weight: bold;
    font-size: 1.2em;
}
.completed {
    text-decoration: line-through;
    font-style: italic;
    opacity: 0.4;

}

and hook up the css with:

styleUrls: ['./todo.css'],

in the component meta data. This pulls in the stylesheet into the component and localizes the styles to this component.

Now lets go back to our page and we should see:

Make it more dynamic

Now that we have the list it might be nice to be able to mark ToDo Items as completed. Let's do this by adding a checkbox to the display.

Add the checkbox next to the remove button:

<div class="pull-right">
    <input type="checkbox" title="check to complete todo" 
           [(ngModel)]="todo.completed" />
    <i class="fa fa-remove" style="color: darkred; cursor: pointer"></i>
</div>

The key item here is the [(ngModel)] setting which two-way binds the completed property of the TodoItem to the checkbox. When rendered the TodoItem.completed status determines whether the checkbox is checked.

If you run the page now you'll see:

Displaying Completed Status

Let's make it a bit more obvious when an item is completed. The CSS styles provided have a .completed style that can be applied to change the font color and strike through the text when an item is complete. We can use the powerful [ngClass] directive to conditionally apply this class to individual elements.

There are two places I'll apply this custom styling:

  • for the entire item
  • for the icon

Let's start with the item which we can define on the looping <div> tag that contains the *ngFor.

<div *ngFor="let todo of todos"
     class="todo-item"
     [ngClass]="{completed: todo.completed}">

This says: When todo.completed is true apply the completed style to the class attribute - so the new class setting will be class="todo-item completed".

Next I'll tweak the icon display so it displays a tag icon when open, and a check mark when it's completed:

<i class="fa"
    [ngClass]="{'fa-bookmark-o': !todo.completed,'fa-check': todo.completed }"
    style="cursor: pointer">
</i>

The syntax uses a map-like expression that specifies the name of the class as a property name, and the truthy expression as a value. Here if not completed we show the fa-bookmark-o style, if completed we show the checkmark.

With the style updates in place we now get:

Capturing Checkbox Status

If you click on the checkbox you'll notice that it just works - when you click the status toggles between completed and uncompleted.

But wait? Don't we have to capture the click or change event or something? No - this is what's cool about two way databinding: the bound value (todo.completed) changes simply because it is bound to the checkbox. When you toggle the checkbox, the todo item's value changes and the template immediately reflects that change!

There's no code to write to make this work!

Getting rid of the Checkbox

Ok now that you've seen how cool the checkbox binding is - let's get rid of it, because lets face it's ugly.

It would be much nicer to just click on the icon to toggle the state. To do this lets add a method to our component class:

toggleCompleted(todo:TodoItem) {
    todo.completed = !todo.completed;
}

Then lets handle the click on the icon like this in the template:

<div class="pull-left" 
     (click)="toggleCompleted(todo)">
    <i class="fa "
       [ngClass]="{'fa-bookmark-o': !todo.completed,'fa-check': todo.completed }"
       style="cursor: pointer">
    </i>
</div>

The key here is the (click)="toggleCompleted(todo). The (click) is an event binding around the click event. You can use parenthesis to bind any event of an element. Notice that I pass the todo item as a parameter to the function called, which allows passung the appropriate context to the method so it knows which todo item to deal with in the list. Again - it's very easy to do.

Removing items

We can use the same approach for the remove button.

We'll use this HTML:

<i class="fa fa-remove"
   (click)="removeTodo(todo)"
   style="color: darkred; cursor: pointer"></i>

and handle it like this:

removeTodo(todo:TodoItem){
	this.todos = this.todos.filter((t) => t.title != todo.title)
}

The Array.filter() function takes a delegate function that returns true or false on whether to return the item in question. So we look for any todo items that don't match the title and return the array. The effect is that we get a new array with the todo item removed.

If you run the sample again and click on the Remove button the item should go away.

Arrow Functions () =>

The ()=> syntax use ES6/Typescript arrow functions - the part in the parenthesis are the parameter follow by a fat arrow and in this case an expression that returns a value (true or false). You can also write expressions as code blocks. The following does the same thing and is required if the code isn't a simple expression:

this.todos = this.todos.filter((t) => {
    return t.title != todo.title;
});

Arrow functions are alternate function syntax. Traditionally you would write the above like:

this.todos = this.todos.filter(function(t) {
    return t.title != todo.title;
});

Here the effect is the same. However, arrow function are subtly different in the way they deal with the notorious this pointer. Arrow functions preserve the scope of this from the parent context calling it so that you can usually use this the way you would expect it to with Arrow functions, while with classic anonymous functions the this context is unpredictable as the calling context determines its value. Suffice it to say, by convention Typescript and Angular prefer using Arrow functions.

Code so far

Here's the code for the component:

import { Component, OnInit } from '@angular/core';

@Component({
    //moduleId: module.id,
    selector: 'todo',
	styleUrls: ['./todo.css'],
    templateUrl: './todo.html'
})
export class Todo implements OnInit {
	todos:TodoItem[]  = [
		{
			title: "SWFox Angular Presentation",
			description: "Try to show up on time this time",
			completed: false
		},
		{
			title: "Do FoxPro presentation",
			description: "Should go good, let's hope I don't fail...",
			completed: false
		},
		{
			title: "Do raffle at the end of day one",
			description: "Should go good, let's hope I don't fail...",
			completed: false
		},
		{
			title: "Arrive at conference",
			description: "Arrive in Phoenix and catch a cab to hotel",
			completed: true
		}
	];
	activeTodo:TodoItem = new TodoItem();

    constructor() { }


    ngOnInit() {
    }

    toggleCompleted(todo:TodoItem) {
    	todo.completed = !todo.completed;
    }
    removeTodo(todo:TodoItem){
    	this.todos = this.todos.filter((t,i) => t.title != todo.title)
    }
}

export class TodoItem {
	title:string = null;
	description:string = null;
	completed:boolean = false;
}

Here's the HTML:

<div class="page-header-text">
    <i class="fa fa-tag"></i>
    ToDo List
</div>


<div class="todo-content">

    <div *ngFor="let todo of todos"
         class="todo-item"  [ngClass]="{completed: todo.completed}">

        <div class="pull-left" (click)="toggleCompleted(todo)">
            <i class="fa "
               [ngClass]="{'fa-bookmark-o': !todo.completed,'fa-check': todo.completed }"
               style="cursor: pointer">
            </i>
        </div>

        <div class="pull-right">
            <!--<input type="checkbox"-->
                   <!--title="check to complete todo"-->
                   <!--[(ngModel)]="todo.completed" />-->
            <i class="fa fa-remove"
               (click)="removeTodo(todo)"
               style="color: darkred; cursor: pointer"></i>
        </div>

        <div class="todo-content">
            <div class="todo-header">
                {{ todo.title}}
            </div>
            <div>
                {{todo.description}}
            </div>
        </div>
    </div>
</div>

And the CSS:

.row li {
    margin-left: -10px;
}
#form1 input.ng-invalid.ng-touched, #form1 textarea.ng-invalid.ng-touched {
    background-color: #ffced1;
}
#form1 input.ng-valid.ng-touched, #form1 textarea.ng-valid.ng-touched {
    background-color: #baffc3;
}
.todo-item {
    padding: 10px;
    margin-left: 10px;
    border-bottom: 1px solid silver;
    transition: opacity 900ms ease-out;
}
.todo-header {
    font-weight: bold;
    font-size: 1.2em;
    color: #457196;
}
.todo-content {
    padding-left: 40px;
}
.todo-content .fa-check {
    color: green !important;
    font-weight: bold;
    font-size: 1.2em;
}
.completed {
    text-decoration: line-through;
    font-style: italic;
    opacity: 0.4;

}

© West Wind Technologies, 1996-2024 • Updated: 09/19/2016
Comment or report problem with topic