This page looks best with JavaScript enabled

Angular Best Practices For Building Single Page Application

 ·  ☕ 10 min read  ·  ✍️ Adesh

This article outlines the practices we use in our application. We’ll also go through some general coding guidelines to help make the application cleaner.

1. Modular approach in Angular App

Angular provides us to create our app in modular fashion. As your application grows, it becomes cumbersome to manage the different parts of an application. Modules in angular are a great way to share and reuse code across your application.

Let’s start with default module of angular app i.e AppModule.

a) App Module: Angular Default Module

App module is the root module of any angular app. Every application has at least one Angular module, the root module that you bootstrap to launch the application.

b) Shared Module

Shared module can have components, directives and pipes that will be shared across multiple modules and components, but not the entire app necessarily.

c) Feature Module

A feature module is an angular ordinary module for specific feature. The main aim for feature modules is delimiting the functionality that focuses on particular internal business inside a dedicated module, in order to achieve modularity. In addition, it restricts the responsibilities of the root module and assists to keep it thin. Another advantage - it enables to define multiple directives with an identical selector, which means avoiding from directive conflicts.

2. Use of lazy loading a feature module

Angular best feature of feature module is its lazy loading. Lazy loading of any feature module is achieved by angular routing. In other words, a feature module won’t be loaded initially, but when you decide to initiate it. Therefore, making an initial load of the Angular app faster too! It’s a nifty feature.

Here is an example on how to initiate a lazy loaded feature module via app-routing.module.ts file.

1
2
3
4
5
6
7
const routes: Routes = [
  {
    path: 'user',
    loadChildren: 'app/user/user.module#UserModule',
    component: UserComponent
  }
];

3. Shortening the import path

Sometimes, we import paths are very long like this:

This is not the ideal situation to use this nested path. You can shorten this import path by doing this setting in tsconfig.json file. Set the @app and @env variables in this file.

1
2
3
4
5
6
7
8
9
{
  "compileOnSave": false,
  "compilerOptions": {
    "paths": {
      "@app/*": ["app/*"],
      "@env/*": ["environments/*"]
    }
  }
}

Once you have set up these path, you can now use short import path like this:

You can check this link as well for more information.

Shorten TypeScript Imports in an Angular Project

4. Typings

One of the cool feature of Typescript is combining the multiple data types to make it easier to work. Look at the below example for better understanding.

1
2
3
4
5
interface Emp {
  name: string;
  age: number;
  createdDate: string | Date;
}

In the above code, we can see, our createdDate field is combination of both string and Date type. Normally, we have to deal date format either in string format or date format as per our requirement. So, this will handle this very efficiently.

Also, you can restrict the value of any field in advance, so that a field can have only these values, like this:

1
2
3
interface Request {
  status: 'pending' | 'approved' | 'rejected';
}

Now, in the above code, a status field can have any of these three values. TS complier will throw an error if any other value is assigned to it. In another way, you can achieve this by using enumeration as well.

5. Component Inheritance

One of the less discussed feature of Angular is component inheritance.

With the help of component inheritance, you can encapsulate your common code in.a base component and extend it to your child components.

Most of the time, for sharing functionality or data we use services/providers across the different components. What if, instead of common data functionality, you want common UI functionality? For example, consider a simple scenario where you want to navigate from one component to another using buttons. A simple way to implement this is to create a button, call a method in the code which then uses the Angular router to navigate to the page. And what if you didn’t want to repeat the same code in each component? Typescript and Angular gives you a pretty easy way to handle this encapsulation; welcome to the world of inherited components.

Component Inheritance

Photo: Medium

I have found a very informative post on component inheritance. Click the below link for more information.

PART 1 — The Case For Component Inheritance In Angular

6. Use Namespace to import multiple interfaces

You can organize all of your interfaces into the TypeScript namespace and then import the single namespace in your working class.

This is how we are using our interfaces without namespace.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// api.model.ts
export interface Customer {
    id: number;
    name: string;
}

export interface User {
    id: number;
    isActive: boolean;
}
1
2
3
4
5
6
// using the interfaces
import { Customer, User } from './api.model'; // this line will grow longer if there's more interfaces used

export class AppComponent {
    cust: Customer; 
}

By using namespace, we can eliminate the needs to import interfaces files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// api.model.ts
namespace ApiModel {
    export interface Customer {
        id: number;
        name: string;
    }

    export interface User {
        id: number;
        isActive: boolean;
    }
}
1
2
3
4
// using the interfaces
export class AppComponent {
    cust: ApiModel.Customer; 
}

7. Delegate complex expression from template to component class file

In order to make our template simple and understandable, we try not to write any complex logic expression in our component template file. If we are checking just expression like simple compare or evaluate true/false value, then it is fine to write in our template file. If our expression is longer and complex, then it is better to move this from template to component class file.

