1. What is Angular

Angular is a modern web framework used for building Web, Server-Side-Rendered, Cross-Platform & Progressive Web applications. Not to confuse it with its older brother, Angular.js, Angular is developed by Google in 2015, it’s used in startups and large-scale enterprises like Sony, Upwork, Google, and others.

Angular is a first-class citizen of TypeScript, ReactiveX, and as of recently Signals. It’s a fully-fledged framework with built-in CLI, Modules, Components, Services, Directives, Pipes, Router, Forms, HTTP Client, Testing framework, and more.

2. Angular CLI

Angular CLI is a built-in tool that speeds up app development by allowing you to generate parts and wire up dependencies automatically.
To start, install Angular on your machine using:

$ npm i -g @angular/cli

Every Angular command starts with these magic letters: ng .
To generate your first Angular app, open up a terminal and type:

$ ng new <APP-NAME>

Besides generating projects, the CLI can also be used to generate:

  • Modules ng g module <PATH> or shorthand ng g m <PATH>
  • Components ng g component <PATH> or ng g c <PATH>
  • Interfaces ng g interface <PATH> or ng g i <PATH>
  • Services ng g service <PATH> or ng g s <PATH>
  • Interceptors ng g interceptor <PATH>
  • Directives ng g directive <PATH> or ng g d <PATH>
  • Pipes ng g pipe <PATH> or ng g p <PATH>,
  • etc.

Upon generating a component (for instance), the CLI will create all required files (template, styles, typescript, and test file) and also wire up the component to its nearest module.

Another powerful command is ng add .

$ ng add @angular/material

The command ng add finds a compatible version of the package your package, installs it, and sets it up in your project.
This can be used for a number of tools in the ecosystem, such as Angular Universal, NGRX, Angular PWA, etc.

If you’re not sure what a command does, use the Dry-Run command that will print to the console what the command you wrote does.

$ ng g ... -dry-run

3. Out of the Box Solution

Upon generating your project Angular gives you everything you need for development, testing, and deployment.

For example, in package.json there are all scripts needed to run, build or test your code using the libraries that Angular already wired up upon creating a project.

  "scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},

There is also an environment.ts file for environment variables, a .gitignore to ignore things like node_modules, and TypeScript with getters, setters, interfaces, classes, and decorators, all configured for development and testing.

To build your app use:

$ ng build

To run the app in development mode use:

$ ng serve // starts the app on port:4200

To create a production build use:

$ ng build --configuration production

To update your Angular application and its packages to the newer Angular version use:

$ ng update

4. Angular JSON

The angular.json file is the main project configuration file that is created upon generating a project.

Preview of angular.json file

Let’s go over what’s inside this file.

  • sourceRoot — The main src directory where your project files live (components/services)
  • prefix — You can set a custom prefix for your component names in the template, e.g.: <abc-home-component>. The default prefix is app.
  • outputPath — This is where Angular will store the distribution (JavaScript) files upon building the project.
  • index — Location of the index.html file.
  • main — Main Angular app starter file that bootstraps the application (by loading the AppModule).
  • tsConfig — TypeScript configuration file.
  • assets — Directory where you store assets such as fonts, images, etc.
  • styles — Location of the main styles.css / styles.scss file.
  • scripts — The place where you set script links for other libraries, like Bootstrap.

You can also set optimizations for the development and production environment.

As well as configuration for the test project.

Furthermore, you can have multiple projects with custom configurations within one project and use angular.json to configure each.

Angular.json with multiple projects

You can run a project using

$ ng serve <project-name>

and set up a starter project (as you’d with Visual Studio in C#) that runs when you type ng serve.

5. Modules

In Angular, everything starts with a module. It’s a key configuration file that groups together components, directives, services, and other modules.

The core class in the Angular project is the app.component class that is part of the app.module. The module is an Angular class with NgModules decorator that contains metadata.

@NgModule({
declarations: [ // components created within the module
AppComponent
],
imports: [ // imports from other files that will be used in components
CommonModule,
OtherModule
],
providers: [ // services and interceptors used within the components
TodosService
],
exports: [ // allowes parts to be used in other modules
AppComponent
]
})
export class AppModule { } // name of the module

Inside modules, we also configure key dependencies for our project like:

  • HttpClientModule to use HttpCient in the app
  • BrowserModule & ServerModule — to setup applications for Web or Server-Side
  • Common Module — Subset of BrowserModule that contains Angular’s built-in directives and pipes
  • Routing Module — That contains all routes used in the application

If you forget the import any of the mandatory modules Angular will immediately complain:

NullInjectorError: No provider for "TOKEN"
// or
Cannot use *ngIf since it's not known element of "Element"

The modules are usually split into groups called features. Feature modules contain all parts that are related to a certain feature of the application. Components can have their own modules too.

You can generate a component inside a particular module using --module flag:

$ ng generate component Hello --module MyCustomModule

6. Components

Components are reusable building blocks that contain business parts and create UIs.

Each component consists of four files:

  • Template (HTML)
  • Styles (CSS/SCSS)
  • Component (TypeScript)
  • Spec (TypeScript Test file)
@Component({
selector: 'app-todos', // component selector <app-todos></app-todos>
templateUrl: './todos.component.html', // path to HTML file
styleUrls: ['./todos.component.scss'] // path to Styles file
})
export class TodosComponent implements OnInit {

constructor() { }

ngOnInit() {
console.log('Hello World!')
}

}

That said we can also have an HTML template and CSS styles within the component.

@Component({
selector: 'app-todos',
template: `<div class="title">Component content</div>`,
styles: `.title { color: red }`
})
export class TodosComponent implements OnInit { }

Every Component is tied to a Module (unless it’s standalone). Components can inject Services. Components can make use of Pipes and Directives in the template.

Two main types of components:

  • Smart
  • Pure (Presentational)

They do not look apart, but it’s a good practice to separate business parts in the Smart components and features related to the UI in the Pure ones.

7. Template Bindings & Interpolation

Now let’s talk about how to display variables from components in the template.

String Interpolation

To display the contents of any property, simply write the property name in the template and wrap it with double curly braces.

<h1>{{ title }}</h1>

This is a String Interpolation, that transforms a component text property into a string.
This is what it looks like inside the component.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'Hello World';
}

Web Browser

This works for strings, numbers, and booleans.
However, some values, like JavaScript objects, may not be able to be transformed.

title = [{ year: '2023' }];

