Angular: componentes a aplicaciones enterprise
Angular es el framework de Google para construir aplicaciones web completas: componentes, routing, inyección de dependencias y HTTP incluidos de serie. Este tutorial te lleva desde la creación de tu primer proyecto hasta una app con servicios y rutas. Prerequisito conceptual: TypeScript.
¿Qué es Angular?
Angular es un framework completo de frontend mantenido por Google. A diferencia de bibliotecas que solo renderizan vistas, Angular te da todo lo necesario para una aplicación enterprise de fábrica: un sistema de componentes con plantillas, enrutador, formularios, cliente HTTP, inyección de dependencias y herramientas de testing integradas.
- Framework completo: no necesitas elegir una combinación de bibliotecas; Angular trae las decisiones tomadas.
- TypeScript nativo: el framework está escrito en TypeScript y lo usa de forma obligatoria.
- CLI potente:
ng generatecrea componentes, servicios, módulos y tests con un comando. - SPA por diseño: el router carga vistas sin recargar la página.
standalone es la norma: los componentes ya no necesitan NgModules para usarse. Todo lo de este tutorial usa ese enfoque moderno.Instalar y crear proyecto
Necesitas Node.js LTS instalado. La CLI de Angular se ejecuta con npx, sin instalación global:
npx -p @angular/cli ng new mi-app --standalone
# El CLI pregunta por estilos y SSR. Elige SCSS o CSS plano
# y responde "No" a server-side rendering si quieres una SPA simple.
cd mi-app
ng serve --open # http://localhost:4200El flag --standalone genera un proyecto sin NgModules, con componentes independientes. Si lo olvidas, Angular 17+ lo pregunta interactivamente.
ng new de una versión con dependencias de otra en el mismo package.json.Estructura de un proyecto
Un proyecto standalone generado con el CLI tiene esta anatomía:
mi-app/
├── src/
│ ├── main.ts # bootstrap de la app
│ ├── app/
│ │ ├── app.config.ts # providers globales (router, http)
│ │ ├── app.routes.ts # definición de rutas
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ └── app.component.css
│ ├── index.html
│ └── styles.css
├── angular.json # config del workspace
├── package.json
└── tsconfig.jsonEl arranque: main.ts
main.ts arranca la aplicación usando el componente raíz y la configuración global:
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));La configuración: app.config.ts
Aquí se registran los providers que toda la app comparte. En un proyecto recién creado ya incluye el router:
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
};Más adelante añadiremos provideHttpClient() aquí para habilitar peticiones HTTP.
Componentes y plantillas
Un componente es una clase TypeScript decorada con @Component que define el selector, la plantilla (template) y los estilos. En modo standalone declaramos todo en un solo archivo:
import { Component } from '@angular/core';
@Component({
selector: 'app-saludo',
standalone: true,
template: `
<h2>{{ nombre }}</h2>
<p>Has hecho clic {{ contador }} veces.</p>
`,
})
export class SaludoComponent {
nombre = 'Ana';
contador = 0;
}La interpolación {{ }} imprime el valor de una propiedad del componente en el HTML. Angular se encarga de re-renderizar cuando cambia.
Las plantillas modernas usan control flow integrado en lugar de directivas estructurales:
@if (usuario) {
<p>Bienvenido, {{ usuario.nombre }}.</p>
} @else {
<p>No hay sesión iniciada.</p>
}
@for (item of items; track item.id) {
<li>{{ item.nombre }}</li>
} @empty {
<li>No hay elementos.</li>
}@if y @for son la sintaxis recomendada desde Angular 17. Las antiguas *ngIf y *ngFor siguen funcionando pero están deprecadas en favor del nuevo control flow. El track de @for es obligatorio y mejora el rendimiento de las listas.Data binding y eventos
Angular distingue tres direcciones de datos entre la clase y la plantilla:
<!-- Property binding: de la clase al DOM -->
<img [src]="avatarUrl" [alt]="nombre" />
<!-- Event binding: del DOM a la clase -->
<button (click)="incrementar()">Sumar</button>
<!-- Interpolación: lectura simple -->
<p>{{ contador }}</p>[propiedad]asigna un valor de la clase al atributo/elemento HTML.(evento)escucha un evento y llama a un método del componente.{{ }}muestra el valor, siempre en texto.
Para formularios, el two-way binding con ngModel sincroniza el input con la propiedad en ambas direcciones. Requiere importar FormsModule en el componente:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-buscador',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="query" placeholder="Buscar..." />
<p>Buscando: {{ query }}</p>
`,
})
export class BuscadorComponent {
query = '';
}[(ngModel)] es azúcar sintáctico de [ngModel] + (ngModelChange). Entender esa descomposición te ayuda a escribir tus propios componentes con binding bidireccional.Directivas
Las directivas modifican el comportamiento o la apariencia de los elementos. Angular trae varias integradas:
<!-- Control flow integrado -->
@if (estaCargando) {
<p>Cargando...</p>
}
@switch (estado) {
@case ('ok') { <p>Todo correcto</p> }
@case ('error') { <p>Algo falló</p> }
@default { <p>Estado desconocido</p> }
}
@for (tarea of tareas; track tarea.id; let i = $index) {
<p>{{ i }}. {{ tarea.titulo }}</p>
}Para estilos condicionales están ngClass y ngStyle:
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'app-alerta',
standalone: true,
imports: [NgClass],
template: `
<div [ngClass]="{
'alerta-exito': tipo === 'exito',
'alerta-error': tipo === 'error'
}">
{{ mensaje }}
</div>
`,
})
export class AlertaComponent {
tipo = 'exito';
mensaje = 'Guardado correctamente';
}La regla general: @if/@for/@switch para estructura del DOM, ngClass/ngStyle para clases y estilos, y directivas de atributo personalizadas para comportamiento reutilizable.
Services e inyección de dependencias
Un service es una clase con lógica reutilizable (llamadas HTTP, estado compartido, cálculos). La inyección de dependencias (DI) de Angular crea y entrega esas instancias donde las pidas, sin que tú gestiones el ciclo de vida:
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ContadorService {
private total = 0;
incrementar(): number {
this.total += 1;
return this.total;
}
}providedIn: 'root' registra el service en el inyector raíz: es un singleton disponible en toda la app. Para consumirlo dentro de un componente se usa la función inject():
import { Component } from '@angular/core';
import { ContadorService } from './contador.service';
@Component({
selector: 'app-contador',
standalone: true,
template: `<button (click)="sumar()">Total: {{ total }}</button>`,
})
export class ContadorComponent {
private contadorService = inject(ContadorService);
total = 0;
sumar() {
this.total = this.contadorService.incrementar();
}
}inject() es la forma funcional moderna de la inyección, alternativa al constructor clásico constructor(private svc: ContadorService). Se llama solo dentro de contextos de inyección (constructores, initializers de campos o factories de providers).HttpClient y APIs
Para consumir APIs primero se habilita provideHttpClient() en 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(),
],
};Después, el service inyecta HttpClient y define métodos tipados que devuelven Observables:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Usuario {
id: number;
nombre: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UsuariosService {
private http = inject(HttpClient);
private api = 'https://jsonplaceholder.typicode.com/users';
listar(): Observable<Usuario[]> {
return this.http.get<Usuario[]>(this.api);
}
obtener(id: number): Observable<Usuario> {
return this.http.get<Usuario>(`${this.api}/${id}`);
}
crear(usuario: Partial<Usuario>): Observable<Usuario> {
return this.http.post<Usuario>(this.api, usuario);
}
}En el componente, asyncPipe se suscribe y desuscribe automáticamente, evitando memory leaks:
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { UsuariosService } from './usuarios.service';
@Component({
selector: 'app-lista-usuarios',
standalone: true,
imports: [AsyncPipe],
template: `
<ul>
@for (usuario of usuarios$ | async; track usuario.id) {
<li>{{ usuario.nombre }}</li>
}
</ul>
`,
})
export class ListaUsuariosComponent {
private usuariosService = inject(UsuariosService);
usuarios$ = this.usuariosService.listar();
}firstValueFrom(). El asyncPipe es la forma idiomática y evita suscripciones manuales en el componente.Routing
El router de Angular mapea URLs a componentes. Las rutas se declaran en app.routes.ts:
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UsuarioDetalleComponent } from './usuario-detalle/usuario-detalle.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'usuarios/:id', component: UsuarioDetalleComponent },
{ path: '**', redirectTo: '' },
];El componente raíz incluye router-outlet (donde se renderiza la ruta activa) y enlaces con routerLink:
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, RouterLink],
template: `
<nav>
<a routerLink="/">Inicio</a>
<a routerLink="/usuarios/1">Usuario 1</a>
</nav>
<router-outlet />
`,
})
export class AppComponent {}Para leer parámetros de la ruta, habilita withComponentInputBinding() al registrar el router. Así el parámetro llega como input del componente:
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
],
};import { Component, Input, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { UsuariosService } from './usuarios.service';
@Component({
selector: 'app-usuario-detalle',
standalone: true,
imports: [AsyncPipe],
template: `
@if (usuario$ | async; as usuario) {
<h1>{{ usuario.nombre }}</h1>
<p>{{ usuario.email }}</p>
}
`,
})
export class UsuarioDetalleComponent {
@Input() id!: string;
private usuariosService = inject(UsuariosService);
usuario$ = this.usuariosService.obtener(Number(this.id));
}ActivatedRoute y leer snapshot.paramMap.get('id'). withComponentInputBinding() es más declarativo: la ruta pasa el parámetro como si fuera un input y tu componente queda testeable de forma aislada.Con esto tienes el ciclo completo: componente que renderiza, service que pide datos, router que navega. Angular te da estructura clara a medida que la app crece, y el modo standalone mantiene el código simple sin renunciar a la escala enterprise.
Ejercicios
Angular brilla en proyectos grandes donde la estructura importa: su inyección de dependencias, el router y el modo standalone te dan una base sólida para escalar de un componente a una aplicación enterprise sin perder el control. Empieza con un componente pequeño, añade servicios cuando haya lógica compartida y deja que el router organice el resto.