Develop a web application with Angular2

Angular2 promises many improvements over the previous version.

It’s simpler to learn thanks to easier and more concise concepts like component-based architecture.

The Angular2 module system makes it easier to develop complex and larger projects.

Angular2 is faster than its ancestor thanks to completely rewritten data binding and change detection.

Router module is improved by providing new features like sibling views and nested states.

The aim of this post is to discover some new Angular2 features by developing a web project that uses apart Angular2 other projects like Express, TypeScript and MongoDb.

The source code is provided on GitHub here. You can run the project at this link.

Project structure

The Angular2 sample project has this structure.

ng2-project
    |
    +-client
    |   +-app 
    |   +-assets (Directoy conatins images and css)
    |   +-index.html
    |   +-tsconfig.json
    |   \-typings.json
    |
    +-server 
    |   +-server.ts
    |   +-tsconfig.json
    |   \-typings.json
    |
    +-dist (Directory generated by gulp)
    |
    +-gulpfile.js
    \-pakage.json

You can explore the project structure on Github here

Project dependencies

The package.json file identifies npm package dependencies for the project.

{
  "name": "ng2-project",
  "version": "1.0.0",
  "main": "server/index.js",
  "license": "ISC",
  "scripts": {
    "start": "concurrently \"npm run tsc:w\" \"npm run lite\" ",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "postinstall": "gulp"
  },
  "dependencies": {
    "@angular/core": "2.0.0",
    "@angular/common": "2.0.0",
    "@angular/compiler": "2.0.0",
    "@angular/http": "2.0.0",
    "@angular/platform-browser": "2.0.0",
    "@angular/platform-browser-dynamic": "2.0.0",
    "@angular/router": "3.0.0",
    "@angular/upgrade": "2.0.0",
    "systemjs": "0.19.27",
    "es6-shim": "^0.35.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.23",
    "angular2-in-memory-web-api": "0.0.20",
    "material-design-lite": "1.1.2",
    "express": "4.13.4",
    "mongodb": "2.1.18",
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.1.0",
    "typescript": "1.8.10",
    "del": "2.2.0",
    "gulp": "3.9.1",
    "gulp-concat": "2.6.0",
    "gulp-sourcemaps": "1.6.0",
    "gulp-typescript": "2.13.4",
    "gulp-tsd": "0.1.1",
    "gulp-typings": "1.3.6",
    "run-sequence": "1.1.5"
  }
}

1- Server Side

We start with the server side implementation, and then we can test the rest API before using it in the client side code.

1.1 Add tsconfig.json file

The tsconfig.json file provides a TypeScript compiler configuration.