In that case, you can make use of Pipes, which we’ll learn more about a bit later.

Property Bindings

To start off, this is how we assign a value to a regular HTML attribute. It’s the same for Angular as well as any static site.

<div id="justString">Title</div>

If you want to say that justString is a dynamic value, e.g. variable, then you declare a property in the component:

export class AppComponent {
justString = 'Some ID'
}

And then wrap square brackets around id property in HTML and assign it to justString .

<div [id]="justString">Title</div>

Now the HTML id attribute will have the value we assigned to justString property in the component ( Some ID ).
In short, it works like this, [HTML attribute]=”component property” .

This can be further expanded as anything between double quotes can also be an expression, like a ternary operator.
In this example, I’m assigning an ‘active’ or ‘default’ class to the HTML element, based on isClicked property declared in the template.

<div [class]="isClicked ? 'active' : 'default'">Title</div>

Notice that I put single quotes within double, indicating that ‘active’ and ‘default’ are plain strings, while isClicked is TypeScript property.

Property Bindings can also be assigned to functions.

<div [class]="callingComponentMethod()">Title</div>

But it’s not a good practice to use functions in the template as they’ll be constantly called, causing unnecessary rerenders and performance hits.
Using Directives or Pipes is a much better choice.

Event Binding

Besides binding plain HTML attributes, you can also bind events, such as button clicks, input, and similar.

First, create a button in HTML.

<button>Click me</button>

Now add a click event to it and assign it to the function you created in the component.

<button (click)="onButtonClick()">Click me</button>

Component.ts

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {

onButtonClick() {
console.log('clicked!')
}
}

You can also pass the event$ object into the function.

<input type="text" (input)="getTextInput($event)"/>

This event is triggered by user input, so you can use the event object to extract the text value.

  getTextInput(event: any) {
console.log(event.target.value); // text on input element
}

Two-way binding

The-Way data binding is a feature of Angular that simplifies the process of capturing user input and maintaining consistent data across the component and the view.

It can read the value to the input as you type and automatically display it in UI without needing to configure the state changes manually.

The prerequisite here is that you need to import FormsModule in the module in which you’ll use this feature.

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule // Mandatory step
],
bootstrap: [AppComponent]
})
export class AppModule { }

Component.ts

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
data: string;
}

Template.html

<input type="text" [(ngModel)]="data">
<p>{{data}}</p>

That’s all it takes. We pick up the value from the input using [(ngModel)] directive and pass it to a component property data. Then we use String Interpolation to read the contents of the data property. Changes will be visible immediately as we start typing.

Demo

Two-way data binding in action

This technique is also called Banana in the Box.
The name came from the wrapper for this directive. Two parentheses () represent a pair of bananas, while the square brackets represent a box []. Banana in the Box [()].

8. Sharing Data between Components

In Angular components can share data in a number of ways, from parent to child, child to parent, shared service, router, etc.

Input

This is used when you pass data directly from parent to child.

Parent component

<app-child greeting="Hello Angular"></app-child>

Child component

@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent {

@Input()
greeting = 'Hello World!'; // default value
}

If the parent component does not pass any values, then the greeting will be ‘Hello World’. If the parent passes a value like ‘Hello Angular’, then the value of the greeting property will be the latter.

As of Angular v16, inputs can have a configuration object within to transform values or be declared mandatory.

@Input({ required: true }) greeting: string;

Output with Event Emitter

This is used when you want to pass a property created in a child component to the parent. To start you create an event emitter with the output decorators.

  @Output()
messageFromChild = new EventEmitter<string>();

You call this emitter whenever you want to send data to the parent, like after a button click:

@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent {

@Output()
messageFromChild = new EventEmitter<string>();

handleClick() {
this.messageFromChild.emit('Geetings from Child component');
}
}

Then in the parent component template, you set this emitter (messageFromChild) on your child component and use it to invoke a function in the parent component and pass the $event parameter.

<app-child (messageFromChild)="handleMessage($event)"></app-child>

Finally, in the parent component, you declare a function and make use of the data passed from the child component.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {

handleMessage(message: string) {
console.log('message from child :>> ', message);
}
}

Input and Output decorators are mainly used to help differentiate properties coming in and going out from other class properties. Input and Output can also be defined in component meta-data.

View Child with AfterViewInit Hook

Used when the parent component wants to call a method created with a child component.

Shared Service

Shared service serves as a singleton used for passing data between sibling components. You can pass any property back and forth or in an immutable state using Rx.js Subjects.

Activated Route

Used when routing a specific page. Data is pulled from the route using the Activated Route service.

9. Directives

Directives are small classes that add new or modify the existing behavior of the elements in the template.

Three types of Directives:

  • Component
  • Structural (ngIf, ngFor, ngSwitch )
  • Attribute (ngClass, ngStyle, ngModel )

The prerequisite for using built-in directives is to import a CommonModule in your component’s module like I showed with FormsModule.

The most common use cases are to validate if something will be visible on the screen using *ngIf :

<div *ngIf="temperatureInCelsisus > 30">
It's Hot day!
</div>
<div *ngIf="temperatureInCelsisus < 0">
It's Cold day!
</div>

Iterate over an array using *ngFor

<ul>
<li *ngFor="let todo of todos">{{ todo.title }}</li>
</ul>

Or style components:

<div [ngClass]="active ? 'highlighted' : 'default'">Angular</div>

Other types of directives include:

  • Ng-Content
    This directive is used when you want to pass HTML content or a component as a child from parent to child component (similar to props.children in React).
  • Ng-Container
    The container is used as a content wrapper similar to the <div> tag, with one exception — it is not rendered in the browser.
  • Ng-Template
    Like Ng-Container, Ng-Template wraps the content, but the content is not visible by default. It is only visible if some template condition becomes truthy.
  • Host & Host-Context
    These are CSS directives that are used to change the CSS of related components

10. Custom Directives

We can build custom directives too. Here we have a dummy directive that takes text from user input and replaces letters with numbers.

@Directive({
selector: '[appLeetWrite]'
})
export class LeetWriteDirective {

constructor(public ef: ElementRef) { }

// listens to input event on the target HTML input element
// and calls inputListener() function
@HostListener('input', ['$event.target.value'])
inputListener(value: string): void {
const val = this.toReplace(value);
this.ef.nativeElement.value = val;
}

private toReplace(text: string) {
return text
.replace(/a/gi, '4') // A => 4
.replace(/e/gi, '3') // E => 3
.replace(/i/gi, '1') // I => 1
.replace(/o/gi, '0') // O => 0
.replace(/t/gi, '7') // T => 7
}

}

