Getting MEAN with Ionic 2

Building a Review App with Ionic 2, MongoDB & Node



·

Throughout your adventures in web and mobile development, you may have heard of the MEAN stack, which (aside from sounding pretty badass) stands for MongoDB, Express.js, Angular, and Node.js. All of these technologies work together to allow you to create an application with a frontend and a backend.

In this tutorial we will be creating a simple review application in Ionic 2 powered by the MEAN stack. In a previous tutorial, An Introduction to NoSQL for HTML5 Mobile Developers, we covered how to get up an running with MongoDB, and now we’ll be looking at how to make use of it in a real life scenario. I’d recommend reading that tutorial if you aren’t already familiar with MongoDB (it gives some background into NoSQL in general, as well as MongoDB) but I will be covering the steps for getting set up in this tutorial as well so it isn’t required.

This is what the app will look like when it’s done:

Review King Screenshots

and this is how we will be using the MEAN stack for this tutorial:

  • MongoDB will be our NoSQL database, which we will use to store and retrieve reviews
  • Express will allow us to create routes for the REST API we will be creating
  • Ionic 2 will be used rather than Angular 2 for the front end of the application (I guess making this the MEIN stack)
  • Node will be our server, which will sit between the frontend of the application and the MongoDB database

This tutorial has been created with the help of these great resources, with bits and pieces being borrowed from each one, so if you’re looking for some more reading I’d recommend checking them out:

Before we Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

1. Creating the Database with MongoDB

The first thing we are going to do is get our MongoDB database set up. We will be setting this up locally (as we will be with the Node server as well), but if you would prefer you could also have your database hosted somewhere else.

The following install steps are specifically for Mac, and we will be using brew. If you don’t have brew installed already just follow the instructions on this page. If you are using a Windows computer, then you can follow the instructions here to install MongoDB.

You can install MongoDB with the following command:

brew install mongodb

and then start it with the following command:

brew services start mongodb

Now you can create a new MongoDB database and connect to it with the following command:

mongo reviewking

You will now be able to interact with the reviewking database through the terminal, and we will also be able to interact with it through our review application once we set up the API. To come back to this same database later you can just run mongo test again.

2. Generating a New Ionic 2 Application

We’re going to be creating our Node server before creating the front end, but let’s get our Ionic 2 project set up now.

Run the following command to create a new Ionic 2 application:

ionic start review-king blank --v2

We are also going to create an extra page (that we will use for adding reviews) and a provider (which we will be using to interact with our API), so let’s create those now too.

Run the following generate commands:

ionic g page AddReviewPage
ionic g provider Reviews

When creating new components in our application, we need to ensure that they are also added to the app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { AddReviewPage } from '../pages/add-review-page/add-review-page';
import { Reviews } from '../providers/reviews';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddReviewPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddReviewPage
  ],
  providers: [Reviews]
})
export class AppModule {}

Now let’s set up our server.

4. Create the Backend API with Node & Express

We are going to keep our server separate to the front end Ionic 2 project, so we will be creating a folder for it outside of the Ionic project.

Create a folder called server outside of your Ionic project

We are also going to need to set up some dependencies for it. You may be familiar with using the package.json file in Ionic projects (or other projects) to specify dependencies to be installed with npm. We can do the same thing for our server folder as well.

Create a file called package.json inside of the server folder and add the following:

{
  "name": "review-king",
  "version": "0.1.0",
  "description": "A sample Node.js app using Express 4",
  "engines": {
    "node": "5.9.1"
  },
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "devDependencies": {
    "mongoose": "^4.6.2",
    "body-parser": "^1.15.2",
    "cors": "^2.8.0",
    "del": "2.2.0",
    "express": "^4.14.0",
    "http": "0.0.0",
    "method-override": "^2.3.6",
    "morgan": "^1.7.0",
    "superlogin": "^0.6.1"
  }
}

Mongoose is a wrapper that will help us interact with MongoDB (clever name right?), and as I mentioned before Express is what we will be using to help create the routes for our API.

The rest of the dependencies are all helpers for our server. Body Parser will help us grab information from POST requests, Method Override provides support for DELETE and PUT, Morgan will output some helpful debugging messages, and Cors will handle CORS (Cross Origin Resource Sharing) issues for us.

