Template (.html), Estructura
El HTML que describe qué se renderiza. Puede contener bindings ({{ }}), directivas
(@if, @for) y eventos ((click)).
Antes de entender qué es Angular, es más útil entender qué problema vino a resolver. Cuando una aplicación web es pequeña, una página con tres botones, JavaScript vanilla es suficiente. El problema aparece cuando la aplicación crece.
En JavaScript puro, el HTML que el usuario ve vive en el DOM. El estado de la aplicación (datos que el código conoce) vive en variables. No hay ningún mecanismo automático que los mantenga sincronizados.
Cada vez que el estado cambia, el desarrollador debe recordar qué parte del DOM reflejar ese cambio y escribir el código imperativo para hacerlo. Esto funciona, pero no escala.
⚠️ Vanilla JS, código imperativo
// El estado vive aquílet contador = 0;
// El DOM vive en otro lugarconst btn = document.querySelector('#btn');const span = document.querySelector('#valor');
// El desarrollador sincroniza manualmentebtn.addEventListener('click', () => { contador++; span.textContent = contador; // ← obligatorio: sin esta línea el valor del contador cambia pero el usuario sigue viendo el anterior});Con 3 variables y 2 elementos funciona. Con 50 variables y 200 elementos, olvidar una sincronización produce bugs silenciosos.
✅ Angular, declarativo
// El estado vive en la clasecontador = 0;
// El template declara la relación// <span>{{ contador }}</span>// <button (click)="contador++">
// Angular se encarga de la sincronización.// El desarrollador solo describe// qué debe mostrarse, no cómo hacerlo.La cantidad de elementos en pantalla puede crecer sin agregar complejidad a la sincronización.
Aquí está el contraste en acción. Nota cómo en el lado vanilla el código sincroniza manualmente, mientras que en Angular el framework sincroniza automáticamente.
<!-- HTML -->
<button id="btn">Incrementar</button>
<p>Resultado: <span id="val">0</span></p>
// TypeScript
const btn = document.querySelector('#btn');
const val = document.querySelector('#val');
let contador = 0;
btn.addEventListener('click', () => {
contador++;
val.textContent = contador;
});contadorEl estado y la pantalla ya no coinciden.
⚠️ El HTML solo pone id; la conexión se arma después, en el TypeScript. Apaga la línea y sigue haciendo clic: la variable cambia, la pantalla no. Mantenerlas iguales es trabajo tuyo.
<!-- Template -->
<button (click)="contador++">
Incrementar
</button>
<p>Resultado: {{ contador }}</p>
// TypeScript
contador = 0;contadorAquí no hay interruptor: no existe esa línea.
✅ La conexión se declara en el propio HTML: (click) dice qué pasa al hacer clic y {{ }} dice qué mostrar. No queda ninguna línea de sincronización que puedas olvidar.
id para que el TypeScript encuentre los elementos y los conecte a mano. En Angular la conexión se declara en el mismo HTML, y por eso no queda una línea de sincronización que olvidar. Cuando se olvida, el error es silencioso: nada se rompe, solo se muestra un valor viejo.Complejidad que aparece
Lo que un framework aporta
El salto mental más importante al adoptar Angular no es aprender su sintaxis, es cambiar la forma de pensar:
Esto es posible porque Angular compila los templates: convierte el HTML con sintaxis Angular en código JavaScript optimizado que sabe exactamente qué partes del DOM deben actualizarse cuando cada pieza del estado cambia.
La unidad fundamental de Angular, el building block, es el componente. Todo lo que el usuario ve en pantalla está construido con componentes. Entender qué es un componente, qué contiene y cómo se relacionan entre sí es la base de todo lo demás.
Un componente es una pieza de UI autocontenida que encapsula tres cosas:
Template (.html), Estructura
El HTML que describe qué se renderiza. Puede contener bindings ({{ }}), directivas
(@if, @for) y eventos ((click)).
Clase (.ts), Comportamiento
TypeScript que define el estado del componente (propiedades) y su lógica (métodos). El template solo accede a lo que la clase expone.
Estilos (.css), Apariencia
CSS encapsulado: los estilos de un componente no afectan a otros componentes por defecto. Cada componente tiene su propio ámbito de estilos. Si no se usa este atributo, los estilos dele componente corresponden a los globales de la aplicación.
@Component({ selector: 'app-album-card', // etiqueta HTML personalizada templateUrl: './album-card.component.html', styleUrl: './album-card.component.css', standalone: true, // Angular 17+: sin NgModule})export class AlbumCardComponent { titulo: string = 'Thriller'; anio: number = 1982;
reproducir() { console.log(`Reproduciendo ${this.titulo}`); }}El template Angular extiende el HTML con sintaxis para reflejar el estado declarativamente:
| Sintaxis | Nombre | Qué hace |
|---|---|---|
{{ expresion }} |
Interpolación | Muestra el valor de una expresión como texto |
[propiedad]="valor" |
Property binding | Pasa un valor a una propiedad del elemento o componente hijo |
(evento)="handler()" |
Event binding | Reacciona a eventos del DOM o del componente hijo |
@if (condicion) { … } |
Control flow | Renderiza el bloque solo si la condición es verdadera |
@for (item of lista; track item) { … } |
Control flow | Repite el bloque por cada elemento de la lista. track es obligatorio |
Aquí puedes practicar la sintaxis de los templates. Hay dos paneles: uno muestra cómo se ve el código ejecutado, el otro muestra ejemplos de cada tipo de binding.
Los cinco ejemplos son de un mismo componente. Cambia el estado en la columna izquierda y observa cómo el template lo refleja.
Muestra el valor de una propiedad como texto
nombre = 'Juan';<p>Hola {{ nombre }}</p>Hola Juan
Enlaza una propiedad del elemento a un valor de la clase
bloqueado = true;<input [disabled]="bloqueado" />Ejecuta un método de la clase cuando ocurre un evento
clicks = 0; contar() { this.clicks++;}<button (click)="contar()"> Sumar</button> <p>Clicks: {{ clicks }}</p>Clicks: 0
Renderiza el bloque solo si la condición se cumple. No lo oculta: lo saca del DOM.
mostrar = false;@if (mostrar) { <p>¡Aparecí!</p>}El nodo no existe mientras la condición sea falsa.
El nodo existe siempre; solo deja de dibujarse.
En pantalla se ven igual. La diferencia está en el marcado, y por eso importa: lo que @if saca del DOM deja de ocupar memoria y de responder a eventos.
Repite el bloque por cada elemento de una lista
items = [ 'Item 1', 'Item 2',];@for (item of items; track item) { <li>{{ item }}</li>}track es obligatorio en @for: le dice a Angular cómo identificar cada elemento para no rehacer la lista entera cuando cambia.Una aplicación Angular es un árbol de componentes. El componente raíz (AppComponent)
contiene a todos los demás. Cada componente puede contener otros componentes, formando
una jerarquía.
── AppComponent (raíz) ├── NavbarComponent ├── RouterOutlet ← el router inserta el componente activo aquí │ ├── AlbumListComponent │ │ └── AlbumCardComponent (se repite con @for) │ └── AlbumDetailComponent └── FooterComponentComponer es poner el selector de un componente dentro del template de otro, como si
fuera una etiqueta HTML propia. Ese acto es el que crea la relación padre → hijo y, al
repetirse, forma el árbol.
<!-- album-list.component.html — el padre --><h2>Mis álbumes</h2>
<app-album-card /> <!-- ← esto es composición -->El hijo no sabe nada de esto: AlbumCardComponent ignora quién lo usa y dónde aparece en
la pantalla. Toda la responsabilidad de ubicarlo es del padre, que es quien lo llama.
import { Component } from '@angular/core';import { AlbumCardComponent } from './album-card.component'; // 1. import de TypeScript
@Component({ selector: 'app-album-list', imports: [AlbumCardComponent], // 2. permiso para usar <app-album-card> en el template templateUrl: './album-list.component.html',})export class AlbumListComponent { }Los componentes se pueden comunicar de distintas formas. Una de ellas es a través de un contrato formal que el hijo declara y el padre completa. La pregunta que decide cuál mecanismo usar no es “¿en qué dirección van los datos?”, sino ¿quién tiene permiso de escribir este valor?
| Mecanismo | Quién escribe el valor | Cómo lo ata el padre | Qué es en el hijo |
|---|---|---|---|
input() |
Solo el padre. El hijo lo lee llamándolo: titulo(). |
[titulo]="album" |
Un InputSignal, de solo lectura: no tiene set() |
output() |
El padre, cuando el hijo se lo pide emitiendo un evento | (reproducir)="poner($event)" |
Un OutputEmitterRef (no es un signal); se usa con .emit(valor) |
model() |
El hijo, y el padre se entera | [(sonando)]="sonando" |
Un ModelSignal, que sí se puede escribir |
| Servicio compartido | Cualquiera que lo inyecte | No se ata en el template: inject(MiServicio) |
Un tercero que guarda el estado, útil entre componentes no relacionados |
Con model() el padre no tiene que declarar nada más: Angular le crea al hijo un evento
de salida con el nombre del input más Change (para sonando, un sonandoChange), y eso
es lo que hace funcionar la sintaxis [( )].
Una tarjeta quiere ponerse a tocar una canción. Pruebe input() y output() —los dos
mecanismos que compiten por resolver este problema— y observe qué pasa con la otra
tarjeta: ahí está la diferencia entre escribir el valor ella misma y pedírselo al padre.
model() y el servicio compartido, de la tabla de arriba, resuelven un problema distinto:
uno en el que el valor sí le pertenece al hijo.
album-list.component.tsexport class AlbumListComponent {
sonando = signal<string | null>(null);
poner(t: string) { this.sonando.set(t); }
}
<!-- template -->
<app-album-card
[titulo]="'Thriller'"
[sonando]="sonando() === 'Thriller'"
(reproducir)="poner($event)" />
<app-album-card [titulo]="'The Wall'" … />export class AlbumListComponent {
sonando = signal<string | null>(null);
}
<!-- template -->
<app-album-card
[titulo]="'Thriller'"
[sonando]="sonando() === 'Thriller'" />
<app-album-card [titulo]="'The Wall'" … />album-card.component.tsimport { input, output } from '@angular/core';
export class AlbumCardComponent {
titulo = input.required<string>();
sonando = input(false);
reproducir = output<string>();
alHacerClic() {
this.reproducir.emit(this.titulo());
}
}import { input } from '@angular/core';
export class AlbumCardComponent {
titulo = input.required<string>();
sonando = input(false);
alHacerClic() {
this.sonando.set(true);
}
}AlbumListComponentaquí vive el estadosonando()[titulo] [sonando]↑ (reproducir)AlbumCardComponenttitulo()sonando()AlbumCardComponenttitulo()sonando()album-card.component.ts:8:16 - error TS2339:
Property 'set' does not exist on type 'InputSignal<boolean>'.AppComponentAlbumListComponentdueño de sonandoAlbumCardComponent[titulo]="'Thriller'"AlbumCardComponent[titulo]="'The Wall'"Es el mismo componente hijo dos veces, con un input() distinto cada uno. Los datos bajan por los corchetes [ ] y los eventos suben por los paréntesis ( ).
input ooutput?", sino ¿quién tiene permiso de escribir este valor? Con input(), solo el padre; por eso el intento de la tarjeta ni compila. Con output(), el padre puede escribirlo cuando el hijo se lo pide, y por eso sí logra coordinar a las dos tarjetas. El estado que coordina a varios hijos tiene que vivir en el padre.Veremos más sobre cómo comunicar componentes más adelante.
Angular es un framework prescriptivo (opinionated, en inglés). Esto significa que el framework ya tomó decisiones de diseño por el equipo: cómo estructurar el proyecto, dónde vive la lógica, qué herramientas usar para routing, HTTP y formularios. No ofrece libertad total, pero reduce la fricción inicial y facilita el trabajo en equipo, porque todos siguen las mismas convenciones. Conocer sus piezas principales permite tomar decisiones de diseño informadas.
| Pieza | Responsabilidad | Analogía |
|---|---|---|
| Componente | Unidad de UI + comportamiento | Un widget con estado propio |
| Servicio | Lógica reutilizable, acceso a datos | La capa de negocio del frontend |
| Router | Navegar entre vistas según la URL | El controlador de flujo de la app |
| HttpClient | Comunicación con APIs REST | El cliente HTTP del backend |
| Formularios reactivos | Captura y validación de input del usuario | Estado del formulario como objeto TypeScript |
| DI (Inyección de Dependencias) | Proveer instancias de servicios a quien los necesite | El contenedor de Spring, en el frontend |
Hasta Angular 15 cada componente debía pertenecer a un modulo, una clase TS decorada con NgModule, la clase declaraba que componentes, directivas y servicios y declaraba sus dependencias a otros módulos. A partir de Angular 17 los componentes pueden ser standalone: se autodescriben, declaran sus propias importaciones y no necesitan NgModule.
Antes, con NgModule
@NgModule({ declarations: [AlbumListComponent], imports: [CommonModule, HttpClientModule], exports: [AlbumListComponent],})export class AlbumModule {}Archivos adicionales obligatorios aunque el módulo hiciera poco.
Ahora, standalone
@Component({ standalone: true, imports: [HttpClientModule], template: `…`,})export class AlbumListComponent {}El componente declara sus dependencias directamente. Sin módulo intermediario.
Angular no ejecuta los templates en tiempo de ejecución. Los templates son compilados por el Angular Compiler durante el build a JavaScript optimizado. Esto tiene dos consecuencias importantes:
Angular 17 introdujo las signals como el mecanismo de reactividad de primera clase, complementando y gradualmente reemplazando a Zone.js.
Angular 1–15 · Zone.js, Angular detecta cambios “envolviendo” las APIs del browser (setTimeout, eventos, HTTP). Cuando cualquiera de ellas se ejecuta, Angular revisa todo el árbol de componentes. Funciona, pero es poco preciso.
Angular 16 · Signals (preview), Se introduce signal(), computed() y
effect(). Un signal notifica exactamente qué cambió, Angular actualiza solo el
componente que depende de ese valor.
Angular 17+ · Signals estables, Signals son la forma recomendada para manejar estado local en componentes. Más predecible, mejor rendimiento, sin necesidad de Zone.js en el futuro.
Angular 19 · La comunicación entre componentes también pasa a signals, input(),
output() y model() se vuelven estables y reemplazan a los decoradores @Input() y
@Output() en código nuevo.
Ya conoce la arquitectura del backend: tres capas (API REST, Lógica de Negocio, Persistencia) construidas en Java y Spring Boot. Angular es la capa cliente que vive en el browser del usuario y se comunica con ese backend a través de HTTP.
Angular y Spring Boot son independientes: hablan a través de HTTP con JSON. El contrato entre los dos lados, las URLs, los verbos, los DTOs, es lo que estudiaste en los artefactos del backend. Angular consume exactamente esos endpoints.
| Concepto backend | Equivalente Angular | Qué conecta |
|---|---|---|
Controller / @RestController |
Componente Angular | Recibe la interacción del usuario, delega al servicio |
Service / @Service |
Servicio Angular (@Injectable) |
Encapsula la lógica de llamadas HTTP y reglas del cliente |
| DTO (Data Transfer Object) | Interface o clase TypeScript | Define la forma del JSON que viaja entre capas |
| Repository | HttpClient en el servicio Angular | Accede a la fuente de datos, en este caso, la API REST |
Validaciones (@NotNull, etc.) |
Validators en formularios reactivos | Reglas de validación, en el cliente y en el servidor |
Angular tiene responsabilidades que no existen en el servidor:
Estado de la UI
Experiencia del usuario
¿Ya tiene claro por qué Angular existe y cómo se arma un componente? Compruébelo con 6 preguntas antes de llegar al primer taller. No afecta la nota, es su radar personal. Si se equivoca, la explicación le dice por qué esa opción no es, sin revelarle cuál sí es, para que lo razone usted mismo.