{
  "version": "1.8.10",
  "compilerOptions": {
    "target": "ES5",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "files": [
    "typings/main.d.ts"
  ],
  "exclude": [
    "typings/browser",
    "typings/browser.d.ts"
  ]
}

1.1 Add typings file

TypeScript compiler doesn’t recognize natively some JavaScript libraries features and syntax.
Some of the included files that provides required description for TypeScript. Others don’t provide it and they will require to be configured in typings.js file.

{
  "version": false,
  "ambientDependencies": {
    "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "express-serve-static-core": "github:DefinitelyTyped/DefinitelyTyped/express-serve-static-core/express-serve-static-core.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "express": "github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "serve-static": "github:DefinitelyTyped/DefinitelyTyped/serve-static/serve-static.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34",
    "mime": "github:DefinitelyTyped/DefinitelyTyped/mime/mime.d.ts#a44529fcb1e1cdc0355e71c42a6048de99b57c34"
  }
}

1.2 – Install and create database

To install MongoDB you can download and install it by following instructions in this link https://docs.mongodb.com/manual/installation.
Next we will need to initialize database data. you can use the code bellow:

use mydata

db.users.insert([
{"id" : "1" , "name" : "Steve" , "mail" : "steve@apple.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "2" , "name" : "Narco" , "mail" : "narco@gamil.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "3" , "name" : "Bombasto" , "mail" : "bombasto@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "4" , "name" : "Celeritas" , "mail" : "celeritas@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "5" , "name" : "Magneta" , "mail" : "magneta@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"},
{"id" : "6" , "name" : "RubberMan" , "mail" : "rubber.man@gmail.com" , "phone" : "+33 0120255555" , "adress" : "280 Central Park West New York, NY 10024"}
]);

You can use MongoDb as a service provided by mLab. All what you have to do is to sign up, create a database and add a user to access the database by using a simple web user interface.

1.3 – Implement the Rest API

In this project we use Express.js to implement the server side API.

Express.js is a web application framework for Node.js, It is designed for building web applications and APIs.

import express = require('express');
import path = require('path');
var port: number = process.env.PORT || 3000;
var app = express();
var MongoClient = require('mongodb').MongoClient;
var database;

app.use('/assets', express.static(path.resolve(__dirname, 'assets')));
app.use('/app', express.static(path.resolve(__dirname, 'app')));
app.use('/libs', express.static(path.resolve(__dirname, 'libs')));

var server = app.listen(port, function() {
    var port = server.address().port;
    console.log("This express app is listening on port " + port);
});

MongoClient.connect('mongodb://test:test@ds021761.mlab.com:21761/mydata', 
    function(err, db) {
    if (err) {
        throw err;
    }
    database = db;
});

app.get("/api/users", (req, res) => {
    // Get all registrations
    database.collection('users').find().toArray(function(err, result) {
        if (err) {
            console.log("Can't get users from database : " + err);
            throw err;
        }
        console.log("/api/users result size = " + result.length);
        res.send(result);
    });
});

app.get("/api/users/:id", (req, res) => {
    database.collection('users').findOne( {id : req.params.id} , function(err, item) {
        if (err) {
            console.log("Can't get users from database : " + err);
            throw err;
        }
        console.log("found user :" + JSON.stringify(item));
        res.send(item);
    });
});

var renderIndex = (req: express.Request, res: express.Response) => {
    res.sendFile(path.resolve(__dirname, 'index.html'));
}

app.get('/*', renderIndex);

1.4 Build and test server side code

To build a project we use Gulp that install typings and compiles TypeScript files.

Bellow the source code of gulpfile.js file

var gulp = require('gulp');
var sourcemaps = require('gulp-sourcemaps');
var ts = require('gulp-typescript');
var runSequence = require('run-sequence');
var gulpTypings = require("gulp-typings");

// TYPINGS
gulp.task("installTypings", function () {
    var stream = gulp.src(["./server/typings.json"])
        .pipe(gulpTypings());
    return stream;
});

// SERVER
gulp.task('buildServer', function () {
    var tsProject = ts.createProject('server/tsconfig.json');
    var tsResult = gulp.src('server/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts(tsProject))
    return tsResult.js
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('dist'))
});

gulp.task('build', function (callback) {
    runSequence('installTypings', 'buildServer', callback);
});

gulp.task('default', ['build']);

To build the project, Open a terminal window and enter this command:

npm install

Next, run the node server:

node dist/server.js

Then test the API in your browseron this url:
http://localhost:8080/api/users

If everything is well done, the browser display an array of users in JSON format.

2- Client side

2.1 Add typings file

Bellow the source code of client typings.json

{
  "ambientDependencies": {
    "es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654"
  }
}

2.2 Add typescript configuration file

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ]
}

2.3 Add SystemJs config file

SystemJs is a dynamic module loader. It loads ES6 modules, AMD, CommonJS and global scripts in the browser and NodeJS.
SystemJs will load libraries when they are required.

The configuration below tells SystemJs where to look for a module when component is imported.