To install all of the dependencies in package.json, you will need to run the npm install command.

Navigate to the server folder in your terminal, and run the following command:

npm install

Now we are going to create a server.js file, which we will run with node to create our server. I’m not going to explain this process in depth (although I will give a brief explanation afterwards), so if you’d like some more information on this step I’d recommend reading one of the resources I linked above.

NOTE: This code is heavily based on this tutorial, so credit to Chris Sevilleja for this. His tutorial also goes into a lot more detail on this step.

Create a file called server.js inside of the server folder and add the following code:

// Set up
var express  = require('express');
var app      = express();                               // create our app w/ express
var mongoose = require('mongoose');                     // mongoose for mongodb
var morgan = require('morgan');             // log requests to the console (express4)
var bodyParser = require('body-parser');    // pull information from HTML POST (express4)
var methodOverride = require('method-override'); // simulate DELETE and PUT (express4)
var cors = require('cors');

// Configuration
mongoose.connect('mongodb://localhost/reviewking');

app.use(morgan('dev'));                                         // log every request to the console
app.use(bodyParser.urlencoded({'extended':'true'}));            // parse application/x-www-form-urlencoded
app.use(bodyParser.json());                                     // parse application/json
app.use(bodyParser.json({ type: 'application/vnd.api+json' })); // parse application/vnd.api+json as json
app.use(methodOverride());
app.use(cors());

app.use(function(req, res, next) {
   res.header("Access-Control-Allow-Origin", "*");
   res.header('Access-Control-Allow-Methods', 'DELETE, PUT');
   res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
   next();
});

// Models
var Review = mongoose.model('Review', {
    title: String,
    description: String,
    rating: Number
});

// Routes

    // Get reviews
    app.get('/api/reviews', function(req, res) {

        console.log("fetching reviews");

        // use mongoose to get all reviews in the database
        Review.find(function(err, reviews) {

            // if there is an error retrieving, send the error. nothing after res.send(err) will execute
            if (err)
                res.send(err)

            res.json(reviews); // return all reviews in JSON format
        });
    });

    // create review and send back all reviews after creation
    app.post('/api/reviews', function(req, res) {

        console.log("creating review");

        // create a review, information comes from request from Ionic
        Review.create({
            title : req.body.title,
            description : req.body.description,
            rating: req.body.rating,
            done : false
        }, function(err, review) {
            if (err)
                res.send(err);

            // get and return all the reviews after you create another
            Review.find(function(err, reviews) {
                if (err)
                    res.send(err)
                res.json(reviews);
            });
        });

    });

    // delete a review
    app.delete('/api/reviews/:review_id', function(req, res) {
        Review.remove({
            _id : req.params.review_id
        }, function(err, review) {

        });
    });


// listen (start app with node server.js) ======================================
app.listen(8080);
console.log("App listening on port 8080");

Quite a lot is going on here, but it is split up into a few distinct sections:

  • Set up
  • Configuration
  • Models
  • Routes
  • Listen

First we set up all the dependencies we need for the server (like Mongoose and Express), then we configure some things which most importantly includes this line:

mongoose.connect('mongodb://localhost/reviewking');

which sets up the connection to our database. If you did not call your database reviewking then you should make sure to change that here. We set up a model called Review which is what is used to store our review object in the database – as you can see this object is made up of a title, description, and a rating.

Next we set up the routes, which are the endpoints we can hit on our API. Eventually we will use the Http service to hit this endpoints. We will be able to:

  • Send a GET request to /api/reviews to retrieve all the reviews
  • Send a POST request to /api/reviews to create a new review
  • Send a DELETE request to /api/reviews/{{review_id}} to delete a specific review

In the code above you can see we’re setting up each of these with app.get, app.post, and **app.delete. For each of these we specify the URL that is going to hit, and we set up handlers for each of these cases. In the case of a POST, it will handle storing the data in the database, but if there is a GET request it will handle retrieving the data and sending it back to our app.

Finally, the listen section starts our server on port 8080, which means we will be able to interact with the server by accessing:

http://localhost:8080