The HostListener() function takes two parameters, a JavaScript event (input, click, hover, etc.) and a target element.
Now all we have to do is put this directive on an HTML input element and will work like magic.

<input type="text" appLeetWrite />

Web Browser

11. Pipes

Pipes are functions used in a template that take in data as an input and transform it into an output.

Angular comes with a list of built-in pipes to format text, JSON, dates, numbers, percentages, currency, slice arrays, etc. We can also pass parameters to pipes.

Component.ts

export class AppComponent {
title = 'Angular';
todaysDate = new Date()
data = {
"blog": "Reactive programming with Rx.js",
"release": "Summer 2023"
};
}

Template.html

<p>{{ title | uppercase }}</p>
<p>{{ todaysDate | date : 'dd-M-YY' }}</p> <!-- pipe name, followed by format -->
<p>{{ data | json }}</p>

Web Browser

Two types of Pipes:

  • Pure
  • Impure

The difference between the two is how they trigger changes in the component. The default variant is a pure pipe.
The impure pipe rerenders on any state detection of the property consumed by the pipe, which is good for tracking changes and bad for performance.

Angular also allows us to create our own pipes. Every Pipe must have a transform() function.
This custom-made pipe can transform letters into numbers.

@Pipe({
name: 'leetSpeak'
})
export class LeetSpeakPipe implements PipeTransform {

transform(value: string): string {

const changedValue = this.toReplace(value);
return changedValue;
}

private toReplace(text: string) {
return text
.replace(/a/gi, '4') // A => 4
.replace(/e/gi, '3') // E => 3
.replace(/i/gi, '1') // I => 1
.replace(/o/gi, '0') // O => 0
.replace(/t/gi, '7') // T => 7
}

}

And it’s triggered just like a built-in one.

<p>{{ title | leetSpeak }}
<!-- Angular => 4ngul4r -->

12. Services

Services are Typescript classes marked with the injectable decorator. Their purpose is to handle business logic parts. Services are most commonly used to connect components to APIs and share data between components.

In order to work with HTTP Services in Angular you need to import HttpClientModule in the providers array of your component/root module.

Example of a Service in Angular

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

constructor(private readonly httpClient: HttpClient) {}

getAllTodos(): Observable<ITodo[]> {
return this.httpClient.get<ITodo[]>('localhost:3000/api/todos');
}

createTodo(todoData: ITodo): Observable<ITodo> {
const headers = new HttpHeaders()
headers.set('content-type', 'application/json')
return this.httpClient.post<ITodo>('/api/...', todoData, { headers });
}
}

The service is then injected via a constructor in the component where we want to use it.

constructor(private readonly todosService: TodosService) {}

ngOnInit(): void {
this.todosService
.getAllTodos()
.subscribe((data: ITodo[]) => console.log(data));
}

13. Dependency Injection

One of the most powerful features of Angular is a Dependency Injection.

Dependency injection is a technique in which a class receives its dependencies from external sources rather than creating itself.

What this means is rather than creating our own dependencies like new instances:

// creating instances manually
service = new TodosService();

We do that behind the scenes inside Providers (usually inside component modules).
This has a number of benefits:

  • The component using the dependencies does not care where the dependencies are coming from,
  • If the dependency we’re instantiating also depends on something, we need to provide an instance of that as well:
    const service = new HTTPService(new HTTPClient(new SomethingElse()))
  • It’s easier to test because when we’re using the new keyword we are creating instances of actual services. However, with DI, the dependency is configured inside a Module, meaning we can swap the original service with the test service without breaking the component.

Injecting Dependencies

There are two ways to inject dependencies in Angular:

  • Constructor (in class-based files)
  • Inject (in functions and class-based files)

The default approach is to use a constructor. A syntax works as follows:

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
constructor(private readonly todosService: TodosService) {}
}

Then we can make use of the injected service throughout the file.

export class AppComponent {

constructor(private readonly todosService: TodosService) {}

doStuff() {
this.todosService.doSomething();
}
}

The second way is to use a newly introduced inject() function. This can benefit you in guards, resolvers, and any other component.

export class AppComponent {

private readonly todosService: TodosService = inject(TodosService)

doStuff() {
this.todosService.doSomething();
}
}

Providers

Every injected dependency needs to be inside a provider.
Components can have their own providers:

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
providers: [TodosService]
})
export class AppComponent {

constructor(private readonly todosService: TodosService) {}
}

But dependencies can also be configured in the module. Once provided in the module, the dependency can be used in any component/directive/pipe within the module.

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
],
providers: [TodosService, OtherService],
bootstrap: [AppComponent]
})

Alternatively, you can use the providedIn property in the Angular service.
If your service has a providedIn flag within the injectable decorator, then you do not need to worry about providers and injectors.

Simply put

@Injectable({
providedIn: 'root' // variations: root, any, platform
})
export class TodosService { }

Tells that service is automatically provided in the root module (AppModule), meaning it does not need to be manually set in the providers array in any module or component/directive. But it still needs to be injected into the constructor.

So rather than saying what are component providers, you say where the service can be provided.

This is just the beginning of DI in Angular. If you’d like to learn more, I advise you to look up these topics:

  • Injectors
  • Injection Tokens
  • Overriding dependencies (useClass, useValue, useFactory)
  • Components relationship (@self , @skipSelf , @host , @optional)

14. Async Pipe

The Async Pipe is a special built-in pipe that is used to subscribe to an observable or resolve a promise directly in the template.

Component.ts

this.promiseTitle = Promise.resolve('Hello Promise!');
this.observableTitle$ = of('Hello Observable!');

Template.html

<p>{{ promiseTitle | async }}</p>
<p>{{ observableTitle$ | async }}</p>

Web Browser

This means that you do not need to subscribe inside the component to pull raw data out of an API (that you want to display on the UI). You do that in the template right away.
Async Pipe is also capable of unsubscribing automatically.

It’s commonly used to conditionally display UI elements that are hidden behind an API call or iterate over an array of items.

Looking back at our service you can make use of the Async pipe to subscribe directly in the template.

Component.ts

constructor(private readonly todosService: TodosService) {}

todos$!: Observable<ITodo[]>

ngOnInit(): void {
this.todos$ = this.todosService.getAllTodos()
}

