Angular 2

Angular 2 represents a big step forward in web application development as long as it comes with support for the Typescript language which basically extends ECMAScript 6 features with typing. This framework has a lot of great features like Routing, Dependency Injection and Components Structure which really allow you to build scalable applications like never before. However a real project needs some ingredients that are not delivered out of the box like Authentication, Translation or better HTTP Request abstraction.

 

1. Angular 2 Build Process with Gulp and NPM

Our main goal is to separate the development and build directories for a more solid structure. We’ll create an app directory for development purposes and a public one where our application will be compiled. We’ll also add an extra assets directory for storing our resources like fonts and images:

|-- app
|    |-- components
|    |-- models
|    |-- services
|    |-- index.html
|    |-- main.ts
|-- assets
|    |-- fonts
|    |-- img
|-- public
|    |-- components // for keeping the html files structure like app directory 
|    |-- css
|    |-- fonts
|    |-- img
|    |-- js
|    |-- index.html
|-- gulpfile.js
|-- package.json
|-- tsconfig.json

 

For a better Gulp task abstraction we’ll make use of Elixir which comes with Laravel Framework. Now we have to create a new file in the root directory called elixir-tasks.js and, for example, we could define our copy task as follows:

elixir.extend('copy', function(src, dest) {

    gulp.task('copy', function () {
      return gulp.src(src).pipe(gulp.dest(dest));
    });

    return this.queueTask("copy");
});

 

Now if we want to copy fonts from /node_modules/bootstrap/dist/fonts and /assets/fonts to /public/fonts we proceed as follows:

elixir(function(mix) {
    mix
        .copy([
            'node_modules/bootstrap/dist/fonts',
            'assets/fonts'
        ], 'public/fonts');
});

 

We could also register watches on specific directories, so when something is changed the task will be automatically run:

elixir.config.registerWatcher("copy", "assets/fonts/**/*");

 

2. Gulp task list

This is the complete list of tasks that we have to automate using Gulp:

  • compile SCSS from /app and other libraries to /public/css
  • concat JavaScript libraries into a single file /public/js/libs.js
  • minify JavaScript files from /public/js
  • minify HTML files and copy them to /public/components
  • minify JSON public files
  • minify images and copy them to /public/img
  • copy fonts to /public/fonts

 

There is also an extra task which is managed by the TypeScript library:

  • compile TypeScript files from /app to /public/js/app.js

 

Our final gulpfile.js for Angular 2 will look like this:

var elixir = require('laravel-elixir');

elixir.config.sourcemaps = false;
elixir.config.registerWatcher("scss",       "app/components/**/*.scss");
elixir.config.registerWatcher("jsonmin",    "app/**/*.json");
elixir.config.registerWatcher("uglify",     "public/js/*.js");
elixir.config.registerWatcher("copy",       "assets/fonts/**/*");
elixir.config.registerWatcher("imagemin",   "assets/img/**/*");
elixir.config.registerWatcher("htmlmin",    ["app/components/**/*.html", "app/index.html"]);

require("./elixir-tasks");

elixir(function(mix) {
    mix
        .copy([
            'node_modules/bootstrap/dist/fonts',
            'assets/fonts'
        ], 'public/fonts')

        .scss([
            'node_modules/bootstrap/dist/css/bootstrap.min.css',
            'app/components/**/*.scss'
        ], 'public/css', 'app.css')

        .concat([
            'node_modules/systemjs/dist/system-polyfills.js',
            'node_modules/angular2/es6/dev/src/testing/shims_for_IE.js',   
            'node_modules/angular2/bundles/angular2-polyfills.js',
            'node_modules/systemjs/dist/system.src.js',
            'node_modules/rxjs/bundles/Rx.js',
            'node_modules/es6-shim/es6-shim.min.js',
            'node_modules/angular2/bundles/angular2.dev.js',
            'node_modules/angular2/bundles/router.dev.js',
            'node_modules/angular2/bundles/http.dev.js',

            'node_modules/jquery/dist/jquery.min.js',
            'node_modules/bootstrap/dist/js/bootstrap.min.js',
        ], 'public/js', 'libs.js')

        .uglify('public/js/*.js', 'public/js')
        .htmlmin('app/**/*.html', 'public')
        .jsonmin('app/**/*.json', 'public')
        .imagemin('assets/img/**/*', 'public/img');
});

 

