One MEAN Ionic 2 Todo App on Heroku, Part 3
Ionic 2 Frontend: Adding More Functionality
In the first two parts of this series, we set the stage for an Ionic 2 Todo app that can be hosted on Heroku. We set up the Node.js backend for the app, with an Express server that defined API endpoints for accessing a MongoDB database. We also laid the groundwork for an Ionic 2 app to the point where the Todos in our database were displaying on the screen. If you’re just joining us now, head back over to Part 1 and start there.
In this third and final part of the series, we’ll beef up the functionality in our Ionic 2 app, play with more cool Angular 2 stuff, and finish up with a very simple, yet very functional app you can show off to all your friends.
Include Add, Edit, and Delete Functionality
In the last post, we only implemented the Get method to grab todos from MongoDB. We’ll now finish up the TodoService
with add
, update
, and delete
functions.
In our loadTodos()
function, the only parameter we needed to pass to http.get()
to access our data was the API endpoint: api/todos
. In our next three methods, because we are sending some sort of JSON data to our server, we’ll create Headers for each HTTP requests.
Add
In the add function, we use the same endpoint as we did in loadTodos()
. In addition, we also set the request body with the new Todo’s description
and POST
it to api/todos
, where an _id
will be generated and isComplete: false
will be appended to the Todo
before saving it to our Todos
collection.
// Add a todo-edit
add(todo: string): Observable<Todo> {
let body = JSON.stringify({description: todo});
let headers = new Headers({'Content-Type': 'application/json'});
return this.http.post(this.todosUrl, body, {headers: headers})
.map(res => res.json())
.catch(this.handleError);
}
Update
The update
method (http.put
) , receives a Todo
object from our component. The Todo
is used to append the todo._id
to the url, telling our API which Todo
we’re updating. For the body of our request, we JSON.stringify
the Todo
before sending it off.
// Update a todo
update(todo: Todo) {
let url = `${this.todosUrl}/${todo._id}`; //see mdn.io/templateliterals
let body = JSON.stringify(todo)
let headers = new Headers({'Content-Type': 'application/json'});
return this.http.put(url, body, {headers: headers})
.map(() => todo) //See mdn.io/arrowfunctions
.catch(this.handleError);
}
Delete
In the delete
method, we simply send over headers and specify the todo._id
in the URL, which our server will use to identify and destroy the Todo
in question.
// Delete a todo
delete(todo: Todo) {
let url = `${this.todosUrl}/${todo._id}`;
let headers = new Headers({'Content-Type': 'application/json'});
return this.http.delete(url, headers)
.catch(this.handleError);
}
The final todo-service.ts
file should look like this:
todo-service.ts
import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import {Todo} from '../../todo.ts';
@Injectable()
export class TodoService {
todosUrl = "/api/todos"
constructor(public http: Http) {}
// Get all todos
load(): Observable<Todo[]> {
return this.http.get(this.todosUrl)
.map(res => res.json())
.catch(this.handleError);
}
// Add a todo-edit
add(todo: string): Observable<Todo> {
let body = JSON.stringify({description: todo});
let headers = new Headers({'Content-Type': 'application/json'});
return this.http.post(this.todosUrl, body, {headers: headers})
.map(res => res.json())
.catch(this.handleError);
}
// Update a todo
update(todo: Todo) {
let url = `${this.todosUrl}/${todo._id}`; //see mdn.io/templateliterals
let body = JSON.stringify(todo)
let headers = new Headers({'Content-Type': 'application/json'});
return this.http.put(url, body, {headers: headers})
.map(() => todo) //See mdn.io/arrowfunctions
.catch(this.handleError);
}
// Delete a todo
delete(todo: Todo) {
let url = `${this.todosUrl}/${todo._id}`;
let headers = new Headers({'Content-Type': 'application/json'});
return this.http.delete(url, headers)
.catch(this.handleError);
}
handleError(error) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
Now, we have access points for our API that we can use in our app components. Let’s jump back up into our HomePage
component to create methods that will use our service’s new HTTP methods.
HomePage
Component: Adding, Updating, and Deleting Todos
For each of the new methods in our TodoService
, we want to write a method that our HomePage
Component template can use to add, update, and delete our todos. From this main list view of our todos, we’ll want to create an addTodo
method for our input form to use, a toggleComplete
method for our checkbox to use for updating the isComplete
status of a given Todo
, and a deleteTodo
method for our nice red trash can button.
Below the load
method in our HomePage
class, let’s add our new methods.
The addTodo
Method
This first function will take in a string
value from our input box, and call todoService.add
. We then subscribe
to the observable, like we did in the loadTodos
function, so the request will go out. Inside our subscribe
statement, we extract the data containing our newly created Todo
and push
it onto our todos
array.
addTodo(todo:string) {
this.todoService.add(todo)
.subscribe(data => {
this.todos.push(data)
});
}
The toggleComplete
Method
Our second method will receive an actual Todo
object from the view. The first step is to set the isComplete
property of our Todo
equal to its opposite (i.e., true
becomes false or vice versa). Then we ask our TodoService
to update
that particular Todo
. Because we toggled a property of a Todo object already in our HomePage
component, the view has already been updated.
toggleComplete(todo: Todo) {
todo.isComplete = !todo.isComplete;
this.todoService.update(todo)
.subscribe(data => {
todo = data;
})
}
The deleteTodo
Method
Our final method will receive a Todo
object, in addition to a second parameter indicating the index of that Todo
in our class variable, the todos
array. We’ll figure out how to get the index
from our HomePage component template in a moment. This method looks like the other two, except that in our subscribe
function, we have logic to remove the Todo
from our todos
array, once we’ve received confirmation that the Todo
was deleted from the database.
deleteTodo(todo: Todo, index:number) {
this.todoService.delete(todo)
.subscribe(response => {
this.todos.splice(index, 1);
});
}
Now, we can use these three methods in our HomePage
component’s template file.
Event Handling in our HomePage
Template
As I pointed out the last time we looked at our home.html
file, there were several spots where we were listening for events but not doing anything with them. Let’s make our app more interactive, starting with our little Add a Todo form in the template file.
Add a Todo
Currently, we have:
<ion-item no-lines>
<ion-input #newTodo (keyup.enter)="" type="text" placeholder="Add new todo...">
</ion-input>
<button clear large item-right (click)="">
<ion-icon name="add"> </ion-icon>
</button>
</ion-item>
Let’s fire update the (keyup.enter)
event listener, which will fire when a user is in the input box and presses the enter
or return
key. We want to add a new todo when a person presses enter, so let’s put our addTodo
method into that line.
(keyup.enter)="addTodo(newTodo.value); newTodo.value=''"
Inside the double quotes, we’ve added addTodo
and passed in newTodo.value
. In Angular 2 templates, I’m able to specify template reference variables, such as #newTodo
on my <ion-input>
element, and work with that element within the template using its reference variable. Thus, we use newTodo
to get the value
of our <ion-input>
, and then set its value to an empty string to clear it out with newTodo.value=''
.
Now, let’s do the same thing for our button, should a user want to click
that, instead of pressing enter
.
(click)="addTodo(newTodo.value); newTodo.value=''"
Delete or Update a Todo
Next, let’s use our toggleComplete
and deleteTodo
. Our Todo List currently looks like this:
<h2>Todo List <small>Swipe a todo to edit or delete</small></h2>
<ion-list no-lines>
<ion-item-sliding #slidingItem *ngFor="let todo of todos; let index = index">
<ion-item>
<ion-checkbox (click)="toggleComplete(todo)" [checked]="todo.isComplete"></ion-checkbox>
<ion-item text-wrap item-left [class.completed]="todo.isComplete">
{{ todo.description }}
</ion-item>
</ion-item>
<ion-item-options>
<button (click)="">Edit</button>
<button danger (click)="deleteTodo(todo, index)">
<ion-icon name="trash"></ion-icon>
</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
As you may remember, we need to get access to the index of the current todo with which we’re dealing. Within the scope of our NgFor
directive, we have access to its local variable ‘index’. All we have to do is define it right within the quotes of our *ngFor
:
*ngFor=”let todo of todos; let index = index”
Now, we have access to the index of any given Todo
in our todos
array within our repeated <ion-item-sliding>
element. Let’s use it on our beautiful red trash can button:
<button danger (click)="">
<ion-icon name="trash"></ion-icon>
</button>
Add the following to our (click)
event:
(click)="deleteTodo(todo, index)"
Finally, for this section, let’s set the (click)
event on the checkbox to fire our toggleComplete
method:
<ion-checkbox (click)="toggleComplete(todo)" [checked]="todo.isComplete">
A Note on Property Binding
Although I’d previously added them without explanation, we have both the [checked]
property of our checkbox and the [class.completed]
property of our <ion-item>
bound to the isComplete
property of each Todo
. When the page loads, if isComplete
is true, our checkbox will be checked, and our description
will be styled by the .completed
class.
Your Todo App is now almost complete! You can now load, add, update, and delete your todos. The final step we’ll take is to add one more page, where we can view an individual Todo
and edit its description
, the TodoEdit page.
Add the TodoEdit Page
Using the Ionic CLI, we can easily generate our new page:
$ /todo-ionic2-heroku > ionic g page TodoEdit
This just created a folder for us app/pages/todo-edit/
with HTML, TypeScript, and SCSS files for our new TodoEdit component. To navigate to it from our home
page to the todo-edit
page, we’ll need to edit a few things.
Here’s the updated and now final home.ts
file:
// home.ts
import {Component} from "@angular/core";
import {NavController, ItemSliding, Item} from 'ionic-angular';
import {TodoEditPage} from '../todo-edit/todo-edit';
import {TodoService} from '../../providers/todo-service/todo-service';
import {Todo} from '../../todo.ts';
@Component({
templateUrl: 'build/pages/home/home.html',
providers: [TodoService]
})
export class HomePage {
public todos: Todo[];
constructor(public todoService: TodoService,
public nav: NavController) {
this.loadTodos();
}
loadTodos() {
this.todoService.load()
.subscribe(todoList => {
this.todos = todoList;
})
}
addTodo(todo:string) {
this.todoService.add(todo)
.subscribe(newTodo => {
this.todos.push(newTodo);
});
}
toggleComplete(todo: Todo) {
todo.isComplete = !todo.isComplete;
this.todoService.update(todo)
.subscribe(updatedTodo => {
todo = updatedTodo;
});
}
deleteTodo(todo: Todo, index:number) {
this.todoService.delete(todo)
.subscribe(res => {
this.todos.splice(index, 1);
});
}
navToEdit(todo: Todo, index: number) {
this.nav.push(TodoEditPage, {
todo: todo,
todos: this.todos,
index: index
});
}
}
The changes are:
* Added import
statements for our TodoEditPage
and NavController
from ionic-angular
* Added NavController
as a dependency in our constructor
* Created a new method navToEdit
, which uses the NavController
to push
on a new page (the TodoEdit
page). We pass the Todo
and its index
from the view, as well as the our todos
array in the push
function, as NavParams that we’ll extract in the TodoEdit
class.
Update your HomePage
template file (home.html
), so the edit button will navigate to the TodoEdit
page and pass along the Todo
:
<button (click)="navToEdit(todo, index); slidingItem.close()">
Edit
</button>
Note: We are also closing our sliding item when clicking the edit
button
In Ionic 2 Beta 9, the ionSwipe
action was introduced. If desired, we could also open a todos by adding this event to our ion-item-options
component:
<ion-item-options (ionSwipe)="navToEdit(todo, index); slidingItem.close()">
<!-- buttons here -->
</ion-item-options>
Now, you can also open the TodoEdit page by swiping a todo!
Adding functionality to our TodoEdit Page
Awesomeness; now, we can navigate between our two pages! But before we call it a day, we have to finish our TodoEdit
component.
Head into your todo-edit.ts
file and update it with the following code:
// app/pages/todo-edit/todo-edit.ts
import {Component} from "@angular/core";
import {NavController, NavParams} from 'ionic-angular';
import {TodoService} from '../../providers/todo-service/todo-service';
import {Todo} from '../../todo.ts';
@Component({
templateUrl: 'build/pages/todo-edit/todo-edit.html',
providers: [TodoService]
})
export class TodoEditPage {
public todo: Todo; // The todo itself
public todos: Todo[]; // The list of todos from the main page
public index: number; // The index of the todo we're looking at
constructor(public todoService: TodoService, public nav: NavController, public navParams: NavParams ) {
this.todo = navParams.get('todo');
this.todos = navParams.get('todos');
this.index = navParams.get('index');
}
saveTodo(updatedDescription: string) {
this.todo.description = updatedDescription;
this.todoService.update(this.todo)
.subscribe(response => {
this.nav.pop(); // go back to todo list
});
}
deleteTodo() {
this.todoService.delete(this.todo)
.subscribe(response => {
this.todos.splice(this.index, 1); // remove the todo
this.nav.pop(); //go back to todo list
});
}
}
At this point, most of this should look pretty similar to our home.ts
. The only new features are NavParams
, which we use to extract the parameters we passed over when we did nav.push
from our HomePage
class.
We also use the TodoService in a similar fashion to save (update) or delete a todo. In both of those methods, upon receiving our response from our TodoService, we navigate back to our previous page, the Home page, using the NavController
’s pop
method.
Updating the TodoEdit Template
In the template for our TodoEdit
page, we’ll use an Ionic TextArea component to display and allow for editing of our todo. Then we simply need a Save button to update the Todo, a Cancel button to ABORT MISSION and go back to the Home
page, and a large, red trash can button, because I’m really awful at writing todo lists and need multiple opportunities to delete them.
Edit todo-edit.html
to contain the following.
<ion-header>
<ion-navbar primary>
<ion-title>Edit</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding class="todo-edit">
<h2>Description</h2>
<ion-textarea #updatedTodo value="{{todo.description}}"></ion-textarea>
<small>Status: {{ todo.isComplete ? "Complete" : "Incomplete" }}</small>
<ion-row>
<ion-col width-40>
<button block (click)="saveTodo(updatedTodo.value)">
Save
</button>
</ion-col>
<ion-col width-40>
<button light block nav-pop>
Cancel
</button>
</ion-col>
<ion-col width-20>
<button danger block (click)="deleteTodo()">
<ion-icon name="trash"></ion-icon>
</button>
</ion-col>
</ion-row>
</ion-content>
Note: I’m using Ionic’s snazzy Grid
components to create a row of different-sized buttons.
And just like that (drum roll, please), we’re done with development. Now, all you have to do is commit your changes:
$ /todo-ionic2-heroku > git add -A
$ /todo-ionic2-heroku > git commit -m "Second Commit, also I rock for finishing this app"
$ /todo-ionic2-heroku > git push heroku master
Again, make sure your app is running:
$/todo-ionic2-heroku > heroku ps:scale web=1
And open your app:
$/todo-ionic2-heroku > heroku open
How awesome is Heroku? So easy, right?
Some Final Thoughts
Congratulations! You made it, and we covered quite a bit of ground. We built a Node.js backend using Express and MongoDB, defining API routes for our frontend to use. We built an Ionic 2 Todo app using Angular 2 and TypeScript that interacts with our API to get, add, update, and delete todos. Lastly, we set up the final app on Heroku. This app, while bare-bones, was intended to demonstrate Ionic 2 and Angular 2 concepts, as they interact with a Node.js backend, and how to host an app on Heroku. The repository will continually be updated as new versions of Ionic are released.