Template.html

<ul>
<li *ngFor="let todo of todos$ | async">{{ todo.title }}</li>
</ul>

15. Interceptors

Angular comes with a powerful and easy-to-use HTTP Interceptor that intercepts and transforms API calls.

@Injectable()
export class ApiInterceptor implements HttpInterceptor {

constructor() {}

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// when setting a prefix, all API calls will have it appended
const baseURL = 'localhost:3000/api/';
const backendAPI = request.clone({
url: `${baseURL}/${request.url}`
})
return next.handle(backendAPI);
}
}

HTTP interceptor is triggered automatically upon sending any request or receiving a response.

It’s commonly used to:

  • prepend the correct base URL for APIs
  • perform logging
  • send headers to the backend (authorization)
  • cache response, etc.

16. Template-Driven Forms

There are two types of forms in Angular:

  • Template Driven forms
  • Model Driven (Reactive) forms

With Template Driven forms the setup and validations are done entirely within the template.
To start, import FormsModule in the module where you’re making a form.

@NgModule({
declarations: [
TodoTemplateFormComponent
],
imports: [
CommonModule,
FormsModule // Mandatory step
],
exports: [
TodoTemplateFormComponent
]
})
export class TodoFormModule { }

The key parts:

  • Create a template variable and assign it to ngForm
    #myForm=”ngForm”
  • Each input field has a ngModel directive tag
  • You can read form values directly from the template variable
    #myForm.value

Template.html

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
<div>
<label>
Email:
<input ngModel name="email" placeholder="Email" />
</label>
</div>
<div>
<label>
Message:
<input ngModel name="message" placeholder="message" />
</label>
</div>
<div></div>
<button type="submit">Send</button>
</form>

Component.ts

@Component({
selector: 'app-todo-template-form',
templateUrl: './todo-template-form.component.html',
styleUrls: ['./todo-template-form.component.scss']
})
export class TodoTemplateFormComponent {

onSubmit(formValues: object) {
console.log('formValues :>> ', formValues);
}

}

To add validations use directives or HTML attributes.
Firstly create template variables for each input and assign it to ngModel, #variable="ngModel" . Then use #variable to check value and validity.

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
<div>
<label>
Email:
<input ngModel #email="ngModel" required name="email" placeholder="Email" />
<div *ngIf="email.invalid && email.touched" style="color: red;">Email is required!</div>
</label>
</div>
<div>
<label>
Message:
<input ngModel #message="ngModel" maxlength="300" name="message" placeholder="message" />
</label>
</div>
<div></div>
<button type="submit">Send</button>
</form>

It’s also worth noting that with Angular forms you do not need to use event.preventDefault upon submitting to prevent default form behavior. Angular already takes care of that for you.

17. Reactive Forms

Angular Reactive Forms is a feature provided by the Angular framework that allows you to create and manage complex forms in a reactive and flexible manner.

You start by importing ReactiveFormsModule in your component module:

@NgModule({
declarations: [
TodoFormComponent
],
imports: [
CommonModule,
ReactiveFormsModule // Mandatory step
],
exports: [
TodoFormComponent
]
})
export class TodoFormModule { }

Then you define the model (shape) of your form fields.

myForm: FormGroup;

constructor(private fb: FormBuilder) { }

ngOnInit(): void {
this.myForm = this.fb.group({
email: [''],
message: ['']
});
}

Each property represents a form control (a form field in the HTML).

You can then add validations to these fields and default values.

this.myForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
message: ['Default message', [Validators.maxLength(300)]],
});

Reactive forms also support Async validators (for validations against API data), as well as custom validators (for validating phone numbers for example). Form fields can also depend on each other, like for instance, the ‘password’ and ‘repeat password’ fields that need to have the same value.

Furthermore, you can check the form value and validity based on user input, if a user has touched the form or entered any value.
The same applies to controls.

console.log('form value :>> ', this.myForm.value); // { email: '' }
console.log('form is valid :>> ', this.myForm.valid); // false
console.log('form is touched :>> ', this.myForm.touched); // true
console.log('form is dirty :>> ', this.myForm.dirty); // false

You can extract data from each field individually, either as plain text or as Observable, that you can later apply Rx.js operators to.

get getEmailControl(): AbstractControl<any, any> | null {
return this.myForm.get('email'); // extract control by name
}

// string
const plainText = this.getEmailControl?.getRawValue();

// Observable<string>
this.getEmailControl
?.valueChanges
.pipe(
debounceTime(333), // 333 ms
distinctUntilChanged()
)
.subscribe((value: string) => {
console.log('value :>> ', value);
});

And if the form controls provided by Angular are not enough, you can also create custom controls for your components (HTML elements). To achieve this your custom component must implement a ControlValueAccessor interface.

Photo by Josh Sorenson from Pexels. Edit my own

18. Angular Router

The Angular Router is a built-in module in Angular that provides navigation and routing functionality for Angular applications. It allows you to define routes and navigate between different pages (components).

Everything starts with AppRoutingModule which is configured with the AppModule.

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule, // this is where you set routing configuration
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Inside you create a routes array and define routes for your components.

const routes = [
{
path: '',
component: HomeComponent
},
{
path: 'about',
component: AboutComponent
},
{
path: 'profile',
component: ProfileComponent
}
]

Angular contains a variety of routing options, such as:

  • Static routes
  • Dynamic parameter routes
  • Child routes
  • Auxillary routes
  • Redirects
  • 404-page
  • Guards
  • Resolvers
  • Eager and Lazy-loaded routes

Once you set up routes you put it into the Router configuration of your RoutingModule.

@NgModule({
imports: [RouterModule.forRoot(routes)], // routes array
exports: [RouterModule]
})
export class AppRoutingModule { } // module injected in the AppModule

Since each component module can have its own set of routes, you can define multiple routing modules.