Now that the server is ready we can start it by running the following command:

node server.js

As you interact with the server through the application a little bit later, you should see something like this in your terminal:

Node Server

Now we just need to create our front end!

5. Create the Frontend with Ionic 2

We’ve already got the structure of our application set up, so we just need to drop in our front end code. Since this is a more advanced tutorial I will be going through this pretty quickly, and only focusing on the important bits.

Create the Reviews Provider

We’re going to start out by creating our Reviews provider, which is the most interesting thing in the application as it is what will interact with the backend.

Modify src/providers/reviews.ts to reflect the following

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class Reviews {

  data: any;

  constructor(public http: Http) {
    this.data = null;
  }

  getReviews(){

    if (this.data) {
      return Promise.resolve(this.data);
    }

    return new Promise(resolve => {

      this.http.get('http://localhost:8080/api/reviews')
        .map(res => res.json())
        .subscribe(data => {
          this.data = data;
          resolve(this.data);
        });
    });

  }

  createReview(review){

    let headers = new Headers();
    headers.append('Content-Type', 'application/json');

    this.http.post('http://localhost:8080/api/reviews', JSON.stringify(review), {headers: headers})
      .subscribe(res => {
        console.log(res.json());
      });

  }

  deleteReview(id){

    this.http.delete('http://localhost:8080/api/reviews/' + id).subscribe((res) => {
      console.log(res.json());
    });    

  }

}

We’ve created three methods here: getReviews, createReview, and deleteReview. Each of these corresponds to one of the end points on our API. In the getReviews function, we simply send a get request to /api/reviews and that will return our review data. The createReview function accepts a review object as a parameter and then posts that to the /api/reviews endpoint (where we already handle inserting it into the database). Finally, our deleteReview function accepts the id (which is automatically created by MongoDB) of a specific review, and will make a request to the API to delete it.

Create the Add Review Page

Now let’s set up the ‘Add Review’ page. This will be triggered as a Modal later, and the user will be able to enter in their title, description, and a rating using Ionic’s range component.

Modify src/pages/add-review-page/add-review-page.html to reflect the following:

<ion-header>
 <ion-toolbar transparent>
  <ion-title>Add Review</ion-title>
  <ion-buttons end>
    <button ion-button icon-only (click)="close()"><ion-icon name="close"></ion-icon></button>
  </ion-buttons>
 </ion-toolbar>
</ion-header>

<ion-content>

  <ion-list no-lines>

    <ion-item>
      <ion-label floating>Title</ion-label>
      <ion-input [(ngModel)]="title" type="text"></ion-input>
    </ion-item>

    <ion-item>
      <ion-label floating>Review</ion-label>
      <ion-textarea [(ngModel)]="description"></ion-textarea>
    </ion-item>

    <ion-item>
      <ion-range min="0" max="100" pin="true" [(ngModel)]="rating">
        <ion-icon range-left name="sad"></ion-icon>
        <ion-icon range-right name="happy"></ion-icon>
      </ion-range>
    </ion-item>

  </ion-list>

  <button ion-button full color="secondary" (click)="save()">Save</button>

</ion-content>

Modify src/pages/add-review-page/add-review-page.ts to reflect the following:

import { Component } from '@angular/core';
import { ViewController } from 'ionic-angular';

@Component({
  selector: 'add-review-page',
  templateUrl: 'add-review-page.html'
})
export class AddReviewPage {

  title: any;
  description: any;
  rating: any;

  constructor(public viewCtrl: ViewController) {

  }

  save(): void {

    let review = {
      title: this.title,
      description: this.description,
      rating: this.rating
    };

    this.viewCtrl.dismiss(review);

  }

  close(): void {
    this.viewCtrl.dismiss();
  }
}

We’re not handling any saving of the data here, we just grab the users input and then send it back to the HomePage through the use of the dimiss() method. Let’s set up the home page now.

Modify src/pages/home/home.html to reflect the following:

<ion-header>
 <ion-navbar transparent>
  <ion-title>
    Review King
  </ion-title>
  <ion-buttons end>
    <button ion-button icon-only (click)="addReview()"><ion-icon name="add"></ion-icon></button>
  </ion-buttons>
 </ion-navbar>