All we have to do now is to configure the package.json file in order to add some additional commands:

  • npm start will run concurrently the TypeScript on Angular 2 and Gulp commands in watch mode and it will open a browser instance for testing the application
  • npm run build will build the application for production running TypeScript command and Gulp with the production flag
"scripts": {
    "start": "concurrently \"npm run tsc:w\" \"gulp watch\" \"npm run lite\" ",
    "build": "npm run tsc && gulp --production"
}

 

3. Lightweight Web Server for Testing Application

Our application needs a way to be tested without installing heavy web servers like Apache or NGINX. For this purpose we’ll use LiteServer, a lightweight web server which comes with some useful features like BrowserSync for detecting file changes and automatically refreshing the browser.

To configure LiteServer we need to place a new file in the root directory called bs-config.json. We need to tell it to serve only files from /public folder on port 8000 and to listen for file changes with some specific extensions:

{
  "port": 8000,
  "files": ["./public/**/*.{html,htm,css,js,json}"],
  "server": { "baseDir": "./public" }
}

 

4. HTTP Request Abstraction

Angular 2 comes with a powerful built-in HTTP service, but most applications only need a basic system to send simple requests to the server such as GET, POST, PUT or DELETE. In order to achieve this we’ll build a new service called HttpService where we’ll add a new layer of abstraction over the built-in HTTP service. Our final goal is to provide an HTTP API like:

# Examples for GET, POST, PUT and DELETE
# params has type Map<string, any>
# these methods return a Promise
this.httpService.sendRequest("GET",    "/resource", params);
this.httpService.sendRequest("POST",   "/resource", params);
this.httpService.sendRequest("PUT",    "/resource", params);
this.httpService.sendRequest("DELETE", "/resource", params);

 

This is the sendRequest method’s body:

public sendRequest(method: string, path: string, params = new Map<string, any>())
{
    switch (method) {
        case "GET":
            return this.get(path, params);
			
        case "POST":
            return this.post(path, params);

        case "PUT":
            return this.put(path, params);

        case "DELETE":
            return this.delete(path, params);
    }
}

 

And this is how we have defined our GET and POST methods:

private get(path: string, params: Map<string, any>)
{
	let {url, body, options} = this.getRequestDetails(path, params);

	return new Promise((resolve, reject) => {
		this.http.get(url + "?" + body, options)
			.toPromise()
			.then(
				data => resolve(data.json().data),
				error => reject(new Set(error.json().errors))
			);
	});
}

private post(path: string, params: Map<string, any>)
{	
	let {url, body, options} = this.getRequestDetails(path, params);

	return new Promise((resolve, reject) => {
		this.http.post(url, body, options)
			.toPromise()
			.then(
				data => resolve(data.json().data),
				error => reject(new Set(error.json().errors))
			);
	});
}

 

5. Authentication Layer over HttpService

Sometimes with Angular 2 we need to place some authenticated requests to a protected resource from the server, for example to get a user’s profile. We’ll pretend the server uses a JWT based authentication, so we have to retrieve JWT Token and send it to server every time an authenticated request is placed. We want to keep things as simple as possible, so our goal is to automate this process and provide a simple interface for placing requests to protected resources:

this.httpService.sendAuthRequest("GET",    "/resource", params);
this.httpService.sendAuthRequest("POST",   "/resource", params);
this.httpService.sendAuthRequest("PUT",    "/resource", params);
this.httpService.sendAuthRequest("DELETE", "/resource", params);

 

We’ll have to extend Angular 2’s HttpService to handle JWT Tokens. One of the JWT Token properties is that it has a limited lifetime so we have to refresh it at a specific interval:

private tokenLifetimeLimit = 3600; // set token lifetime limit to 1 hour

public getAuthToken()
{
    return localStorage.getItem('authToken');
}

public setAuthToken(token: string)
{
    # save token and it's creation time to localStorage
    localStorage.setItem('authTokenCreationTime', (new Date()).getTime().toString());
    localStorage.setItem('authToken', token);
}

private getTokenLifetime()
{
    # token lifetime = now - token creation time
    let tokenCreationTime = Number.parseInt(localStorage.getItem('authTokenCreationTime'));
    let now = (new Date()).getTime();

    return Math.abs((now - tokenCreationTime) / 1000);
}