const routes: Routes = [
{
path: '',
component: UnitTestingPageComponent
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class UnitTestingRoutingModule { }

Notice that this time you use RouterModule.forChild(routes) as opposed to RouterModule.forRoot(routes) as was set in the AppRoutingModule.

The Router for Root is used to configure the router at the root level of the application and you can only have one root router. The Router for Child is used to configure additional child routes within a feature module or a component, You can have multiple child routing modules.

Routing between components

In order to show content on different pages you need to place a
<router-outlet></router-outlet> directive in your root component (app.component)

<div>Navbar</div>

<!-- Content that will be present on all screens -->

<router-outlet></router-outlet>

<!-- Dynamic Content that will change from page to page -->

To navigate between routes use the routerLink directive.

<a [routerLink]="['/profile']">
Link to profile page
</a>

<!-- Router link with dynamic property -->

<a [routerLink]="routerLinkVariable">
Link to profile page
</a>

The routerLink directive can also be used with query parameters and to transfer the state between routes.

<a 
[routerLink]="['/user/bob']"
[queryParams]="{debug: true}"
[state]="{tracingId: 123}
queryParamsHandling="merge">
link to user component
</a>

Learn more on routerLink.

You can also manually navigate pages within the component using router.navigate([‘/page’]).

export class AppComponent implements OnInit {

constructor(private readonly router: Router) {}

onButtonClick() {
this.router.navigate(['/profile'])
}
{

19. Lazy and Eager Loading

With Eager loading, all the necessary modules and components are loaded when the application is initialized. Lazy loading is a technique where modules and components are loaded and rendered on-demand when the user navigates to a specific route.

In this example, a profile is a lazy loaded route and its modules are activated only after you land on the profile route, via the loadChildren function.

const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'profile',
// lazy loaded route
loadChildren: () => import('./profile.module').then(m => m.ProfileModule),
},
{
path: 'about',
component: AboutComponent
},

Other routes like home and about will be eagerly loaded as soon as this module is launched.

Using lazy-loaded routes directly impacts the performance of your application. You should try to only load the modules/components you need.

With Angular v16 and Standalone APIs, lazy-loaded routes received an overhaul.

const routes: Routes = [
{
path: 'profile',
loadChildren: () => import('./profile/profile.routes')
}
];

Since Standalone components do not require modules it means that you can now lazy load components as well.

const routes: Routes = [
{
path: 'profile',
loadComponent: () => import('./profile/profile.component')
.then(m => m.ProfileComponent)
}
];

20. Route Guards & Resolvers

A route guard is a service that controls the accessibility of a route based on the conditions provided. Guards are classes (functions, as of v16) that contain logic that checks whether a user has access to content or not.

There are five types of guards in Angular:

  • Can Activate
    The CanActivate guard blocks access to routes for users who are not authorized to access. This is a useful feature to block off routes that require login.
  • Can Deactivate
    This type of guard is used to prevent users from exiting the page. An example of this can be a form that throws a warning pop-up for a user who is attempting to leave with unsaved changes.
  • Can Load
    CanLoad guard prevents child modules to be loaded until criteria guarding is satisfied.
  • Can Activate Child
    Similar to the ones above, this guard prevents access to child routes.
  • Route Resolver
    This type of guard does not protect anything but rather allows you to complete a task before landing on a route. That can be an action dispatch, subscription to a service, or passing data to a component. Resolver sits in front of the route and is executed before the component on that route loads.

To use one, go to your routing module and place a guard on the route you wish to protect. To get started I’ll generate a new guard and right off the bat, Angular CLI will ask what type of guard I want to create.

$ ng g guard guards/authorizer
Which interfaces would you like to implement?
>(*) CanActivate
( ) CanActivateChild
( ) CanDeactivate
( ) CanLoad

Choose the first option

Which interfaces would you like to implement? CanActivate
CREATE src/app/guards/authorizer.guard.spec.ts (361 bytes)
CREATE src/app/guards/authorizer.guard.ts (463 bytes)

Inside the guard, we need to implement a CanActivate interface and set the desired behavior.

@Injectable({
providedIn: 'root',
})
export class AuthorizerGuard implements CanActivate {
constructor(
private readonly service: UsersService,
private readonly router: Router
) { }

canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> {
if (this.service.isLoggedIn()) {
return Promise.resolve(true);
}
return this.router.navigate(['/home'])
}
}

In this example, if a user is logged in, let him visit the route. Otherwise, redirect to the home page.

In Angular v16 the guards changed from being classes to functions and we can make use of inject() function to inject dependencies without a constructor.

export const authorizerGuard = (): CanActivateFn => {
const service = inject(UsersService)
const router = inject(Router)

if (service.isLoggedIn()) {
return Promise.resolve(true)
}

return router.navigate(['/home'])
}

To apply a guard to a route you wish to protect, add canActivate property on your route and assign it to a guard.

{
path: 'users',
component: UsersComponent,
canActivate: [AuthorizerGuard]
}

Here is an overview of what a routing module could look like:

{
path: 'users/:id',
component: UsersComponent,
canActivate: [AuthorizerGuard, OtherGuard],
canActivateChild: [ChildRoutesGuard],
resolve: [UsersResolver]
children: [
{path: '', redirectTo: 'hobbies'},
{path: 'hobbies', component: HobbiesComponent},
{path: 'friends', component: FriendsComponent},
],
{
path: 'not-found',
component: NotFoundComponent,
},
{
path: '**', // if route is not defined
redirectTo: 'not-found'
}
}

21. Reactive Framework (Rx.js)

Angular & Rx.js are a perfect match. Being a Reactive Framework, Angular makes use of ReactiveX Observable streams out of the box with:

  • Services
  • Guards
  • Forms
  • NGRX
  • Any custom solutions, etc.

The real magic happens when you attach a pipe() function on Observable. From that point, you can use any of the 100+ Rx.js operators to transform, filter, schedule, repeat, combine,… the data however you like.

Looking once again at the service, notice that it returns an Observable<T>, meaning that you can attach Rx.js operators right to it.

  todos$?: Observable<string[]>;

constructor(private readonly todosService: TodosService) {}

ngOnInit(): void {
this.todosService.getAllTodos().pipe(
filter((x: ITodo[]) => !!x),
map((todos: ITodo[]) => todos.map((t) => t.title))
);
}

The important thing to remember of working with Observables is that Rx.js does not know when the Angular component is no longer been used. To resolve this Angular team introduced the Async pipe that is used in the template and automatically completes the Observable when the component is destroyed.

Should you need to subscribe manually in the component, you can make use of subjects and the ngOnDestroy lifecycle hook to unsubscribe from the Observable when the component is destroyed.

  todos$?: Observable<string[]>;
subject$ = new Subject(); // create a subject

constructor(private readonly todosService: TodosService) {}

ngOnInit(): void {
this.todosService.getAllTodos().pipe(
...
takeUntil(this.subject$) // apply it within a pipe
);
}

// triggered when component is destroyed
ngOnDestroy(): void {
this.subject.next();
this.subject.complete();
// here we're forcing subject to complete
// this action will terminate the observable above
};

In version 16, the Angular team introduced a new mechanism to achieve this using the destroyRef provider.

import { Component, DestroyRef, inject, OnInit } from '@angular/core';
// replacement for Rx.js takeUntil
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {

// injecting properties without constructor
private readonly todosService: TodosService = inject(TodosService);
private readonly unsubscribe: DestroyRef = inject(DestroyRef);

ngOnInit(): void {
this.todosService.getAllTodos()
.pipe(
takeUntilDestroyed(this.unsubscribe) // set it in the pipe
)
.subscribe();
}
}

The new provider does more of the same thing, just with fewer lines of code.

22. Element Reference & Document Interface

Sometimes you want to pick up the value from the element in the template that has nothing to do with forms or inputs or button events.
One way to do this is to use Element References. First, set a reference # to the desired element.

<input type="text" #myInput />

Now pick up this element by declaring the property within the ViewChild decorator and assign it to ElementRef.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit, AfterViewInit {

// myInput => #myInput
@ViewChild('myInput') inputElementRef: ElementRef<HTMLInputElement>
}

Finally, create the ngAfterViewInit hook in which you have access to the element you’ve selected. It also works in any other method as long as it fires after the view initializes.

  ngAfterViewInit() {
console.log(this.inputElementRef); // <input>
console.log(this.inputElementRef.nativeElement); // target element
console.log(this.inputElementRef.nativeElement.value); // input value
}

Using nativeElement you can also change the HTML or CSS on the fly using Renderer2 from angular/core.

constructor(private renderer: Renderer2) {}
// set HTML attribute
this.renderer.setAttribute(
this.inputElementRef.nativeElement, 'id', 'component-id'
);

// set custom class
this.renderer.addClass(
this.inputElementRef.nativeElement, 'active'
);

// set custom styles
this.renderer.setStyle(
this.inputElementRef.nativeElement, 'background', '#310594'
);

If you’re a fan of good old Document interface from vanilla JavaScript, Angular has you covered. By injecting the Document you can make use of the Document object to interact with elements in the template.

Start with importing the DOCUMENT injection token from angular/common.

import { DOCUMENT } from '@angular/common';

Then inject it in the constructor.

constructor(@Inject(DOCUMENT) private doc: Document) {}

Now you have access to the JavaScript Document object where you can use the good old document.getElementById('...') method, as well as a query selector querySelector(‘#element-id’).

23. Lifecycle Hooks

Every component in Angular goes through a lifecycle. The Lifecycle Hooks are various methods that are invoked at different phases of the lifecycle of a component.

  • OnChanges — Called when the component receives props
  • OnInit — Called when the component initializes
  • DoCheck — Called on every Change Detection
  • OnDestroy — Called before the component is destroyed
  • AfterContentInit — Called when the component’s content is initialized
  • AfterContentChecked — Called when the component’s content is updated or checked for updates
  • AfterViewInit — Called when the component’s projected view has been initialized
  • AfterViewChecked — Called after the projected view has been checked
  • DoBootstrap — Called upon bootstrapping an application in App Module

Example of a component that uses OnInit and OnDestroy hooks.

@Component({
selector: "app-component",
template: `<div>AppComponent</div>`,
styles: ``
})
class AppComponent implements OnInit, OnDestroy {

ngOnInit() {
// ...
}

ngOnDestroy() {
// ...
}
}

Furthermore, you can create a custom lifecycle hook as explained here.

24. NgZone (Zone.js)

NgZone​ is an Angular wrapper for the Zone.js library that is in charge of tracking asynchronous operations, such as API calls, timers, and mouse and keyboard events.

By default, Angular runs change detection after every asynchronous task completes. This ensures that the application’s UI stays in sync with the underlying data.

NgZone is directly connected to the Change Detection mechanism. Every time new API data arrives, NgZone causes components to rerender.

For the most part, we do not need to care how all of it works under the hood. Should you need to, you can use NgZone to interact with other browser libraries (such as Jquery) that do not have anything to do with Angular.

Using NgZone you can also execute code outside your Angular application.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {

constructor(private ngZone: NgZone) {
this.ngZone.runOutsideAngular(() => {
// whatever happens in here won't trigger Angular change detection
});
}

}

If you call API outside Angular it won’t trigger the Change Detection mechanism nor rerender the component.
You can track changes in Angular applications using the ngDoCheck hook.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements DoCheck {

constructor(private ngZone: NgZone) {
this.ngZone.runOutsideAngular(() => {
// ...
});
}

// this is called on every change detection in the component
ngDoCheck() {
// ...
}

25. Change Detection Variations

Angular lets you customize how your component rerenders. By default, when something changes in one component, Angular traverses the entire components tree to check if any of the components have changed. This is super inefficient on larger projects with many components.

To fix this, Angular also has OnPush change detection that focuses solely on changes within the component at hand. With OnPush, components are completely isolated, meaning changes that happen in one component will not cause the whole component tree to rerender, resulting in faster applications.

But there is a catch. Component no longer rendered on any change, but rather on specific ones:

  • When new Input (props) is received by component
  • When a new event is triggered in the component
  • When an Observable you’re component is subscribed to receives a new value

To switch the change detection strategy, set changeDetection property in the Component meta to ChangeDetectionStrategy.OnPush.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush // that's it
})
export class AppComponent implements OnInit { }

Keep in mind that choosing the detection strategy will impact the behavior and the performance of your applications.

Photo by StackOverflow

Expression Changed After It Has Been Checked Error

This is a common error that occurs in Angular apps that is directly connected to the change detection mechanism.

It’s caused when the property is changing unexpectedly (in the template) during the lifecycle of the component. This error is only present in the development and servers as a warning mechanism indicating unexpected rerenders.

When something changes in the Angular component, Angular will trigger the change detection process. The framework checks the components tree for changes twice and remembers the previous state, and compares it to the new one. If there is a mismatch between the two, it will throw this error.

Common causes:

  • The property is been updated after the change detection process has finished
  • The property is repeatedly updated in the template
  • The child component is updating a property in the parent after change detection has already ran in the parent component

How to avoid:

  • Use pipes/directives as opposed to functions in the template
  • Change properties from parent to child using a proper lifecycle hook
  • Use ChangeDetctionRef.markForCheck() to manually inform change detection of any updates in the template
  • Use shared service/state to share the data between components
  • Update properties within timeouts (if there is no other way)

26. SEO Optimizations

Although primarily a frontend framework Angular comes with a list of interesting features for SEO optimizations features like:

Another SEO trick is the isPlatformBrowser function from the angular/common package that is used to verify if code is executing on the server or on the client.

import { Inject, InjectionToken, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

private isBrowser: boolean;
constructor(@Inject(PLATFORM_ID) platformId: InjectionToken<Object>) {
this.isBrowser = isPlatformBrowser(platformId);
}
}

For more complex solutions there is the Angular Universal framework that improves server-side-rendering for Angular by using Express.js as the backbone for server-side work.
As of recently, there is also an Angular Meta framework called Analog.

Component Hydration

New with Angular 16 is the Non-Destructive Hydration for components. Angular can reuse existing DOM structures on the client that were rendered by the server without having to destroy and re-render all of them, as well as cache requests made on the server to avoid re-fetching that same data again on the client.

To add component hydration set up the newly added client hydration provider in the app.module.

import {provideClientHydration} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

@NgModule({
declarations: [AppComponent],
exports: [AppComponent],
bootstrap: [AppComponent],
providers: [provideClientHydration()], // and that's it
})
export class AppModule {}

To skip hydration over components or certain parts of the template you can use the ngSkipHydration directive. Learn more.

27. Angular Animations

Another out-of-the-box feature in Angular is Angular Animations. Angular animations are built on top of CSS animations that allow you to create and manage animations for various UI elements in components.

To hit the ground running, you need to import the BrowserAnimationsModule to the module where you want to use the animations.

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule // mandatory step
],
bootstrap: [AppComponent]
})
export class AppModule { }

