- Introduction
- Prerequisites
- Getting Started
- Project Structure
- Basic Concepts
- Intermediate Concepts
- Advanced Concepts
- Best Practices
- Resources
- Troubleshooting
Angular 22 is the latest version of a modern, TypeScript-based framework for building dynamic web applications. It provides a complete solution for building scalable, maintainable, and efficient applications with powerful built-in features including standalone components, signals for reactive state, dependency injection, routing, forms handling, and reactive programming with RxJS.
- π― Standalone Components: Default for new projects (no NgModules required)
- β‘ Signals: Modern reactive state management with automatic optimization
- π¨ Control Flow Syntax: New @if, @for, @switch, @defer syntax
- π§ Functional APIs: Guards, interceptors, and dependency injection without classes
- π¦ Esbuild: Faster builds and dev server
- π Better DevTools: Enhanced Angular DevTools experience
- π Performance: Improved change detection and bundling
- β Full-featured, opinionated framework
- β Strong typing with TypeScript
- β Excellent tooling and CLI
- β Built-in testing support
- β Powerful dependency injection
- β Reactive programming with Signals and RxJS
- β Large ecosystem and community support
- β Enterprise-grade features
Before starting with Angular 22, ensure you have:
-
Node.js & npm (v20 or higher required for Angular 22)
- Download from nodejs.org
- Verify installation:
node --versionandnpm --version
-
TypeScript Knowledge
- Basic understanding of TypeScript syntax
- Familiarity with classes, interfaces, and decorators
-
HTML & CSS
- Solid understanding of HTML structure
- CSS fundamentals and layouts
-
JavaScript ES6+
- Arrow functions, let/const, destructuring
- Promises and async/await
- Spread operator and template literals
-
Text Editor
- Visual Studio Code (recommended)
- Install Angular Language Service extension
Before starting with Angular 22:
-
Node.js Version Check:
node --version # Should be v20 or higher npm --version # Should be v10 or higher
-
Install Angular CLI:
npm install -g @angular/cli@latest
-
Verify Installation:
ng version # Should show Angular 22+
-
Install Angular CLI globally:
npm install -g @angular/cli
-
Create a new Angular project:
ng new my-angular-app cd my-angular-app -
Serve the application:
ng serve # or ng serve --open # Opens in default browser
-
Access the application:
- Navigate to
http://localhost:4200
- Navigate to
# Create new component
ng generate component components/my-component
# Create new service
ng generate service services/my-service
# Create new module
ng generate module modules/my-module
# Create new directive
ng generate directive directives/my-directive
# Create new pipe
ng generate pipe pipes/my-pipe
# Create new guard
ng generate guard guards/auth-guard
# Create new interceptor
ng generate interceptor interceptors/http-interceptormy-angular-app/
βββ src/
β βββ app/
β β βββ components/ # Standalone components
β β βββ pages/ # Standalone page/route components
β β βββ services/ # Business logic services
β β βββ models/ # TypeScript interfaces/classes
β β βββ guards/ # Route guards (functional)
β β βββ interceptors/ # HTTP interceptors (functional)
β β βββ pipes/ # Standalone pipes
β β βββ directives/ # Standalone directives
β β βββ app.component.ts # Root standalone component
β β βββ app.routes.ts # Route definitions
β β βββ app.config.ts # Application configuration
β βββ assets/ # Static files
β βββ styles/ # Global styles
β βββ index.html # Main HTML file
β βββ main.ts # Application entry point
β βββ styles.css # Global styles
βββ angular.json # Angular configuration
βββ tsconfig.json # TypeScript configuration
βββ package.json # Dependencies
βββ README.md # Documentation
Standalone Components are the modern way in Angular 22. They eliminate the need for NgModules.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-hello',
standalone: true,
imports: [CommonModule], // Import dependencies directly
template: `<h1>Hello {{ name }}!</h1>`,
styles: [`h1 { color: blue; }`]
})
export class HelloComponent {
name = 'Angular 22';
}Signals provide a powerful way to manage reactive state:
import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubledCount() }}</p>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
count = signal(0);
doubledCount = computed(() => this.count() * 2);
increment() {
this.count.update(val => val + 1);
}
}import { Component, OnInit, OnDestroy, effect } from '@angular/core';
@Component({
selector: 'app-example',
standalone: true,
template: ``
})
export class ExampleComponent implements OnInit, OnDestroy {
ngOnInit() {
// Called after component initialized
}
ngOnDestroy() {
// Clean up resources
}
}
### 2. Templates and Data Binding
#### Interpolation:
```html
<p>{{ message }}</p>
<p>{{ 2 + 2 }}</p><img [src]="imageUrl" />
<button [disabled]="isDisabled">Click me</button><button (click)="handleClick()">Click</button>
<input (keyup)="handleKeyup($event)" /><input [(ngModel)]="username" />
<p>Username: {{ username }}</p><div [class.active]="isActive">Active</div>
<div [style.color]="textColor">Colored Text</div>
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">Classes</div>Angular 22 introduces new built-in control flow with @ syntax:
<!-- New @if syntax -->
@if (condition) {
<p>This shows if condition is true</p>
} @else if (otherCondition) {
<p>Other condition is true</p>
} @else {
<p>None of the conditions are true</p>
}
<!-- New @for syntax with better performance -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<p>No items available</p>
}
<!-- New @switch syntax -->
@switch (value) {
@case ('A') { <p>Case A</p> }
@case ('B') { <p>Case B</p> }
@default { <p>Default case</p> }
}
<!-- Deferred loading with @defer -->
@defer (on interaction) {
<app-heavy-component />
} @placeholder {
<p>Click to load component</p>
}<!-- NgIf - old syntax still works -->
<p *ngIf="condition">This shows if condition is true</p>
<!-- NgFor - old syntax still works -->
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<!-- NgClass -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">Classes</div>
<!-- NgStyle -->
<div [ngStyle]="{'color': textColor, 'fontSize': fontSize}">Styled Text</div>Services use the providedIn: 'root' pattern for tree-shakeable singleton services:
import { Injectable } from '@angular/core';
import { signal } from '@angular/core';
@Injectable({
providedIn: 'root' // Available throughout the app
})
export class UserService {
private users = signal(['User 1', 'User 2', 'User 3']);
getUsers() {
return this.users.asReadonly();
}
addUser(name: string) {
this.users.update(users => [...users, name]);
}
}Using a service in a standalone component:
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
@for (user of users(); track user) {
<p>{{ user }}</p>
}
`
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users = signal<string[]>([]);
ngOnInit() {
this.users.set(this.userService.getUsers());
}
}Key Improvements:
- Use
inject()function instead of constructor injection (still supports both) - Services with signals for reactive state
- No need for NgModule declarations
In Angular 22, you can bootstrap your app directly without NgModules:
// main.ts
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));// app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(),
// Add your services and providers here
]
};// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet />`
})
export class AppComponent {}Modules organize related components, directives, and services:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyComponent } from './my.component';
@NgModule({
declarations: [MyComponent],
imports: [CommonModule],
exports: [MyComponent]
})
export class MyModule {}Navigation between pages using Angular Router with standalone components:
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { AboutComponent } from './pages/about/about.component';
import { UserDetailComponent } from './pages/user-detail/user-detail.component';
import { NotFoundComponent } from './pages/not-found/not-found.component';
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'user/:id', component: UserDetailComponent },
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard]
},
// Lazy loading with standalone components
{
path: 'dashboard',
loadComponent: () => import('./pages/dashboard/dashboard.component').then(m => m.DashboardComponent),
canActivate: [authGuard]
},
{ path: '**', component: NotFoundComponent }
];// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
router.navigate(['/login']);
return false;
};Using routing in templates:
<nav>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
<a [routerLink]="['/user', userId]">User Detail</a>
</nav>
<router-outlet />Accessing route parameters:
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { signal } from '@angular/core';
@Component({
selector: 'app-user-detail',
standalone: true,
template: `<p>User ID: {{ userId() }}</p>`
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
userId = signal<string | null>(null);
ngOnInit() {
this.route.paramMap.subscribe(params => {
this.userId.set(params.get('id'));
});
}
}import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" placeholder="Email" />
@if (loginForm.get('email')?.hasError('required')) {
<span>Email is required</span>
}
<input formControlName="password" type="password" placeholder="Password" />
<button [disabled]="loginForm.invalid">Login</button>
</form>
`
})
export class LoginComponent implements OnInit {
private fb = inject(FormBuilder);
loginForm!: FormGroup;
ngOnInit() {
this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
onSubmit() {
if (this.loginForm.valid) {
console.log(this.loginForm.value);
}
}
}import { Component, signal, effect, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, FormGroup } from '@angular/forms';
@Component({
selector: 'app-form-signals',
standalone: true,
imports: [ReactiveFormsModule],
template: ``
})
export class FormSignalsComponent {
private fb = inject(FormBuilder);
formData = signal<any>(null);
form = this.fb.group({
name: [''],
email: ['']
});
constructor() {
// Watch form value changes
effect(() => {
this.formData.set(this.form.value);
});
}
}<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">
<input name="email" ngModel required email />
<input name="password" ngModel type="password" required />
<button [disabled]="loginForm.invalid">Login</button>
</form>Making API calls with HttpClient in standalone components:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private apiUrl = '/api';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.apiUrl}/users`);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/users/${id}`);
}
createUser(user: User): Observable<User> {
return this.http.post<User>(`${this.apiUrl}/users`, user);
}
updateUser(id: number, user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/users/${id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/users/${id}`);
}
}Using in component with signals:
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ApiService, User } from '../services/api.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
@if (loading()) {
<p>Loading...</p>
} @else if (error()) {
<p>{{ error() }}</p>
} @else {
@for (user of users(); track user.id) {
<div>{{ user.name }} - {{ user.email }}</div>
}
}
`
})
export class UserListComponent implements OnInit {
private apiService = inject(ApiService);
users = signal<User[]>([]);
loading = signal(true);
error = signal<string | null>(null);
ngOnInit() {
this.apiService.getUsers().subscribe({
next: (data) => {
this.users.set(data);
this.loading.set(false);
},
error: (err) => {
this.error.set('Failed to load users');
this.loading.set(false);
}
});
}
}// interceptors/auth.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const router = inject(Router);
// Add auth token
const token = authService.getToken();
if (token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
authService.logout();
router.navigate(['/login']);
}
return throwError(() => error);
})
);
};
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor])
)
]
};Parent-child communication using inputs and outputs:
Parent component:
import { Component, signal } from '@angular/core';
import { ChildComponent } from './child/child.component';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-parent',
standalone: true,
imports: [CommonModule, ChildComponent],
template: `
<h2>Message: {{ message() }}</h2>
<app-child
[message]="message()"
(sendMessage)="handleChildEvent($event)">
</app-child>
`
})
export class ParentComponent {
message = signal('Hello from parent!');
handleChildEvent(data: string) {
console.log('Child said:', data);
this.message.set(data);
}
}Child component:
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-child',
standalone: true,
imports: [CommonModule],
template: `
<p>{{ message() }}</p>
<button (click)="sendToParent()">Send to Parent</button>
`
})
export class ChildComponent {
// Using new input() and output() functions (Angular 17+)
message = input<string>('');
sendMessage = output<string>();
sendToParent() {
this.sendMessage.emit('Hello from child!');
}
}Traditional @Input/@Output (Still Supported):
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-traditional-child',
standalone: true,
template: ``
})
export class TraditionalChildComponent {
@Input() message: string = '';
@Output() sendMessage = new EventEmitter<string>();
sendToParent() {
this.sendMessage.emit('Hello!');
}
}Signals provide a modern reactive primitive for Angular 22:
import { Component, signal, computed, effect, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-signals-demo',
standalone: true,
imports: [CommonModule],
template: `
<h3>Counter: {{ count() }}</h3>
<h3>Doubled: {{ doubledCount() }}</h3>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
`
})
export class SignalsDemoComponent {
// Create a writable signal
count = signal(0);
// Create a computed signal (read-only derived value)
doubledCount = computed(() => this.count() * 2);
constructor() {
// Watch signal changes with effect
effect(() => {
console.log('Count changed to:', this.count());
});
}
increment() {
this.count.update(val => val + 1);
}
decrement() {
this.count.set(this.count() - 1);
}
}Key Benefits of Signals:
- Automatic change detection optimization
- Fine-grained reactivity
- Eliminates subscription boilerplate
- Better performance than RxJS for UI state
While signals are preferred for UI state, RxJS is still essential for async operations:
import { Component, OnInit, signal, inject } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { map, filter, takeUntil } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-rxjs-demo',
standalone: true,
imports: [CommonModule],
template: ``
})
export class RxJsDemoComponent implements OnInit {
private http = inject(HttpClient);
private destroy$ = new Subject<void>();
ngOnInit() {
// Use RxJS for HTTP requests
this.http.get<any[]>('/api/data')
.pipe(
map(items => items.filter(item => item.active)),
takeUntil(this.destroy$)
)
.subscribe(filtered => console.log(filtered));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}When to Use RxJS:
- HTTP requests and async operations
- Complex async workflows
- Real-time data streams
- WebSocket communication
Using NgRx for centralized state management:
// actions.ts
import { createAction, props } from '@ngrx/store';
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersError = createAction(
'[User] Load Users Error',
props<{ error: string }>()
);
// reducer.ts
import { createReducer, on } from '@ngrx/store';
export interface UserState {
users: User[];
loading: boolean;
error: string | null;
}
const initialState: UserState = {
users: [],
loading: false,
error: null
};
export const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false
})),
on(loadUsersError, (state, { error }) => ({
...state,
error,
loading: false
}))
);
// effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, switchMap } from 'rxjs/operators';
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.apiService.getUsers().pipe(
map(users => loadUsersSuccess({ users })),
catchError(error => [loadUsersError({ error })])
)
)
)
);
constructor(private actions$: Actions, private apiService: ApiService) {}
}
// selector.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
export const selectUserState = createFeatureSelector<UserState>('user');
export const selectUsers = createSelector(
selectUserState,
state => state.users
);Add headers and handle errors globally:
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// Add auth token to headers
const token = this.authService.getToken();
if (token) {
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.authService.logout();
}
return throwError(() => error);
})
);
}
}
// Register in app.module.ts
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
]
})
export class AppModule {}Protect routes with authentication/authorization:
import { inject } from '@angular/core';
import { Router, CanActivateFn, RedirectCommand } from '@angular/router';
import { AuthService } from '../services/auth.service';
// Functional guard (Modern Angular 15+)
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// Automatically redirect to login
return new RedirectCommand(router.parseUrl('/login'));
};
// Role-based guard
export const adminGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
return authService.hasRole('admin');
};
// Usage in routes
export const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [adminGuard]
},
{
path: 'user-profile',
component: ProfileComponent,
canActivate: [authGuard]
}
];import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-efficient',
template: `<div>{{ data }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EfficientComponent {
@Input() data: string = '';
}const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
];export class Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(private service: Service) {}
ngOnInit() {
this.service.data$
.pipe(takeUntil(this.destroy$))
.subscribe(data => console.log(data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Create reusable directive logic:
import { Directive, ElementRef, HostListener, input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
highlightColor = input<string>('yellow');
constructor(private el: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter() {
this.el.nativeElement.style.backgroundColor = this.highlightColor();
}
@HostListener('mouseleave')
onMouseLeave() {
this.el.nativeElement.style.backgroundColor = 'transparent';
}
}
// Usage in standalone component
@Component({
selector: 'app-highlight-demo',
standalone: true,
imports: [HighlightDirective],
template: `<p appHighlight [appHighlight]="'blue'">Hover me!</p>`
})
export class HighlightDemoComponent {}Transform data in templates:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'phoneNumber',
standalone: true
})
export class PhoneNumberPipe implements PipeTransform {
transform(value: string): string {
if (!value || value.length < 10) return value;
return `(${value.slice(0, 3)}) ${value.slice(3, 6)}-${value.slice(6)}`;
}
}
// Usage in standalone component
@Component({
selector: 'app-phone-demo',
standalone: true,
imports: [PhoneNumberPipe],
template: `<p>{{ '1234567890' | phoneNumber }}</p>`
})
export class PhoneDemoComponent {}
// Output: (123) 456-7890import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
// Use standalone component directly
await TestBed.configureTestingModule({
imports: [MyComponent]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display title with signals', () => {
component.title.set('Test Title');
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Test Title');
});
it('should handle signal updates', () => {
const count = component.count;
count.update(val => val + 1);
expect(count()).toBe(1);
});
});-
Use Standalone Components
- Default to standalone components
- No NgModules unless required for legacy code
- Simplifies code and reduces boilerplate
-
Leverage Signals for State
- Use signals for UI state management
- Use computed() for derived values
- Use effect() for side effects
- Better performance than observables for UI state
-
Use Functional APIs
- Functional route guards instead of class-based
- Functional HTTP interceptors
- inject() instead of constructor injection
- New control flow syntax (@if, @for, @switch)
-
Component Design
- Keep components small and focused
- Use input() and output() functions for props
- Prefer composition over inheritance
- Implement OnPush change detection
-
Naming Conventions
- Components:
PascalCase+.component.ts - Services:
PascalCase+.service.ts - Files: kebab-case (e.g.,
user-profile.component.ts)
- Components:
-
Dependency Injection
- Use
providedIn: 'root'for singleton services - Use inject() function for cleaner code
- Provide services at appropriate level
- Use
-
Reactive Forms
- Prefer reactive forms over template-driven
- Use FormBuilder for cleaner syntax
- Combine with signals for better state management
-
Performance Optimization
- Use OnPush change detection strategy
- Implement lazy loading with loadComponent
- Use @defer for deferred loading
- Use trackBy in @for loops
- Implement proper unsubscribe patterns
-
Security
- Always validate user inputs
- Use Angular's built-in sanitization
- Implement proper authentication/authorization
- Use functional interceptors for token management
-
Code Organization
- Group related components in feature folders
- Separate concerns (components, services, models)
- Use barrel exports (
index.ts) for cleaner imports - Follow Angular style guide
-
Error Handling
- Use error handlers in HTTP interceptors
- Provide user-friendly error messages
- Log errors properly for debugging
-
Documentation
- Document complex logic with comments
- Keep README updated
- Add JSDoc comments to public methods
- Angular 22 Docs
- Angular CLI
- Angular Material
- Standalone Components Guide
- Signals Overview
- Control Flow Syntax
- RxJS
- NgRx
- Angular Material
- Bootstrap for Angular
- Tailwind CSS
- Ionic (Mobile Apps)
- Storybook (Component Development)
Issue: Module not found error
# Solution: Install dependencies
npm installIssue: Port 4200 already in use
# Solution: Use different port
ng serve --port 4300Issue: Changes not reflected
# Solution: Stop and restart the server
# Kill the process and run ng serve againIssue: Build size too large
# Solution: Check bundle size
ng build --stats-json
npx webpack-bundle-analyzer dist/*/stats.jsonIssue: Circular dependency error
Solution: Reorganize module structure
- Move shared logic to a dedicated module
- Use barrel exports correctly
- Check for circular imports
Issue: Change detection not working
Solution:
- Check if ngOnInit is implemented
- Verify @Input/@Output decorators
- Review change detection strategy
- Use async pipe in templates
Issue: Memory leak with subscriptions
// Solution: Always unsubscribe
private destroy$ = new Subject<void>();
ngOnInit() {
this.service.data$
.pipe(takeUntil(this.destroy$))
.subscribe(...);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}ng new app-name # Create new app (standalone by default)
ng serve # Start dev server
ng generate component comp-name # Create component (standalone by default)
ng generate service service-name # Create service
ng generate guard guards/auth-guard # Create functional guard
ng generate pipe pipes/my-pipe # Create pipe
ng generate directive directives/my-dir # Create directive
ng build # Production build
ng build --configuration production # Build for production
ng test # Run tests
ng lint # Lint code
ng e2e # End-to-end tests- Standalone Components: Default component type
- Signals: Reactive state management
- Control Flow Syntax: @if, @for, @switch, @defer
- Functional Interceptors: Cleaner HTTP interceptors
- Functional Guards: Simpler route protection
- input() & output(): Modern prop definition
- inject(): Cleaner dependency injection
- Esbuild: Faster build and dev server
- Angular 22 (Latest)
- Angular 21
- Angular 20
- Angular 19
- Angular 18
-
Update Angular CLI:
npm install -g @angular/cli@latest
-
Update your project:
ng update @angular/cli @angular/core
-
Migrate to Standalone Components:
ng generate @angular/cdk:standalone
-
Migrate to Signals (Optional but Recommended):
- Gradually convert component state to signals
- Use computed() for derived values
- Replace ngOnInit with constructor initialization
- Node.js 20+ is required
- NgModules are deprecated (but still supported)
- Some RxJS patterns may need updates
- Zone.js configuration changes
- β¨ Enhanced Signal APIs
- β¨ Improved standalone component support
- β¨ Better performance optimizations
- β¨ Enhanced developer tooling
- β¨ Improved testing utilities
Angular 22 represents the modern evolution of the Angular framework with standalone components, signals, and functional APIs becoming the standard. The framework has become more streamlined, performant, and developer-friendly.
- Start with Basics: Components, templates, data binding
- Master Signals: State management with signals
- Learn Routing: Standalone routing with functional guards
- Explore Forms: Reactive forms with signals
- Advanced Patterns: RxJS, performance optimization, testing
- Build Projects: The best way to solidify your knowledge
- β Use standalone components by default
- β Leverage signals for UI state
- β Use functional APIs (guards, interceptors, inject)
- β Implement OnPush change detection
- β Follow Angular style guide and best practices
- β Build real projects to master the concepts
Angular 22 is a powerful, modern framework that rewards developers who invest time in learning its concepts deeply. Start with the basics, practice with small projects, gradually move to intermediate topics, and finally master advanced patterns. Remember that the best way to learn is by building projects!
Happy coding! π