private tryTokenRefresh()
{
    let tokenLifetime = this.getTokenLifetime();

    if(tokenLifetime > this.tokenLifetimeLimit) {
        # token lifetime limit excedeed
        let params = new Map<string, any>();
        params.set('token', this.getAuthToken());
        
        # we pretend our token refresh resource is placed at /auth/refresh-token
        this.sendRequest("GET", '/auth/refresh-token', params)
            .then((data:any) => {
                if(data.token) {
                    this.setAuthToken(data.token);
                }
            })
           .catch((errors) => this.handleInvalidTokenErrors(errors));
        }
}

 

Now we’ll construct the sendAuthRequest as follows:

public sendAuthRequest(method: string, path: string, params = new Map<string, any>())
{
    # Every authenticated request need the token
    params.set('token', this.getAuthToken());

    return new Promise((resolve, reject) => {
        this.sendRequest(method, path, params)
            .then((data) => {
                # if the request succeeded check if the token has to be refreshed 
                this.tryTokenRefresh();
                resolve(data);
            })
            .catch((errors) => reject(this.handleInvalidTokenErrors(errors)))
    });
}

 

6. Models Integration

We would like to set some rules on how specific layers of abstraction have to interact with each others. Of course we could simply place some HTTP Requests in our components, but most probably we’ll have requests that solve specific problems, like user login or retrieving user’s profile. The solution for a modular code in Angular 2 would be to add logical Models to our application and place all HTTP Requests there:

Components  <–>  Models  <–>  HttpService

In our components we could simply use:

user.login()
    .then((data) => onLoginSuccess(data))
    .catch((errors) => handleLoginErrors(errors))

 

We’ll build the User Model as follows:

# Model class provides access to buildParams methods which build the params for HTTP Requests based on Model's properties
export class User extends Model
{
	public firstName: string = "";
	public lastName: string  = "";
	public email: string  	 = "";
	public password: string  = "";

	public constructor(private httpService: HttpService)
	{
		super();
	}

	public register()
	{
		let params = this.buildParams([
			'firstName',
			'lastName',
			'email',
			'password'
		]);

		return this.httpService.sendRequest("POST", "/auth/register", params);
	}

	public login()
	{
		let params = this.buildParams([
			'email',
			'password'
		]);

		return this.httpService.sendRequest("POST", "/auth/login", params);
	}

	public getProfile()
	{
		return this.httpService.sendAuthRequest("GET", "/user");
	}
}

 

7. Translation Service

Our application will need to handle different languages, so we’ll build a TranslateService for Angular 2, a simple and powerful service for translate our strings. The goal is to have the possibility to add a strings directory globally or to each component apart. We’ll place JSON files for each language that we are using:

|-- app
|    |-- components
|    |    |-- login
|    |    |    |-- strings
|    |    |    |    |-- en.json
|    |    |    |    |-- ro.json
|    |    |-- register
|    |    |    |-- strings
|    |    |    |    |-- en.json
|    |    |    |    |-- ro.json

 

Each language file will contain translation strings in JSON format for example:

{
	"action": "Login",
	"message": "Hello {{username}}",
	"email": "Email",
	"password": "Password",

	"errors": {
		"invalid_email": "Invalid Email"
	}
}

 

To use the TranslateService into a component we have to inject and configure it as follows:

import {Component} from 'angular2/core';

import {TranslateService} from '../../services/translate';

@Component({
    selector: 'login',
    templateUrl: 'components/login/login.html'
})
export class LoginComponent 
{ 
	public constructor(public ts: TranslateService)
	{
		# first params represents the path to our language files for this component
		# the second one represents a prefix that will be used to retrieve strings for this component
		ts.load('components/login/strings', 'login');
	}
}

 

The default language is en, so the service will try to load en.json. In order to change the language we could proceed as follows:

ts.setLanguage('ro');

 

As long as the TranslateService has been loaded and configured we could make use of it in our templates as in the following example:

# Simple string retrieval
{{ ts.get('login.title') }}

# Nested string retrieval
{{ ts.get('login.errors.invalid_email') }}

# Custom variables replacement
{{ ts.get('message', {username: "Mihai"}) }}

 

You can see more information about how TranslateService and our Angular 2 Starter Pack have been implemented by checking our repository at https://github.com/agvision/Angular_Seed.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>