Then in your component, you can configure the animations using the animations property in the component meta.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [] // set Animations
})
export class AppComponent implements OnInit { }

Then you create the animation trigger. A trigger represents a named animation state and associated transitions. It defines when an animation should be triggered and specifies the transitions to be applied.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [
trigger('toggleColor', [
// logic goes here
])
]
})

To change the element in the template we put the trigger on it.

<button [@toggleColor]="animationState">Toggle Color!</button>

Now move to the state. Animation has two states, what was before and what will happen next.

 state('state-name', LOGIC),

In this case, I’m creating a button that toggles the state from light to dark variant. With Animations, you can use all your favorite CSS properties to manage UI.

  animations: [
trigger('toggleColor', [
state('dark', style({
background: 'black',
color: '#FFF',
opacity: 0.75
})),
state('light', style({
background: 'white',
color: '#000',
opacity: 1
}))
])
]

Here I defined two states:

  • When it’s in a dark state, the UI element will have a black background, white text, and reduced opacity.
  • When in the light state, the UI element will have a white background, black text, and full opacity.

Inside the component, you write the toggler logic that switches from a light to a dark state.

  animationState: string; // dark or light
isBlack: boolean;

changeAnimationState() {
this.isBlack = !this.isBlack;
this.animationState = this.isBlack ? 'dark' : 'light'
}

