Angular: Mastering the Framework
Introduction
Welcome
Angular is not just a framework; it's a powerful tool that can transform the way you develop web applications. In this course, we will explore every corner of Angular, from the basics to advanced techniques that will make you a front-end master.
Who am I?
I am Alan, known as Gentleman, Google Developer Expert in Angular and Microsoft MVP. I have over 15 years of software development experience and I'm here to share what I know. This course is designed to be clear, comprehensive, and most importantly, practical.
What will we cover?
- Why Angular: What makes it unique and when to choose it?
- TypeScript in Angular: How to use it to write solid and maintainable code.
- Angular Elements: Components, directives, and services.
- Control Flow and Syntax: Understanding Angular's key structures.
- Angular 19 Updates: Discover the latest improvements.
- Services and Architectures: Design scalable and modular applications.
- Forms in Angular: Work with reactive and validated forms.
- Interceptors: Handle HTTP communication like a pro.
- Testing: With Jest, Playwright, and Testing Library.
Why Angular
The Journey of Angular
Angular has a fascinating history that sets it apart from other frameworks. It started as AngularJS in 2010, implementing the MVC (Model-View-Controller) pattern and popularizing SPAs (Single Page Applications). However, this initial approach had limitations in large projects, such as performance and code maintainability.
Frameworks like Backbone laid the groundwork for SPAs, but AngularJS took the concept further with directives that extended the DOM and a clear data flow between Model, View, and Controller. Later, the MVC model was challenged by MVVM (Model-View-ViewModel), and frameworks like React changed the game with simpler and more efficient approaches.
Angular and React combine features of MVC and MVVM, leveraging the best of both paradigms. AngularJS evolved to Angular 2 in 2016, with a component-based approach and dropping compatibility with AngularJS.
Directives and Components
Directives in AngularJS were essential for extending DOM functionality. In modern Angular, they evolved into reusable and powerful components, marking a significant change in how web applications are developed.
Change Detection: From Zone.js to Signals
Angular initially introduced Zone.js to manage change detection, monitoring events throughout the DOM. While effective, this approach caused performance issues in large applications due to unnecessary event bubbling.
The ChangeDetectionStrategy.OnPush strategy improved this process by limiting change detection to explicit inputs, asynchronous streams, and user events. This reduced performance impact but still relied on Zone.js.
The introduction of Signals removed this dependency. Signals allow managing changes directly and efficiently, affecting only the components that need to be updated. Inspired by modern frameworks like QwikJS, Signals represent a step forward towards optimized performance.
Reactive Programming and Signals
Reactive programming allows handling data flows asynchronously and declaratively. In Angular, this is implemented with RxJS, which introduces concepts like:
- Observables: Unidirectional channels where a single emitter sends events.
- Subject: Bidirectional channels where multiple emitters and receivers interact.
- BehaviorSubject: Similar to a Subject but retains the last emitted event.
Signals complement this paradigm by providing a simpler and more efficient alternative. They act as a channel where changes only impact the components that directly depend on them.
Why Did Angular Change?
The shift from AngularJS to Angular 2 and later versions was not just technical; it responded to community criticisms and challenges:
- High Learning Curve: AngularJS had an innovative approach, but its initial complexity was overwhelming.
- Inconsistent Documentation: Developers often faced incomplete or confusing documentation.
- Community Fragmentation: The transition from AngularJS to Angular 2 split the community, with many developers migrating to React.
As I mentioned in my interview to become a GDE: "Learning Angular is horrible, the documentation is a mess, and the learning curve is too high." Despite this, Angular remains a solid choice due to its comprehensive ecosystem and focus on scalability.
Angular in Real Life vs. Angular at Work
In work environments, Angular often deals with the legacy of older versions that depend on complex modules and configurations. Before Angular 16, each feature required an associated module, complicating the architecture.
With the introduction of Standalone Components, Angular removed this dependency, allowing components to be autonomous and easier to manage.
Angular and the Modern Ecosystem
- Partial Hydration: Angular optimizes initial load by sending static HTML to the client and hydrating only the necessary parts. This improves speed and reduces resource consumption.
- Advanced Reactivity:
Signals
offer more efficient control over change detection, replacing Zone.js. - Frequent Updates: Angular follows a six-month release cycle, ensuring compatibility with the latest practices.
- Enterprise Support: Angular is ideal for large projects due to its modularity and robustness.
RxJS: The Backbone of Reactivity
RxJS is fundamental in Angular for handling data flows and asynchronous events. By facilitating communication between components and state management, RxJS becomes an indispensable tool in scalable projects.
Angular Universal and SSR
Previously a separate tool, Angular Universal is now integrated into the core of the framework. This functionality allows implementing SSR (Server-Side Rendering) and SSG (Static Site Generation), improving SEO and initial load times.
TypeScript and How to Use It in Angular
Introduction to TypeScript
TypeScript is the core of modern development in Angular. This JavaScript superset, created by Microsoft, introduces static typing and decorators, promoting scalable and predictable development. Let's analyze how TypeScript powers Angular and how learning its fundamentals allows us to leverage it fully.
TypeScript Fundamentals
Static Typing and Classes
TypeScript provides tools like interfaces and classes to define clear structures and avoid common errors. Consider the following example:
interface User {
name: string;
getName: () => string;
setName: (name: string) => void;
}
class UserClass {
private name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
setName(name: string) {
this.name = name;
}
}
const user: User = {
name: 'El_Blaki',
getName: () => 'Pepe',
setName: (name: string) => {},
};
const userClass: UserClass = new UserClass('Pepe');
Here we define a User
interface to ensure that any object implementing it
meets its contract, while the UserClass
class organizes data handling and
logic.
Combining Types
Often, we need to combine types to represent more complex entities. This is
achieved with type
:
interface Student {
id: string;
}
type UserType = User & Student;
This allows building reusable and precise models for our applications.
Decorators in TypeScript and Angular
Decorators are a key feature of TypeScript, and Angular uses them extensively. Let's break down their main uses.
Class Decorator
A class decorator can modify or extend functionalities:
function gentlemanApproves<T extends { new (...args: any[]): {} }>(
constructor: T,
): T {
return class extends constructor {
gentleman = 'Yes';
};
}
@gentlemanApproves
class MyClass {}
const instance = new MyClass();
console.log((instance as any).gentleman); // "Yes"
This decorator adds the gentleman
property to any class that uses it.
Component Decorator (Angular Inspiration)
Angular uses decorators like @Component
to define components:
function Component(config: { selector: string; template: string }) {
return function (target: any) {
target.prototype.selector = config.selector;
target.prototype.template = config.template;
};
}
@Component({
selector: 'app-component',
template: '<h1>{{ title }}</h1>',
})
class MyComponent {
title: string = 'I am a component made by the Gentleman';
}
This demonstrates how decorators facilitate declarative configuration.
Method Decorator
We can intercept method calls to extend their behavior:
function logMethod(method: Function, context: ClassMethodDecoratorContext) {
return function (...args: any[]) {
console.log(
`Method ${String(context.name)} called with arguments: ${args}`,
);
const result = method.apply(this, args);
console.log(`Method ${String(context.name)} returned: ${result}`);
return result;
};
}
class Calculator {
@logMethod
sum(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
calc.sum(1, 2);
Property Decorator
Property decorators can modify how a property is accessed and defined:
function uppercase(_target: any, context: ClassAccessorDecoratorContext) {
return {
get(this: any) {
return this[`_${String(context.name)}`].toUpperCase();
},
set(this: any, value: string) {
this[`_${String(context.name)}`] = value.toUpperCase();
},
};
}
class Person {
@uppercase
accessor name: string;
constructor(name: string) {
this.name = name;
}
}
This decorator automatically converts any assigned value to uppercase.
TypeScript in Angular
Decorators in Angular
Angular uses decorators to simplify the configuration of its component and service structure. The most common ones are:
- @Component: Defines components.
- @Injectable: Configures services.
- @Directive: Extends the DOM.
Interfaces and Models
Interfaces are key to defining clear data structures in Angular:
export interface User {
id: number;
name: string;
}
Generics in Services
Generics are essential for building reusable services:
export class DataService<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
get(): T[] {
return this.data;
}
}
This allows handling data of different types without duplicating code.
Best Practices
- Enable Strict Mode: Configure
tsconfig.json
to detect errors during development:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
- Avoid
any
: Use explicit types orunknown
. - Specify Return Types: Improves clarity and avoids errors.
- Leverage Decorators: Use them to simplify complex configurations.
Fundamental Elements of Angular
Angular Introduction
Angular is a structural platform designed to build robust, modular, and reusable applications. Its main elements include components, directives, and services. These form the core of any Angular application.
It is worth noting that the recommendations here are personal perspectives based on practical experience and do not necessarily follow Angular's official guidelines.
Components
What is a Component?
A component in Angular is the smallest logical unit that handles a single task or part of the user interface (UI). It consists of a selector, a template, optional styles, and a class that contains the related logic and data.
Types of Components
- Presentational Components: Handle displaying data and managing the UI, without business logic.
- Container Components: Handle business logic and communicate with any external entity, such as APIs, services, or databases.
Example of Container/Presentational Pattern:
@Component({
standalone: true,
selector: 'app-user',
template: `<app-user-container [userName]="userNameSignal()" />`,
imports: [UserProfileComponent],
})
export class UserComponent {
userService = inject(UserService);
userNameSignal = this.userService.userNameSignal;
}
@Component({
standalone: true,
selector: 'app-user-profile',
template: `<div [style]="{ color: red }">{{ userName }}</div>`,
imports: [ReactiveFormsModule],
})
export class UserProfileComponent {
userName: string = 'Gentleman';
}
Standalone Components
Since Angular 16, modules are no longer essential for defining components. However, they have not completely disappeared. Now, each component behaves as its own autonomous "module," containing everything it needs to exist independently. This simplifies the architecture and improves performance.
Benefits of Standalone Components:
- Reduces overhead by eliminating unnecessary modules.
- Greater clarity in project structure.
- Facilitates code migration and maintenance.
Directives
What are Directives?
Directives are Angular tools that allow manipulating the DOM to add or alter functionalities of existing HTML elements.
Types of Directives
- Structural: Modify the DOM structure. Examples:
*ngIf
,*ngFor
. - Attribute: Change the appearance or behavior of an element. Examples:
ngClass
,ngStyle
. - Custom Directives: Developed according to the specific needs of the project.
Example of Structural Directive
<div *ngIf="isVisible">Visible Content</div>
<div *ngFor="let item of items">{{ item }}</div>
Example of Attribute Directive
<div [ngClass]="{ 'active': isActive }">Styled Content</div>
Example of Custom Directive
A directive that shows content based on screen size - Structural Directive:
@Directive({
standalone: true,
selector: '[appShowOnScreenSize]',
})
export class ShowOnScreenSizeDirective {
@Input() appShowOnScreenSize!: 'small' | 'medium' | 'large';
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
) {}
ngOnInit() {
this.updateView();
window.addEventListener('resize', this.updateView.bind(this));
}
private updateView() {
const width = window.innerWidth;
this.viewContainer.clear();
if (this.shouldShowContent(width)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
}
private shouldShowContent(width: number): boolean {
if (this.appShowOnScreenSize === 'small' && width < 600) {
return true;
}
if (this.appShowOnScreenSize === 'medium' && width >= 600 && width < 1024) {
return true;
}
if (this.appShowOnScreenSize === 'large' && width >= 1024) {
return true;
}
return false;
}
}
Attribute Directive Example
Adds a yellow highlight when hovering over an element
@Directive({
standalone: true,
selector: '[appHighLight]',
})
export class HighLightDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('mouseenter') onMouseEnter() {
this.renderer.setStyle(this.el.nativeElement, 'background-color', 'yellow');
}
@HostListener('mouseleave') onMouseLeave() {
this.renderer.removeStyle(this.el.nativeElement, 'background-color');
}
}
<p appHighLight>Hover over this text to highlight it</p>
Control Flow and Syntax
What is Control Flow in Angular?
Control flow in Angular allows managing conditional logic and iteration within
templates. With new directives and decorators introduced in Angular 16, the
syntax has been significantly simplified, removing the need to learn elements
like ng-container
and ng-template
.
New Features in Angular 16+ for Control Flow
These new tools not only improve code readability but also reduce the learning curve, allowing developers to focus on business logic without worrying about unnecessary technical details.
Decorators
@if
and @else
The @if
decorator replaces *ngIf
providing a more natural and compact
syntax. It allows handling conditional content in templates without needing
ng-template
.
Example with the new syntax:
@if (isVisible) {
<div>Visible Content</div>
} @else {
<span>Hidden Content</span>
}
How it was before:
<div *ngIf="isVisible; else hiddenContent">Visible Content</div>
<ng-template #hiddenContent>
<span>Hidden Content</span>
</ng-template>
Component Code:
@Component({
selector: 'app-if',
standalone: true,
templateUrl: './if.component.html',
styleUrls: ['./if.component.scss'],
})
export class IfComponent {
protected isVisible = true;
}
@for
The @for
decorator is a significant improvement for iteration. Now, using
track
is mandatory, allowing Angular to optimize rendering by identifying
individual changes in large lists and improving performance.
Example with the new syntax:
<ul>
@for (name of names; track name) {
<li>{{ name }}</li>
} @empty {
<li>No Names</li>
}
</ul>
How it was before:
<ul>
<li *ngFor="let name of names">{{ name }}</li>
</ul>
Component Code:
@Component({
selector: 'app-for',
standalone: true,
templateUrl: './for.component.html',
styleUrls: ['./for.component.scss'],
})
export class ForComponent {
names: string[] = ['Maximiliano', 'Gabriel', 'Rhood', 'Acronimax', 'PNZITOO'];
}
@switch
The @switch
decorator organizes multiple conditions intuitively, eliminating
the complexity of using *ngSwitch
and *ngSwitchCase
.
Example with the new syntax:
@switch (selectedValue) { @case ("option 1") {
<p>Option 1 selected</p>
} @case ("option 2") {
<p>Option 2 selected</p>
} @case ("option 3") {
<p>Option 3 selected</p>
} }
How it was before:
<div [ngSwitch]="selectedValue">
<p *ngSwitchCase="'option 1'">Option 1 selected</p>
<p *ngSwitchCase="'option 2'">Option 2 selected</p>
<p *ngSwitchCase="'option 3'">Option 3 selected</p>
</div>
Component Code:
@Component({
selector: 'app-switch',
standalone: true,
templateUrl: './switch.component.html',
styleUrls: ['./switch.component.scss'],
})
export class SwitchComponent {
protected selectedValue = 'option 1';
}
@defer
The @defer
decorator allows loading deferred content based on specific
conditions, optimizing application performance.
Example with the new syntax:
@defer (when isImageVisible) {
<img src="image-url" alt="Deferred Image" />
} @if (!isImageVisible) {
<button (click)="showImage()">See Image</button>
}
How it was before:
Handling deferred loads required custom solutions.
Component Code:
@Component({
selector: 'app-defer',
standalone: true,
templateUrl: './defer.component.html',
styleUrls: ['./defer.component.scss'],
})
export class DeferComponent {
isImageVisible = false;
showImage() {
this.isImageVisible = true;
}
}
@loading
The @loading
decorator offers a simple way to show temporary markers while
deferred content is loading.
Example with the new syntax:
@defer (when isImageVisible) {
<img src="image-url" alt="Deferred Image" />
} @placeholder {
<p>Loading...</p>
} @if (!isImageVisible) {
<button (click)="showImage()">See Image</button>
}
How it was before:
Handling loading content depended on additional logic.
Component Code:
@Component({
selector: 'app-placeholder',
standalone: true,
templateUrl: './placeholder.component.html',
styleUrls: ['./placeholder.component.scss'],
})
export class PlaceholderComponent {
isImageVisible = false;
showImage() {
setTimeout(() => {
this.isImageVisible = true;
}, 4000);
}
}
@error
The @error
decorator allows handling errors in loading deferred content,
improving user experience.
Example with the new syntax:
@defer (when isContentReady) {
<p>Content Loaded</p>
} @loading {
<p>Loading...</p>
} @placeholder {
<p>Setting up...</p>
} @error {
<p>Error loading content</p>
}
How it was before:
Errors were handled at the component logic level, making the code more complex.
Component Code:
@Component({
selector: 'app-error',
standalone: true,
templateUrl: './error.component.html',
styleUrls: ['./error.component.scss'],
})
export class ErrorComponent {
isContentReady = false;
ngOnInit() {
setTimeout(() => {
this.isContentReady = true;
}, 5000);
}
}
The Power of the @defer
Decorator
The @defer
decorator in Angular marks a turning point in managing deferred
content loading in applications. This feature optimizes user experience and
performance, especially in complex applications where initial load can be a
challenge. Now, handling content that previously required ng-template
and
ng-container
has been simplified, making the learning curve and implementation
much more accessible.
Why @defer
is Relevant?
-
Deferred Loading with Precise Control:
- You can specify exactly when and how content will load.
- Provides an adaptive experience for the user.
-
Performance Improvement:
- Avoids loading unnecessary resources during the initial phase.
- Increases the time until visible content is interactive.
-
Development Simplification:
- No need to master concepts like
ng-container
andng-template
. - Reduces code complexity, making it more readable.
- No need to master concepts like
-
Incredible Flexibility:
- Compatible with different triggers to adapt to multiple use cases.
Use Cases and Available Triggers
1. @defer (when)
Loads content based on a specific boolean condition.
Ideal Use:
- Load data only when available in memory or after a successful query.
@defer (when dataReady) {
<p>Data has been loaded</p>
} @placeholder {
<p>Waiting for data...</p>
}
2. @defer (on idle)
Renders content when the browser is idle.
Ideal Use:
- Load non-essential content like statistics or decorative elements.
@defer (on idle) {
<p>Content loaded while the browser was idle</p>
} @placeholder {
<p>Placeholder for deferred content</p>
}
3. @defer (on viewport)
Loads content when an element enters the browser's visible area.
Ideal Use:
- Lazy-loading images or components that appear on scroll.
<div #triggerElement>Hello!</div>
@defer (on viewport(triggerElement)) {
<p>Content loaded when in viewport</p>
}
4. @defer (on interaction)
Renders content after interacting with an element.
Ideal Use:
- Secondary forms, dropdown menus, or click-based content.
<div #interactionElement>Click here!</div>
@defer (on interaction(interactionElement)) {
<p>Content loaded after interaction</p>
}
5. @defer (on hover)
Loads content when hovering over an element.
Ideal Use:
- Show tooltips or previews.
<div #specificElement>Hover here</div>
@defer (on hover(specificElement)) {
<p>Content loaded after hover</p>
} @placeholder {
<p>Placeholder for hover</p>
}
6. @defer (on timer)
Renders content after a defined time.
Ideal Use:
- Show promotional banners or animations after the initial load.
@defer (on timer(5000ms)) {
<p>Content loaded after 5 seconds</p>
} @placeholder {
<p>Waiting...</p>
}
7. @defer
with Prefetch
Combines triggers with the ability to preload resources.
Ideal Use:
- Anticipate loading critical content based on interaction patterns.
@defer (on interaction; prefetch on idle) {
<p>Content preloaded or loaded after interaction</p>
} @placeholder {
<p>Interact with me</p>
}
Relationship Between @defer
and @placeholder
The combined use of @defer
and @placeholder
efficiently manages the user
experience:
@defer
controls when content should load.@placeholder
ensures the user has visual feedback while waiting.
Example:
@defer (on timer(3000ms)) {
<p>Content loaded after 3 seconds</p>
} @placeholder {
<p>Loading content...</p>
}
Best Practices new Control Flow
-
Use
@defer
for Non-Critical Load:- Secondary menus, heavy images, or graphics.
-
Provide Clear Feedback with
@placeholder
:- Don't leave the user in uncertainty.
-
Combine Prefetch to Anticipate Needs:
- Improve experience by preloading resources.
-
Leverage Triggers Smartly:
- Use
viewport
for lazy-loading orhover
for tooltips.
- Use
Impact for Developers
- Reduction of Manual Work:
- No need to write custom logic to handle events.
- Cleaner Code:
- Improves code readability and maintenance.
- Optimal Performance:
- Avoids unnecessary resource loading, optimizing the application.
With @defer
, Angular takes a step forward in managing deferred content,
simplifying the developer's life and improving the end-user experience.
Forms in Angular: Traditional Approach vs Custom Solution
In this chapter, we explore how to handle complex forms in Angular, where each control is another form. This is often a challenge in the traditional approach, but my proposal significantly simplifies the process.
1. Traditional Approach: Nested Forms
When each section of a general form is a separate form, validating the entire form can become complicated. The traditional approach uses observables to track changes in each form and requires additional logic to consolidate states and values.
File: traditional-form.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
@Component({
selector: 'app-traditional-form',
templateUrl: './traditional-form.component.html',
})
export class TraditionalFormComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
sections: this.fb.array([]),
});
this.addSection();
}
get sections() {
return this.form.controls['sections'] as FormArray;
}
addSection() {
const id = this.sections.length + 1;
const sectionForm = this.fb.group({
title: ['', Validators.required],
controls: this.fb.array([
this.fb.group({
id: [id],
name: ['', Validators.required],
value: ['', Validators.required],
}),
]),
});
this.sections.push(sectionForm);
}
validateForm() {
this.sections.controls.forEach(section => {
(section.get('controls') as FormArray).controls.forEach(control => {
control.markAsTouched();
});
});
}
submit() {
if (this.form.valid) {
console.log('Form data:', this.form.value);
} else {
this.validateForm();
console.log('Form is invalid');
}
}
}
Disadvantages:
- Consolidating states and values requires additional logic.
- Complexity in handling dynamic forms.
- Difficult to maintain performance with extensive forms.
2. My Solution: Simplifying Forms with signals
My proposal uses signals
to manage dynamic forms and calculate values
reactively. It also simplifies validation and allows efficient lazy loading to
optimize performance.
File: app.component.ts
import { Component, computed, inject } from '@angular/core';
import {
FormArray,
FormControl,
FormGroup,
NonNullableFormBuilder,
Validators,
} from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
export interface SectionForm {
id: FormControl<number>;
name: FormControl<string>;
value: FormControl<number>;
}
export type CustomFormGroup = FormGroup<SectionForm>;
export class AppComponent {
fb = inject(NonNullableFormBuilder);
form = this.fb.group({
sections: this.fb.array<CustomFormGroup>([]),
});
get sections() {
return this.form.controls.sections;
}
sectionChanges = toSignal(this.form.valueChanges);
totalValue = computed(() => {
const value = this.sectionChanges()?.sections?.reduce(
(total, item) => total + (Number(item?.value) || 0),
0,
);
console.log('computing total value: ', value);
return value;
});
addSection() {
const id = this.sections.length + 1;
const sectionForm = this.fb.group<SectionForm>({
id: this.fb.control(id),
name: this.fb.control('', { validators: [Validators.required] }),
value: this.fb.control(0, { validators: [Validators.required] }),
});
this.form.controls.sections.push(sectionForm);
}
}
Advantages:
signals
eliminate the need for manual subscriptions.computed
simplifies derived calculations like the total.- Excellent support for lazy loading, loading only visible inputs.
File: form-child.component.ts
import { Component, input } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { SectionForm } from '../app.component';
import { CustomInputComponent } from '../custom-input/custom-input.component';
@Component({
selector: 'app-form-child',
imports: [ReactiveFormsModule, CustomInputComponent],
templateUrl: './form-child.component.html',
styleUrl: './form-child.component.scss',
})
export class FormChildComponent {
formGroup = input.required<FormGroup<SectionForm>>();
}
Comparison
| Feature | Traditional | Custom Solution |
| -------------------- | ------------------ | ------------------------- |
| Validation | Manual and tedious | Simplified with signals
|
| Derived calculations | Complex | Automatic with computed
|
| Lazy loading | Difficult | Easily integrated |
| Maintenance | Complex | Simple and declarative |
My approach demonstrates how to handle nested forms in Angular more efficiently, making development more agile and less error-prone.
How to Use My Method in HTML
My method leverages modern Angular features like @let
and @for
to make
handling dynamic forms much clearer and more efficient. Here's how it works in
HTML:
Simplified HTML Using My Approach
<div>
<!-- Assign form array controls to 'items' -->
@let items = form.controls.items.controls;
<!-- Button to add a new form item -->
<button (click)="addItem()">Add Item</button>
<!-- Iterate over form items with 'track' for performance optimization -->
@for (formGroup of items; track formGroup.controls.id.value) {
<!-- Each child form is managed by a separate component -->
<app-form-child [formGroup]="formGroup" />
}
<!-- Calculate and display the total value in real-time -->
<h3>Total value: {{ totalValue() }}</h3>
</div>
Explanation by Parts
-
@let items = form.controls.items.controls;
:- Dynamically assign form array controls to the
items
variable. - This eliminates the need for additional logic in the
.ts
file to get the controls.
- Dynamically assign form array controls to the
-
"Add Item" Button:
- Allows adding new forms dynamically to the form array.
-
@for (formGroup of items; track formGroup.controls.id.value)
:- Iterate over the forms in the array.
track
ensures Angular can identify specific changes, optimizing performance.- Simplifies logic compared to the traditional approach of using
*ngFor
and manually handling keys.
-
<app-form-child [formGroup]="formGroup" />
:- Delegates the management of each child form to a separate component
(
form-child.component
), promoting modularity.
- Delegates the management of each child form to a separate component
(
-
<h3>Total value: {{ totalValue() }}</h3>
:- Uses a computed
signal
to calculate the total value in real-time, avoiding the need for additional logic or explicit subscriptions.
- Uses a computed
Interceptors in Angular and Server-Side Rendering
In this chapter, we will explore how interceptors and the use of PLATFORM_ID
in Angular can improve the way we handle HTTP requests, especially in
applications with Server-Side Rendering (SSR). Additionally, we will analyze
how my custom approach optimizes this functionality.
Interceptors and Their Role in Angular
Interceptors in Angular allow modifying HTTP requests before they leave the client and processing the responses when they return. This mechanism is useful for adding headers, handling global errors, or, as in this case, managing authentication tokens.
Example: Authentication with Interceptor
File: auth.interceptor.ts
import { isPlatformServer } from '@angular/common';
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject, PLATFORM_ID } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const platformId = inject(PLATFORM_ID);
const authService = inject(AuthService);
// If we are on the server, do not make modifications
if (isPlatformServer(platformId)) {
return next(req);
}
const token = localStorage.getItem('token');
let headers = req.headers.set('Content-Type', 'application/json');
if (token) {
headers = headers.set('Authorization', `Bearer ${token}`);
}
const authReq = req.clone({ headers });
// Global error handling
return next(authReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 || error.status === 403) {
return authService.refreshToken().pipe(
switchMap(newToken => {
localStorage.setItem('token', newToken);
const updatedHeaders = req.headers.set(
'Authorization',
`Bearer ${newToken}`,
);
const newRequest = req.clone({ headers: updatedHeaders });
return next(newRequest);
}),
);
}
return throwError(() => error);
}),
);
};
In this interceptor, we check if we are on the server with isPlatformServer
.
If we are, we pass the request without modifications. If we are in the browser,
we add an authentication token to the headers and handle authentication errors
globally.
The Role of PLATFORM_ID
in SSR
In SSR applications, it is crucial to distinguish between the server and the
browser to avoid errors related to browser APIs, such as localStorage
,
document
, or window
. This is where PLATFORM_ID
comes in, allowing us to
determine the execution environment.
Usage in Interceptors
import { isPlatformServer } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
const platformId = inject(PLATFORM_ID);
if (isPlatformServer(platformId)) {
// Logic for the server
} else {
// Logic for the browser
}
This pattern allows adapting behavior according to the environment, ensuring that browser APIs are not attempted to be used on the server.
Token Management with the Authentication Service
File: auth.service.ts
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, map, Observable, tap, throwError } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
baseURL = 'http://localhost:4000';
router = inject(Router);
http = inject(HttpClient);
refreshToken(): Observable<string> {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
this.logOut();
return throwError(() => new Error('No refresh token found'));
}
return this.http
.post<{ refreshToken: string }>(`${this.baseURL}/token`, { refreshToken })
.pipe(
map(response => response.refreshToken),
tap(newAccessToken => {
localStorage.setItem('token', newAccessToken);
}),
catchError(error => {
this.logOut();
return throwError(() => error);
}),
);
}
logOut() {
localStorage.clear();
this.router.navigate(['/login']);
}
}
This service manages authentication, including token renewal and logout.
Application Configuration
File: app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
};
This configuration registers the interceptor and other necessary services for the application.
Reflection: Advantages of the Approach
-
Efficiency in SSR:
- Avoids errors on the server by distinguishing the environment with
PLATFORM_ID
.
- Avoids errors on the server by distinguishing the environment with
-
Global Error Management:
- Centralizes token handling and renewals.
-
Scalability:
- Facilitates integration with multiple services without duplicating logic.
-
Improved Performance:
- Reduces failed requests by automatically handling authentication.
Testing in Angular
Today we reach a crucial moment: the end of the Angular course. This chapter not only marks the end of this journey but also introduces a topic that can be a turning point in your development as a programmer: testing.
Testing is not just a technical tool; it is a philosophy that, when adopted, transforms the way we build software. In this final class, we will explore three fundamental types: unit testing, functional testing, and end-to-end testing. Each of them fulfills a specific role, and understanding how they fit together is key to ensuring robust and maintainable applications.
Introduction to Testing Tools
Before delving into the examples, let's review how to configure and install the main tools we will use:
Jest
Jest is a testing framework that allows writing tests easily and efficiently. It is ideal for unit testing and integrates well with Angular.
Installation
npm install --save-dev jest @types/jest jest-preset-angular
Configuration
Create a jest.config.js
file:
module.exports = {
preset: 'jest-preset-angular',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'.*\\.e2e\\.spec\\.ts$',
'.*\\.functional\\.spec\\.ts$',
],
globalSetup: 'jest-preset-angular/global-setup',
};
Testing Library
Testing Library makes it easy to write tests focused on user interaction.
Installation Testing Library
npm install --save-dev @testing-library/angular @testing-library/jest-dom
Playwright
Playwright is a powerful tool for functional testing and end-to-end testing.
Installation Playwright
npm install --save-dev @playwright/test
Configuration Playwright
Create a playwright.config.ts
file:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './',
testMatch: ['e2e/**/*.spec.ts', 'functional/**/*.spec.ts'],
fullyParallel: true,
reporter: 'html',
use: {
baseURL: process.env['PLAYWRIGHT_TEST_BASE_URL'] ?? 'http://localhost:4200',
trace: 'on-first-retry',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
Necessary changes in package.json
Add the following scripts in the package.json
file:
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "jest",
"test:coverage": "jest --coverage",
"serve:ssr:angular-testing-boilerplate": "node dist/angular-testing-boilerplate/server/server.mjs",
"e2e": "ng e2e"
},
Necessary changes in angular.json
To integrate these tools, adjustments were also made to the project's
angular.json
file. These changes allow proper configuration of tests and
optimize the project.
Addition of the Playwright builder
Include a new builder to run end-to-end tests with Playwright:
{
"projects": {
"architect": {
"e2e": {
"builder": "playwright-ng-schematics:playwright",
"options": {
"devServerTarget": "angular-testing-boilerplate:serve"
},
"configurations": {
"production": {
"devServerTarget": "angular-testing-boilerplate:serve:production"
}
}
}
}
}
}
Unit Testing: The Foundation
Unit testing focuses on verifying individual parts of our code. In Angular, this means testing components, services, or pipes in isolation.
Unit Testing Example
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
import { provideHttpClient, withFetch } from '@angular/common/http';
import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing';
import { firstValueFrom } from 'rxjs';
describe('AuthService', () => {
let service: AuthService;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideHttpClient(withFetch()),
provideHttpClientTesting(),
AuthService,
],
});
service = TestBed.inject(AuthService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTesting.verify();
});
it('should log in correctly', async () => {
const mockResponse = { token: 'fake-jwt-token' };
const login$ = service.login('user@example.com', 'password123');
const loginPromise = firstValueFrom(login$);
const req = httpTesting.expectOne('/api/login');
expect(req.request.method).toBe('POST');
req.flush(mockResponse);
expect(await loginPromise).toEqual(mockResponse);
});
});
Functional Testing: Testing the Flow
Functional testing verifies that different parts of our application interact correctly.
Functional Testing Example
import { test, expect } from '@playwright/test';
test('should redirect to dashboard on successful login', async ({ page }) => {
await page.route('**/api/login', async route => {
await route.fulfill({ status: 200 });
});
await page.goto('http://localhost:4200');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password1234');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('http://localhost:4200/dashboard');
});
End-to-End Testing: From Start to Finish
These tests simulate the end-user experience.
End-to-End Testing Example
import { test, expect } from '@playwright/test';
test('complete successful login flow', async ({ page }) => {
await page.route('**/api/login', async route => {
const requestBody = await route.request().postDataJSON();
if (
requestBody.email === 'user@example.com' &&
requestBody.password === 'password1234'
) {
route.fulfill({
status: 200,
body: JSON.stringify({ success: true }),
});
} else {
route.fulfill({
status: 401,
body: JSON.stringify({ message: 'Invalid email or password' }),
});
}
});
await page.goto('http://localhost:4200');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password1234');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('http://localhost:4200/dashboard');
});
Detailed Jest and Playwright
In this chapter, we will explore Jest and Playwright in depth, explaining how to use them in detail to write robust and effective tests. Let's break down each tool and the key features they offer.
What is Jest?
Jest is a testing framework designed to be simple and powerful. Its main purpose is to facilitate the development of unit tests and ensure that the code works as expected.
Basic Concepts of Jest
-
Describe and it:
- Jest organizes tests using
describe
andit
blocks. describe
groups related tests.it
defines a specific test case.
describe('Math operations', () => { it('should add two numbers correctly', () => { const result = 2 + 2; expect(result).toBe(4); }); });
- Here, the
describe
block groups math operation tests, and theit
test case verifies that the addition works.
- Jest organizes tests using
-
Matchers:
- Matchers like
toBe
,toEqual
,toContain
allow verifying expected results.
expect([1, 2, 3]).toContain(2); // Verifies that the array contains the value 2. expect('hello').toMatch(/ell/); // Verifies that the string contains "ell".
- Matchers like
-
Mocking:
- Jest allows simulating functions or modules with
jest.fn
andjest.mock
.
const mockFn = jest.fn(); mockFn('hello'); expect(mockFn).toHaveBeenCalledWith('hello');
- Jest allows simulating functions or modules with
Detailed Example
// auth.service.ts
export class AuthService {
login(username: string, password: string): boolean {
return username === 'admin' && password === '1234';
}
}
// auth.service.spec.ts
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
service = new AuthService();
});
it('should return true for correct credentials', () => {
const result = service.login('admin', '1234');
expect(result).toBe(true);
});
it('should return false for incorrect credentials', () => {
const result = service.login('user', 'wrongpassword');
expect(result).toBe(false);
});
});
- Explanation:
beforeEach
initializes the service before each test.- The first test verifies correct credentials.
- The second test ensures that incorrect credentials return
false
.
What is Playwright?
Playwright is a tool for graphical user interface (UI) testing. It allows automating browsers to simulate real user interactions.
Basic Concepts of Playwright
-
Page:
- Represents a browser tab where actions occur.
const page = await browser.newPage(); await page.goto('http://example.com');
-
Selectors:
- Identify elements on the page to interact with them.
await page.click('button#submit'); await page.fill('input[name="username"]', 'user123');
-
Asserts:
- Verify that the page has the expected state.
await expect(page).toHaveURL('http://example.com/dashboard'); await expect(page.locator('h1')).toHaveText('Welcome');
Detailed Playwright Example
import { test, expect } from '@playwright/test';
test('Login functionality', async ({ page }) => {
await page.goto('http://example.com/login');
await page.fill('input[name="username"]', 'admin');
await page.fill('input[name="password"]', '1234');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('http://example.com/dashboard');
await expect(page.locator('h1')).toHaveText('Dashboard');
});
- Explanation:
- The test opens the login page.
- Fills in the username and password fields.
- Clicks the login button.
- Verifies the redirection and that the header is as expected.
Common Best Practices
In Jest
-
Keep tests small and focused:
- Each test should verify a specific behavior.
-
Use mocks for external dependencies:
- Simulate external APIs or services to avoid dependencies in unit tests.
-
Group related tests:
- Use
describe
blocks to organize similar cases.
- Use
In Playwright
-
Reuse configurations:
- Set up routes or users in a common
beforeEach
.
- Set up routes or users in a common
-
Avoid hardcoding data:
- Use reusable variables or test data.
-
Leverage accessible selectors:
- Use attributes like
aria-label
orname
for robust selectors.
- Use attributes like
Best practices with await
, mocking, and environment simulation
await
, mocking, and environment simulationIn this chapter, we will explore advanced concepts and best practices for
working with await
, mocking services, and simulating global elements like
window
. This will allow you to write more robust and realistic tests.
Understanding await
and async in tests
Using await
in tests is crucial for handling async operations like API calls
or UI interactions. It's important to understand how to use it correctly:
Best practices with await
-
Always await async operations:
- Tests should wait for operations to complete to avoid false positives.
it('should wait correctly', async () => { const result = await asyncFunction(); expect(result).toBe(true); });
-
Avoid mixing
await
withthen
:- Use one or the other, but not both in the same line.
// Not recommended asyncFunction().then(result => { expect(result).toBe(true); }); // Recommended const result = await asyncFunction(); expect(result).toBe(true);
-
Use
await
with async matchers:- For async verifications, Jest offers matchers like
resolves
orrejects
.
await expect(asyncFunction()).resolves.toBe(true); await expect(asyncFunction()).rejects.toThrow('Error');
- For async verifications, Jest offers matchers like
Mocking services in Jest
Mocking is essential for simulating external dependencies like APIs or services.
Jest facilitates this process with utilities like jest.fn
and jest.mock
.
Example of service mocking
Suppose you have a service that makes an HTTP call:
// user.service.ts
export class UserService {
fetchUser(userId: string): Promise<{ id: string; name: string }> {
return fetch(`/api/users/${userId}`).then(response => response.json());
}
}
Mocking the service in Jest
// user.service.spec.ts
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
global.fetch = jest.fn();
});
it('should return a user', async () => {
const mockResponse = { id: '1', name: 'John Doe' };
(global.fetch as jest.Mock).mockResolvedValueOnce({
json: jest.fn().mockResolvedValueOnce(mockResponse),
});
const user = await userService.fetchUser('1');
expect(user).toEqual(mockResponse);
expect(global.fetch).toHaveBeenCalledWith('/api/users/1');
});
});
- Explanation:
global.fetch
is mocked usingjest.fn
.- The
fetch
response is simulated withmockResolvedValueOnce
. - We verify that the service calls the correct URL and returns the expected data.
Mocking global elements like window
In some tests, it's necessary to simulate global elements like window
or
localStorage
.
Example of mocking window
and localStorage
// localStorage.mock.ts
export const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value;
},
clear: () => {
store = {};
},
removeItem: (key: string) => {
delete store[key];
},
};
})();
global.localStorage = localStorageMock;
Using the mock in tests
// window.spec.ts
describe('localStorage', () => {
it('should save and retrieve a value', () => {
localStorage.setItem('key', 'value');
const result = localStorage.getItem('key');
expect(result).toBe('value');
});
it('should remove a value', () => {
localStorage.setItem('key', 'value');
localStorage.removeItem('key');
const result = localStorage.getItem('key');
expect(result).toBeNull();
});
});
Simulating timers and time functions
Jest includes functions to control timers like setTimeout
and setInterval
.
Example with timers
// timer.spec.ts
describe('Timers', () => {
jest.useFakeTimers();
it('should call the function after 1 second', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(1);
});
});
- Explanation:
jest.useFakeTimers
enables fake timers.jest.runAllTimers
advances time and executes scheduled callbacks.
Conclusion
Effective handling of await
, mocking, and global elements like window
or
localStorage
allows writing more comprehensive and reliable tests. These
techniques are essential for handling complex scenarios and ensuring your code
behaves as expected under any circumstances.
Best practices with Playwright for mocking and async handling
Playwright is a powerful tool for performing UI tests and complex simulations. In this chapter, we will explore how to handle async operations effectively and mock services and global elements.
Handling async with Playwright
Using await
is essential in Playwright, as many of its functions are async.
Here are some best practices:
Best practices with await
Playwright
-
Use
await
for all interactions:- Each action in Playwright should wait for completion before proceeding.
await page.click('button#submit'); // Waits for the button to be clicked.
-
Combine
await
with assertions:- Verify the page state immediately after an action.
await page.fill('input#username', 'user123'); await expect(page.locator('input#username')).toHaveValue('user123');
-
Avoid hardcoding times with
waitForTimeout
:- Use selectors and conditions instead of waiting for fixed times.
await page.waitForSelector('div#loaded'); // Better than waiting a fixed time.
Mocking network requests
Playwright allows intercepting and simulating network requests, useful for testing scenarios like server errors or slow responses.
Example of mocking an API
Suppose we want to simulate an API that returns user information:
test('Should display user information', async ({ page }) => {
// Intercept the request
await page.route('**/api/user', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: '1', name: 'John Doe' }),
});
});
// Navigate to the page
await page.goto('http://localhost:4200/profile');
// Verify the information is displayed correctly
await expect(page.locator('h1')).toHaveText('John Doe');
});
- Explanation:
page.route
intercepts the request.route.fulfill
responds with simulated data.- The page displays the simulated data, and we verify the result.
Simulating network errors
test('Should display an error message if the API fails', async ({ page }) => {
await page.route('**/api/user', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Server error' }),
});
});
await page.goto('http://localhost:4200/profile');
const errorMessage = page.locator('div.error');
await expect(errorMessage).toHaveText('Error loading information');
});
- Explanation:
- We simulate a server error.
- We verify that the page displays an error message.
Mocking global elements
Playwright also allows simulating global elements like localStorage
or
window
.
Mocking localStorage
test('Should load the theme from localStorage', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem('theme', 'dark');
});
await page.goto('http://localhost:4200');
const themeClass = page.locator('body');
await expect(themeClass).toHaveClass(/dark-theme/);
});
- Explanation:
- We use
addInitScript
to set uplocalStorage
before loading the page. - We verify that the page applies the correct theme.
- We use
Mocking global window
functions
test('Should show an alert when submitting a form', async ({ page }) => {
await page.goto('http://localhost:4200/contact');
// Simulate window.alert
page.on('dialog', dialog => {
expect(dialog.message()).toBe('Form submitted');
dialog.dismiss();
});
await page.click('button#submit');
});
- Explanation:
- We listen for dialog events with
page.on('dialog')
. - We verify the alert message and close it.
- We listen for dialog events with
Simulating delays and times
Playwright can simulate delays in responses or executions:
Example with simulated delays
test('Should show a spinner during loading', async ({ page }) => {
await page.route('**/api/data', async route => {
await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate delay
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [] }),
});
});
await page.goto('http://localhost:4200');
const spinner = page.locator('.spinner');
await expect(spinner).toBeVisible();
await page.waitForSelector('.spinner', { state: 'hidden' }); // Wait for it to disappear
});
Final Chapter: All the New Features of Angular 19
Angular 19 arrives loaded with significant improvements focused on developer experience, performance, and reactivity. This chapter explores these new features in detail, with practical examples illustrating how to make the most of them.
Incremental Hydration
Angular now supports a developer preview version of incremental hydration, a feature that improves the performance of server-rendered applications by hydrating components in a deferred manner, based on user interaction.
Example
import {
provideClientHydration,
withIncrementalHydration,
} from '@angular/platform-browser';
bootstrapApplication(AppComponent, {
providers: [provideClientHydration(withIncrementalHydration())],
});
In the template:
@defer (hydrate on viewport) {
<shopping-cart></shopping-cart>
}
In this example, the <shopping-cart>
component will not
hydrate until it enters the viewport.
Default Event Replay
Event Replay ensures that user events captured before the code is downloaded and hydrated are executed correctly once the code is ready.
Configuration Default Event Replay
bootstrapApplication(AppComponent, {
providers: [provideClientHydration(withEventReplay())],
});
This significantly improves the user experience in server-rendered applications.
Route-Based Rendering Modes
You can now define which routes should be prerendered, server-rendered, or client-rendered.
Example Rendering Modes
import { ServerRoute, RenderMode } from '@angular/platform-server';
export const serverRouteConfig: ServerRoute[] = [
{ path: '/login', mode: RenderMode.Server },
{ path: '/dashboard', mode: RenderMode.Client },
{ path: '/**', mode: RenderMode.Prerender },
];
This allows optimizing the behavior of your application according to the needs of each route.
Enhanced Reactivity
Inputs, Outputs, and Queries
Angular 19 stabilizes these APIs and provides schematics to migrate to the new versions:
ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migration
linkedSignal
This new API simplifies the handling of mutable states dependent on other states.
const options = signal(['apple', 'banana']);
const choice = linkedSignal(() => options()[0]);
choice.set('banana');
options.set(['pear', 'kiwi']);
console.log(choice()); // 'pear'
resource
Introduces reactive handling for asynchronous operations.
@Component({...})
export class UserProfile {
userId = input<number>();
userService = inject(UserService);
user = resource({
request: this.userId,
loader: async ({ request: id }) => await this.userService.getUser(id),
});
}
This API is experimental but opens the door to more efficient handling of asynchronous data.
Improvements in Angular Material and CDK
-
Time Picker Component:
- A new component for time selection, accessible and highly requested.
-
Bidirectional Drag & Drop:
- You can now drag elements in two dimensions using the CDK.
<div cdkDropList cdkDropListOrientation="mixed"> @for (item of items) { <div cdkDrag>{{ item }}</div> } </div>
-
Themes with Improved API:
- Simplifies the creation of custom themes with
mat.theme
.
@use '@angular/material' as mat; html { @include mat.theme( ( color: ( primary: mat.$violet-palette, secondary: mat.$orange-palette, ), ) ); }
- Simplifies the creation of custom themes with
Hot Module Replacement (HMR)
Angular 19 introduces HMR for styles and experimental support for templates.
NG_HMR_TEMPLATES=1 ng serve
This allows you to see changes in styles or templates without reloading the page or losing the application state.
Usage Examples
Hydration
hydrate.component.html
<p>hydrate works!</p>
@for (character of characters; track character.id) {
<app-character [character]="character"></app-character>
}
hydrate.component.ts
import { isPlatformBrowser } from '@angular/common';
import { Component, inject, PLATFORM_ID } from '@angular/core';
@Component({
selector: 'app-hydrate',
standalone: true,
imports: [],
templateUrl: './hydrate.component.html',
styleUrl: './hydrate.component.scss',
})
export class HydrateComponent {
isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
constructor() {
if (this.isBrowser) localStorage.setItem('key', 'test');
}
}
app.component.html
@defer (on idle; hydrate on interaction) {
<app-hydrate />
} @let greeting = "Hello "+greeting
<p>{{ greeting}}</p>
@let user = user$ | async;
<p>User: {{ user.name }}</p>
app.component.ts
import {
Component,
inject,
input,
linkedSignal,
resource,
signal,
} from '@angular/core';
import { HydrateComponent } from './hydrate/hydrate.component';
@Component({
selector: 'app-root',
imports: [HydrateComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'angular-19';
options = signal(['apple', 'banana', 'strawberry']);
userId = input<number>();
usersService = inject(UsersService);
choice = linkedSignal(() => this.options()[0]);
greetings = 'hi';
user = resource({
request: () => this.userId,
loader: async ({ request: id }) => await this.usersService.getUser(id),
});
constructor() {
this.choice.set('strawberry');
console.log(this.choice()); // "strawberry"
this.options.set(['kiwi', 'pineapple']);
console.log(this.choice()); // "kiwi"
}
}
Angular 19 marks a significant advancement in developer experience and application performance. From enhanced reactivity to incremental hydration, these tools allow building faster and more efficient applications. Take advantage of these new features to take your projects to the next level.