</ion-header>

<ion-content>

  <ion-list no-lines>

    <ion-item-sliding *ngFor="let review of reviews">

      <ion-item>

        <ion-avatar item-left>
          <img src="https://api.adorable.io/avatars/75/{{review.title}}">
        </ion-avatar>

        <h2>{{review.title}}</h2>
        <p>{{review.description}}</p>

        <ion-icon *ngIf="review.rating < 50" danger name="sad"></ion-icon>
        <ion-icon *ngIf="review.rating >= 50" secondary name="happy"></ion-icon> 
        {{review.rating}}

      </ion-item>

      <ion-item-options>
        <button ion-button color="danger" (click)="deleteReview(review)">
          <ion-icon name="trash"></ion-icon>
          Delete
        </button>
      </ion-item-options>
    </ion-item-sliding>

  </ion-list>

</ion-content>

This template is a little more interesting than the last. We’re looping over all of our reviews using *ngFor (we will define reviews in the class definition shortly). For each of these we display the data associated with the review, as well as an automatically generated avatar from adorable.io based on the title value. We also display a happy face if the rating is 50 or above, and a sad face is the rating is below 50.

Also notice that we are using the <ion-item-sliding> component, which will allow us to reveal the delete button for each review when it is swiped to the left.

Modify src/pages/home/home.ts to reflect the following:

import { Component } from "@angular/core";
import { NavController, ModalController } from 'ionic-angular';
import { AddReviewPage } from '../add-review-page/add-review-page';
import { Reviews } from '../../providers/reviews';

@Component({
  selector: 'home-page',
  templateUrl: 'home.html'
})
export class HomePage {

  reviews: any;

  constructor(public nav: NavController, public reviewService: Reviews, public modalCtrl: ModalController) {

  }

  ionViewDidLoad(){

    this.reviewService.getReviews().then((data) => {
      console.log(data);
      this.reviews = data;
    });

  }

  addReview(){

    let modal = this.modalCtrl.create(AddReviewPage);

    modal.onDidDismiss(review => {
      if(review){
        this.reviews.push(review);
        this.reviewService.createReview(review);        
      }
    });

    modal.present();

  }

  deleteReview(review){

    //Remove locally
      let index = this.reviews.indexOf(review);

      if(index > -1){
        this.reviews.splice(index, 1);
      }   

    //Remove from database
    this.reviewService.deleteReview(review._id);
  }

}

This class defines the functions that our home template calls. The addReview function launches our Add Review page as a modal, and when it receives the review data back from it, it calls the createReview function in the Review service we created before. This will add the review to our MongoDB database, but we also add the data to the local reviews array so that it displays right away without requiring a reload.

For the deleteReview function, we pass in the review and grab its id. First we remove that review locally (again, so that the change takes effect right away without having to reload), and then pass the id to the delete method of the review service (which will delete the review from our MongoDB database).

Finally, we’re going to add a bit of styling to the application.

Modify $background-color in theme/variables.scss to reflect the following:

$background-color:  #74efc4;

Modify home.scss to reflect the following:

.ios, .md {

  home-page {

    ion-item {
      background-color: #fff;
      border-bottom: none !important;
    }

    ion-label {
      white-space: normal;
    }

    ion-item-sliding {
      margin: 10px;
    }

  }

}

Modify add-review-page.scss to reflect the following:

.ios, .md {

  add-review-page {

    ion-label {
      margin-left: 10px;
    }

    ion-item {
      background-color: #74efc4 !important;
    }

    ion-input, ion-select, ion-textarea {
      background-color: #f2f2f2;
      padding: 5px 10px;
    }

  }

}

and that’s it! Your app (once you add some data to it) should now look something like this:

Review King Screenshots

Summary

Admittedly, this is a pretty basic app and its not exactly something you would submit to the app store. But we’ve done a tremendous amount of work here with very little code. We have a NoSQL database which we are able to interact with through the REST API we set up with Node and a front end to display and modify all of that data. The concepts we have covered here can be used as the basis for just about any app.

In future, we are going to be covering even more different backend options so stay tuned!

What to watch next...