In the template, you call the changeAnimationState() method to trigger a state change.

<button (click)="changeAnimationState()" 
[@toggleColor]="animationState">Toggle Color!
</button>
Animation preview

To improve the animation you can add transitions between states. A transition specifies the timing, duration, easing, and style changes involved in the animation.

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
animations: [
trigger('toggleColor', [
state('dark', style({
background: 'black',
color: '#FFF',
opacity: 0.75
})),
state('light', style({
background: 'white',
color: '#000',
opacity: 1
})),
// transitions from one state to another
transition('dark => light', animate('1000ms ease-in')),
transition('light => dark', animate('1000ms linear'))
])
]
})
Animation with transitions

This is just a small sneak peek into the world of Angular Animations. If you’re into it, I suggest the official Angular Animations documentation.

28. Multiple Compilation Modes

Angular has two different compilation modes:

  • Just-In-Time (JIT)
  • Ahead-Of-Time (AOT)

During development, Angular compiles the templates, components, and other resources of the application in the browser’s memory when the application is loaded. JIT compilation provides a faster development workflow because changes made to the code are immediately reflected without the need for a separate build step.

AOT compilation is ais used for production builds. With AOT, the Angular application is compiled before it is deployed to the browser. This eliminates the need for the browser to perform the compilation step at runtime, resulting in faster application startup time and improved performance.

The two work hand-in-hand with Angular’s new engine — Ivy.
Ivy is the next-generation rendering engine in Angular that enhances performance, smaller bundle sizes, improves debugging, and better template type checking.

Whether using JIT or AOT mode, leveraging Ivy brings significant benefits to Angular applications.

29. Testing Frameworks

Angular out of the box brings built-in test runners to the table:

  • Jasmine & Karma for Unit testing
  • Protractor for End to End testing

Jasmine brings many testing utilities to the table including mocking, spies, fakes, debugger, testing observables, and asynchronous code without the need to include SinonJS or similar libraries. It’s all there.

On the E2E side, Protractor is a Selenium-based testing tool but is a bit outdated compared to modern frameworks. That’s why the Angular team shifted focus from Protractor to Cypress.

As of version 16, Jest became a supported test runner in Angular.

To begin your testing journey type:

$ ng test

30. Standalone Components

Angular v15 introduced Standalone Components as an easy way to build module-less applications. This means that components marked as standalone can be wired up without needing to use NgModule.

To create a standalone component use:

$ ng new hero-app --standalone

Upon generating a project file open an app.component.ts.

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'hero-app';
}

Right off the bat, you’ll notice a few new things:

  • standalone: true — This tells Angular that this component is standalone, meaning it does not require a module.
  • imports: [...] — Now you can directly import other modules (such as Common, Router, FormsModule, etc.) in your component.

To create another standalone component use:

$ ng g c blog --standalone

-- output:
CREATE src/app/blog/blog.component.html (19 bytes)
CREATE src/app/blog/blog.component.spec.ts (540 bytes)
CREATE src/app/blog/blog.component.ts (290 bytes)
CREATE src/app/blog/blog.component.scss (0 bytes)

Notice that upon generating a component we did not update any module whatsoever. To import this component into your AppComponent simply add it to the imports array:

import { BlogComponent } from './blog/blog.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, BlogComponent],
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})

And then in the template as well.

<app-blog></app-blog>

Going forward you can get rid of the constructor and use the inject() function, lazy-load components as I showed above, write templates and styles directly in the component (as it was possible before), or do any of the other Angular things.