(function(global) {

    // map tells the System loader where to look for things
    var map = {
        'app':      'app',        
        // angular bundles
        '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
        '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
        '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
        '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
        '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
        '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
        '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
        'rxjs':     'libs/rxjs',
        '@angular': 'libs/@angular'
    };

    // packages tells the System loader how to load when no filename and/or no extension
    var packages = {
        'app':      { main: './main.js',  defaultExtension: 'js' },
        'rxjs':     { defaultExtension: 'js' },
        'angular2-in-memory-web-api': {defaultExtension: 'js'}
    };

    var config = {
        paths: { // paths serve as alias
          'npm:': 'libs/'
        },
        map: map,
        packages: packages
    }

    System.config(config);
})(this);

2.4 Add index.html page

Here, we import style sheets, load libraries and configure SystemJs.

We provide to SystemJS the root module (app) of the application. Then SytemJs will look for all dependencies referenced by the app module.

<html lang="en">
<head>
    <base href="/"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <title>ng2-project</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en">
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
    <link rel="stylesheet" href="https://code.getmdl.io/1.1.2/material.indigo-pink.min.css">
    <link rel="stylesheet" href="assets/css/styles.css">

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="libs/es6-shim/es6-shim.min.js"></script>
    <script src="libs/zone.js/dist/zone.js"></script>
    <script src="libs/reflect-metadata/Reflect.js"></script>
    <script src="libs/systemjs/dist/system.src.js"></script>

    <!-- 2. Configure SystemJS -->
    <script src="app/systemjs.config.js"></script>
    <script>
        System.import('app').catch(function (err) {
            console.error(err);
        });
    </script>

    <script src="libs/material-design-lite/dist/material.min.js"></script>

</head>

<!-- 3. Display the application -->
<body class="mdl-color--grey-100">
    <my-app>Loading...</my-app>
</body>
</html>

2.5 Add application module : app.module.ts

NgModule is a decorator function that describes the module. The most important properties are:

  • declarations – the view classes that belong to this module.
  • imports – other modules whose exported classes are needed by component templates declared in this module.
  • providers – creators of services that this module contributes to the global collection of services; they become accessible in all parts of the app.
  • bootstrap – the main application view, called the root component, that hosts all other app views. Only the root module should set this bootstrap property.

Here’s a root module of our application:

import { NgModule }             from '@angular/core';
import { BrowserModule }        from '@angular/platform-browser';
import { HttpModule }           from '@angular/http';
import { AppComponent }         from './app.component';
import { routing, appRoutingProviders }  from './app.routing';
import {HomeComponent}          from './home/home.comonent';
import {UserListComponent}      from './users/user-list.component';
import {UserDetailComponent}    from './users/user-detail.component';

@NgModule({
  imports: [
    BrowserModule,
    routing,
    HttpModule
  ],
  declarations: [
    AppComponent,
    HomeComponent,
    UserListComponent,
    UserDetailComponent
  ],
  providers: [
    appRoutingProviders
  ],
  bootstrap: [ AppComponent ]
})

export class AppModule { }

2.6 Add main.ts file

Here we launch the application by bootstrapping its root module.

/// <reference path="../typings/browser.d.ts" />
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);

2.7 Configure router : app.routing.ts

Here we configure the router that looks for a corresponding Route from which it can determine the component to display.

In the following example, we configure our application with three route definitions.

import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {HomeComponent}         from './home/home.comonent';
import {HomeResolver}         from './home/home.resolver';
import {HomeService}         from './home/home.service';
import {UserListComponent}     from './users/user-list.component';
import {UserDetailComponent}   from './users/user-detail.component';
import {UserDetailResolver}   from './users/user-detail.resolver';
import {UserListResolver}   from './users/user-list.resolver';
import {UserService}   from './users/user.service';

export const appRoutes : Routes =[
    {path: '',  
     component: HomeComponent,
     resolve : {technologies: HomeResolver}
    },
    {path: 'users', 
     component: UserListComponent,
     resolve : {users: UserListResolver}
    },
    {path: 'user/:id', 
     component: UserDetailComponent, 
     resolve : {user: UserDetailResolver}
    }];

export const appRoutingProviders: any[] = [HomeResolver, UserListResolver, 
        UserDetailResolver, HomeService, UserService];
export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes);

To achieve a better user-experience when browsing between pages, we use resolver.

