Angular Pro Tips

22.01.2021Tom Trapp
Mobile Angular TypeScript

What tips and tricks are there to make a modern Angular project more maintainable and robust?

If you are new to Angular, take a look at our Angular b-nova ToDo List Tutorial.

Strict mode

Wouldn’t it be nice if potential errors related to null or undefined variables could be detected early on? When you are almost ‘forced’ to keep your code clean and robust? This is exactly what Angular and TypeScript allow with the so-called Strict Mode.

For new projects, the strict mode can be activated with the parameter --strict at ng new.

For existing projects, the strict mode is activated with some configs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// tsconfig.json
{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "angularCompilerOptions": {
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}
1
2
3
4
5
6
// angular.json, inside projects.[projectName]
"schematics": {
    "@schematics/angular:application": {
        "strict": true
    }
}

For example, if Strict-Mode is activated, the compiler would output an error for the following code because the string content is not initialized.

1
2
3
4
5
6
7
export class ToDo {
    content:string;
    completed:boolean = false;
}

// (property) ToDo.content: string
// Property 'content' has no initializer and is not definitely assigned in the constructor.ts(2564)

We get to know another part of the strict mode in the next point.

Use Bundle Budgets

As with many other projects, Angular is also about performance and the size of the application. In Angular, certain parts of the application are divided into so-called Bundles, for example all imported dependencies are in the vendor bundle.

With the so-called Bundle Budgets functionality, the size of these bundles can be monitored, you receive warnings or even build errors if certain threshold values (above or below) are violated.

The bundle budgets are activated with the strict mode and defined in the angular.json file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "configurations": {
    "production": {
      "budgets": [
        {
          "type": "bundle",
          "name": "vendor",
          "baseline": "500kb",
          "warning": "100kb",
          "error": "200kb"
        }
      ]
    }
  }
}

It is also important to mention that budgets only really make sense with a productive build, since all optimizations (Minifying, Tree Shaking etc.) are applied there. In the example above, a warning would appear if the bundle vendor becomes smaller than 400kb or larger than 600 kb. The build would also fail if the bundle is smaller than 300kb or larger than 700kb.

But when will a bundle be enormously enlarged? This can happen, for example, with incorrect import statements (e.g. with auto import) or the like.

Aliases for Import Statements

Every Angular developer knows the problem with the relative paths in Import statements. These usually look like this:

1
2
3
import { ToDo } from "../../../interfaces/ToDo";
import { HelloWorld } from "../../../services/HelloWorld";
import { Fruit } from "../../../models/Fruit";

A simple trick allows us to use aliases for certain folders in import statements, these aliases have to be specified in the tsconfig.json file:

1
2
3
4
5
6
7
8
9
// tsconfig.json
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@interfaces/*": ["src/app/interfaces/*"],
      "@services/*": ["src/app/services/*"],
      "@models/*": ["src/app/models/*"]
    }
  }

It should be noted here that the imports are always relative to the baseUrl (in this case the root level of the project).

If these aliases are defined, we can use them in the import statements, much cleaner and just cool!

1
2
3
import { ToDo } from "@interfaces/ToDo";
import { HelloWorld } from "@services/HelloWorld";
import { Fruit } from "@models/Fruit";

Use Services

Another tip is to outsource logic from the component to decentralized, reusable services. This generally increases maintainability and the overview in the code. But there are little things to consider.

Services can be created very easily using Angular CLI Command:

1
ng generate service service/MyService

A service is defined with the Injectable annotation:

1
2
3
4
5
6
7
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MyService {
}

Another big advantage of a Injectable service is that it is tree-shakeable and is therefore left out of the bundle if it is not used.

In general, one should keep in mind that all services function as a singleton by default and thus the instance of the service is always shared between the components.

If you want an instance of the service per component (non-singleton) you can do this via the Providers definition in the component.

1
2
3
4
5
6
7
8
@Component({
    selector: 'app-mycomponent',
    templateUrl: './app.mycomponent.html',
    styleUrls: ['./app.mycomponent.css'],
    providers: [MyService]
})

export class MyComponent {}

The instance of MyService can then be used in this component and its parent and child components.

In general, however, a service should be reusable and independent - i.e. a singleton.

Use pipes

So-called Pipes enable the transformation of texts, numbers, dates, currencies etc. into Template-Expressions. For example, a date can be correctly formatted for output.

Angular already offers numerous pipes out-of-the-box, such as the UpperCasePipe. For example, if you want to always display the variable name in capital letters, you can do this directly in the HTML in the template expression:

1
<p>{{ name | uppercase }}</p>

When calling macher Pipes, parameters such as a date format can also be passed:

1
<p>My birthday is {{ birthday | date:"dd/MM/yy" }} </p>

Of course, pipes can also be lined up:

1
<p>My birthday is {{ birthday | date:"dd/MM/yy" | uppercase }} </p>

It becomes more interesting when you implement pipes yourself in order to achieve certain transformations. As an example, we want to create a pipe that writes “super cool” in front of the value.

Fortunately, the Angular CLI also offers a practical command here:

1
ng generate pipe pipes/SuperCool

This creates a new pipe and automatically references it in app.modules.ts.

Our pipe is ultimately a TypeScript class, with a Pipe annotation, which implements the interface PipeTransform and has a transform method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'superCool'
})
export class SuperCoolPipe implements PipeTransform {

  transform(value: string): string {
    return "super cool " + value;
  }
}

In the annotation Pipe there is a variable name, with this value the pipe in the Template-Expression is called.

1
2
3
<p>{{ toDo.content | superCool }}</p>

<!-- super cool Hello World -->

It is important to know that a pipe is by default pure and its values are therefore cached. This also means that the pipe is only executed again in the event of a real (pure) change to the value. This increases the performance and you should always use the pipe when formatting or similar.

Now you know a few tips and tricks for your Angular project! If you need support in this area, please contact us!


This text was automatically translated with our golang markdown translator.

Tom Trapp

Tom Trapp – Problemlöser, Innovator, Sportler. Am liebsten feilt Tom den ganzen Tag an der moderner Software und legt viel Wert auf objektiv sauberen, leanen Code.