August 19, 2016
  • All
  • Framework
  • Ionic 2
  • Tutorials

One MEAN Ionic 2 Todo App on Heroku, Part 3

Justin Leatherwood

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.


Justin Leatherwood