Cheatsheet Angular
Framework frontend da Google baseado em TypeScript
Angular
Setup e CLI
Instalación Global
npm install -g @angular/cli ng version ng new mi-proyecto cd mi-proyecto ng serve --open
Instala @angular/cli globalmente con npm install -g. El comando ng new crea el proyecto con toda la configuración. ng serve --open inicia el dev server en el puerto 4200 y abre el navegador automáticamente.
Estructura del Proyecto
mi-proyecto/ ├── src/ │ ├── app/ │ │ ├── app.component.ts │ │ ├── app.component.html │ │ ├── app.config.ts │ │ └── app.routes.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── angular.json ├── package.json └── tsconfig.json
La estructura moderna (v17+) tiene app.config.ts (providers) y app.routes.ts (rutas) en vez de app.module.ts. El main.ts hace el bootstrap con bootstrapApplication(). El angular.json configura builds, assets y budgets.
Environments
// src/environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api'
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.ejemplo.com'
};
// Uso: import { environment } from '../environments/environment';Los environments permiten configuraciones distintas por entorno. El ng build usa el archivo de producción automáticamente. En desarrollo, el ng serve usa el archivo base. Accede vía environment.apiUrl.
Estilos Globales
/* src/styles.css — global */
@import 'normalize.css';
:root {
--primary: #DD0031;
--spacing: 1rem;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
}El styles.css (definido en angular.json) contiene CSS global. Los estilos de los componentes están encapsulados por ViewEncapsulation.Emulated (default) — no afectan a otros componentes. Usa ViewEncapsulation.None para estilos globales en un componente.
ng new — Opciones
ng new app --routing ng new app --skip-tests ng new app --ssr ng new app --style=scss ng new app --standalone
ng new acepta flags: --routing añade routing, --style=scss define el preprocesador CSS, --ssr activa Server-Side Rendering con Hydration. Desde Angular 17, los proyectos son standalone por defecto.
main.ts — Bootstrap
import { bootstrapApplication }
from '@angular/platform-browser';
import { AppComponent }
from './app/app.component';
import { appConfig }
from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));El main.ts es el punto de entrada. bootstrapApplication() inicializa el componente raíz con la configuración global. Sustituye al antiguo platformBrowserDynamic().bootstrapModule() de los proyectos con NgModule.
Comandos CLI Útiles
ng serve --port 3000 ng serve --proxy-config proxy.conf.json ng lint ng test ng e2e ng update @angular/core ng add @angular/material ng deploy
El CLI tiene comandos para el día a día: ng serve --port cambia el puerto, ng add instala y configura libraries (ej: @angular/material), ng update actualiza dependencias con migrations automáticas.
Assets y Archivos Estáticos
// angular.json
"assets": [
"src/favicon.ico",
"src/assets",
{ "glob": "**/*", "input": "src/i18n", "output": "/i18n" }
]
// Acceso en el template
<img src="assets/logo.png">
<img [src]="'assets/' + image">La carpeta src/assets/ se copia al build. Configúrala en angular.json en el array assets. Puedes añadir objetos con glob, input y output para mapear carpetas externas. En el template, usa rutas relativas a assets/.
ng generate
ng generate component components/header ng g c components/header ng g s services/api ng g d directives/highlight ng g p pipes/truncate ng g g guards/auth ng g interceptor interceptors/token
ng generate (atajo ng g) crea archivos con boilerplate. La ruta define la carpeta: ng g c components/header lo crea en src/app/components/header/. Todos los artefactos son standalone por defecto desde Angular 17.
app.config.ts
import { ApplicationConfig } 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(),
]
};El app.config.ts define los providers globales usando funciones provide*: provideRouter() para rutas, provideHttpClient() para HTTP, provideAnimations() para animaciones. Sustituye al array imports del antiguo NgModule.
TypeScript Config
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ES2022",
"experimentalDecorators": true,
"paths": {
"@app/*": ["src/app/*"],
"@env/*": ["src/environments/*"]
}
}
}El tsconfig.json configura TypeScript. strict: true activa todas las verificaciones. experimentalDecorators es necesario para los decorators @Component, @Injectable, etc. paths permite aliases de import como @app/.
Build y Deploy
ng build ng build --watch ng build --configuration production # Output en dist/ # npx serve dist/mi-proyecto/browser
ng build compila a dist/. En producción aplica AOT compilation, tree-shaking, minificación y budget checks (avisos si el bundle excede los límites definidos en angular.json).
angular.json — Configuración
{
"projects": {
"mi-proyecto": {
"architect": {
"build": {
"options": {
"outputPath": "dist/mi-proyecto",
"index": "src/index.html",
"main": "src/main.ts",
"styles": ["src/styles.css"],
"assets": ["src/favicon.ico", "src/assets"]
}
}
}
}
}
}El angular.json configura builds, serve, tests y assets. En build.options define styles (CSS global), assets (archivos estáticos) y budgets (límites de tamaño). El serve hereda de la configuración de build.
Path Aliases
// tsconfig.json
"paths": {
"@components/*": ["src/app/components/*"],
"@services/*": ["src/app/services/*"],
"@models/*": ["src/app/models/*"]
}
// Uso en los imports
import { HeaderComponent } from '@components/header/header.component';
import { ApiService } from '@services/api.service';Los path aliases simplifican imports anchos. Defínelos en tsconfig.json en la sección compilerOptions.paths. En vez de ../../services/api.service, usa @services/api.service. El Angular CLI resuelve los aliases automáticamente en el build.
Componentes
Componente Standalone
import { Component } from '@angular/core';
@Component({
selector: 'app-header',
standalone: true,
imports: [],
templateUrl: './header.component.html',
styleUrl: './header.component.css'
})
export class HeaderComponent {
title = 'Mi App';
}Un componente standalone no necesita NgModule. El decorator @Component define el selector (tag HTML), el template y los estilos. La propiedad imports lista las dependencias del componente (otros componentes, pipes, directivas).
Ciclo de Vida
import { OnInit, OnChanges, OnDestroy, SimpleChanges }
from '@angular/core';
export class UserComponent
implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges) {
console.log(changes['userId']);
}
ngOnInit() {
this.loadData();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}Los hooks de ciclo de vida: ngOnChanges (los inputs cambian), ngOnInit (inicialización), ngDoCheck (detección manual), ngAfterViewInit (view lista), ngOnDestroy (limpieza). Implementa las interfaces correspondientes.
Host Binding y Host Listener
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({
selector: 'app-clickable',
template: `<ng-content />`
})
export class ClickableComponent {
@HostBinding('class.active') active = false;
@HostBinding('attr.role') role = 'button';
@HostListener('click')
onClick() { this.active = !this.active; }
@HostListener('document:keydown.escape')
onEscape() { this.active = false; }
}@HostBinding enlaza una propiedad del componente a un atributo/clase del elemento host. @HostListener escucha eventos en el host o en el documento (document:keydown.escape). Alternativa moderna: usar la propiedad host en el decorator @Component.
model() — Two-Way Binding
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `
<button (click)="active.update(v => !v)">
{{ active() ? 'ON' : 'OFF' }}
</button>
`
})
export class ToggleComponent {
active = model(false);
}
// Padre: <app-toggle [(active)]="on" />model() crea un signal con two-way binding automático. El hijo lee con active() y actualiza con active.set() o active.update(). El padre usa [(active)]="variable" — la sintaxis "banana in a box" funciona automáticamente con model().
Template y Style Inline
@Component({
selector: 'app-badge',
standalone: true,
template: `
<span class="badge" [class.active]="active">
{{ text }}
</span>
`,
styles: `
.badge { padding: 4px 8px; border-radius: 4px; }
.active { background: #DD0031; color: white; }
`
})
export class BadgeComponent {
text = '';
active = false;
}Para componentes pequeños, usa template y styles inline (con template literals). Evita archivos separados. El styleUrl (singular) es la forma moderna; styleUrls (array) todavía funciona pero está deprecated.
ng-content — Proyección
@Component({
selector: 'app-card',
template: `
<div class="card">
<header><ng-content select="h2" /></header>
<div class="body"><ng-content /></div>
<footer><ng-content select=".actions" /></footer>
</div>
`
})
// Uso:
// <app-card>
// <h2>Título</h2>
// <p>Contenido principal</p>
// <div class="actions"><button>OK</button></div>
// </app-card><ng-content> proyecta contenido del padre al hijo (slot). El atributo select filtra por selector CSS: select="h2" capta headings, select=".actions" capta elementos con esa clase. Sin select, capta todo lo que no fue distribuido.
Host Metadata (moderno)
@Component({
selector: 'app-btn',
template: `<ng-content />`,
host: {
'class': 'btn',
'[class.disabled]': 'disabled',
'[attr.aria-disabled]': 'disabled',
'(click)': 'onClick()',
'(mouseenter)': 'hover = true',
'(mouseleave)': 'hover = false'
}
})
export class BtnComponent {
disabled = false;
hover = false;
onClick() { /* ... */ }
}La propiedad host en el decorator sustituye a @HostBinding y @HostListener. Usa [class.x] para clases, [attr.x] para atributos y (evento) para listeners. Más declarativo y sin decorators extra.
Query con viewChild()
import { Component, viewChild, viewChildren, ElementRef }
from '@angular/core';
@Component({
selector: 'app-list',
template: `<input #field /><p *ngFor="let i of items">{{ i }}</p>`
})
export class ListComponent {
field = viewChild.required<ElementRef>('field');
paragraphs = viewChildren<ElementRef>('p');
focus() {
this.field().nativeElement.focus();
}
}viewChild() y viewChildren() son la alternativa signal-based a @ViewChild. Retornan signals: lee con field(). viewChild.required() lanza error si no encuentra. No necesita ngAfterViewInit — el signal se actualiza automáticamente.
@Input — Recibir Datos
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-card',
template: `<h2>{{ title }}</h2><p>{{ description }}</p>`
})
export class CardComponent {
@Input() title = '';
@Input() description = '';
@Input({ required: true }) id!: number;
}
// Uso: <app-card [title]="t" [id]="1" />@Input() declara propiedades que el padre puede enlazar con [propiedad]="valor". @Input({ required: true }) hace el input obligatorio — error de compilación si no se proporciona. El ! (non-null assertion) indica que será inicializado externamente.
@ViewChild y @ContentChild
import { ViewChild, ContentChild, ElementRef, AfterViewInit }
from '@angular/core';
export class ParentComponent implements AfterViewInit {
@ViewChild('inputRef') input!: ElementRef;
@ViewChild(ChildComponent) child!: ChildComponent;
@ContentChild('projected') projected!: ElementRef;
ngAfterViewInit() {
this.input.nativeElement.focus();
this.child.publicMethod();
}
}@ViewChild accede a elementos/componentes del propio template. @ContentChild accede a contenido proyectado vía ng-content. Usa #ref en el template como referencia. Solo están disponibles después de ngAfterViewInit / ngAfterContentInit.
Componentes Dinámicos
import { ViewChild, ViewContainerRef } from '@angular/core';
import { AlertComponent } from './alert.component';
@Component({ template: `<ng-container #container />` })
export class HostComponent {
@ViewChild('container', { read: ViewContainerRef })
container!: ViewContainerRef;
creupTolert(message: string) {
const ref = this.container.createComponent(AlertComponent);
ref.setInput('message', message);
ref.instance.closed.subscribe(() => ref.destroy());
}
}ViewContainerRef permite crear componentes dinámicamente con createComponent(). Usa setInput() para definir inputs e instance para acceder a outputs. ref.destroy() elimina el componente. Útil para modales, tooltips y notificaciones.
@Output — Emitir Eventos
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-search',
template: `<input (input)="onInput($event)">`
})
export class SearchComponent {
@Output() searched = new EventEmitter<string>();
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.searched.emit(value);
}
}
// Padre: <app-search (searched)="search($event)" />@Output() expone eventos con EventEmitter. El hijo llama a .emit(valor) y el padre escucha con (evento)="handler($event)". El $event contiene el valor emitido. Combínalo con @Input para comunicación bidireccional.
Signal Inputs (v17.1+)
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-user',
template: `
<p>{{ name() }} ({{ age() }})</p>
<button (click)="removed.emit()">X</button>
`
})
export class UserComponent {
name = input.required<string>();
age = input(0);
removed = output<void>();
}
// Uso: <app-user [name]="'Ana'" [age]="25" />Los signal inputs (input()) son la alternativa moderna a @Input(). Son signals: lee con name(). input.required<T>() lo hace obligatorio. output() sustituye a @Output() + EventEmitter. Más type-safe y sin decorators.
Encapsulamiento de Estilos
import { ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-global',
template: `...`,
styles: `p { color: red; }`,
encapsulation: ViewEncapsulation.None
})
// Opciones:
// Emulated (default) — estilos aislados con _ngcontent
// None — estilos globales (sin aislamiento)
// ShadowDom — usa Shadow DOM nativaViewEncapsulation.Emulated (default) añade atributos _ngcontent-xxx para aislar estilos. None hace los estilos globales. ShadowDom usa la Shadow DOM nativa del navegador. Para estilizar contenido proyectado, usa :host ::ng-deep (deprecated).
Template e Sintaxe
Interpolación
<p>{{ title }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ user?.name }}</p>
<p>{{ items.length }}</p>
<p>{{ active ? 'Sí' : 'No' }}</p>
<p>{{ name | uppercase }}</p>La interpolación {{ }} renderiza expresiones en el template. Soporta propiedades, operadores ternarios, optional chaining (?.) y pipes. No permite declaraciones de variables, acceso a window/document ni efectos secundarios.
@if — Condicional (v17+)
@if (user) {
<p>Hola, {{ user.name }}</p>
} @else if (loading) {
<p>Cargando...</p>
} @else {
<p>Sin usuario</p>
}
@if (data$ | async; as data) {
<ul>
@for (item of data; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
}La nueva sintaxis @if (v17+) sustituye a *ngIf. Soporta @else if y @else. La forma @if (expr; as var) crea una variable local con el valor. No necesita <ng-container> — el bloque no crea un elemento extra en el DOM.
Template Reference Variables
<input #nameField> <button (click)="greet(nameField.value)">Saludar</button> <app-form #form> <button (click)="form.validate()">Validar</button> </app-form> <input #email="ngModel" [(ngModel)]="mail" required> <p *ngIf="email.invalid">Email inválido</p>
Las variables de referencia #nombre dan acceso directo a elementos, componentes o directivas en el template. #field referencia el HTMLElement. #comp referencia la instancia del componente. #x="ngModel" referencia la directiva NgModel con su estado de validación.
Eventos con $event
<input (input)="onInput($event)">
// onInput(e: Event) {
// const v = (e.target as HTMLInputElement).value;
// }
<div (click)="onClick($event)">
// onClick(e: MouseEvent) {
// e.preventDefault();
// }
<button (click)="save()">Guardar</button>El $event contiene el objeto del evento nativo. Para (input) es Event, para (click) es MouseEvent, para (keydown) es KeyboardEvent. Si no necesitas el evento, omite el $event. Para outputs personalizados, $event es el valor emitido por el EventEmitter.
Property Binding
<img [src]="imageUrl"> <button [disabled]="!form.valid"> <div [innerHTML]="safeHtml"> <td [attr.colspan]="columnCount"> <button [attr.aria-label]="description"> <div [class.active]="isActive"> <div [style.color]="textColor"> <div [style.width.px]="width">
[propiedad]="expresion" enlaza una propiedad DOM a una expresión. Para atributos HTML que no son propiedades, usa [attr.x]. Para clases: [class.nombre]="booleano". Para estilos: [style.prop]="valor" con unidad opcional (.px, .%).
@for — Iteración (v17+)
@for (item of items; track item.id;
let i = $index, first = $first,
last = $last, count = $count) {
<li [class.first]="first">
{{ i + 1 }}/{{ count }}: {{ item.name }}
</li>
} @empty {
<li>Lista vacía</li>
}@for sustituye a *ngFor. El track es obligatorio (mejor performance que trackBy). El bloque @empty se renderiza cuando la lista está vacía. Variables implícitas: $index, $first, $last, $even, $odd, $count.
ng-container
<ng-container *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</ng-container>
@if (user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}
<ng-container *ngTemplateOutlet="headerTmpl">
</ng-container><ng-container> agrupa elementos sin renderizar un elemento extra en el DOM. Útil con *ngIf y *ngFor cuando necesitas aplicarlos a múltiples elementos. Con la nueva sintaxis @if/@for, el ng-container es menos necesario.
Sintaxis Antigua vs Nueva
<!-- *ngIf (antiguo) -->
<p *ngIf="active">Visible</p>
<!-- @if (nuevo, v17+) -->
@if (active) { <p>Visible</p> }
<!-- *ngFor (antiguo) -->
<li *ngFor="let x of items; let i = index">
<!-- @for (nuevo) -->
@for (x of items; track x.id; let i = $index) {
<li>{{ x }}</li>
}La nueva sintaxis de control (@if, @for, @switch) es más legible, no necesita <ng-container> y tiene mejor performance. El track es obligatorio en el @for. La sintaxis antigua (*ngIf, *ngFor) todavía funciona pero se desaconseja en proyectos nuevos.
Event Binding
<button (click)="save()">Guardar</button> <input (input)="onInput($event)"> <form (ngSubmit)="submit()"> <input (keydown.enter)="search()"> <input (keydown.control.s)="save($event)"> <div (scroll)="onScroll($event)"> <input (blur)="validate()"> <select (change)="onChange($event)">
(evento)="handler()" escucha eventos del DOM. El $event pasa el objeto del evento. Para teclas específicas: (keydown.enter), (keydown.escape). Modificadores: (keydown.control.s). El (ngSubmit) previene el reload de la página.
@switch — Múltiples Condiciones
@switch (status) {
@case ('active') {
<span class="green">Activo</span>
}
@case ('pending') {
<span class="yellow">Pendiente</span>
}
@case ('inactive') {
<span class="red">Inactivo</span>
}
@default {
<span class="gray">Desconocido</span>
}
}@switch sustituye múltiples *ngIf / ngSwitch. Cada @case compara con el valor de la expresión. @default es el fallback. Más legible y performante que cadenas de @if / @else if para valores discretos.
ng-template y ngTemplateOutlet
<ng-template #loading>
<p>Cargando...</p>
</ng-template>
<ng-template #itemTmpl let-item="data">
<li>{{ item.name }}</li>
</ng-template>
<ng-container
*ngTemplateOutlet="isLoading ? loading : null">
</ng-container>
<ng-container *ngTemplateOutlet="itemTmpl;
context: { data: product }">
</ng-container><ng-template> define un template que no se renderiza hasta ser instanciado. *ngTemplateOutlet renderiza el template con un context opcional. let-x="clave" declara variables locales que reciben valores del contexto. Esencial para templates personalizables en bibliotecas.
Two-Way Binding
<!-- Con ngModel (FormsModule) -->
<input [(ngModel)]="user.name">
<!-- Equivalente expandido: -->
<input [ngModel]="user.name"
(ngModelChange)="user.name = $event">
<!-- Con signal model() (v17.1+): -->
<app-toggle [(active)]="on" />
<!-- Equivalente: -->
<app-toggle [active]="on"
(activeChange)="on = $event" />El two-way binding [(x)]="var" combina property binding [x] con event binding (xChange). Con ngModel requiere FormsModule. Con model() signals, funciona automáticamente sin módulos. La convención es: input x + output xChange.
ngClass y ngStyle
<div [ngClass]="{
'active': isActive,
'highlight': focused,
'error': hasError
}">
<div [ngClass]="['base', theme, size]">
<div [ngStyle]="{
'color': textColor,
'font-size.px': fontSize,
'background-color': background
}">[ngClass] aplica múltiples clases condicionalmente (objeto, array o string). [ngStyle] aplica múltiples estilos inline. Para una única clase/estilo, prefiere [class.x] y [style.x] — son más performantes.
Safe Navigation y Non-Null
<p>{{ user?.address?.city }}</p>
<p>{{ user!.name }}</p>
<p>{{ user?.name ?? 'Anónimo' }}</p>
<p>{{ (user$ | async)?.name }}</p>El operador ?. (safe navigation) previene errores cuando la propiedad puede ser null/undefined. El ! (non-null assertion) le dice a TypeScript que el valor existe. Combínalo con ?? (nullish coalescing) para valores default. Con el async pipe, usa paréntesis: (x$ | async)?.prop.
Serviços e DI
Crear Servicio
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserService {
private users = ['Ana', 'Bruno'];
getUsers(): string[] {
return [...this.users];
}
addUser(name: string): void {
this.users.push(name);
}
}@Injectable({ providedIn: 'root' }) crea un singleton global — una instancia compartida por toda la app. El servicio encapsula lógica reutilizable (API, estado, utilidades). El providedIn: 'root' permite tree-shaking: si ningún componente lo usa, se elimina del bundle.
InjectionToken
import { InjectionToken, inject } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
// Provider
providers: [
{ provide: API_URL, useValue: 'https://api.com' }
]
// Consumo
export class ApiService {
private apiUrl = inject(API_URL);
}InjectionToken crea tokens para valores que no son clases (strings, configs, objetos). Defínelo con new InjectionToken<T>('NOMBRE'). Provéelo con { provide: TOKEN, useValue: valor }. Consúmelo con inject(TOKEN). Esencial para bibliotecas configurables.
toSignal y toObservable
import { toSignal, toObservable } from '@angular/rxjs-interop';
users$ = this.http.get<User[]>('/api/users');
users = toSignal(this.users$, { initialValue: [] });
filter = signal('');
filter$ = toObservable(this.filter);
results$ = this.filter$.pipe(
debounceTime(300),
switchMap(f => this.search(f))
);toSignal() convierte un Observable en signal (con initialValue opcional). toObservable() convierte un signal en Observable — útil para usar operadores RxJS con signals. Ambos son de @angular/rxjs-interop. Facilitan la migración gradual de RxJS a signals.
inject() — Inyección Moderna
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private logger = inject(LoggerService);
}
// En componentes:
// private userService = inject(UserService);inject() es la forma moderna de inyección de dependencias. Funciona en componentes, servicios, guards, interceptors y funciones. Sustituye la inyección vía constructor. Ventajas: menos código, funciona en funciones standalone y simplifica la herencia (sin super()).
useFactory y useExisting
{ provide: ApiService, useFactory: () => {
const env = inject(EnvironmentService);
return new ApiService(env.apiUrl, env.debug);
}
}
{ provide: AbstractLogger, useExisting: ConsoleLogger }
{ provide: LoggerService, useClass: ProductionLogger }Los providers pueden usar: useValue (valor estático), useClass (sustituir implementación), useFactory (crear dinámicamente con lógica), useExisting (alias). useFactory puede usar inject() internamente para acceder a otras dependencias.
takeUntilDestroyed
import { takeUntilDestroyed } from '@angular/rxjs-interop';
@Component({ ... })
export class ListComponent {
users = toSignal(
this.http.get<User[]>('/api/users')
.pipe(takeUntilDestroyed()),
{ initialValue: [] }
);
ngOnInit() {
this.service.data$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(d => this.data = d);
}
}takeUntilDestroyed() hace unsubscribe automáticamente cuando el componente/servicio es destruido. Sustituye el patrón manual Subject + takeUntil + ngOnDestroy. Fuera del contexto de inyección, pasa DestroyRef como argumento. De @angular/rxjs-interop.
Inyección vía Constructor
import { Component } from '@angular/core';
import { UserService } from '../services/user.service';
@Component({ selector: 'app-list', template: `...` })
export class ListComponent {
constructor(
private userService: UserService,
private http: HttpClient
) {}
ngOnInit() {
this.users = this.userService.getUsers();
}
}La inyección vía constructor es la forma clásica. Angular resuelve las dependencias automáticamente por el tipo. private crea la propiedad automáticamente. Todavía funciona, pero inject() es preferido en código nuevo por ser más flexible y funcionar fuera de clases.
Servicio con Estado (Signal Store)
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
readonly cartItems = this.items.asReadonly();
readonly total = computed(() =>
this.items().reduce((s, i) => s + i.price, 0)
);
readonly count = computed(() => this.items().length);
addItem(item: CartItem) {
this.items.update(list => [...list, item]);
}
clear() { this.items.set([]); }
}Un patrón moderno es usar signals como store: estado privado con signal(), expuesto como asReadonly(). computed() deriva valores. Los métodos públicos modifican con .set() / .update(). Sustituye a BehaviorSubject y NgRx para estado simple.
Resolver (Route Data)
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
export const userResolver: ResolveFn<User> = (route) => {
const service = inject(UserService);
const id = Number(route.paramMap.get('id'));
return service.getUser(id);
};
// app.routes.ts
{ path: 'user/:id', component: UserComponent,
resolve: { user: userResolver } }Un ResolveFn precarga datos antes de activar la ruta. El componente accede vía route.data['user']. La forma funcional con inject() sustituye la clase con Resolve. Si el resolver retorna un Observable, Angular espera hasta que complete antes de navegar.
Providers Jerárquicos
@Injectable({ providedIn: 'root' })
@Component({ providers: [FormService] })
@Injectable({ providedIn: 'platform' })
constructor(@Self() private svc: MyService) {}
constructor(@Optional() private svc: MyService) {}
constructor(@SkipSelf() private svc: MyService) {}Angular tiene inyección jerárquica: providedIn: 'root' crea un singleton global. providers en el componente crea una instancia por componente. @Self() restringe al injector actual. @Optional() no lanza error si no encuentra. @SkipSelf() búsqueda en el padre.
Servicio HTTP Completo
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private apiUrl = inject(API_URL);
getUsers() {
return this.http.get<User[]>(`${this.apiUrl}/users`);
}
createUser(data: CreateUserDto) {
return this.http.post<User>(`${this.apiUrl}/users`, data);
}
deleteUser(id: number) {
return this.http.delete<void>(`${this.apiUrl}/users/${id}`);
}
}Un servicio HTTP encapsula todas las llamadas a la API. Usa inject(HttpClient) e inject(API_URL). Cada método retorna un Observable<T> tipado. El componente se suscribe o usa el async pipe. Centraliza URLs, manejo de errores e interceptors.
APP_INITIALIZER
import { APP_INITIALIZER, inject } from '@angular/core';
export function initApp() {
const config = inject(ConfigService);
return () => config.load();
}
providers: [
{ provide: APP_INITIALIZER, useFactory: initApp, multi: true }
]APP_INITIALIZER ejecuta código antes de que la app arranque. Útil para cargar configuraciones remotas, verificar auth o inicializar SDKs. multi: true permite múltiples initializers. Si la factory retorna una Promise u Observable, el bootstrap espera hasta que resuelva.
HTTP e Interceptors
provideHttpClient
import { provideHttpClient, withInterceptors, withFetch }
from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor]),
withFetch()
)
]
};provideHttpClient() activa el HttpClient en la app. withInterceptors() registra interceptors funcionales. withFetch() (v15+) usa la Fetch API nativa en vez de XMLHttpRequest. Sin esto, inyectar HttpClient lanza el error "No provider for HttpClient".
Interceptor de Errores
import { HttpInterceptorFn, HttpErrorResponse }
from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) inject(Router).navigate(['/login']);
else if (error.status === 0) console.error('Sin conexión');
else console.error(`Error ${error.status}:`, error.message);
return throwError(() => error);
})
);
};Un interceptor de errores usa catchError en el pipe. HttpErrorResponse contiene status, message y error. El status 0 indica fallo de red/CORS. Siempre relanza con throwError(() => error) para que el subscriber lo trate.
Descarga de Archivos
download(id: number) {
this.http.get(`/api/files/${id}`, {
responseType: 'blob', observe: 'response'
}).subscribe(res => {
const blob = res.body!;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'download.pdf';
a.click();
URL.revokeObjectURL(url);
});
}Para descargas, usa responseType: 'blob'. Crea un URL.createObjectURL() y un enlace temporal con a.click(). El nombre del archivo viene del header Content-Disposition. URL.revokeObjectURL() libera la memoria. Funciona para PDFs, imágenes, Excel, etc.
GET con Tipado
import { HttpClient, HttpParams } from '@angular/common/http';
export class UserService {
private http = inject(HttpClient);
getUsers(filter?: string) {
let params = new HttpParams();
if (filter) params = params.set('q', filter);
return this.http.get<User[]>('/api/users', { params });
}
getUser(id: number) {
return this.http.get<User>(`/api/users/${id}`);
}
}http.get<T>() retorna un Observable<T> tipado. HttpParams construye query strings de forma inmutable (.set() retorna nueva instancia). El tipo es solo compile-time — Angular no valida la respuesta en runtime. Usa interfaces para los modelos de datos.
Manejo de Errores
getUsers() {
return this.http.get<User[]>('/api/users').pipe(
retry(3),
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
if (error.error?.message)
return throwError(() => new Error(error.error.message));
return throwError(() => new Error('Error en el servidor'));
}
// En el componente:
this.service.getUsers().subscribe({
next: users => this.users = users,
error: err => this.error = err.message
});retry(n) reintenta en caso de error. catchError transforma el error. En el componente, usa el observer con next y error. Accede al mensaje de la API con error.error?.message. Para errores globales, usa un interceptor de errores.
Cache Simple
@Injectable({ providedIn: 'root' })
export class CachedService {
private http = inject(HttpClient);
private cache = new Map<string, any>();
get<T>(url: string): Observable<T> {
if (this.cache.has(url)) return of(this.cache.get(url));
return this.http.get<T>(url).pipe(
tap(data => this.cache.set(url, data))
);
}
invalidate(url?: string) {
if (url) this.cache.delete(url);
else this.cache.clear();
}
}Un cache simple usa un Map para almacenar respuestas. Si la URL ya está en cache, retorna of(valor) (Observable síncrono). tap() almacena la respuesta tras el primer request. invalidate() limpia el cache. Para caches avanzados, usa HttpInterceptorFn o bibliotecas como @ngneat/cashew.
POST, PUT, DELETE
createUser(data: CreateUserDto) {
return this.http.post<User>('/api/users', data);
}
updateUser(id: number, data: User) {
return this.http.put<User>(`/api/users/${id}`, data);
}
patchUser(id: number, data: Partial<User>) {
return this.http.patch<User>(`/api/users/${id}`, data);
}
deleteUser(id: number) {
return this.http.delete<void>(`/api/users/${id}`);
}http.post() envía datos en el body. http.put() sustituye el recurso completo. http.patch() actualiza parcialmente. http.delete() elimina. Todos aceptan un tipo genérico para la respuesta. El body se serializa como JSON automáticamente.
Subida de Archivos
upload(file: File) {
const formData = new FormData();
formData.append('file', file, file.name);
return this.http.post<{ url: string }>('/api/upload', formData);
}
uploadWithProgress(file: File) {
const req = new HttpRequest('POST', '/api/upload', formData,
{ reportProgress: true });
return this.http.request(req).pipe(
filter(e => e.type === HttpEventType.UploadProgress),
map(e => Math.round(100 * e.loaded / e.total!))
);
}Para subidas, usa FormData — Angular define el Content-Type: multipart/form-data automáticamente. Para el progreso, crea un HttpRequest con reportProgress: true y filtra eventos HttpEventType.UploadProgress.
Requests Paralelos
import { forkJoin } from 'rxjs';
loadDashboard() {
return forkJoin({
users: this.http.get<User[]>('/api/users'),
posts: this.http.get<Post[]>('/api/posts'),
stats: this.http.get<Stats>('/api/stats')
});
}
this.service.loadDashboard()
.subscribe(({ users, posts, stats }) => {
this.users = users;
this.posts = posts;
this.stats = stats;
});forkJoin ejecuta múltiples Observables en paralelo y emite cuando todos completan. Acepta un objeto con claves nombradas — la respuesta tiene la misma estructura. Si uno falla, el error se propaga (usa catchError en cada uno para resiliencia). Ideal para dashboards y páginas con múltiples fuentes.
Interceptor Funcional
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.getToken();
if (token) {
const cloned = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(cloned);
}
return next(req);
};Los interceptors funcionales (HttpInterceptorFn) sustituyen clases con HTTP_INTERCEPTORS. Reciben req y next. req.clone() crea una copia modificada (los requests son inmutables). next(req) pasa al siguiente interceptor. Regístralo con withInterceptors([]).
Headers y Opciones
import { HttpHeaders } from '@angular/common/http';
const headers = new HttpHeaders()
.set('X-Api-Key', 'abc123')
.set('Accept-Language', 'es-ES');
this.http.get('/api/data', { headers });
this.http.get('/api/data', { observe: 'response' })
.subscribe(res => {
console.log(res.status);
console.log(res.headers.get('X-Total'));
console.log(res.body);
});
this.http.get('/api/csv', { responseType: 'text' });HttpHeaders es inmutable — .set() retorna nueva instancia. observe: 'response' da acceso a la respuesta completa (status, headers, body). responseType: 'text' retorna string en vez de JSON. responseType: 'blob' para archivos binarios.
Base URL y Proxy
// Opción 1: environment
this.http.get(`${environment.apiUrl}/users`)
// Opción 2: proxy.conf.json (dev)
{ "/api": { "target": "http://localhost:3000",
"secure": false, "changeOrigin": true } }
// ng serve --proxy-config proxy.conf.json
// Opción 3: Interceptor con baseUrl
const cloned = req.clone({ url: `${baseUrl}${req.url}` });Para la base URL: usa environment.apiUrl (simple), proxy.conf.json en dev (evita CORS), o un interceptor que prefija todas las URLs. El proxy solo funciona con ng serve — en producción, configura el servidor web (nginx, Apache) o usa CORS en el backend.
Avançado e Performance
Change Detection — OnPush
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
export class ListComponent {
@Input() items: Item[] = [];
}
// OnPush solo verifica cuando:
// 1. Un input cambia (referencia)
// 2. Un evento del componente dispara (click, etc.)
// 3. Un Observable con async pipe emite
// 4. Un signal cambiaChangeDetectionStrategy.OnPush optimiza el change detection: solo verifica cuando los inputs cambian (nueva referencia), los eventos del componente disparan, el pipe async emite o los signals cambian. Con signals, OnPush es el default implícito. Usa siempre OnPush en componentes nuevos.
ContentChildren y QueryList
import { ContentChildren, QueryList,
AfterContentInit } from '@angular/core';
@Component({ selector: 'app-tabs' })
export class TabsComponent implements AfterContentInit {
@ContentChildren(TabComponent)
tabs!: QueryList<TabComponent>;
ngAfterContentInit() {
this.tabs.first.active = true;
this.tabs.changes.subscribe(() => {
// Se añadieron/eliminaron tabs
});
}
}
// <app-tabs>
// <app-tab title="A">...</app-tab>
// <app-tab title="B">...</app-tab>
// </app-tabs>@ContentChildren consulta todos los componentes/directivas proyectados vía ng-content. QueryList es una colección reactiva — .changes emite cuando la lista cambia. .first, .last, .toArray() acceden a los ítems. Útil para componentes compuestos (tabs, accordion, steps).
Buenas Prácticas
// 1. Componentes pequeños y enfocados // 2. Servicios para lógica de negocio // 3. OnPush + signals (sin mutaciones) // 4. takeUntilDestroyed() en subscriptions // 5. Lazy loading para secciones grandes // 6. @defer para componentes pesados // 7. track obligatorio en @for // 8. inject() en vez de constructor // 9. input()/output() en vez de decorators // 10. Reactive Forms para forms complejos
Buenas prácticas Angular: componentes pequeños con OnPush, servicios para lógica, signals para estado, takeUntilDestroyed() para subscriptions, lazy loading y @defer para rendimiento, inject() y signal inputs/outputs para código moderno. Evita mutaciones y any.
@defer — Lazy Load de Componentes
@defer (on viewport) {
<app-chart [data]="data" />
} @placeholder {
<p>Cargando gráfico...</p>
} @loading (after 200ms) {
<app-spinner />
} @error {
<p>Error al cargar</p>
}
// Triggers:
// on viewport — cuando es visible
// on idle — cuando el navegador está libre
// on timer(3s) — tras 3 segundos
// on hover — al pasar el ratón
// when condition — cuando la condición es true@defer (v17+) hace lazy load de componentes en el template. El bloque @placeholder se muestra antes, @loading durante la carga (con after 200ms para evitar flicker), @error en caso de fallo. Triggers: on viewport, on idle, on timer(), on hover, when. Reduce el bundle inicial.
ngZone y Rendimiento
import { NgZone } from '@angular/core';
export class ChartComponent {
private zone = inject(NgZone);
initChart() {
// Fuera del Angular zone (sin change detection)
this.zone.runOutsideAngular(() => {
this.chart = new Chart(this.canvas, {
// callbacks frecuentes (mousemove, etc.)
onHover: () => { /* no dispara CD */ }
});
});
}
update(data: any) {
// Vuelve al zone para actualizar el template
this.zone.run(() => {
this.data = data;
});
}
}NgZone controla el change detection. runOutsideAngular() ejecuta código sin disparar CD — ideal para listeners frecuentes (scroll, mousemove, WebSocket). run() vuelve al zone para actualizar el template. Con signals, la necesidad de NgZone disminuye. Úsalo para bibliotecas externas (charts, maps, editors).
Estructura de Carpetas (Feature-Based)
src/app/ ├── core/ # singleton services, guards │ ├── services/ │ └── interceptors/ ├── shared/ # componentes/pipes/directivas reutilizables │ ├── components/ │ ├── pipes/ │ └── directives/ ├── features/ # módulos por funcionalidad │ ├── users/ │ │ ├── users.routes.ts │ │ ├── users.component.ts │ │ └── components/ │ └── dashboard/ └── app.routes.ts
Estructura feature-based: core/ para singletons (servicios, guards, interceptors), shared/ para componentes reutilizables, features/ para funcionalidades aisladas con sus rutas. Cada feature es lazy-loaded. Evita la estructura plana por tipo (components/, services/) que no escala.
SSR e Hydration
// Crear proyecto con SSR:
ng new app --ssr
// app.config.ts
import { provideClientHydration }
from '@angular/platform-browser';
providers: [
provideClientHydration()
]
// app.config.server.ts
import { provideServerRendering }
from '@angular/platform-server';
// El HTML se renderiza en el servidor
// y se hidrata en el cliente (sin re-render)SSR (Server-Side Rendering) renderiza HTML en el servidor para mejor SEO y First Contentful Paint. provideClientHydration() activa hydration — Angular reutiliza el DOM del servidor en vez de re-renderizar. --ssr en ng new lo configura todo. Requiere Node.js en el servidor.
Tests Unitarios
import { TestBed } from '@angular/core/testing';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
UserService,
{ provide: HttpClient, useValue: httpMock }
]
});
service = TestBed.inject(UserService);
});
it('debe retornar users', () => {
const users = service.getUsers();
expect(users.length).toBe(2);
});
it('debe añadir user', () => {
service.addUser('Carlos');
expect(service.getUsers()).toContain('Carlos');
});
});TestBed configura el módulo de tests. TestBed.configureTestingModule() define providers e imports. TestBed.inject() obtiene servicios. Usa useValue para mocks. ng test ejecuta con Karma/Jasmine. Para componentes, usa TestBed.createComponent() y fixture.detectChanges().
Seguridad (DomSanitizer)
import { DomSanitizer, SafeHtml, SafeUrl }
from '@angular/platform-browser';
export class SafeContentComponent {
private sanitizer = inject(DomSanitizer);
safeHtml: SafeHtml;
safeUrl: SafeUrl;
constructor() {
this.safeHtml = this.sanitizer
.bypassSecurityTrustHtml('<b>HTML confiable</b>');
this.safeUrl = this.sanitizer
.bypassSecurityTrustUrl('https://example.com');
}
}
// <div [innerHTML]="safeHtml"></div>Angular sanitiza HTML automáticamente para prevenir XSS. Para HTML confiable, usa DomSanitizer.bypassSecurityTrustHtml(). Métodos: bypassSecurityTrustHtml, bypassSecurityTrustUrl, bypassSecurityTrustResourceUrl, bypassSecurityTrustScript. Úsalo con extremo cuidado — solo para contenido 100% confiable.
Animaciones
import { trigger, state, style,
transition, animate } from '@angular/animations';
@Component({
animations: [
trigger('fade', [
state('visible', style({ opacity: 1 })),
state('hidden', style({ opacity: 0 })),
transition('visible <=> hidden',
animate('300ms ease-in-out'))
])
]
})
export class FadeComponent {
state = 'visible';
}
// <div [@fade]="state">Contenido</div>Las animaciones de Angular usan trigger, state y transition. state define estilos por estado. transition define la animación entre estados (=>, <=>). Aplícala con [@trigger]="state". Requiere provideAnimations() o provideAnimationsAsync().
Tests de Componente
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('CounterComponent', () => {
let fixture: ComponentFixture<CounterComponent>;
let component: CounterComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
});
it('debe incrementar', () => {
component.increment();
fixture.detectChanges();
const el = fixture.nativeElement.querySelector('span');
expect(el.textContent).toContain('1');
});
});ComponentFixture da acceso al componente y al DOM. fixture.componentInstance es la instancia. fixture.detectChanges() dispara change detection. fixture.nativeElement accede al DOM. compileComponents() compila templates externos. Con standalone, usa imports en vez de declarations.
Zoneless (v18+ experimental)
import { provideExperimentalZonelessChangeDetection }
from '@angular/core';
// app.config.ts
providers: [
provideExperimentalZonelessChangeDetection()
]
// Sin Zone.js:
// - Change detection solo con signals
// - Menos overhead (sin monkey-patching)
// - Mejor rendimiento
// - Requiere código 100% signal-basedZoneless change detection (v18+ experimental) elimina Zone.js. El change detection se dispara solo con signals — sin monkey-patching de eventos. Requiere código 100% signal-based (sin setTimeout, addEventListener manual). Mejor rendimiento y bundle más pequeño. Actívalo con provideExperimentalZonelessChangeDetection().
Pipes
Pipes de Texto
{{ name | uppercase }}
{{ name | lowercase }}
{{ name | titlecase }}
{{ text | slice:0:100 }}
{{ list | slice:0:5 }}
{{ obj | json }}Los pipes de texto transforman strings: uppercase, lowercase, titlecase. El pipe slice funciona con strings y arrays (inicio:fin). El pipe json serializa objetos — útil para debug en el template.
Encadenar Pipes
{{ name | lowercase | titlecase }}
{{ date | date:'dd/MM' | uppercase }}
{{ text | slice:0:50 | uppercase }}
{{ price | currency:'EUR':'symbol':'1.2-2' }}
<p>{{ (items | slice:0:3).length }} de {{ items.length }}</p>Los pipes se pueden encadenar con | — cada pipe recibe el output del anterior. El orden importa: lowercase | titlecase es diferente de titlecase | lowercase. Los parámetros se separan con :. Usa paréntesis para aplicar pipes a subexpresiones.
Pipes con Parámetros
@Pipe({ name: 'money', standalone: true })
export class MoneyPipe implements PipeTransform {
transform(
value: number,
symbol = '€',
decimals = 2,
position: 'before' | 'after' = 'after'
): string {
const formatted = value.toFixed(decimals);
return position === 'before'
? `${symbol}${formatted}`
: `${formatted}${symbol}`;
}
}
// {{ 49.9 | money:'R$':2:'before' }} → R$49.90Los pipes aceptan múltiples parámetros separados por :. Define valores default en los parámetros del transform(). Usa union types para restringir valores. El pipe es reutilizable en toda la app — añádelo al array imports de los componentes que lo usan.
Pipes de Fecha y Número
{{ today | date:'dd/MM/yyyy' }}
{{ today | date:'fullDate' }}
{{ today | date:'HH:mm:ss' }}
{{ 3.14159 | number:'1.2-2' }}
{{ 1234567 | number }}
{{ 0.75 | percent }}
{{ 49.99 | currency:'EUR' }}
{{ 49.99 | currency:'BRL':'R$' }}El pipe date formatea fechas con patrones (dd/MM/yyyy, fullDate). El pipe number formatea con el formato minInt.minDec-maxDec. percent multiplica por 100 y añade %. currency formatea monedas con símbolo.
KeyValue Pipe
config = { theme: 'dark', language: 'es', version: 2 };
@for (item of config | keyvalue; track item.key) {
<p>{{ item.key }}: {{ item.value }}</p>
}
@for (item of config | keyvalue: originalOrder; track item.key) {
<p>{{ item.key }} = {{ item.value }}</p>
}
// originalOrder = () => 0;El pipe keyvalue itera sobre objetos y Maps, retornando pares { key, value }. Por defecto ordena alfabéticamente por clave. Pasa una función de comparación como argumento para ordenación personalizada. Útil para renderizar objetos dinámicos sin conocer las claves.
Decimal y Locale
{{ 3.14159 | number:'1.2-4' }}
{{ 42 | number:'3.0-0' }}
{{ 1234.5 | number:'1.0-0' }}
{{ 0.1234 | percent:'1.2-2' }}
{{ 1234.56 | number:'1.2-2':'pt-PT' }}El formato minInt.minDec-maxDec controla dígitos: 1.2-4 = mínimo 1 entero, 2 a 4 decimales. El tercer parámetro del pipe number es el locale (pt-PT usa coma decimal y espacio como separador de miles). Registra locales con registerLocaleData().
Async Pipe
<p>{{ user$ | async }}</p>
@if (users$ | async; as users) {
<ul>
@for (u of users; track u.id) {
<li>{{ u.name }}</li>
}
</ul>
}
// Con signal: users = toSignal(this.http.get(url));
// <p>{{ users()?.length }}</p>El async pipe se suscribe a un Observable o Promise y retorna el último valor. Hace unsubscribe automáticamente cuando el componente se destruye — previene memory leaks. Con @if (x$ | async; as x), evita múltiples suscripciones y da acceso al valor.
i18n
<p i18n="@@greeting">¡Hola, mundo!</p>
<img i18n-alt alt="Logotipo de la empresa">
<!-- ng extract-i18n -->
<p i18n>
{counter, plural,
=0 {Sin ítems}
=1 {Un ítem}
other {{{counter}} ítems}
}
</p>El sistema i18n de Angular usa el atributo i18n para marcar textos para traducción. @@id define un ID personalizado. ng extract-i18n genera archivos XLIFF. Soporta pluralización y selección de género. Para runtime translation, usa @angular/localize.
Pipe Personalizado
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50, suffix = '...'): string {
if (value.length <= limit) return value;
return value.slice(0, limit) + suffix;
}
}
// Uso: {{ text | truncate:100:' [más]' }}Un pipe personalizado implementa PipeTransform con el método transform(). El primer parámetro es el valor; los siguientes son argumentos (| truncate:100). Declárarlo como standalone: true y añádelo al array imports del componente que lo usa.
Pure vs Impure Pipes
// Pure (default) — solo recalcula si el input cambia
@Pipe({ name: 'sort', pure: true })
// Impure — recalcula en cada change detection
@Pipe({ name: 'filter', pure: false })
// Pure: eficiente, pero no detecta mutaciones
// Impure: detecta mutaciones, pero menos performante
// Prefiere signals o inmutabilidadLos pipes pure (default) solo se ejecutan cuando la referencia del input cambia. Los pipes impure (pure: false) se ejecutan en cada ciclo de change detection. Prefiere pipes pure con datos inmutables. Para filtrar/ordenar arrays, prefiere transformar en el componente con computed() signals.
Formulários
Reactive Forms — Setup
import { ReactiveFormsModule, FormControl } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="name">
<p>{{ name.value }}</p>
`
})
export class FormComponent {
name = new FormControl('');
}Reactive Forms requiere importar ReactiveFormsModule en el array imports del componente. FormControl representa un campo individual. El valor se accede con .value y se actualiza con .setValue(). Más testable y escalable que Template-Driven Forms.
Validador Personalizado
import { AbstractControl, ValidationErrors, ValidatorFn }
from '@angular/forms';
export function forbiddenWord(word: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const has = control.value?.toLowerCase()
.includes(word.toLowerCase());
return has ? { forbiddenWord: { value: control.value } } : null;
};
}
// Uso: name: ['', [Validators.required, forbiddenWord('admin')]]Un validador personalizado es una función que retorna un ValidatorFn. Recibe el AbstractControl y retorna null (válido) o un objeto ValidationErrors. Para validadores con parámetros, usa una factory function. Verifica errores con hasError('forbiddenWord').
setValue vs patchValue
this.form.setValue({
name: 'Ana', email: 'ana@mail.com',
age: 25, terms: true
});
this.form.patchValue({ name: 'Ana' });
this.form.get('name')?.setValue('Bruno');
this.form.reset();
this.form.reset({ name: '' });setValue() requiere todos los campos del grupo (error si falta alguno). patchValue() acepta campos parciales — ideal para actualizar solo algunos. reset() limpia el form y lo marca como pristine y untouched. Usa patchValue para rellenar desde una API.
Template-Driven Forms
import { FormsModule } from '@angular/forms';
@Component({
imports: [FormsModule],
template: `
<form #f="ngForm" (ngSubmit)="submit(f)">
<input name="name" [(ngModel)]="user.name"
required minlength="3" #name="ngModel">
@if (name.invalid && name.touched) {
<span>Nombre inválido</span>
}
<button [disabled]="f.invalid">Enviar</button>
</form>
`
})
export class FormComponent {
user = { name: '', email: '' };
}Template-Driven Forms usa FormsModule y [(ngModel)]. La validación es declarativa en el template (required, minlength). #f="ngForm" da acceso al form. Más simple para formularios pequeños, pero menos testable y escalable que Reactive Forms.
FormBuilder
import { FormBuilder, Validators } from '@angular/forms';
export class RegisterComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [18, [Validators.min(18)]],
terms: [false, [Validators.requiredTrue]]
});
submit() {
if (this.form.valid) console.log(this.form.value);
}
}FormBuilder simplifica la creación de forms. this.fb.group({}) crea un FormGroup. Cada campo es [valorInicial, [validadores]]. inject(FormBuilder) inyecta el servicio. El form.value retorna el objeto con todos los valores. form.valid verifica si todos los campos son válidos.
Validación Cross-Field
export function passwordsMatch(): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get('password')?.value;
const confirm = group.get('confirm')?.value;
return password === confirm ? null : { passwordsMismatch: true };
};
}
form = this.fb.group({
password: ['', Validators.required],
confirm: ['', Validators.required]
}, { validators: passwordsMatch() });La validación cross-field compara múltiples campos. El validador recibe el FormGroup (como AbstractControl). Regístralo en el segundo argumento de fb.group() con { validators: [...] }. El error queda en el grupo: form.hasError('passwordsMismatch').
Estado del Formulario
form.valid
form.invalid
form.pristine
form.dirty
form.touched
form.untouched
form.pending
form.get('name')?.errors
form.get('name')?.hasError('required')
form.get('name')?.touched
form.markAllAsTouched();Cada control tiene estados: valid/invalid, pristine/dirty, touched/untouched, pending. Usa touched para mostrar errores solo tras interacción. markAllAsTouched() fuerza la exhibición de errores (ej: al enviar).
Form con NonNullable
private fb = inject(FormBuilder).nonNullable;
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
active: true
});
// form.value.name → string (no string | null)
// this.form.reset(); // name = '', no nullFormBuilder.nonNullable (v14+) elimina null de los tipos. form.value.name es string en vez de string | null. El reset() restaura los valores iniciales en vez de null. Reduce verificaciones de null y mejora el type-safety.
Template del Formulario
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="name"
[class.error]="nameCtrl.invalid && nameCtrl.touched">
@if (nameCtrl.hasError('required')) {
<span class="error">Campo obligatorio</span>
}
<button [disabled]="form.invalid">Enviar</button>
</form>
get nameCtrl() { return this.form.get('name')!; }En el template: [formGroup]="form" vincula el form, formControlName="name" vincula cada campo. (ngSubmit) previene el reload y llama al handler. Usa getters para acceder a los controles. Muestra errores con hasError() y condiciónalos con touched (solo tras interacción).
FormArray
import { FormArray } from '@angular/forms';
form = this.fb.group({
emails: this.fb.array<string>([''])
});
get emails(): FormArray {
return this.form.get('emails') as FormArray;
}
addEmail() {
this.emails.push(this.fb.control('', Validators.email));
}
removeEmail(index: number) {
this.emails.removeAt(index);
}FormArray gestiona una lista dinámica de controles. Añade con .push(), elimina con .removeAt(). En el template, itera con @for y usa [formControlName]="$index". Útil para listas de emails, teléfonos, ítems de formulario dinámicos.
Validación Asíncrona
import { AsyncValidatorFn } from '@angular/forms';
export function uniqueEmail(service: UserService): AsyncValidatorFn {
return (control) => {
return timer(500).pipe(
switchMap(() => service.checkEmail(control.value)),
map(exists => exists ? { emailExists: true } : null),
catchError(() => of(null))
);
};
}
// email: ['', [Validators.email], [uniqueEmail(this.userService)]]La validación asíncrona usa AsyncValidatorFn — retorna un Observable. El tercer argumento de fb.control() acepta validadores async. Usa timer(500) + switchMap para debounce. El estado pending es true mientras valida. Ideal para verificar unicidad.
Validadores Built-in
import { Validators } from '@angular/forms';
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3),
Validators.maxLength(100)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.min(18), Validators.max(120)]],
website: ['', Validators.pattern('https?://.+')],
terms: [false, Validators.requiredTrue]
});Validadores integrados: required, email, minLength(n), maxLength(n), min(n), max(n), pattern(regex), requiredTrue (para checkboxes). Combina múltiples en un array. Todos retornan null (válido) o un objeto de error.
FormGroup Anidado
form = this.fb.group({
name: ['', Validators.required],
address: this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required],
zipCode: ['', Validators.pattern('\d{4}-\d{3}')]
})
});
// Template: <div formGroupName="address">
// <input formControlName="street">
// </div>Los FormGroup anidados organizan formularios complejos. En el template, usa formGroupName="address" para crear el contexto. Accede a valores con form.value.address.street o form.get('address.street'). El form.value retorna la estructura anidada completa.
valueChanges
this.form.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntilDestroyed()
)
.subscribe(value => this.search(value.query));
this.form.get('country')?.valueChanges
.pipe(takeUntilDestroyed())
.subscribe(country => this.updateCities(country));valueChanges es un Observable que emite en cada cambio. Combínalo con debounceTime para búsqueda en tiempo real, distinctUntilChanged para evitar emisiones duplicadas. Usa takeUntilDestroyed() para auto-unsubscribe. Ideal para filtros, autocompletar y efectos colaterales.
RxJS e Observables
Observable — Básico
import { Observable } from 'rxjs';
const data$ = new Observable<string>(subscriber => {
subscriber.next('Primero');
subscriber.next('Segundo');
setTimeout(() => {
subscriber.next('Tercero');
subscriber.complete();
}, 1000);
});
data$.subscribe({
next: value => console.log(value),
error: err => console.error(err),
complete: () => console.log('Completo')
});Un Observable es un stream de datos lazy — solo se ejecuta cuando alguien se suscribe. subscriber.next() emite valores, .complete() termina, .error() lanza error. El sufijo $ es convención para Observables. El subscriber recibe next, error y complete.
Subject y BehaviorSubject
import { Subject, BehaviorSubject } from 'rxjs';
const events$ = new Subject<string>();
events$.next('click');
const counter$ = new BehaviorSubject(0);
counter$.value;
counter$.next(1);
// Patrón: exponer como Observable (read-only)
private _data$ = new BehaviorSubject<User[]>([]);
readonly data$ = this._data$.asObservable();
update(data: User[]) { this._data$.next(data); }Subject es un Observable que también puede emitir (.next()). BehaviorSubject tiene valor inicial y guarda el último valor (.value). Exponlo como asObservable() para read-only. Sustituido por signal() en la mayoría de los casos (v17+).
takeUntilDestroyed (moderno)
import { takeUntilDestroyed } from '@angular/rxjs-interop';
import { DestroyRef, inject } from '@angular/core';
export class ListComponent {
data$ = this.service.data$.pipe(takeUntilDestroyed());
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.otherService.stream$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(d => this.data = d);
}
}takeUntilDestroyed() sustituye el patrón Subject + takeUntil + ngOnDestroy. En el contexto de inyección (constructor, field initializer), no necesita argumentos. Fuera de él, pasa DestroyRef. De @angular/rxjs-interop. Previene memory leaks automáticamente.
Patrón: Búsqueda con Debounce
import { toSignal, toObservable } from '@angular/rxjs-interop';
export class SearchComponent {
private http = inject(HttpClient);
term = signal('');
results = toSignal(
toObservable(this.term).pipe(
debounceTime(300),
distinctUntilChanged(),
filter(t => t.length >= 2),
switchMap(term =>
this.http.get<Result[]>(`/api/search?q=${term}`)
.pipe(catchError(() => of([])))
)
),
{ initialValue: [] }
);
}Patrón de búsqueda: signal → toObservable → debounceTime(300) (espera) → distinctUntilChanged (ignora iguales) → filter (mínimo 2 chars) → switchMap (cancela anterior) → catchError (resiliencia). toSignal convierte de vuelta para uso en el template.
Operadores de Transformación
import { map, scan, startWith } from 'rxjs';
this.http.get<User[]>('/api/users').pipe(
map(users => users.map(u => u.name))
);
this.clicks$.pipe(
scan((acc, _) => acc + 1, 0)
);
this.search$.pipe(
startWith(''),
switchMap(q => this.search(q))
);map transforma cada valor emitido. scan acumula valores (contador, suma). startWith emite un valor inicial antes del stream. Todos retornan un nuevo Observable — el stream original no se modifica.
combineLatest y zip
import { combineLatest, zip } from 'rxjs';
combineLatest([filter$, sorting$]).pipe(
map(([filter, sorting]) =>
this.applyFilter(filter, sorting)
)
);
zip(
this.http.get('/api/names'),
this.http.get('/api/ages')
).pipe(
map(([names, ages]) =>
names.map((n, i) => ({ name: n, age: ages[i] }))
)
);combineLatest emite siempre que cualquier Observable emite (con los últimos valores de todos). Ideal para combinar filtros. zip empareja emisiones en orden — espera a que todos emitan. Usa combineLatest para reactividad y zip para sincronización.
fromEvent e interval
import { fromEvent, interval, timer } from 'rxjs';
import { map, throttleTime } from 'rxjs/operators';
const clicks$ = fromEvent(document, 'click');
const resize$ = fromEvent(window, 'resize').pipe(
throttleTime(200),
map(() => window.innerWidth)
);
const tick$ = interval(1000);
const delay$ = timer(3000);
const periodic$ = timer(1000, 5000);fromEvent convierte eventos DOM en Observable. Combina con throttleTime para performance. interval(ms) emite números incrementales cada X ms. timer(delay) emite una vez tras el delay; timer(delay, periodo) emite periódicamente. Todos necesitan unsubscribe.
RxJS vs Signals
// RxJS: streams, eventos, async complejo
this.search$.pipe(
debounceTime(300),
switchMap(q => this.http.get(q))
);
// Signals: estado síncrono, computado
counter = signal(0);
double = computed(() => this.counter() * 2);
// Interop
import { toSignal, toObservable } from '@angular/rxjs-interop';
const search$ = toObservable(this.term);
const data = toSignal(this.http.get(url));Usa signals para estado síncrono y valores computados. Usa RxJS para streams de eventos, debounce, requests HTTP y operaciones complejas. toObservable() y toSignal() hacen de puente. La tendencia es signals para estado y RxJS para flujos asíncronos.
Operadores de Filtrado
import { filter, distinctUntilChanged, debounceTime,
take, first } from 'rxjs';
this.data$.pipe(filter(d => d.active === true));
this.input$.pipe(distinctUntilChanged());
this.search$.pipe(debounceTime(300));
this.data$.pipe(take(5));
this.data$.pipe(first());filter solo deja pasar valores que satisfacen la condición. distinctUntilChanged ignora emisiones iguales a la anterior. debounceTime(ms) espera silencio antes de emitir — ideal para búsqueda. take(n) limita a N emisiones. first() emite el primero y completa.
tap y finalize
import { tap, finalize } from 'rxjs';
this.http.get<User[]>('/api/users').pipe(
tap(users => {
console.log(`${users.length} users`);
this.loading.set(false);
}),
finalize(() => {
this.loading.set(false);
this.spinner.hide();
})
);tap ejecuta efectos colaterales sin modificar el stream (logs, actualizar estado, cache). finalize se ejecuta cuando el stream completa o da error — ideal para esconder spinners. Ninguno de los dos altera el valor emitido. Usa finalize en vez de repetir lógica en next y error.
shareReplay
import { shareReplay } from 'rxjs';
private users$ = this.http.get<User[]>('/api/users')
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
getUsers() { return this.users$; }
getCount() {
return this.users$.pipe(map(u => u.length));
}shareReplay comparte un Observable entre múltiples subscribers y hace replay del último valor. bufferSize: 1 guarda el último valor. refCount: true hace unsubscribe de la fuente cuando no hay subscribers. Evita requests HTTP duplicados. Ideal para datos que múltiples componentes consumen.
switchMap, mergeMap, concatMap
import { switchMap, mergeMap, concatMap, exhaustMap }
from 'rxjs';
// switchMap: cancela el request anterior
this.search$.pipe(switchMap(q => this.http.get(`/api?q=${q}`)));
// mergeMap: ejecuta en paralelo
this.ids$.pipe(mergeMap(id => this.http.get(`/api/${id}`)));
// concatMap: ejecuta en serie (orden)
this.actions$.pipe(concatMap(action => this.save(action)));
// exhaustMap: ignora mientras procesa
this.clicks$.pipe(exhaustMap(() => this.http.post('/api', data)));switchMap: cancela el Observable anterior (búsqueda). mergeMap: ejecuta en paralelo (requests independientes). concatMap: ejecuta en serie, mantiene el orden (operaciones secuenciales). exhaustMap: ignora nuevas emisiones mientras procesa (prevenir double-submit).
catchError y retry
import { catchError, retry, throwError } from 'rxjs';
this.http.get('/api/data').pipe(retry(3));
this.http.get('/api/data').pipe(
retry({ count: 3, delay: 1000 })
);
this.http.get('/api/data').pipe(
catchError(err => {
if (err.status === 404) return of([]);
return throwError(() => err);
})
);retry(n) se vuelve a suscribir en caso de error. retry({ count, delay }) añade delay entre intentos. catchError permite recuperar con un valor default (of([])) o relanzar con throwError. Combina: retry(3) antes de catchError.
of, from y throwError
import { of, from, throwError, EMPTY } from 'rxjs';
of(1, 2, 3);
of({ name: 'Ana' });
from([1, 2, 3]);
from(fetch('/api/data'));
throwError(() => new Error('Fallo'));
EMPTY;
catchError(err => {
if (err.status === 404) return of(null);
return throwError(() => err);
});of() crea un Observable que emite valores y completa. from() convierte arrays, Promises o iterables. throwError() crea un Observable que emite error. EMPTY completa inmediatamente sin emitir. Útiles como retornos en catchError, mocks y tests.
Routing e Navegação
Definir Rutas
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id', component: UserDetailComponent },
{ path: '**', component: NotFoundComponent }
];
// app.config.ts
providers: [provideRouter(routes)]
// Template: <router-outlet />Define las rutas en app.routes.ts como array de Routes. path es el segmento de la URL. :id es un parámetro dinámico. ** es el wildcard (404). provideRouter(routes) activa el routing. <router-outlet /> renderiza el componente de la ruta activa.
Guards Funcionales
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) return true;
return router.createUrlTree(['/login'], {
queryParams: { redirect: state.url }
});
};
// Uso: { path: 'admin', canActivate: [authGuard] }Los guards funcionales (CanActivateFn) sustituyen clases con CanActivate. Retornan true (permitir), false (bloquear) o UrlTree (redirigir). Usa inject() para servicios. canActivate protege rutas individuales. Regístralo en el array de la ruta.
Named Outlets
{ path: 'chat', component: ChatComponent, outlet: 'sidebar' }
<router-outlet />
<router-outlet name="sidebar" />
<a [routerLink]="[{ outlets: {
primary: ['home'],
sidebar: ['chat']
}}]">Home + Chat</a>
// URL: /home(sidebar:chat)<router-outlet name="sidebar"> crea un outlet nombrado. Las rutas con outlet: 'sidebar' se renderizan en él. Navega con { outlets: { primary: [...], sidebar: [...] } }. La URL usa paréntesis: /home(sidebar:chat). Permite múltiples vistas independientes en la misma página.
RouterLink y Navegación
<a routerLink="/">Home</a>
<a routerLink="/users">Users</a>
<a [routerLink]="['/users', user.id]">Detalle</a>
<a [routerLink]="['/users']"
[queryParams]="{ page: 1, filter: 'active' }"
fragment="top">Users</a>
<a routerLink="/about" routerLinkActive="active">Acerca de</a>
this.router.navigate(['/users', id]);
this.router.navigateByUrl('/login?redirect=/home');routerLink navega sin reload. [routerLink]="['/users', id]" para parámetros dinámicos. [queryParams] añade query string. routerLinkActive aplica una clase cuando la ruta está activa. router.navigate() para navegación vía código (ej: tras login).
Tipos de Guards
import { CanActivateFn, CanActivateChildFn,
CanDeactivateFn, CanMatchFn, ResolveFn }
from '@angular/router';
canActivate: [authGuard]
canActivateChild: [adminGuard]
canDeactivate: [unsavedGuard]
canMatch: [featureFlagGuard]
resolve: { user: userResolver }Tipos de guards: canActivate (¿puede entrar?), canActivateChild (¿puede entrar en las hijas?), canDeactivate (¿puede salir? — ej: form sin guardar), canMatch (¿la ruta existe? — ej: feature flags), resolve (precargar datos). Todos tienen versión funcional.
Route Data y Title
{
path: 'users',
component: UsersComponent,
title: 'Usuarios',
data: { breadcrumb: 'Users', roles: ['admin'] }
}
@Injectable({ providedIn: 'root' })
export class AppTitleStrategy extends TitleStrategy {
override updateTitle(snapshot: RouterStateSnapshot) {
const title = this.buildTitle(snapshot);
document.title = title ? `${title} | Mi App` : 'Mi App';
}
}La propiedad title en la ruta define el título de la página automáticamente. data almacena metadatos (breadcrumbs, roles). Para títulos dinámicos, extiende TitleStrategy. Accede a data con route.data o route.snapshot.data.
Parámetros de Ruta
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/rxjs-interop';
export class UserDetailComponent {
private route = inject(ActivatedRoute);
userId = toSignal(
this.route.paramMap.pipe(
map(params => Number(params.get('id')))
)
);
id = Number(this.route.snapshot.paramMap.get('id'));
page = toSignal(
this.route.queryParamMap.pipe(
map(params => Number(params.get('page') ?? 1))
)
);
}ActivatedRoute da acceso a los parámetros. paramMap es un Observable (reacciona a cambios). snapshot.paramMap es el valor actual (no reacciona). queryParamMap para query params. Usa toSignal para convertir en signal. Prefiere paramMap si la ruta puede cambiar sin destruir el componente.
CanDeactivate (Form Sin Guardar)
import { CanDeactivateFn } from '@angular/router';
export interface FormGuard {
hasUnsavedChanges(): boolean;
}
export const unsavedGuard: CanDeactivateFn<FormGuard> =
(component) => {
if (component.hasUnsavedChanges()) {
return confirm('Tienes cambios sin guardar. ¿Salir?');
}
return true;
};
// En el componente:
hasUnsavedChanges() { return this.form.dirty; }CanDeactivateFn verifica si el usuario puede salir de la ruta. Recibe la instancia del componente. Si el form tiene cambios (form.dirty), muestra una confirmación. Retorna true (salir), false (quedarse) u Observable<boolean> para un diálogo asíncrono.
Router Events y Scroll
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
provideRouter(routes,
withInMemoryScrolling({
scrollPositionRestoration: 'top',
anchorScrolling: 'enabled'
}),
withRouterConfig({ onSameUrlNavigation: 'reload' })
);
inject(Router).events.pipe(
filter(e => e instanceof NavigationEnd)
).subscribe(e => console.log('Navegó a:', e.url));withInMemoryScrolling configura el scroll: scrollPositionRestoration: 'top' vuelve arriba, anchorScrolling: 'enabled' activa los fragments. Router.events emite eventos de navegación (NavigationStart, NavigationEnd). Filtra con instanceof.
Lazy Loading
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.routes').then(m => m.ADMIN_ROUTES)
},
{
path: 'settings',
loadComponent: () =>
import('./settings/settings.component')
.then(m => m.SettingsComponent)
}
];
export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminDashboardComponent },
{ path: 'users', component: AdminUsersComponent }
];loadChildren carga un módulo de rutas bajo demanda (lazy). loadComponent carga un componente standalone. El archivo solo se descarga cuando el usuario navega a la ruta. Reduce el bundle inicial. Úsalo para secciones grandes (admin, settings, informes).
Rutas Anidadas (Children)
export const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
children: [
{ path: '', component: OverviewComponent },
{ path: 'stats', component: StatsComponent },
{ path: 'reports', component: ReportsComponent }
]
}
];
// dashboard.component.html
<a routerLink="./">Overview</a>
<a routerLink="stats">Estadísticas</a>
<router-outlet />children define rutas anidadas. El componente padre tiene su propio <router-outlet />. routerLink="./" es la ruta hija default. La URL queda /dashboard/stats. Úsalo para layouts con sidebar/tabs donde el contenido cambia pero el wrapper permanece.
Preload Strategies
import { PreloadAllModules, withPreloading }
from '@angular/router';
provideRouter(routes, withPreloading(PreloadAllModules));
@Injectable({ providedIn: 'root' })
export class SelectivePreload implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>) {
if (route.data?.['preload']) return load();
return of(null);
}
}
// En la ruta: data: { preload: true }PreloadAllModules carga todos los módulos lazy en background tras la carga inicial. Para control fino, implementa PreloadingStrategy con lógica personalizada (ej: solo rutas con data.preload). Mejora la percepción de velocidad sin aumentar el bundle inicial.
Signals
signal() — Estado Reactivo
import { signal } from '@angular/core';
counter = signal(0);
name = signal('Ana');
items = signal<string[]>([]);
user = signal<User | null>(null);
console.log(this.counter());
this.counter.set(5);
this.counter.update(v => v + 1);
this.items.update(list => [...list, 'nuevo']);
this.user.update(u => u ? { ...u, name: 'Bruno' } : u);signal() crea un valor reactivo. Léelo llamándolo como función: counter(). Actualiza con .set(valor) o .update(fn). Para arrays/objetos, crea nueva referencia (inmutabilidad). Angular actualiza el template automáticamente cuando el signal cambia.
input() — Signal Inputs
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-product',
template: `<h3>{{ name() }}</h3><p>{{ formattedPrice() }}</p>`
})
export class ProductComponent {
name = input.required<string>();
price = input(0);
currency = input('EUR');
formattedPrice = computed(() =>
`${this.price().toFixed(2)} ${this.currency()}`
);
}input() crea signal inputs (v17.1+). input.required<T>() es obligatorio. input(default) tiene valor default. Léelo como función: name(). Combina con computed() para valores derivados. Sustituye @Input() — más type-safe y funciona con signals.
untracked()
import { signal, computed, untracked } from '@angular/core';
counter = signal(0);
name = signal('Ana');
summary = computed(() => {
const c = this.counter();
const n = untracked(() => this.name());
return `${n}: ${c}`;
});
// Solo counter() es rastreado
// Cambiar name() NO recalcula summaryuntracked() lee un signal sin registrarlo como dependencia. Dentro de computed o effect, el signal leído con untracked no dispara recálculo. Útil para leer valores secundarios que no deben causar reactividad. Úsalo con cuidado — puede causar inconsistencias.
computed() — Valores Derivados
import { signal, computed } from '@angular/core';
price = signal(100);
quantity = signal(2);
discount = signal(0.1);
subtotal = computed(() => this.price() * this.quantity());
total = computed(() => this.subtotal() * (1 - this.discount()));
formatted = computed(() => `${this.total().toFixed(2)} €`);
// En el template: {{ total() }} → 180
// Read-only: no puede hacer total.set()computed() crea un signal derivado de otros signals. Recalcula automáticamente cuando las dependencias cambian (lazy — solo cuando se lee). Es read-only. Puede depender de otros computed. Ideal para totales, filtros, formateos y cualquier valor derivado.
output() — Signal Outputs
import { Component, output } from '@angular/core';
@Component({
selector: 'app-item',
template: `
<span>{{ name() }}</span>
<button (click)="removed.emit(this.id())">X</button>
`
})
export class ItemComponent {
id = input.required<number>();
name = input.required<string>();
removed = output<number>();
}
// Padre: <app-item (removed)="onRemove($event)" />output() crea signal outputs (v17.1+). Emite con .emit(valor). El padre escucha con (evento)="handler($event)". Sustituye @Output() + new EventEmitter(). Más conciso y type-safe. Combina con input() para componentes totalmente signal-based.
Signal con Arrays y Objetos
items = signal<string[]>([]);
user = signal<User>({ name: 'Ana', age: 25 });
// Añadir (inmutable)
this.items.update(list => [...list, 'nuevo']);
// Eliminar
this.items.update(list => list.filter(i => i !== 'x'));
// Actualizar objeto
this.user.update(u => ({ ...u, age: 26 }));
// NO funciona (mutación):
// this.items().push('x'); ← ¡no notifica!Los signals con arrays/objetos requieren inmutabilidad. .update() con spread ([...list, x]) o filter crea nueva referencia. La mutación directa (.push(), .name = x) no notifica a Angular porque la referencia no cambia. Siempre crea nuevos arrays/objetos.
effect() — Efectos Colaterales
import { signal, effect } from '@angular/core';
export class SearchComponent {
term = signal('');
constructor() {
effect(() => {
const t = this.term();
console.log('Buscó:', t);
this.saveHistory(t);
});
}
}effect() ejecuta código cuando los signals leídos dentro de él cambian. Rastrea dependencias automáticamente. Úsalo para efectos colaterales: logs, analytics, persistencia, llamadas API. No lo uses para derivar estado (usa computed). El effect se destruye con el componente.
toSignal/toObservable (Interop)
import { toSignal, toObservable } from '@angular/rxjs-interop';
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] }
);
filter = signal('');
filter$ = toObservable(this.filter);
results$ = this.filter$.pipe(
debounceTime(300),
switchMap(f => this.search(f))
);
results = toSignal(this.results$, { initialValue: [] });toSignal() convierte un Observable en signal (con initialValue). toObservable() convierte un signal en Observable para usar operadores RxJS. Patrón: signal → toObservable → operadores RxJS → toSignal para el template. De @angular/rxjs-interop.
effect() con Cleanup
import { effect, signal, EffectRef } from '@angular/core';
export class PollingComponent {
interval = signal(5000);
private effectRef!: EffectRef;
constructor() {
this.effectRef = effect((onCleanup) => {
const ms = this.interval();
const id = setInterval(() => this.poll(), ms);
onCleanup(() => clearInterval(id));
});
}
// Destruir manualmente (opcional)
stop() { this.effectRef.destroy(); }
}effect() acepta un callback onCleanup que registra limpieza. La limpieza se ejecuta antes del siguiente efecto o cuando el effect es destruido. Útil para clearInterval, removeEventListener, cancelar requests. EffectRef.destroy() destruye manualmente.
model() — Signal Bidireccional
import { Component, model } from '@angular/core';
@Component({
selector: 'app-volume',
template: `
<input type="range" [value]="volume()"
(input)="volume.set(+$any($event.target).value)">
<span>{{ volume() }}%</span>
`
})
export class VolumeComponent {
volume = model(50);
}
// Padre: <app-volume [(volume)]="appVolume" />model() crea un signal con two-way binding. El hijo lee con volume() y actualiza con volume.set(). El padre usa [(volume)]="var". Sustituye @Input + @Output + EventEmitter para valores que el hijo puede modificar. Requiere Angular 17.1+.
asReadonly()
@Injectable({ providedIn: 'root' })
export class AuthService {
private _user = signal<User | null>(null);
private _loading = signal(false);
readonly user = this._user.asReadonly();
readonly loading = this._loading.asReadonly();
readonly isLoggedIn = computed(() => this._user() !== null);
login(email: string, password: string) {
this._loading.set(true);
this.http.post('/login', { email, password }).subscribe({
next: user => {
this._user.set(user);
this._loading.set(false);
}
});
}
}asReadonly() expone un signal sin .set() / .update(). El patrón: estado privado con signal(), expuesto como asReadonly(). Los métodos públicos controlan la mutación. Garantiza que solo el servicio modifica el estado — los componentes solo leen.
linkedSignal (v19+)
import { signal, linkedSignal } from '@angular/core';
userId = signal(1);
// Recalcula cuando userId cambia, pero es editable
userName = linkedSignal(() => {
const id = this.userId();
return this.getUser(id)?.name ?? '';
});
// Puede modificarse manualmente:
this.userName.set('Nombre Custom');
// Pero recalcula si userId() cambia
this.userId.set(2); // userName recalculalinkedSignal() (v19+) es un signal que recalcula cuando las dependencias cambian (como computed), pero también puede modificarse manualmente con .set(). Ideal para formularios que se rellenan desde una API pero el usuario puede editar. Combina reactividad con mutabilidad.
Diretivas
Directiva de Atributo
import { Directive, ElementRef, HostListener,
input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
color = input('yellow', { alias: 'appHighlight' });
private el = inject(ElementRef);
@HostListener('mouseenter')
onEnter() {
this.el.nativeElement.style.backgroundColor = this.color();
}
@HostListener('mouseleave')
onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
// <p [appHighlight]="'lightblue'">Texto</p>Una directiva de atributo usa @Directive con un selector entre corchetes ([appHighlight]). Modifica el comportamiento de un elemento existente. input() con alias permite pasar valores. @HostListener reacciona a eventos del elemento.
Composición de Directivas (hostDirectives)
import { Component } from '@angular/core';
import { TooltipDirective } from './tooltip.directive';
import { TrackClickDirective } from './track-click.directive';
@Component({
selector: 'app-btn',
template: `<button><ng-content /></button>`,
hostDirectives: [
TooltipDirective,
{ directive: TrackClickDirective,
inputs: ['trackLabel'] }
]
})
export class BtnComponent {}
// <app-btn appTooltip="Haz clic aquí" trackLabel="CTA">hostDirectives (v15+) aplica directivas a un componente sin herencia. Las directivas se componen en el host. inputs expone inputs de la directiva en el componente. Sustituye la necesidad de aplicar directivas manualmente en cada uso. Ideal para comportamientos transversales (tooltip, tracking, accesibilidad).
Directiva de Debounce
@Directive({
selector: '[appDebounceInput]',
standalone: true
})
export class DebounceInputDirective {
debounceMs = input(300, { alias: 'appDebounceInput' });
debouncedValue = output<string>();
private el = inject(ElementRef);
constructor() {
fromEvent(this.el.nativeElement, 'input').pipe(
debounceTime(this.debounceMs()),
map((e: any) => e.target.value),
distinctUntilChanged(),
takeUntilDestroyed()
).subscribe(v => this.debouncedValue.emit(v));
}
}
// <input [appDebounceInput]="500"
// (debouncedValue)="search($event)">La directiva de debounce usa fromEvent + debounceTime + distinctUntilChanged para emitir valores con retraso. input() con alias permite configurar los milisegundos. output() emite el valor debounced. Útil para búsqueda en tiempo real sin sobrecargar la API.
Directiva Estructural
import { Directive, TemplateRef, ViewContainerRef,
input, effect } from '@angular/core';
@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
private templateRef = inject(TemplateRef);
private viewContainer = inject(ViewContainerRef);
appRepeat = input.required<number>();
constructor() {
effect(() => {
const count = this.appRepeat();
this.viewContainer.clear();
for (let i = 0; i < count; i++) {
this.viewContainer.createEmbeddedView(
this.templateRef, { $implicit: i }
);
}
});
}
}
// <p *appRepeat="3; let i">Item {{ i }}</p>Las directivas estructurales manipulan el DOM con TemplateRef y ViewContainerRef. createEmbeddedView() renderiza el template. $implicit es la variable default del let. El prefijo * en el template es azúcar sintáctico para ng-template. Ejemplos: *ngIf, *ngFor.
Directiva de Tooltip
@Directive({
selector: '[appTooltip]',
standalone: true,
host: {
'(mouseenter)': 'show()',
'(mouseleave)': 'hide()'
}
})
export class TooltipDirective {
appTooltip = input.required<string>();
private overlayRef: OverlayRef | null = null;
private overlay = inject(Overlay);
show() {
this.overlayRef = this.overlay.create({
positionStrategy: this.overlay.position()
.flexibleConnectedTo(this.el)
.withPositions([{ originX: 'center', originY: 'top',
overlayX: 'center', overlayY: 'bottom' }])
});
this.overlayRef.attach(
new TemplatePortal(this.tooltipTmpl, this.vcr)
);
}
hide() { this.overlayRef?.dispose(); }
}Una directiva de tooltip usa el Overlay del Angular CDK para posicionar elementos flotantes. host en el decorator registra mouseenter/mouseleave. flexibleConnectedTo posiciona relativo al elemento. TemplatePortal renderiza el template en el overlay. dispose() lo elimina al salir.
Directiva de Permiso
@Directive({
selector: '[appHasPermission]',
standalone: true
})
export class HasPermissionDirective {
private templateRef = inject(TemplateRef);
private viewContainer = inject(ViewContainerRef);
private auth = inject(AuthService);
appHasPermission = input.required<string>();
constructor() {
effect(() => {
const perm = this.appHasPermission();
this.viewContainer.clear();
if (this.auth.hasPermission(perm)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
});
}
}
// <button *appHasPermission="'user:delete'">Eliminar</button>La directiva de permiso es una directiva estructural que muestra/oculta elementos según permisos. effect() reacciona a cambios en el permiso. viewContainer.clear() elimina, createEmbeddedView() renderiza. Usa * en el template. Esencial para UI basada en roles/permisos.
Directiva con ngTemplateOutlet
@Directive({
selector: '[appIfRole]',
standalone: true
})
export class IfRoleDirective {
private templateRef = inject(TemplateRef);
private viewContainer = inject(ViewContainerRef);
private auth = inject(AuthService);
appIfRole = input.required<string>();
constructor() {
effect(() => {
const role = this.appIfRole();
this.viewContainer.clear();
if (this.auth.hasRole(role)) {
this.viewContainer.createEmbeddedView(this.templateRef);
}
});
}
}
// <div *appIfRole="'admin'">Panel Admin</div>Una directiva estructural condicional verifica permisos con inject(AuthService). effect() reacciona a cambios en el signal appIfRole(). viewContainer.clear() elimina views anteriores. createEmbeddedView() renderiza si la condición es verdadera. Patrón para control de acceso basado en roles.
Directiva de Click Outside
import { Directive, output, ElementRef,
HostListener } from '@angular/core';
@Directive({
selector: '[appClickOutside]',
standalone: true
})
export class ClickOutsideDirective {
clickOutside = output<void>();
private el = inject(ElementRef);
@HostListener('document:click', ['$event'])
onClick(event: MouseEvent) {
if (!this.el.nativeElement.contains(event.target)) {
this.clickOutside.emit();
}
}
}
// <div (appClickOutside)="closeMenu()">La directiva ClickOutside escucha clics en el document y verifica si el objetivo está fuera del elemento con contains(). Si es así, emite clickOutside. Útil para cerrar dropdowns, modales y menús. Usa output() para el evento. El selector [appClickOutside] se aplica como atributo.
Directiva de TrackBy
@Directive({
selector: '[appTrackById]',
standalone: true
})
export class TrackByIdDirective {
// Usado con @for:
// @for (item of items; track item.id) { ... }
// O con el ngFor antiguo:
// <li *ngFor="let item of items; trackBy: trackById">
trackById(index: number, item: any): any {
return item.id;
}
}
// Con @for (v17+), track es obligatorio:
// @for (item of items; track item.id) { ... }trackBy ayuda a Angular a identificar ítems en listas por una clave única (ej: id) en vez del índice. Con @for (v17+), track es obligatorio: track item.id. Mejora el rendimiento en listas grandes — Angular solo re-renderiza los ítems que cambiaron, no toda la lista.
Directiva de Validación
import { Directive, input } from '@angular/core';
import { NG_VALIDATORS, Validator,
AbstractControl, ValidationErrors } from '@angular/forms';
@Directive({
selector: '[appForbiddenWord]',
standalone: true,
providers: [{
provide: NG_VALIDATORS,
useExisting: ForbiddenWordDirective,
multi: true
}]
})
export class ForbiddenWordDirective implements Validator {
appForbiddenWord = input.required<string>();
validate(control: AbstractControl): ValidationErrors | null {
const has = control.value?.includes(this.appForbiddenWord());
return has ? { forbidden: true } : null;
}
}Una directiva de validación implementa Validator y se registra como NG_VALIDATORS con multi: true. El método validate() recibe el AbstractControl. Usa input() para el parámetro. Aplícala en el template: <input appForbiddenWord="admin">.
Directiva de Lazy Load (IntersectionObserver)
@Directive({
selector: '[appLazyLoad]',
standalone: true
})
export class LazyLoadDirective {
appLazyLoad = input.required<string>();
private el = inject(ElementRef);
constructor() {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
this.el.nativeElement.src = this.appLazyLoad();
observer.disconnect();
}
},
{ rootMargin: '200px' }
);
observer.observe(this.el.nativeElement);
}
}
// <img appLazyLoad="assets/photo.jpg" src="placeholder.jpg">La directiva de lazy load usa IntersectionObserver para cargar imágenes cuando entran en el viewport. rootMargin: '200px' precarga 200px antes. observer.disconnect() deja de observar tras cargar. Sustituye bibliotecas de lazy loading para casos simples.
Directiva de Accesibilidad
@Directive({
selector: '[appA11y]',
standalone: true,
host: {
'role': 'button',
'tabindex': '0',
'[attr.aria-label]': 'appA11y()',
'(keydown.enter)': 'onClick()',
'(keydown.space)': 'onClick()'
}
})
export class A11yDirective {
appA11y = input.required<string>();
clicked = output<void>();
onClick() { this.clicked.emit(); }
}
// <div [appA11y]="'Cerrar modal'" (clicked)="close()">La directiva de accesibilidad añade role, tabindex, aria-label y handlers de teclado (Enter, Space) a elementos no interactivos. Usa host en el decorator para binding declarativo. input() recibe el label. Esencial para hacer divs/spans accesibles como botones.