It avoids displaying pages before data finished loading.

It also makes the controller’s code much cleaner in contrast to fetching data inside the controller.

import {Injectable} from '@angular/core';
import {Resolve, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
import {Observable} from 'rxjs/Observable';
import { User, UserService } from './user.service';

@Injectable()
export class UserListResolver implements Resolve<User[]> {
 constructor(
        private service: UserService
 ) {}
 resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User[]> {
        return this.service.getUsers();
 }
}

2.7 Add components

2.6.1 Add application main component

import {Component} from '@angular/core';
import {MDL} from './directives/MaterialDesignLite';
import './rxjs-operators';

@Component({
    selector: 'my-app',
    templateUrl: 'app/app.component.html'
})

export class AppComponent { }

“rxjs-oppertors” references the “rxjs-oppertors.ts” file where we will imports all required rxjs statics and operators we need for this application.

// Statics
import 'rxjs/add/observable/throw';
// Operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/toPromise';

Next, we write the template html (app.component.html) code.

<div mdl class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
  <header class="mdl-layout__header">
    <div class="mdl-layout__header-row">
      <!-- Title -->
      <span class="mdl-layout-title">Title</span>
      <!-- Add spacer, to align navigation to the right -->
      <div class="mdl-layout-spacer"></div>
      <!-- Navigation. We hide it in small screens. -->
      <nav class="mdl-navigation mdl-layout--large-screen-only">
        <a class="mdl-navigation__link" [routerLink]="['/']" >Home</a>
        <a class="mdl-navigation__link" [routerLink]="['users']">Users</a>
      </nav>
    </div>
  </header>
  <div class="mdl-layout__drawer">
    <span class="mdl-layout-title">Title</span>
    <nav class="mdl-navigation">
      <a class="mdl-navigation__link" [routerLink]="['/']" >Home</a>
      <a class="mdl-navigation__link" [routerLink]="['/users']">Users</a>
    </nav>
  </div>
  <main class="mdl-layout__content">
       <router-outlet></router-outlet>
  </main>
</div>

2.6.2 Add users list component

This page calls a rest web service and display users list.

First, we develop service module.

import {Injectable} from '@angular/core';
import {Http, Headers, Response} from '@angular/http';
import {Observable}     from 'rxjs/Observable';

export class User {
    constructor(public id:number, public name:string, 
         public email:string, public phone:string, public adress:string) {
    }
}

@Injectable()
export class UserService {
    constructor (private http: Http) {}
    private _usersUrl = 'api/users';

    private extractData(res: Response) {
        if (res.status < 200 || res.status >= 300) {
            throw new Error('Bad response status: ' + res.status);
        }
        return res.json();
    }

    private handleError (error: any) {
        let errMsg = error.message || 'Server error';
        console.error(errMsg); // log to console instead
        return Observable.throw(errMsg);
    }

    getUsers() : Observable<User[]> {
        return this.http.get(this._usersUrl)
            .map(this.extractData)
            .catch(this.handleError);
    }

    getUser(id:number | string) : Observable<User> {
        return this.http.get(this._usersUrl + "/" + id)
            .map(this.extractData)
            .catch(this.handleError);
    }
}

Next, we develop the UsersList component.

import {Component, OnInit}   from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {User}   from './user.service';


@Component({
    templateUrl: 'app/users/user-list.component.html'
})
export class UserListComponent implements OnInit {
    users:User[];
    errorMessage:string;
    private selectedId:number;

    constructor(private route: ActivatedRoute,
                private router:Router) {
    }

    ngOnInit() {
        this.users = this.route.snapshot.data['users'];
    }

    isSelected(user:User) {
        return user.id === this.selectedId;
    }

    onSelect(user:User) {
        this.router.navigate(['/user', user.id]);
    }
}

The source code bellow presents the template html code

<div class="page-content mdl-grid">
    <div class="mdl-grid--no-spacing mdl-card mdl-shadow--4dp mdl-cell mdl-cell--12-col">
    <div class="users-card-wide">
        <div class="mdl-card__title mdl-color--purple-700">
            <h1 class="mdl-card__title-text">Users</h1>
        </div>
        <div class="mdl-list">
          <div class="mdl-list__item mdl-list__item--two-line" *ngFor="let user of users"
            [class.selected]="isSelected(user)"
            (click)="onSelect(user)">
            <span class="mdl-list__item-primary-content">
              <img class="user-list-avatar mdl-list__item-avatar" 
                   src="assets/img/users/user-{{user.id}}.jpg">
              <span>{{user.name}}</span>
              <span class="mdl-list__item-sub-title">
                {{user.mail}}
              </span>
            </span>
            <a class="mdl-list__item-secondary-action" href="#">
                <i class="material-icons">star</i>
            </a>
          </div>
        </div>
    </div>
</div>

3. Build project

To build the client side and the server side code of our project, we update gulpfile.js.

var gulp = require('gulp');
var path = require('path');
var sourcemaps = require('gulp-sourcemaps');
var ts = require('gulp-typescript');
var del = require('del');
var runSequence = require('run-sequence');
var gulpTypings = require("gulp-typings");


gulp.task('clean', function () {
    return del('dist')
});


// TYPINGS
gulp.task("installTypings", function () {
    var stream = gulp.src(["./client/typings.json", "./server/typings.json"])
        .pipe(gulpTypings());
    return stream;
});

gulp.task("deleteDupTypings", function () {
    return del(["./client/typings/main.d.ts",
        "./client/typings/main",
        "./server/typings/browser.d.ts",
        "./server/typings/browser"]);
});


// SERVER
gulp.task('buildServer', function () {
    // code provided previded in the previous section
});

// CLIENT
/*
 jsNPMDependencies, sometimes order matters here! so becareful!
 */
var jsNPMDependencies = [
    'material-design-lite/dist/material.min.js',
    'es6-shim/es6-shim.min.js',
    'zone.js/dist/zone.js',
    'reflect-metadata/Reflect.js',
    'systemjs/dist/system.src.js',
    'rxjs/**/*.js',
    '@angular/**/*.js'
];

gulp.task('buildIndex', function () {
    var mappedPaths = jsNPMDependencies.map(function (file) {
        return path.resolve('node_modules', file)
    });

    //Let's copy our head dependencies into a dist/libs
    var copyJsNPMDependencies = gulp.src(mappedPaths, {base: 'node_modules'})
        .pipe(gulp.dest('dist/libs'));

    //Let's copy html and js to dist
    var copyIndex = gulp.src(['client/**/*.html', 'client/**/*.js'])
        .pipe(gulp.dest('dist'));

    var copyAsserts = gulp.src('client/assets/**/*')
        .pipe(gulp.dest('dist/assets'));

    return [copyJsNPMDependencies, copyIndex, copyAsserts];
});

gulp.task('buildClient', function () {
    var tsProject = ts.createProject('client/tsconfig.json');
    var tsResult = gulp.src('client/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts(tsProject));
    return tsResult.js
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('dist'));
});

// WATCH
gulp.task('watch', ['buildServer'], function () {
    gulp.watch('server/*.ts', ['buildServer']);
    gulp.watch('client/**/*.ts', ['buildClient']);
});

// BUILD
gulp.task('build', function (callback) {
    runSequence('clean', 'installTypings', 'deleteDupTypings', 'buildServer', 
                  'buildIndex', 'buildClient', callback);
});

gulp.task('default', ['build']);

Next, Open a terminal window and enter this command:

npm install

4. Run the project

Finally, we launch the node server.

node dist/server.js

Open your browser and test the application at this URL http://localhost:8080.

angular2_sample_prinscreen

Conclusion

First impression after developing this sample project, Angular2 needs more investment than its ancestor from developer to start developing application. It requires learning Typescript and understanding other features like SystemJs and Typings.

Angular2 have the advantage to be modular and more structured than its previous version that make it a good choice for large applications development.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s