Let’s take an example of this. For example, you want to add a ‘has-error’ class to all form controls which are not properly filled in (not all validations have been successful). You can do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component({
  selector: 'app-component',
  template: `
        <div [formGroup]="form"
        [ngClass]="{
        'has-error': (form.controls['firstName'].invalid && (submitted || form.controls['firstName'].touched))
        }">
        <input type="text" formControlName="firstName"/>
        </div>
    `
})
class AppComponent {
  form: FormGroup;
  submitted: boolean = false;

  constructor(private formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required]
    });
  }
}

If you notice line 6, we have written a very long and complex expression for 'has-error' class in our template file, which looks ugly as well as hard to debug. We can make it simple by removing it from here. All this logic will be moved to the component class file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component({
  selector: 'app-component',
  template: `
        <div [formGroup]="form" [ngClass]="{'has-error': hasFieldError('firstName')}">
            <input type="text" formControlName="firstName"/>
        </div>
    `
})
class AppComponent {
  form: FormGroup;
  submitted: boolean = false;

  constructor(private formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      firstName: ['', Validators.required],
      lastName: ['', Validators.required]
    });
  }
  
  hasFieldError(fieldName: string): boolean {
    return this.form.controls[fieldName].invalid && (this.submitted || this.form.controls[fieldName].touched);
  }
}

Now we have just a nice piece of template, and can even easily test whether our validations work correctly with unit tests, without diving into the view.

8. Global Error Handling

To catch synchronous errors in our code, we can use try-catch block. If error is thrown in our try block, we can catch it in catch block. But this is not an ideal situation to write try-catch everywhere. We need global error handling.

Angular provides a hook for centralized exception handling.

The default implementation of ErrorHandler prints error messages to the console. To intercept error handling, write a custom exception handler that replaces this default as appropriate for your app.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyErrorHandler implements ErrorHandler {
  handleError(error) {
    // do something with the exception
  }
}

@NgModule({
  providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]
})
class MyModule {}

9. Use HttpInterceptor to handle all http errors

Intercepts HttpRequest or HttpResponse and handles them.

It provides a way to intercept HTTP requests and responses to transform or handle them before passing them along.

There are two use cases that we can implement in the interceptor.

First, we can retry the HTTP call once or multiple times before we throw the error. In some cases, for example, if we get a timeout, we can continue without throwing the exception.

We can then check the status of the exception and see if it is a 401 unauthorized error. With token-based security, we can try to refresh the token. If this does not work, we can redirect the user to the login page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Injectable } from '@angular/core';
import { 
  HttpEvent, HttpRequest, HttpHandler, 
  HttpInterceptor, HttpErrorResponse 
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';

@Injectable()
export class ServerErrorInterceptor implements HttpInterceptor {

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return next.handle(request).pipe(
      retry(1),
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // refresh token
        } else {
          return throwError(error);
        }
      })
    );
  }
}

We also need to provide the interceptor we created.

1
2
3
4
providers: [
  { provide: ErrorHandler, useClass: GlobalErrorHandler },
  { provide: HTTP_INTERCEPTORS, useClass: ServerErrorInterceptor, multi: true }
]

10. Perform multiple Http requests using ForkJoin

There are some use cases, when we need to perform multiple http requests at once, wait until all requests are completed and then proceed next just like synchronous http calls. So, Angular provides this feature with the help of ForkJoin.

ForkJoin waits for each HTTP request to complete and group’s all the observables returned by each HTTP call into a single observable array and finally return that observable array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { forkJoin } from 'rxjs';  // RxJS 6 syntax

@Injectable()
export class DataService {

  constructor(private http: HttpClient) { }

  public requestDataFromMultipleSources(): Observable<any[]> {
    let response1 = this.http.get(requestUrl1);
    let response2 = this.http.get(requestUrl2);
    let response3 = this.http.get(requestUrl3);
    // Observable.forkJoin (RxJS 5) changes to just forkJoin() in RxJS 6
    return forkJoin([response1, response2, response3]);
  }
}

The above example shows making three HTTP calls, but in a similar way, you can request as many HTTP calls as required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit } from '@angular/core';
import { DataService } from "../data.service";

@Component({
    selector: 'app-page',
    templateUrl: './page.component.html',
    styleUrls: ['./page.component.css']
})
export class DemoComponent implements OnInit {
    public responseData1: any;
    public responseData2: any;
    public responseData3: any;

    constructor(private dataService: DataService) {}

    ngOnInit() {
        this.dataService.requestDataFromMultipleSources().subscribe(responseList => {
            this.responseData1 = responseList[0];
            this.responseData2 = responseList[1];
            this.responseData3 = responseList[1];
        });
    }
}

As shown in the above code snippet, at the component level you subscribe to single observable array and save the responses separately.

Summary

In this blog, we have learned about best practices to create any angular app. These are really cool practices which helps you to write efficient and cool code.

Further Reading

How To Listen Changes In Reactive Form Controls Using valueChanges In Angular

Stop Using ElementRef! For DOM Manipulation In Angular

Angular Material Table With Paging, Sorting And Filtering

Share on

Adesh
WRITTEN BY
Adesh
Technical Architect