Lesson 19-Angular Core Modules Analysis

Core Module Design Principles

Modular Architecture

Angular’s modular design is central to its architecture. The @NgModule decorator defines modules, allowing developers to organize components, directives, pipes, services, and other modules together. This modular approach enhances code readability, maintainability, and promotes reusability.

Creating a Module

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyComponent } from './my.component';

@NgModule({
  declarations: [MyComponent],
  imports: [CommonModule],
  exports: [MyComponent],
  providers: [MyService],
  bootstrap: [MyComponent]
})
export class MyModule { }

Dependency Injection

Dependency Injection (DI) is a key design principle in Angular, enabling decoupling between components and services. Angular’s DI system leverages TypeScript’s type system, allowing components to declaratively request dependencies.

Registering a Service

import { NgModule } from '@angular/core';
import { MyService } from './my.service';

@NgModule({
  providers: [MyService]
})
export class MyModule { }

Using a Service

import { Component } from '@angular/core';
import { MyService } from './my.service';

@Component({
  selector: 'app-root',
  template: `<h1>{{ message }}</h1>`
})
export class AppComponent {
  message: string;

  constructor(private myService: MyService) {
    this.message = this.myService.getMessage();
  }
}

Compilation Process

Angular’s compilation process has two modes: Just-In-Time (JIT) and Ahead-Of-Time (AOT). JIT compilation occurs at runtime, while AOT compilation happens during the build process, improving application load speed and runtime efficiency.

AOT Compilation

ng build --prod

Change Detection Mechanism

Angular uses a change detection mechanism to update the application’s view when component input properties change. Change detection supports two strategies: ChangeDetectionStrategy.Default and ChangeDetectionStrategy.OnPush.