Where is the App.Module ?

Looking at the project structure you might find it odd that App.Module file is no longer there.

Angular v16 project preview

It became obsolete since components can now import modules and components. However, you still need to bootstrap an application somehow and that’s been done in the main.ts file.

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

The main.ts file directly imports the root component (AppComponent), followed by any project configuration set in app.config.ts.

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes) ]
};

And that’s how it all works.

31. Rich Ecosystem

Although Angular delivers a plateau of tools from the get-go, should feel the need to expand, there is a rich ecosystem of tools to help you build:

32. Security

Like any web app, Angular apps are targeted by hackers.
That’s why the Angular team built tools to help us defend against these threats.

XSS Protection

A common way to attack websites is by injecting HTML script tags. If you attempt that in Angular, it will translate HTML tags into plain text.
Angular is smart enough not to let you do that. However, if you need to there is an inner HTML directive that allows you to do that.

<div [innerHtml]="HTML Content"></div>

DomSantizier

DomSanitizer helps to prevent Cross Site Scripting Security bugs (XSS) by sanitizing values to be safe to use in the different DOM contexts. Should you feel the need to bypass the checks, you can use this service to unblock the code you consider safe.

HTTP Client

Angular HTTP Client is also built with security in mind. It can automatically detect and remove Cross-site script inclusion from the server response.

CSRF protection

Angular comes and automatically setups up an XSRF strategy to protect against CSRF attacks. Should you need to customize, you can also set up your strategy and token.

Angular Guards

Angular Guards can be used in a way to block unauthorized users from accessing routes they’re not allowed to be on.

Keep Angular up to date

The best way to protect yourself is to read Angular documentation, and best practices and frequently update the framework to the latest LTS version.

33. Share Components between Frameworks

Angular Elements is a feature of the Angular framework that allows you to package Angular components as web components that can be used across different frameworks (React, Vue) and even without any framework.

Start by installing the package:

$ npm install @angular/elements

Then update the app.module. I’ve created a simple component called GreetingComponent. Now all I have to do is tell Angular to bootstrap it instead of AppComponent.

import { createCustomElement } from '@angular/elements';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
],
bootstrap: [GreetingComponent], // instead of AppComponent
})
export class AppModule implements DoBootstrap {

constructor(private readonly injector: Injector) { }

ngDoBootstrap(): void {
const customComponent = createCustomElement(GreetingComponent, { injector: this.injector });
customElements.define('name-your-component', customComponent);
}

}

And in the ngDoBootstrap method, we create a custom element using the previously imported GreetingComponent and give this web component a name.

$ ng build --configuration production --output-hashing none

After running the production build, look up the dist directory. Inside there should be your custom component element.
Learn more on Angular Elements.

Photo generated by Midjourney AI

34. Signals

One of the biggest new features of Angular v16 is Signals. Angular Signals is a new way to handle reactivity (alongside Rx.js) by granularly tracking how and where your state is used throughout an application, allowing the framework to optimize rendering updates.

What this means in English is that Signals do not rely on Zone.js to track changes in the components.
Meaning that when changes occur in Signals, Angular does not need to traverse the whole component tree to see what changed (Zone.js way), but rather it looks at the signal within the component — making rerendering Angular components much faster than before.

Three new primitives:

  • writeable signals
  • computed signals
  • effects

Writable Signals

These types of signals are used to set values that can change over time.

Component.ts

  export class AppComponent {
title = signal<string>('Angular Signals')
year = signal<number>(2023)
features = signal<string[]>([
'Components',
'Directives',
'Services',
'Pipes'
]);
}

Template.html

To tell Angular that the property is a signal we need to call it like a function.

<p>{{ title() }}</p>
<p>{{ year() }}</p>
<p>{{ features() | json }}</p>

Web browser

In addition to this, you can use the set, update and mutate functions to change the value of the signal.

export class AppComponent {
title = signal<string>('Angular Signals')
year = signal<number>(2023)
features = signal<string[]>([
'Components',
'Directives',
'Services',
'Pipes'
]);

onButtonClick() {
// changes year from 2023 to 1444
this.year.set(1444);

// appends new item to the features array
this.features.update(
existingFeatures => [...existingFeatures, 'Signals']
)
}
}

Computed Signals

These types of signals are used when you want to create a signal based on the value of another signal.

isOddYear = computed(() => this.year() % 2 !== 0 ) // true

Keep in mind that property the isOddYear is not a writable signal. This means that you cannot use methods such as set, update, or mutate to change its value.
The value of isOddYear can only be changed when the writable signal (year()) changes.

Effects

Signals are useful because they can notify interested consumers when the signals. An effect is an operation that runs at least once and whenever one or more signal values change.

export class AppComponent {
year = signal<number>(2023)

// this effect is called twice
// once for year 2023
// then for 1444
myEffect = effect(() => {
console.log(`The current count is: ${this.year()}`);
});

onButtonClick() {
this.year.set(1444);
}
}

In this example, the console.log() function is called every time the signal changes. This is similar to the useEffect() hook in React.

It’s important to note that in v16, Signals are still in the developer preview.

Learn more on Signals in Angular.

35. Consistency

Angular is complex, but Angular is also Consistent. Once you grasp the basic concepts and rules like, how to:

  • Create layouts and bind events (components, directives, pipes, services)
  • Wire up dependencies (Dependency Injection)
  • Create forms (Template Driven or Reactive)
  • Add routing
  • Manage asynchronous data (Rx.js), etc.

You can easily migrate from one Angular project to another, as other projects are using the same features built into Angular, with one or few minor additions like Angular Universal, Angular-PWA, NGRX, Signals, etc, in comparison to some other frameworks where you have to reinvent the wheel when a new addition goes live.

Photo generated by Midjourney AI

Wrapping up

I’ve used Angular for years on big and small projects and although it has a tough learning curve, the features it brings to the table are simply unmatched. That’s not to say that the framework is without its faults, but the improvements made in each new version make it a better experience.

If you want to see Angular more content, be sure to hit a follow button and support me with a Cup of Coffee.

Buy me a coffee cup

Next in line is a blog on Rx.js and then the new chapter on Express.js.
Stay tuned for that.

Bye for now 👋

--

--

Mirza Leka

Web Developer. DevOps Enthusiast. I share my experience with the rest of the world. Follow me on https://twitter.com/mirzaleka for news & updates #FreePalestine