Default Change Detection Strategy

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<h1>{{ title }}</h1>`,
  changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent {
  title = 'My App';
}

Core Module Functionality Analysis

Angular’s core modules, such as BrowserModule, FormsModule, and HttpClientModule, provide essential functionality for building applications.

BrowserModule

BrowserModule is the foundation for all Angular applications, enabling browser interactions like DOM manipulation and event handling.

FormsModule

FormsModule supports template-driven forms, simplifying form data handling and validation.

HttpClientModule

HttpClientModule enables HTTP requests for communication with backend servers.

Deep Dive into Core Modules

Each core module addresses a specific problem domain, such as form handling, HTTP requests, or routing. Designed with the single responsibility principle, they are independently understandable and testable.

BrowserAnimationsModule

Part of Angular Material, BrowserAnimationsModule provides predefined animations to enhance UI component visual feedback.

RouterModule

RouterModule offers routing and navigation, allowing you to define the application’s URL structure and page transitions.

How NgModule Works

NgModule Basics

The @NgModule decorator defines Angular modules, which are the building blocks of Angular applications. Modules combine components, directives, pipes, services, and other modules into reusable units.

Creating a Module

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

NgModule Configuration Options

The @NgModule decorator accepts a configuration object with several optional fields:

  • declarations: Declares components, directives, and pipes belonging to the module.
  • imports: Imports other modules to use their functionality in the current module.
  • exports: Exports components, directives, or pipes for use in other modules.
  • providers: Registers services and dependency injection providers.
  • bootstrap: Specifies the root component for application startup.

NgModule Internal Mechanics

When Angular compiles an application, it parses all NgModule metadata to build the dependency injection tree and component tree.

Dependency Injection Tree

Angular uses the providers field to construct the dependency injection tree. Each module can provide services, registering them in the DI system for use within the module.

Component Tree

Angular builds the component tree based on the declarations and imports fields, defining the hierarchical and dependency relationships between components.

NgModule Lifecycle

Unlike components, NgModules lack lifecycle hooks, but their creation and destruction are tied to the application’s startup and termination.

Application Startup

When Angular starts, it parses the AppModule (typically the top-level module) metadata and renders the application based on the bootstrap component.

Application Termination

When the Angular application terminates, all NgModule instances and their components are destroyed.

NgModule in Practical Applications

NgModules organize code and enable functional separation, such as dividing an application into UI, data service, or shared modules.

Feature Module

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from './user.service';

@NgModule({
  providers: [UserService]
})
export class UserModule { }

Shared Module

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MySharedComponent } from './shared.component';

@NgModule({
  declarations: [MySharedComponent],
  imports: [CommonModule],
  exports: [MySharedComponent]
})
export class SharedModule { }

Advanced NgModule Usage

Beyond basic usage, NgModules support advanced features like dynamic module loading and inter-module communication.

Dynamic Module Loading

Angular supports lazy loading, loading modules on demand during runtime to improve performance in large applications.

const routes: Routes = [
  { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) }
];

NgModule and Standalone Components

Since Angular 14, standalone components allow components to exist independently of NgModules. However, NgModules remain critical for organizing large applications and managing dependency injection.

Internal Mechanics of the Dependency Injection (DI) System

DI Basics

Dependency Injection is a design pattern where objects receive their dependencies during creation rather than creating them internally. In Angular, the DI system manages dependency creation and ensures components and services receive the objects they need.

Role of DI in Angular

Angular’s DI system provides a unified way to register, locate, and instantiate components and services. Built on TypeScript’s type system, it ensures clear and traceable dependency relationships.

DI Components

Angular’s DI system revolves around three core concepts:

  • Provider: Responsible for creating and providing dependencies.
  • Injector: Creates dependencies based on provided information.
  • Token: Identifies dependencies, typically a class or string.

DI Registration and Usage

Registering a Service

Use the providers array in an NgModule to register services.

import { NgModule } from '@angular/core';
import { MyService } from './my.service';

@NgModule({
  providers: [MyService]
})
export class AppModule { }

Using a Service

Inject services via the component’s constructor.

import { Component } from '@angular/core';
import { MyService } from './my.service';

@Component({
  selector: 'app-root',
  template: '<h1>{{ message }}</h1>'
})
export class AppComponent {
  message: string;

  constructor(private myService: MyService) {
    this.message = this.myService.getMessage();
  }
}

DI Internal Mechanics

Angular’s DI system uses a hierarchical injector tree, with each component associated with an injector. The injector tree mirrors the component tree, allowing dependencies to be shared or isolated.

Injector Tree

When Angular creates a component, it generates an injector for it. If a component is a child of another, its injector is a child of the parent’s injector.

Resolving Dependencies

When creating a component or service, Angular starts at the current injector and traverses up the injector tree until it finds a provider for the dependency.

DI Lifecycle

A service’s lifecycle depends on its registration in the NgModule. Angular supports three main lifecycle strategies:

  • Singleton: A single service instance for the entire application.
  • Per-Component: A new service instance for each component.
  • Per-Child: A new service instance for each child component.

Advanced DI Features

Scoping

Customize service creation and scoping using useFactory and multi options.

import { NgModule } from '@angular/core';
import { MyService } from './my.service';

@NgModule({
  providers: [
    {
      provide: MyService,
      useFactory: () => new MyService(),
      multi: true // Allows multiple instances
    }
  ]
})
export class AppModule { }

Dependency Injection Tokens

Use any type (class, string, or symbol) as a token to identify services.

import { InjectionToken } from '@angular/core';

export const MY_TOKEN = new InjectionToken<string>('My Token');

@NgModule({
  providers: [
    {
      provide: MY_TOKEN,
      useValue: 'Hello, World!'
    }
  ]
})
export class AppModule { }

Conditional Injection

Use useClass, useValue, or useFactory for conditional service provision.

import { NgModule } from '@angular/core';
import { MyServiceA } from './my.service.a';
import { MyServiceB } from './my.service.b';

@NgModule({
  providers: [
    {
      provide: MyService,
      useClass: window.isProduction ? MyServiceA : MyServiceB
    }
  ]
})
export class AppModule { }

Compiler and Change Detection Mechanisms

Angular Compiler

The Angular compiler transforms TypeScript and template code into JavaScript and DOM structures executable by browsers. The compilation process has two main modes: JIT and AOT.

JIT (Just-In-Time) Compilation
JIT compilation occurs at runtime, converting components and templates into JavaScript. It allows dynamic compilation but increases startup time due to runtime compilation.

AOT (Ahead-Of-Time) Compilation
AOT compilation happens during the build process, converting TypeScript and templates into optimized JavaScript. This reduces startup time and improves runtime performance by eliminating browser compilation.

Change Detection Mechanism

Angular’s change detection mechanism monitors application state changes and updates the view accordingly, enabling reactive programming.

Change Detection Strategies
Angular uses “dirty checking” to detect component state changes. After events or asynchronous operations, Angular traverses the component tree to check for input property changes, updating the view if changes are detected.

Optimizing Change Detection
Angular offers two change detection strategies:

  • Default: Angular checks the entire component tree after each event.
  • OnPush: Angular only checks when a component’s input properties or asynchronous events change, reducing unnecessary checks.

Code Analysis

Let’s analyze Angular’s compilation and change detection through code.

Compilation Process

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1>`
})
export class AppComponent {
  title = 'My App';
}

When building with ng build --prod, Angular CLI performs AOT compilation, converting the code into optimized JavaScript.

Change Detection

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<h1>{{title}}</h1>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  title = 'My App';

  updateTitle() {
    this.title = 'Updated Title';
  }
}

Using the OnPush strategy, Angular only updates the view when input properties or asynchronous events change.

Optimization Strategies

To enhance performance, consider:

  • Using OnPush Strategy: Apply OnPush for components that don’t update views based on internal state changes.
  • Reducing Unnecessary Computations: Avoid expensive calculations in components, especially during change detection.
  • Using Pure Pipes: Pure pipes recompute only when input values change, minimizing unnecessary view updates.
Share your love