Apariencia
💡 Conceptos — Angular
Conceptos clave explicados con mis propias palabras.
🌐 Frontend = HTML + CSS + JavaScript
El frontend es la parte de la web que el usuario ve y con la que interactúa (lo que se ejecuta en el navegador). Se sostiene sobre 3 pilares:
| Pilar | Para qué sirve | Analogía |
|---|---|---|
| HTML | La estructura y el contenido (textos, imágenes, botones). | El esqueleto |
| CSS | El estilo: colores, tamaños, posición, diseño. | La ropa / apariencia |
| JavaScript | El comportamiento: interactividad, lógica, peticiones. | El cerebro / músculos |
💡 Angular es un framework de frontend basado en estos tres + TypeScript (JavaScript con tipos).
🎨 Pre-procesadores CSS
Un pre-procesador es un lenguaje que extiende el CSS con funciones que el CSS puro no tenía (o tardó en tener): variables, anidación, mixins, funciones… Escribes en ese lenguaje y una herramienta lo compila a CSS normal (el navegador solo entiende CSS).
Escribes SCSS ──(compilación)──► CSS puro ──► el navegador lo entiende| Pre-procesador | Nota |
|---|---|
| Sass / SCSS | El más popular. SCSS usa {} y ; (parecido a CSS); Sass usa indentación. |
| Less | Similar a SCSS, más antiguo, hoy poco usado. |
💡 Conecta con la pregunta de
ng newsobre estilos: ahí eliges si tu proyecto usará CSS puro o un pre-procesador. → verComandos/04-ng-new.md.
⚠️ Hoy el CSS moderno ya tiene variables nativas (
--mi-color) y anidación, así que los pre-procesadores son menos imprescindibles que antes, pero siguen muy usados.
🧩 Pre-procesador vs Framework CSS (¡no es lo mismo!)
El profe separó el mundo del CSS en dos ramas. Ojo con la diferencia:
| Qué es | Ejemplos | |
|---|---|---|
| Pre-procesador CSS | Un lenguaje que extiende el CSS (variables, anidación…) y se compila a CSS. | Sass / SCSS, Less |
| Framework CSS | Una librería de estilos ya hechos que usas en tu HTML para maquetar más rápido. | Tailwind, Bootstrap |
🌬️ Tailwind = framework utility-first
Utility-first ("primero utilidades") significa que en vez de escribir CSS aparte, maquetas combinando clases pequeñas directamente en el HTML, donde cada clase hace una sola cosa:
html
<!-- Tailwind: clases utilitarias en el propio HTML -->
<div class="flex p-4 text-center bg-blue-500 rounded">Hola</div>
<!-- │ │ │ │ └ bordes redondeados
│ │ │ └ fondo azul
│ │ └ texto centrado
│ └ padding (espaciado interno)
└ display: flex -->💡 El profe escribió "first utility"; el término correcto es "utility-first".
💡 Resumen: Sass/Less te ayudan a escribir CSS; Tailwind te da clases para no escribir CSS propio casi nunca. Por eso en
ng newaparecen juntos como opciones de estilo, aunque sean conceptos distintos.
🏷️ Semántica web (HTML semántico)
HTML semántico = usar etiquetas que describen el significado del contenido, no solo cómo se ve. Ayuda al SEO, a la accesibilidad (lectores de pantalla) y a que el código sea más legible.
| Etiqueta | Qué representa |
|---|---|
<head> | Metadatos del documento (no visible): título, enlaces a CSS, etc. |
<body> | Todo el contenido visible de la página. |
<header> | Cabecera de la página o de una sección (logo, menú). |
<section> | Una sección temática del contenido. |
<footer> | Pie de página (créditos, enlaces, contacto). |
⚠️ Ojo (apunte de entrevista):
<div>NO es semántica — es una caja neutra sin significado. Se usa solo para agrupar/maquetar cuando ninguna etiqueta semántica encaja. Lo semántico es preferir<header>,<section>,<footer>,<nav>,<article>,<main>,<aside>antes que llenar todo de<div>.
🧩 Anatomía de un componente (lo más importante de Angular)
Un componente es la pieza básica con la que construyes una app Angular: un trozo de interfaz reutilizable. La app es, en el fondo, un árbol de componentes.
🧱 ¿Qué puede ser un componente? (casi todo)
Un componente puede representar muchísimas cosas de la interfaz, desde algo pequeño hasta una página entera:
| Pequeños | Medianos | Grandes |
|---|---|---|
| 🔘 un botón | 🪪 una card (tarjeta) | 📋 un formulario |
| 🏷️ una etiqueta | 🍔 un menú | 🗔 un modal / ventana |
| 🔍 un input | 🧭 una barra de navegación | 📄 una página completa |
💡 La idea: divides la interfaz en piezas (componentes) y las combinas y reutilizas. Un mismo componente
botonpuede usarse 50 veces; unacardse repite por cada producto; unmodalse reutiliza en toda la app. Eso es componetizar.
❓ ¿Por qué Angular usa componentes?
Porque permiten:
- ✂️ Dividir la aplicación en piezas pequeñas y manejables.
- ♻️ Reutilizar código (escribes una vez, usas muchas).
- 🗂️ Organizar responsabilidades (cada componente hace una cosa).
- 🔧 Facilitar el mantenimiento (arreglas/cambias una pieza sin tocar el resto).
- 👥 Trabajar en equipo (cada quien desarrolla componentes distintos a la vez).
🎬 Ejemplo concreto: MovieCardComponent + reutilización
Un componente MovieCard (tarjeta de película) representa: 🖼️ imagen, 📝 título, ⏱️ duración, 🔞 clasificación y 🛒 botón comprar. Todo eso empaquetado en una sola pieza.
Lo potente es la reutilización: defines la MovieCard una vez y la repites por cada película, cambiando solo sus datos:
html
<MovieCard /> <!-- Aída y vuelta -->
<MovieCard /> <!-- Hokum, La maldición de la Bruja -->
<MovieCard /> <!-- Obsesión -->
<MovieCard /> <!-- ...y así por cada película -->💡 Misma estructura, distintos datos: todas las tarjetas comparten el mismo diseño y comportamiento (el componente), pero muestran una película diferente. Esos datos se le pasan al componente con inputs (lo verás más adelante:
@Input/input()).
Cada componente se reparte normalmente en 3 archivos (separa responsabilidades):
| Archivo | Capa | Qué contiene |
|---|---|---|
app.ts | 🧠 Lógica | La clase TypeScript: datos, métodos, comportamiento. |
app.html | 🦴 Estructura/vista | El HTML (la "semántica") que se ve en pantalla. |
app.scss | 🎨 Estilos | El CSS/SCSS que aplica solo a este componente. |
💡 Los estilos de un componente están encapsulados: por defecto no afectan a otros componentes (a diferencia de
styles.scss, que es global).
🔌 El decorador @Component (el "pegamento")
El archivo de lógica (app.ts) tiene un decorador @Component({...}) que le dice a Angular cómo es ese componente y conecta las 3 capas:
ts
@Component({
selector: 'app-root', // 🏷️ nombre de etiqueta para usarlo: <app-root>
imports: [RouterOutlet], // 📦 qué otros componentes/directivas usa este
templateUrl: './app.html', // 🦴 enlaza su HTML (la vista)
styleUrl: './app.scss', // 🎨 enlaza sus estilos
})
export class App { } // 🧠 aquí va la lógica| Propiedad | Para qué sirve |
|---|---|
selector | El nombre de etiqueta con el que insertas el componente en otro HTML (<app-root>). |
imports | Lo que este componente necesita usar (otros componentes, directivas, pipes). Propio de los componentes standalone. |
templateUrl | Ruta al archivo HTML del componente. (Alternativa: template: con el HTML en línea.) |
styleUrl | Ruta al archivo de estilos. (Alternativa: styles: en línea.) |
💡 Decorador = etiqueta que añade info.
@Component"marca" una clase normal de TS como un componente de Angular y le adjunta su configuración (vista, estilos, selector).
🔗 Así se cierra el ecosistema:
@Componentenapp.tsapunta contemplateUrlalapp.htmly constyleUrlalapp.scss. Los 3 archivos forman un solo componente.
app.ts (@Component)
┌─────────────────────────┐
│ selector → <app-root> │
│ templateUrl → app.html ─┼──► 🦴 vista (HTML)
│ styleUrl → app.scss ─┼──► 🎨 estilos (SCSS)
│ class App { ... } │──► 🧠 lógica (TS)
└─────────────────────────┘🧪 Tip de entrevista: "¿Qué es
@Component?" → un decorador que convierte una clase en un componente Angular y enlaza su plantilla (templateUrl), estilos (styleUrl) y nombre de uso (selector).
🏷️ El selector en detalle
ts
selector: 'app-root',El selector es el nombre de etiqueta HTML con el que usas el componente en otra plantilla. Es como crear tu propia etiqueta de HTML:
html
<!-- usar el componente = escribir su selector como etiqueta -->
<app-root></app-root>- Donde escribas
<app-root></app-root>, Angular inserta ahí todo el HTML de ese componente. (Por eso funciona el<app-root>delindex.html.) - Puedes reutilizarlo las veces que quieras: si tienes un componente
selector: 'app-tarjeta', pones<app-tarjeta>muchas veces y aparece repetido.
🔤 Convención de nombres del selector
- Suele llevar el prefijo
app-(app-root,app-header,app-tarjeta). Ese prefijo evita choques con etiquetas HTML reales o de otras librerías. - Se escribe en kebab-case (minúsculas con guiones), igual que las etiquetas HTML.
- El prefijo se puede cambiar al crear el proyecto/componente (
--prefix).
💡 Regla de oro: el
selectorque pongas en@Componentes exactamente la etiqueta que debes escribir en el HTML. Si no coinciden, el componente no aparece.
⚠️ El
selectordel componente raíz (app-root) es especial porque coincide con la etiqueta delindex.html. Los demás componentes los insertas dentro de otros componentes, no en elindex.html.
📝 Nombres antiguos: en proyectos viejos verás
app.component.ts,app.component.html,app.component.scssystyleUrls: [...](en plural/array). Mismo concepto, nomenclatura previa.
🔀 Data Binding (los "canales de comunicación" vista ↔ lógica)
Data Binding es el nombre general para las distintas formas en que se comunican la vista (app.html) y la lógica (app.ts) de un componente. Cada tipo es un "canal" distinto, según qué dirección viaja la información:
| # | Tipo | Sintaxis | Dirección |
|---|---|---|---|
| 1 | Interpolación | {{ }} | Lógica → vista (mostrar un dato). |
| 2 | Event Binding | (evento) | Vista → Lógica (la vista avisa que pasó algo, p. ej. un clic). |
| 3 | Two-way Binding | [(ngModel)] | Lógica ↔ vista (las dos direcciones a la vez). |

🗺️ Mismo diagrama que en
01-Clases/cocinero-principal.md, aplicado al código real de esa clase (cocineroPrincipal,llamarMesero()/llamarSeguridad(),comentarioPlato).
🖱️ Event Binding (evento) en detalle
Es la forma de reaccionar desde la vista a algo que hace el usuario (clic, tecla, envío de formulario…) y ejecutar un método de la clase. Se escribe el nombre del evento entre paréntesis:
html
<button (click)="saltar()">Saltar</button>
<button (click)="dibujar()">Dibujar</button>(click)= el evento que escuchas (entre paréntesis)."saltar()"/"dibujar()"= el método de la clase que se ejecuta cuando ocurre ese evento. El profe usósaltarydibujarcomo ejemplos de acciones.
🍽️ Ejemplo real de la clase (restaurante TecyLab)
ts
// app.ts
cocineroPrincipal: string = "Señor tecylab";
llamarMesero() {
console.log("alerta mesero la mesa de sandivel necesita asistencia");
}html
<!-- app.html -->
<h3>Bienvenido a su mesa, {{cocineroPrincipal}}</h3>
<button (click)="llamarMesero()">LLAMAR AL MESERO</button>Aquí se ven los dos canales juntos: {{cocineroPrincipal}} muestra el dato (interpolación) y (click)="llamarMesero()" reacciona al clic (Event Binding), ejecutando el método que imprime la alerta en consola. → ver 01-Clases/2026-07-04-cocinero-principal.md.
💡 Regla mental: interpolación
{{ }}saca datos de la clase hacia la vista; Event Binding( )manda avisos de la vista hacia la clase. Son las dos caras del Data Binding, en direcciones opuestas.
📚 Categorías de eventos en Angular
Teoría que compartió el profe: todo Event Binding usa la misma sintaxis — (evento)="metodo()" —, pero los eventos que puedes escuchar se agrupan en 3 categorías según de dónde vienen:
| Categoría | De dónde viene | Ejemplo |
|---|---|---|
| Eventos del DOM (nativos) | El navegador (clics, teclado, formularios) | (click), (input), (keyup.enter) |
| Eventos personalizados | Otro componente (comunicación hijo → padre) | @Output() cambio = new EventEmitter() |
| Eventos del ciclo de vida | El propio Angular | ngOnInit(), ngOnChanges(), ngOnDestroy() → ver más arriba (sección Ciclo de vida de un componente) |
1️⃣ Eventos del DOM (nativos)
Permiten que el template reaccione a lo que hace el usuario:
| Grupo | Eventos | Ejemplo |
|---|---|---|
| Ratón | (click), (dblclick), (mouseenter), (mouseleave), (mousemove) | <button (click)="saltar()"> |
| Teclado | (keydown), (keyup), (keypress) — se pueden filtrar por tecla | (keyup.enter)="guardar()" (solo dispara con Enter) |
| Formularios/Inputs | (input), (change), (blur), (focus) — el envío del formulario usa (ngSubmit) | (ngSubmit)="procesarFormulario()" |
El objeto
$event— Angular le pasa al método la información nativa del evento del navegador. Con inputs de texto, lo normal es leer$event.target.value(o castearevento.target as HTMLInputElementen TypeScript, como en el ejemplo real de abajo):html<input (input)="onInput($event)">tsonInput(evento: Event): void { const input = evento.target as HTMLInputElement; console.log(input.value); }→ Ejemplo real de esto en
01-Clases/ciclo-de-vida.md(componenteinformacion, captura el texto de un<input>con(input)+evento.target as HTMLInputElement).
📌 Las otras 2 categorías (Eventos personalizados con
@Output()/EventEmitterpara que un hijo le avise algo a su padre, y el detalle de los Eventos del ciclo de vida) quedan pendientes de completar cuando se vean con más profundidad en clase.
🔄 Two-way Binding [(ngModel)]
El profe lo agregó como punto 3 de la lista, con su sintaxis: [(ngModel)]. Combina los corchetes [ ] (property binding, dato hacia el HTML) con los paréntesis ( ) (event binding, dato desde el HTML) — por eso se le dice "banana in a box" 🍌📦 (el ( ) envuelto en [ ]).
🍽️ Ejemplo real de la clase (restaurante TecyLab)
ts
// app.ts
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [FormsModule, RouterOutlet], // 👈 FormsModule habilita ngModel
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
comentarioPlato: string = '';
}html
<!-- app.html -->
<input type="text" [(ngModel)]="comentarioPlato" placeholder="Aqui puede agregar notas">FormsModulees obligatorio enimportspara poder usarngModel(viene de@angular/forms, no de@angular/core).- Escribes en el
<input>→comentarioPlatose actualiza sola en la clase (vista → lógica). Si cambiarascomentarioPlatodesde el código, el<input>mostraría el nuevo valor (lógica → vista). Las dos direcciones a la vez, de ahí el nombre.
⚠️ Error real que dio este ejemplo: al agregar
FormsModuleaimports, se reemplazóRouterOutleten vez de dejar los dos (imports: [FormsModule]en vez deimports: [FormsModule, RouterOutlet]). Como el HTML seguía usando<router-outlet/>, Angular tiróNG8001: 'router-outlet' is not a known element.importses una lista: agregar uno no debe borrar los que ya estaban. → ver../06-Errores/Angular-TypeScript/2026-07-04-router-outlet-no-reconocido.md.
💡 La idea general: todos son formas de conectar el HTML con la clase TS del componente, para que se mantengan sincronizados sin que tengas que tocar el DOM a mano.
🔗 Interpolación {{ }} (mostrar datos en la vista)
La interpolación es la forma de mostrar en el HTML un dato que vive en la clase del componente. Se usan dobles llaves {{ }} y dentro pones el nombre de la propiedad.
ts
// mi-componente.ts (la clase = lógica/datos)
export class MiComponente {
nombre: string = 'Leonardo';
edad: number = 18;
}html
<!-- mi-componente.html (la vista) -->
soy {{ nombre }} <br>
tengo {{ edad }} años.➡️ En pantalla se ve: soy Leonardo / tengo 18 años. Angular reemplaza{{ nombre }} por el valor de la propiedad nombre.
- Es one-way (un solo sentido): de la clase → la vista. Si cambia el dato, la vista se actualiza sola.
- Dentro de
{{ }}va una expresión simple: una propiedad ({{ edad }}), una operación ({{ edad + 1 }}) o llamar un método ({{ saludar() }}). - No se ponen llaves para atributos HTML (eso es property binding con
[ ], lo verás después). La interpolación es para texto/contenido.
💡 Regla mental: "lo que pongas entre
{{ }}en el HTML se rellena con el valor que tenga esa variable en el.ts". Es el puente lógica → vista.
🔤 Tipos de las propiedades (TypeScript)
Como Angular usa TypeScript, a cada propiedad le pones su tipo:
ts
nombre: string = 'Leonardo'; // texto
edad: number = 18; // número
haceEjercicio: boolean = true; // verdadero/falso
random: number | string = ''; // "union": puede ser número O texto| Tipo | Qué guarda | Ejemplo |
|---|---|---|
string | Texto | 'Leonardo' |
number | Números | 18 |
boolean | true / false | true |
number | string | Unión: uno u otro tipo | 5 o 'hola' |
💡 El
|(barra) crea un tipo unión:number | stringsignifica "puede ser número o string". TypeScript te avisará si intentas meter otro tipo.
⚙️ Métodos en un componente
Además de propiedades (datos), una clase de componente puede tener métodos (acciones/funciones). Anatomía de un método en TypeScript:
ts
metodo(parame: string): string { // nombre(parámetro: tipo): tipo-que-devuelve
console.log('hola' + parame); // hace algo (aquí, imprime en la consola)
return 'hola' + parame; // devuelve un valor
}| Parte | Qué es |
|---|---|
metodo | El nombre del método (cómo lo llamas). |
(parame: string) | El parámetro que recibe y su tipo (aquí, un string). |
: string (tras el )) | El tipo de lo que devuelve (lo del return). |
console.log(...) | Imprime en la consola del navegador (útil para depurar). |
return ... | Devuelve un valor a quien llamó el método. |
- Para llamarlo desde la vista:
{{ metodo('Leonardo') }}(interpolación) o, más común, desde un evento:<button (click)="metodo('hola')">(lo verás más adelante). - Si un método no devuelve nada, su tipo de retorno es
void.
💡 Propiedades vs métodos: las propiedades guardan datos (
nombre,edad); los métodos ejecutan acciones (calcular, llamar una API, responder a un clic). Juntos forman la lógica del componente (el.ts).
📝 Template literals (comillas invertidas ` + ${ })
Un template literal es una forma de escribir strings que permite incrustar variables directamente adentro del texto, en vez de concatenar con +. Se escribe con comillas invertidas (backticks, `) y cada variable va dentro de ${ }.
ts
servirPlatillo(plato: IPlatillo) {
console.log(`El ${this.cocineroPrincipal} esta preparando: ${plato.nombre}`);
}| Parte | Qué es |
|---|---|
` ` | Las comillas del template literal (ni ' simples ni " dobles). |
${this.cocineroPrincipal} | Inserta el valor de esa propiedad ahí mismo, en medio del texto. |
${plato.nombre} | Puede ir cualquier expresión, no solo una propiedad simple. |
Comparación con concatenar (+), la forma antigua:
ts
// forma antigua (concatenar con +)
console.log('El ' + this.cocineroPrincipal + ' esta preparando: ' + plato.nombre);
// template literal (más legible)
console.log(`El ${this.cocineroPrincipal} esta preparando: ${plato.nombre}`);💡 Por qué se usa: es más legible que sumar strings con
+, y permite saltos de línea dentro del texto sin trucos raros (\n).
🧪 Tip de entrevista: "¿Qué es un template literal?" → un string con comillas invertidas que permite interpolar expresiones con
${ }, sin concatenar con+.
🧾 Interfaces en TypeScript (el "contrato" de forma)
Una interfaz describe qué propiedades debe tener un objeto (nombre y tipo de cada una), sin implementar nada: es solo un molde/contrato. TypeScript la usa para avisarte si un objeto le falta o le sobra algo.
ts
interface IPlatillo {
id: number;
nombre: string;
precio: number;
}ts
// un objeto que "cumple" la interfaz IPlatillo
const sopa: IPlatillo = {
id: 1,
nombre: 'Sopa del día',
precio: 8.5,
};| Parte | Qué es |
|---|---|
interface IPlatillo | El nombre del contrato (convención: prefijo I + PascalCase). |
id: number / nombre: string / precio: number | Cada propiedad y su tipo obligatorio. |
const sopa: IPlatillo | Un objeto tipado con esa interfaz: TS exige que tenga esas 3 propiedades con esos tipos. |
💡 ¿Para qué sirve si no "hace" nada? Para que TypeScript te avise en tiempo de escritura si te falta una propiedad, si pusiste el tipo equivocado, o si escribiste mal un nombre (
preciovsPrecio). Es documentación verificada por el compilador.
💡 Dónde se usa en Angular: las interfaces son ideales para tipar datos que vienen de una API o que defines tú (un
IPlatillo, unIUsuario…), y luego usarlos como tipo de una propiedad, un array (platillos: IPlatillo[]) o la respuesta de un servicio HTTP.
🧪 Tip de entrevista: "¿Para qué sirve una interfaz en TypeScript?" → para definir la forma que debe tener un objeto (propiedades y tipos), sin implementación. Ayuda a detectar errores en tiempo de compilación, no en producción.
🔀 Control flow en el template: @if / @else if / @else
Es la forma moderna (desde Angular v17) de escribir condicionales dentro del HTML de un componente, para mostrar un bloque u otro según una condición. Reemplaza a la directiva antigua *ngIf.
html
@if (promedio <= 13) {
<p>Eres un alumno normal</p>
} @else if (promedio <= 17) {
<p>Eres un alumno bueno</p>
} @else {
<p>Eres un alumno excelente</p>
}| Parte | Qué hace |
|---|---|
@if (condición) { ... } | Si la condición es verdadera, muestra ese bloque. |
@else if (condición) { ... } | Se evalúa solo si el @if de arriba fue falso. Puedes encadenar varios. |
@else { ... } | El "de lo contrario": si ninguna condición anterior se cumplió. Va sin condición y siempre al final. |
📝 Ojo con la sintaxis: cada rama intermedia necesita el
ifdespués del@else(@else if (...)). Un@elsesolo (sinif) no lleva condición — es el catch-all final. Si escribes@elsecon una condición pero sin la palabraif, no es válido.
💡 Por qué reemplaza a
*ngIf: esta sintaxis con@es nativa del template de Angular (no necesita importar ninguna directiva enimports), se lee más parecido a unifnormal de JavaScript, y Angular la optimiza mejor por dentro.
🧪 Tip de entrevista: "¿Cómo se hace un condicional en un template de Angular moderno?" → con los bloques
@if (cond) {…} @else if (cond) {…} @else {…}, disponibles desde Angular v17, en reemplazo de*ngIf.
🔐 Otro ejemplo: @if / @else simple (sin @else if)
Cuando solo hay dos casos (sí/no), no hace falta @else if, alcanza con @if + @else:
html
@if (tieneAcceso) {
<div>Acceso permitido</div>
} @else {
<div>No tiene acceso permitido</div>
}tieneAcceso sería una propiedad boolean de la clase (tieneAcceso: boolean = true;). Si es true → se muestra el primer <div>; si es false → el del @else.
🔁 Control flow en el template: @for (repetir por cada elemento de una lista)
Es el reemplazo moderno de *ngFor: repite un bloque de HTML una vez por cada elemento de un array.
html
<h3>Lista de PCs seleccionadas</h3>
<ul>
@for (producto of listaDePcs; track producto.id) {
<li>Producto seleccionado {{producto.id}}: {{producto.nombre}}</li>
} @empty {
<li>No has seleccionado nada</li>
}
</ul>| Parte | Qué es |
|---|---|
producto of listaDePcs | Por cada vuelta, producto es un elemento del array listaDePcs. |
track producto.id | Obligatorio. Le dice a Angular cómo identificar de forma única cada elemento (normalmente su id), para que sepa qué reciclar cuando la lista cambia (más rápido que redibujar todo). |
@empty { ... } | Bloque opcional: se muestra solo si el array está vacío (listaDePcs.length === 0). Reemplaza el viejo truco de un *ngIf aparte para el caso vacío. |
📝 Corrección sobre
track: en la pizarra se viotrack ida secas, peroidpor sí solo no existe en ese contexto — tiene que ser una propiedad del elemento actual del@for, o seatrack producto.id(usando el nombre que le diste después delof, aquíproducto). Sin elproducto.delante, TypeScript no sabe de dónde sacaridy marca error.
💡
trackes obligatorio (a diferencia del viejo*ngFordondetrackByera opcional). Si tus elementos no tienen un id único, puedes usartrack $index(la posición en el array), aunque no es lo ideal si la lista se reordena.
🧪 Tip de entrevista: "¿Para qué sirve
tracken@for?" → para que Angular identifique cada elemento de forma única y solo actualice/mueva lo que cambió en el DOM, en vez de volver a dibujar toda la lista.
🔗 Angular tiene un bloque más de este estilo:
@switch(reemplaza a*ngSwitch). Se documenta aquí en cuanto se vea en clase.
🧴 Pipes básicos (transformar datos directo en el template)
Un pipe (|) es una función que transforma un dato justo antes de mostrarlo, sin tener que cambiar la propiedad original en la clase. Se escribe dentro de la interpolación, después del dato, con el símbolo |:
html
<p>Mesero: {{ 'pepito' | uppercase }}</p>
<p>Precio total: {{ 225.50 | currency:'PEN' }}</p>➡️ En pantalla: Mesero: PEPITO / Precio total: S/ 225.50.
| Pipe | Qué hace | Ejemplo |
|---|---|---|
uppercase | Pasa el texto a mayúsculas. | `'pepito' |
currency | Da formato de moneda (símbolo + 2 decimales). | `225.50 |
dato | pipe= "tomadatoy pásalo por el pipepipeantes de mostrarlo".dato | pipe:argumento— algunos pipes reciben un argumento después de:(aquí,currency:'PEN'le dice en qué moneda formatear: soles peruanos).- El dato original de la clase no cambia — el pipe solo transforma lo que se muestra.
📝 2 correcciones sobre el ejemplo visto en clase:
- El pipe se llama
currency(en inglés), nocurrendcy— nombre mal escrito en la pizarra.- El código de moneda va entre comillas, como string:
currency:'PEN'. Escribircurrency: PEN(sin comillas) haría que Angular busque una propiedad llamadaPENen la clase (que no existe) en vez de tratarlo como texto.
💡 Por qué usar un pipe y no cambiarlo en el
.ts: mantienes el dato puro en la clase (precio: number = 225.50) y solo lo formateas al mostrarlo. Si en otro lugar necesitas el número sin formato (para sumarlo, por ejemplo), sigue intacto.
🧪 Tip de entrevista: "¿Qué es un pipe en Angular?" → una función que transforma un valor en el template, con sintaxis
valor | nombrePipe:argumento, sin alterar el dato original de la clase.
⚠️ Los pipes también van en imports (componentes standalone)
En un componente standalone (como los de este curso, sin NgModule), los pipes no vienen "gratis" — hay que importarlos en @Component({ imports: [...] }), igual que FormsModule o RouterOutlet:
ts
import { UpperCasePipe, CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-root',
imports: [UpperCasePipe, CurrencyPipe], // sin esto, Angular no reconoce el pipe
templateUrl: './app.html',
})
export class App { }⚠️ Si usas
{{ dato | uppercase }}en el HTML pero olvidas importarUpperCasePipe, Angular tira un error de compilación tipo "The pipe 'uppercase' could not be found". Cada pipe viene de@angular/commoncon su propio nombre de exportación (UpperCasePipe,LowerCasePipe,CurrencyPipe,DatePipe,DecimalPipe…).
💰 currency con más argumentos (moneda, símbolo, decimales)
El pipe currency acepta hasta 4 argumentos, separados por :, no solo el código de moneda:
html
<p>{{ item.precio | currency : 'PEN' : 'symbol' : '1.2-2' }}</p>| Argumento (en orden) | Qué controla | Ejemplo |
|---|---|---|
| 1. código de moneda | Qué moneda usar. | 'PEN' (soles), 'USD' (dólares) |
| 2. formato de visualización | Cómo se ve el prefijo: 'symbol' (S/), 'code' (PEN) o 'symbol-narrow'. | 'symbol' |
3. digitsInfo (decimales) | 'minEnteros.minDecimales-maxDecimales'. | '1.2-2' → siempre 2 decimales exactos |
➡️ 225.50 | currency:'PEN':'symbol':'1.2-2' se ve como S/ 225.50.
💡
'1.2-2'desglosado:1= mínimo 1 dígito entero;2-2= mínimo y máximo 2 decimales (fuerza siempre 2, ni más ni menos — así20se ve20.00, no20).
📅 El pipe date (formatear fechas)
Igual que currency formatea números como dinero, date formatea una fecha (un Date, un texto ISO, o un timestamp) en un texto legible:
ts
import { DatePipe } from '@angular/common';
@Component({
imports: [DatePipe], // igual que CurrencyPipe: hay que importarlo
...
})html
<p>Última actualización: {{ item.actualizado_en | date:'short' }}</p>| Formato | Se ve como |
|---|---|
'short' | 7/4/26, 9:56 PM (fecha + hora, corto) |
'medium' | Jul 4, 2026, 9:56:35 PM |
'shortDate' | 7/4/26 (solo fecha) |
'longDate' | July 4, 2026 |
⚠️ Requisito importante: el pipe
datesolo convierte bien la zona horaria si el texto de fecha indica explícitamente que es UTC (termina enZ, ej."2026-07-05T02:56:16Z"). Si el backend guarda la fecha sin esa marca (ej."2026-07-05 02:56:16", como dadatetime('now')en SQLite), el pipe la muestra sin convertir — mostrando la hora de Greenwich como si fuera la tuya. → ver sección "El pipedate+ SQLite: cuidado con la zona horaria" más abajo para el caso real y la solución (strftime(...'Z')).
🗒️ Comentarios como separadores de sección (estilo de código, no de Angular)
En el HTML de un componente que ya tiene varias secciones (encabezado, formulario, listado…), ayuda marcarlas con un comentario HTML "banner" antes de cada una:
html
<!-- ========================================= -->
<!-- ENCABEZADO DE LA PÁGINA -->
<!-- ========================================= -->
<header>
...
</header>
<!-- ========================================= -->
<!-- SECCIÓN DE NOTAS -->
<!-- ========================================= -->
<section>
...
</section>- Es un comentario HTML normal (
<!-- -->), sin ningún significado especial para Angular — es pura convención de legibilidad, igual que separar capítulos en un documento. - Sirve para ubicarse rápido en un archivo
app.htmlque va creciendo con varias secciones, sin tener que leer todo el árbol de etiquetas. - Es opcional y es gusto del que escribe el código (aquí, un hábito del profe/curso), no una regla de Angular ni de HTML.
💡 Mismo truco funciona en TypeScript: un comentario
// ===== NOMBRE =====antes de un grupo de métodos relacionados cumple la misma función de "separador visual".
🔡 Objeto de JavaScript vs JSON (no es lo mismo)
Surgió practicando el backend (node:sqlite + Express) del ejercicio app-semana02-practica. Al correr esto en la terminal:
bash
node -e "const db = require('./db'); console.log(db.prepare('SELECT * FROM platillos').all());"Se ve algo así en pantalla:
[
[Object: null prototype] {
id: 1,
nombre: 'lomo saltado',
precio: 20,
disponible: 1,
categoria: 'segundos'
},
...
]Se parece a JSON, pero no lo es:
| Qué es | Dónde vive | |
|---|---|---|
| Objeto/array de JavaScript | Datos en memoria: puedes hacer resultado[0].nombre directo. | Solo mientras el programa Node está corriendo. |
| JSON | Un string (texto) con ese mismo contenido, en un formato específico. | Se usa para enviar datos por la red (HTTP) o guardarlos en un archivo .json. |
console.log(...)imprime los objetos de JS usandoutil.inspectpor dentro — por eso se ve "bonito" y parecido a JSON, pero sigue siendo JavaScript, no texto JSON.- Para convertir un objeto JS a JSON de verdad (un string), se usa
JSON.stringify(objeto). Para el camino inverso (string JSON → objeto JS),JSON.parse(string). [Object: null prototype]: detalle denode:sqlite— las filas que devuelve.all()no heredan deObject.prototype(optimización interna). No afecta en nada al convertirlas a JSON;JSON.stringifyyres.json()de Express funcionan igual.
Node en memoria: { nombre: 'lomo saltado', precio: 20 } ← objeto JS
│ JSON.stringify(...) / res.json(...) en Express
▼
Por la red (HTTP): '{"nombre":"lomo saltado","precio":20}' ← JSON (string)
│ JSON.parse(...) / Angular HttpClient lo hace solo
▼
Angular en memoria: { nombre: 'lomo saltado', precio: 20 } ← objeto JS de nuevo💡 Por qué importa: cuando el backend (Express) responde con
res.json(datos), Express hace elJSON.stringifypor ti. Cuando Angular pide esos datos conHttpClient, hace elJSON.parsepor ti también — por eso, en la práctica, casi nunca escribes esas conversiones a mano, pero es clave saber que por el medio viaja texto, no el objeto original.
🧪 Tip de entrevista: "¿Qué diferencia hay entre un objeto de JS y JSON?" → un objeto de JS vive en memoria dentro del programa; JSON es su representación en texto, pensada para transportar o guardar esos datos. Se convierten entre sí con
JSON.stringify()/JSON.parse().
🧰 Servicios, Inyección de Dependencias y HttpClient
Surgió al conectar Angular (app-semana02-practica) a un backend propio (Node + Express + SQLite, → ver 02-Ejercicios/app-semana02-practica/README.md). Hasta ahora los datos (menu) vivían fijos en app.ts; para traerlos de un servidor real hacen falta 3 piezas nuevas.
1️⃣ app.config.ts — dónde se "activan" las piezas globales de la app
ts
// src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(), // activa HttpClient en TODA la app
],
};| Parte | Qué es |
|---|---|
appConfig | La configuración raíz de la app. main.ts la usa para arrancarla (bootstrapApplication(App, appConfig)). |
providers | La lista de servicios globales disponibles para inyectar en cualquier componente o servicio de la app. |
provideHttpClient() | Activa el servicio HttpClient. Sin esto, cualquier clase que lo pida por su constructor falla en tiempo de ejecución con "no provider for HttpClient". |
⚠️ Mismo patrón que el error de
RouterOutletde la clase original:providerses una lista — agregarprovideHttpClient()debe sumarse, nunca reemplazar los que ya estaban.
2️⃣ Un servicio — la clase que sabe hablar con el backend
ts
// src/app/platillo.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface IPlatillo {
id: number;
nombre: string;
precio: number;
disponible: boolean;
categoria: string;
}
@Injectable({
providedIn: 'root', // un único servicio compartido en toda la app
})
export class Platillo {
private readonly apiUrl = 'http://localhost:3000/api/platillos';
constructor(private http: HttpClient) {}
obtenerTodos(): Observable<IPlatillo[]> {
return this.http.get<IPlatillo[]>(this.apiUrl);
}
}- Componente vs servicio: un componente (
app.ts) muestra cosas en pantalla; un servicio (platillo.ts) hace un trabajo (hablar con el backend) que cualquier componente puede pedir, sin duplicar código. @Injectable({ providedIn: 'root' }): marca la clase como servicio e indica que Angular debe crear una sola instancia, compartida por toda la app (no una por componente).
3️⃣ Inyección de dependencias — constructor(private http: HttpClient)
En vez de que la clase cree su propio HttpClient a mano (new HttpClient()), Angular se lo entrega ya listo por el constructor:
provideHttpClient() en app.config.ts
│ "aquí hay un HttpClient para repartir"
▼
constructor(private http: HttpClient) en el servicio
│ Angular se lo inyecta automáticamente al crear el servicio
▼
this.http.get(...) ya se puede usar dentro de la clase💡 Inyección de dependencias (DI): un patrón donde una clase recibe las herramientas que necesita (por su constructor) en vez de crearlas ella misma. Angular arma y entrega esas dependencias automáticamente.
4️⃣ Observable — un dato que llega después
this.http.get<IPlatillo[]>(url) no devuelve el array directo — devuelve un Observable<IPlatillo[]> (de la librería rxjs), algo parecido a una promesa: representa un dato que todavía no existe, hasta que alguien se suscribe (.subscribe(...)) y dispara la petición HTTP real.
🧪 Tip de entrevista: "¿Qué diferencia hay entre un servicio inyectado por constructor y crear el objeto a mano (
new)?" → con inyección de dependencias, Angular controla el ciclo de vida (por ejemplo, una sola instancia compartida conprovidedIn: 'root') y facilita testear el componente reemplazando esa dependencia por una falsa (mock). Connew, quedas atado a esa implementación exacta.
5️⃣ ngOnInit() — cuándo pedir los datos (el ciclo de vida del componente)
Un componente Angular no nace y muere de golpe: pasa por momentos distintos ("ciclo de vida"), y Angular te avisa de cada uno llamando un método con nombre fijo, si lo declaras. El primero y más usado es ngOnInit:
ts
import { Component, OnInit } from '@angular/core';
export class App implements OnInit {
menu: IPlatillo[] = []; // empieza vacío
constructor(private platilloService: Platillo) {}
// Angular lo llama automáticamente UNA SOLA VEZ, apenas termina de
// crear el componente (después del constructor).
ngOnInit(): void {
this.platilloService.obtenerTodos().subscribe((datos) => {
this.menu = datos;
});
}
}| Parte | Qué es |
|---|---|
implements OnInit | Le avisa a TypeScript "esta clase promete tener un método ngOnInit()" (si lo olvidas, TS marca error). |
constructor(...) | Solo para recibir dependencias (inyección). No es el lugar para hacer trabajo pesado ni pedir datos. |
ngOnInit() | El lugar recomendado para cargar datos iniciales (llamadas HTTP, etc.), justo cuando el componente ya está listo. |
⚠️ ¿Por qué no pedir los datos directo en el constructor? El constructor existe para recibir lo que Angular inyecta (
platilloService), no para hacer cosas. Separar "recibir dependencias" de "usarlas" es una convención que evita bugs sutiles (por ejemplo, si el componente todavía no terminó de inicializar sus propiedades).
💡 Hay más "momentos" del ciclo de vida (
ngOnDestroy,ngOnChanges, etc.) — se documentan aquí a medida que aparezcan en el curso. Por ahora,ngOnInites el único que necesitas para cargar datos al arrancar un componente.
🧪 Tip de entrevista: "¿Por qué no hacer la petición HTTP en el constructor?" → el constructor es para inyección de dependencias, no para lógica; Angular garantiza que
ngOnInitse ejecuta cuando el componente ya está listo para trabajar, por eso es el lugar correcto para el "arranque".
6️⃣ .subscribe(...) en detalle — "desenvolver" el Observable
ts
this.platilloService.obtenerTodos().subscribe((datos) => {
this.menu = datos;
});obtenerTodos()devuelve unObservable— en ese instante todavía no se disparó ninguna petición HTTP. UnObservablesin.subscribe()no hace nada (a diferencia de una promesa, que arranca sola)..subscribe(callback)es lo que dispara la petición real y le dice a RxJS "avísame cuando tengas el dato, ejecutando estecallback".- El
callback((datos) => { this.menu = datos; }) recibe como argumento el array de platillos ya convertido de JSON a objetos JS (Angular lo hace solo, conHttpClient).
obtenerTodos() ← crea el Observable (aún no pasa nada)
│ .subscribe(datos => ...) ← esto SÍ dispara la petición HTTP
▼
(tiempo de espera: el backend procesa la consulta a SQLite)
▼
el callback se ejecuta con los datos ya listos💡 Diferencia con una
Promise: unaPromisearranca apenas se crea; unObservablees "perezoso" (lazy) — no hace nada hasta que alguien se suscribe. Por eso, si nunca llamas.subscribe(), la petición HTTP nunca se envía.
7️⃣ CORS — por qué el navegador bloquea la respuesta del backend
Al conectar Angular (localhost:4200) con el backend propio (localhost:3000), la primera petición falló con un HttpErrorResponse en consola y la lista de platillos quedó vacía, aunque el backend sí respondía bien (ya lo habíamos probado con curl). La causa: CORS.
CORS (Cross-Origin Resource Sharing) es una regla de seguridad que aplican los navegadores (no Angular, no Node): JavaScript de una página no puede leer la respuesta de un servidor en otro origen, a menos que ese servidor lo permita explícitamente con un header.
📌 ¿Qué cuenta como "otro origen"? La combinación protocolo + host + puerto.
http://localhost:4200yhttp://localhost:3000son orígenes distintos aunque el host (localhost) sea el mismo — el puerto ya alcanza para que cuenten como diferentes.
Angular (localhost:4200) ──HTTP──► Backend (localhost:3000)
│ │
│ el backend SÍ responde, │
│ pero el NAVEGADOR bloquea │
│◄── que Angular lea la respuesta ───┘
(si el backend no manda el header
Access-Control-Allow-Origin)- El backend sí recibe la petición y sí procesa la consulta a SQLite — el bloqueo pasa después, cuando el navegador decide si le entrega esa respuesta al código de Angular o no.
- Solución: el backend debe mandar el header
Access-Control-Allow-Originindicando qué origen(es) tienen permiso de leer sus respuestas:
js
// backend/index.js
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:4200');
next();
});- Es un
middlewarede Express: código que se ejecuta antes de cada ruta (app.get('/api/platillos', ...)), y connext()deja seguir la petición hacia la ruta correspondiente.
💡 CORS no es un bug de tu código ni de Angular — es una protección del navegador para que una página cualquiera no pueda leer en silencio datos de otro sitio/servidor sin que ese servidor lo autorice.
🧪 Tip de entrevista: "¿Qué es CORS y quién lo aplica?" → una política de seguridad que aplican los navegadores: bloquean que JS de un origen lea respuestas de otro origen (protocolo+host+puerto), salvo que el servidor lo permita con el header
Access-Control-Allow-Origin. El servidor sí procesa la petición — el navegador es quien decide si entrega la respuesta.
🚦 Segunda vuelta: PATCH/POST necesitan un permiso EXTRA (preflight)
El GET de arriba funcionó con un solo header. Pero al agregar un botón que hace PATCH (para guardar validarStock de verdad en SQLite), volvió a fallar — aunque el header Access-Control-Allow-Origin ya estaba puesto.
Motivo: el navegador distingue entre peticiones "simples" (un GET normal) y "no simples" (PATCH/POST/PUT/DELETE, o cualquiera con un body JSON y el header Content-Type: application/json). Para las no-simples, el navegador manda primero una petición de "permiso" — el preflight — con el método OPTIONS, preguntando "¿me dejas mandar un PATCH con este header?". Si el servidor no responde ese OPTIONS correctamente, el navegador nunca llega a enviar el PATCH real.
Angular hace PATCH ──► el navegador manda antes un OPTIONS (preflight)
│
¿el backend responde con los headers correctos?
│ │
SÍ NO
│ │
ahora sí manda el PATCH bloquea todo, ni siquiera
real intenta el PATCHSolución — el backend debe responder el OPTIONS con 3 headers, y devolver 204 (sin contenido) para ese preflight:
js
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:4200');
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.sendStatus(204); // solo confirma el permiso, sin lógica de negocio
return;
}
next();
});| Header | Para qué |
|---|---|
Access-Control-Allow-Origin | Qué origen puede leer la respuesta (ya lo teníamos, para el GET). |
Access-Control-Allow-Methods | Qué métodos HTTP están permitidos (PATCH, POST, etc. — no solo GET). |
Access-Control-Allow-Headers | Qué headers custom puede mandar el cliente (aquí, Content-Type, porque mandamos JSON). |
💡 Por qué
GETno lo necesitó: unGETsin headers especiales cuenta como petición "simple" para el navegador — no dispara preflight. En cuanto agregas un método distinto o unContent-Type: application/json, sí lo dispara.
🧪 Tip de entrevista: "¿Qué es un preflight en CORS?" → una petición
OPTIONSque el navegador manda antes de unPATCH/POST/PUT/DELETE(o cualquier petición "no simple"), preguntando si el servidor lo permite. Si el servidor no responde con los headersAccess-Control-Allow-Methods/-Headerscorrectos, el navegador bloquea la petición real sin siquiera enviarla.
🧬 inject() — la forma funcional de pedir dependencias
Hasta ahora, la única forma vista de inyección de dependencias fue el constructor (constructor(private http: HttpClient) {}, ver sección "Servicios, Inyección de Dependencias..." de arriba). inject() es una función que hace lo mismo — pedirle a Angular una dependencia ya lista — pero sin necesitar un constructor.
ts
// Antes (constructor) — ya visto con HttpClient
export class Platillo {
constructor(private http: HttpClient) {}
}
// Con inject() — mismo resultado, sin constructor
export class Platillo {
private http = inject(HttpClient);
}inject(HttpClient)— le pide a Angular la instancia deHttpClientque ya está registrada (víaprovideHttpClient()enapp.config.ts) y la asigna directo a la propiedadhttp. Es el mismoHttpClientque llegaría por el constructor, solo que pedido de otra forma.- Funciona porque Angular ejecuta el código de la clase dentro de un contexto de inyección válido —
inject()"sabe" en qué contexto está corriendo y de ahí saca la dependencia. Por eso no se puede llamarinject()en cualquier lado (por ejemplo, dentro de unsetTimeouto de un método normal llamado después) — solo funciona en el momento en que Angular está construyendo la clase o ejecutando la función (ver caso de guards abajo).
⚠️ Cuidado con la frase "
inject()se puede usar en cualquier lugar" — es una simplificación que se dice mucho en clase, pero no es exacta. Lo correcto:inject()funciona en más lugares que el constructor (no solo en clases — también en guards, resolvers e interceptors funcionales), pero igual solo dentro de un contexto de inyección válido. Fuera de ese contexto (unsetTimeout, un callback deaddEventListener, un método llamado más tarde por el usuario)inject()falla con un error en tiempo de ejecución ("inject() must be called from an injection context"). No es libertad total, es una restricción distinta a la del constructor, no la ausencia de restricción.
🆕 ¿Por qué existe si el constructor ya funcionaba?
Porque hay lugares donde no hay una clase con constructor: los guards funcionales de rutas (CanActivateFn, ver routing-avanzado.md), los resolvers, los interceptors funcionales, y en general cualquier función suelta que Angular ejecuta dentro de un contexto de inyección:
ts
// auth-guard.ts — una FUNCIÓN, no una clase, no tiene constructor
export const authGuard: CanActivateFn = () => {
const authService = inject(Auth); // 👈 única forma de pedir el servicio aquí
const router = inject(Router);
// ...
};- Antes de
inject()(Angular 14), este tipo de guards se escribía como una clase que implementabaCanActivatecon el servicio en el constructor — más código, y una clase entera solo para una función de sí/no.
📌
inject()se introdujo en Angular 14. Desde la actualización de la guía de estilo oficial de Angular (alrededor de la v17-v18), se recomienda usarinject()en vez del constructor para pedir dependencias en general — no solo en guards/resolvers, sino en componentes y servicios normales también. El profe lo mencionó como "desde Angular 17 se puede usar un servicio directamente [sin constructor]" — la función existe desde antes (v14), pero es correcto que la recomendación oficial de usarla por defecto es más reciente (v17+).
| Constructor (clásico) | inject() (moderno) | |
|---|---|---|
| Dónde funciona | Solo en clases (constructor(...)) | Clases y funciones sueltas (guards, resolvers, interceptors funcionales) |
| Sintaxis | constructor(private http: HttpClient) {} | private http = inject(HttpClient); |
| Desde qué versión | Angular 2+ (siempre existió) | Angular 14+ (recomendado como default desde ~v17) |
| Ventaja | Explícito, familiar | Más corto con muchas dependencias; único modo posible fuera de clases |
🧪 Tip de entrevista: ¿
inject()reemplaza por completo al constructor? → No es obligatorio — ambos siguen funcionando y hacen lo mismo dentro de una clase. La guía de estilo oficial de Angular ahora recomiendainject()como default, pero la razón por la que existe es que hay contextos (guards funcionales, resolvers, interceptors) donde no hay clase ni constructor, y ahíinject()es la única opción.
📎 Ver routing-avanzado.md para el caso real de
inject()usado en unCanActivateFn(Route Guard).
🔂 Patrón Singleton (y cómo Angular ya lo aplica solo)
Singleton es un patrón de diseño (no específico de Angular, existe en cualquier lenguaje orientado a objetos): garantiza que una clase tenga una sola instancia en toda la aplicación, y da un punto de acceso único a esa instancia — nadie más puede crear otra copia por su cuenta.
ts
// Singleton "a mano" (fuera de Angular, solo para ver la idea)
class Configuracion {
private static instancia: Configuracion;
private constructor() {} // 👈 privado: nadie puede hacer "new Configuracion()" desde afuera
static obtenerInstancia(): Configuracion {
if (!Configuracion.instancia) {
Configuracion.instancia = new Configuracion();
}
return Configuracion.instancia;
}
}
const a = Configuracion.obtenerInstancia();
const b = Configuracion.obtenerInstancia();
// a === b → true, siempre es LA MISMA instanciaconstructorprivado — es la pieza clave: impide que cualquier otra parte del código haganew Configuracion()directamente. La única forma de conseguir el objeto es a través deobtenerInstancia().static instancia— se guarda una vez (la primera llamada la crea); todas las llamadas siguientes devuelven esa misma instancia guardada, nunca una nueva.
🅰️ Angular ya te da esto gratis con providedIn: 'root'
Todos los servicios que se vieron en el curso (Platillo, Auth, Productoservice, …) usan este patrón sin escribirlo a mano:
ts
@Injectable({
providedIn: 'root', // 👈 esto ES el patrón Singleton, manejado por Angular
})
export class Auth { /* ... */ }- Angular actúa como la "fábrica" que ya tiene el
Configuracion.obtenerInstancia()de arriba, pero automático: la primera vez que alguien pideAuth(por constructor oinject(Auth)), Angular la crea; todas las veces siguientes, en cualquier componente/servicio/guard de la app, entrega esa misma instancia. - Por eso el estado de un servicio (como el signal
logueadoenAuth) es compartido: si un componente llamaauth.iniciarSesion(), y otro componente distinto (en otra parte de la app) haceauth.estaLogueado(), ve el cambio — porque es literalmente el mismo objeto en memoria, no una copia. - No hace falta escribir el constructor privado ni el
static obtenerInstancia()— elInjectorde Angular (el sistema de Inyección de Dependencias) hace ese trabajo por detrás.providedIn: 'root'le dice "esta clase es singleton, a nivel de toda la app".
💡 Por qué importa para depurar bugs: si dos componentes muestran datos "desincronizados" de un mismo servicio, casi nunca es porque haya "dos instancias" (con
providedIn: 'root'eso no puede pasar) — el problema suele ser change detection (la vista no se refrescó) o estado mutado sin que nadie se entere (ver sección de Signals/reactividad).
🧪 Tip de entrevista: ¿Cómo implementa Angular el patrón Singleton? → Con el Injector (sistema de Inyección de Dependencias) y
providedIn: 'root': la primera vez que se pide una dependencia, el injector raíz la crea y la guarda; cualquier inyección posterior (por constructor oinject()) devuelve esa misma instancia, en cualquier parte de la app.
🧪 Tip de entrevista: ¿Un servicio con
providedIn: 'root'es SIEMPRE un singleton global único? → Casi siempre sí, pero hay una excepción: si además se declara ese mismo servicio en el arrayprovidersde un componente específico, ese componente (y sus hijos) reciben su propia instancia nueva, distinta de la del root — el patrón Singleton en Angular es "por injector", y cada componente con sus propiosproviderscrea un injector hijo nuevo.
🛡️ Route Guards — proteger o interceptar la navegación
Un guard es una función (o clase, en la forma clásica) que Angular Router consulta antes de completar una navegación, para decidir si la deja pasar. Se registran en la ruta (app.routes.ts), no en el componente.
| Tipo | Se ejecuta... | Para qué |
|---|---|---|
CanActivate | Antes de entrar a una ruta | Bloquear acceso (p. ej. sin login) |
CanActivateChild | Antes de entrar a una ruta hija | Proteger toda una sección de una vez |
CanDeactivate | Antes de salir de una ruta | Confirmar "¿salir sin guardar?" |
CanMatch | Antes de que el router decida si la ruta aplica | Como CanActivate, pero puede dejar que se pruebe otra ruta con el mismo path |
ts
// forma moderna: una función (CanActivateFn), con inject()
export const authGuard: CanActivateFn = () => {
const auth = inject(Auth);
return auth.estaLogueado(); // true = deja pasar, false = bloquea
};
// en app.routes.ts
{ path: 'dashboard', component: Dashboard, canActivate: [authGuard] }🧪 Tip de entrevista: ¿
CanActivatevsCanDeactivate? →CanActivatedecide si se puede entrar;CanDeactivatedecide si se puede salir (y por eso recibe la instancia del componente como parámetro, para poder preguntarle su estado — p. ej. "¿tienes cambios sin guardar?").
📎 Código real, con
CanActivateyCanDeactivatefuncionando (más el error debeforeunloadvsCanDeactivate) → ver routing-avanzado.md.
⚡ Lazy Loading — cargar código de una ruta solo cuando hace falta
Por defecto, todos los componentes de las rutas (component: Producto, etc.) se importan arriba de app.routes.ts con un import normal — viajan juntos en el mismo bundle inicial (main.js), aunque el usuario nunca visite esa ruta. Lazy Loading carga ese código bajo demanda, solo cuando el usuario navega ahí.
ts
// Eager (de siempre): viaja en main.js
import { Contacto } from './contacto/contacto';
{ path: 'contacto', component: Contacto }
// Lazy: import() dinámico, queda en su propio chunk aparte
{ path: 'contacto', loadComponent: () => import('./contacto/contacto').then(m => m.Contacto) }loadComponent— carga un componente de forma perezosa.loadChildren— carga un array de rutas completo (útil para una sección con varias sub-rutas, agrupadas en un solo chunk).- Se verifica con
ng build: las rutas lazy aparecen como "Lazy chunk files" separados demain.js; las eager quedan dentro del bundle inicial.
🧪 Tip de entrevista: ¿Cómo confirmas que el lazy loading realmente funciona? → Con
ng build, viendo que la ruta genera un chunk separado; la pestaña Network del navegador en modo desarrollo (ng serve) NO sirve para esto, porque el dev server (Vite/esbuild) sirve cada componente bajo demanda sea eager o lazy.
📎 Código real y evidencia del
ng build→ ver routing-avanzado.md.
📝 Formularios: Template-driven vs Reactivos
Angular tiene dos formas de construir formularios:
Template-driven (ngModel) | Reactivos (FormGroup/FormBuilder) | |
|---|---|---|
| Módulo | FormsModule | ReactiveFormsModule |
| Dónde vive la estructura | Repartida: clase + validadores en el HTML | Centralizada: un FormGroup armado en la clase |
| Validadores | Atributos HTML (required, minlength...) | Funciones TS (Validators.required...) |
| Bueno para | Formularios simples y pequeños | Formularios grandes/dinámicos, testeo sin DOM |
ts
// Reactivo, con FormBuilder (la forma más corta)
formulario = inject(FormBuilder).group({
nombre: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
});⚠️ Con
FormBuilder, el array de cada campo es[valorInicial, validadorSíncrono, validadorAsíncrono](hasta 3 posiciones) — varios validadores síncronos van juntos en un array en la 2ª posición ([Validators.required, Validators.email]), nunca sueltos como elementos separados del array principal.
🧪 Tip de entrevista: ¿Cuándo usar cada uno? → Template-driven para formularios simples; Reactivos cuando se necesita testear la validación sin depender del DOM, o armar/cambiar controles desde código.
📎 Código completo de los dos enfoques (y el error real del array de
FormBuilder) → ver formularios.md.
📅 El pipe date + SQLite: cuidado con la zona horaria (UTC vs hora local)
Al agregar una columna actualizado_en (para mostrar "última actualización" con el pipe date), la hora se veía mal — mostraba 2:56 AM cuando en Perú eran las 9:57 PM.
Causa: datetime('now') en SQLite devuelve la hora en UTC (Perú está en UTC-5), pero como texto sin ninguna marca de que es UTC:
"2026-07-05 02:56:16" ← ¿es UTC? ¿es hora local? el texto no lo diceEl navegador, al no ver una Z (UTC) ni un +/- (offset) al final, asume que ya es tu hora local y la muestra tal cual, sin convertir nada. Por eso salía la hora de Greenwich disfrazada de hora peruana.
Solución: guardar la fecha en formato ISO 8601 completo, con T (en vez de espacio) y Z al final (marca explícita de UTC):
sql
-- en vez de: datetime('now') -> "2026-07-05 02:56:16"
strftime('%Y-%m-%dT%H:%M:%SZ', 'now') -- -> "2026-07-05T02:56:16Z"Con la Z presente, el navegador (y el pipe date de Angular) sí sabe que debe convertir de UTC a tu zona horaria antes de mostrarlo:
SQLite guarda (UTC): 2026-07-05T02:56:16Z
│ el navegador ve la "Z" → SÍ convierte a tu zona horaria
▼
Pantalla (Perú, UTC-5): 4/7/26, 9:56 PM ✅ correcto💡 Regla mental: cualquier fecha/hora que guardes para mostrar después debe llevar su zona horaria explícita en el texto (
Zpara UTC, o un offset+05:00). Sin eso, cada quien la interpreta como quiera — normalmente "como si ya fuera la hora local de quien la lee", que casi nunca es correcto.
🧪 Tip de entrevista: "¿Por qué una fecha se ve con la hora equivocada en el navegador?" → probablemente el string de fecha no indica su zona horaria (falta la
Zo un offset). Sin esa marca,Datede JavaScript la interpreta como hora local en vez de convertirla desde UTC.
❌ Manejar errores de HttpClient: subscribe({ next, error })
Hasta ahora .subscribe((datos) => { ... }) solo tenía un callback: el que se ejecuta cuando todo sale bien. Pero una petición HTTP también puede fallar (sin internet, el servidor cae, o el backend la rechaza a propósito). Para manejar los dos casos, .subscribe(...) acepta un objeto con dos funciones:
ts
enviarACocina(idPlato: number) {
this.platilloService.hacerPedido(idPlato).subscribe({
next: (actualizado) => {
// Se ejecuta si el backend respondió bien (200 OK).
this.menu.update((items) => items.map((p) => (p.id === idPlato ? actualizado : p)));
},
error: () => {
// Se ejecuta si el backend respondió con un error (ej. 400, 404, 500).
this.errorPedido.set('⚠️ No se pudo enviar el pedido: sin stock disponible.');
},
});
}| Parte | Cuándo se ejecuta |
|---|---|
next | La petición funcionó: llegó una respuesta exitosa (200 OK). |
error | La petición falló: el servidor respondió con un código de error, o ni siquiera hubo respuesta (sin conexión). |
Del lado del backend, para que el error llegue con sentido, el servidor responde con un código de estado de error y un mensaje (no un 200):
js
// backend/index.js
if (fila.stock <= 0) {
// VALIDACIÓN: 400 = "Bad Request", la petición no se puede cumplir tal como está.
res.status(400).json({ error: 'Sin stock disponible' });
return;
}HttpClient en Angular detecta automáticamente que 400 es un código de error, y por eso ejecuta el callback error del subscribe(...) en vez del next — no hay que revisar el código de estado a mano.
💡
.subscribe((datos) => ...)de un solo callback (como enngOnInit) sigue siendo válido — es solo una forma más corta cuando no te interesa manejar el caso de error explícitamente. Se usa la forma{ next, error }cuando sí quieres reaccionar distinto ante un fallo.
🧪 Tip de entrevista: "¿Cómo maneja Angular los errores de una petición HTTP?" →
HttpClientconvierte cualquier respuesta con código de error (4xx/5xx) o fallo de red en el canalerrordelObservable; se captura consubscribe({ next, error })en vez de una función sola.
🔘 [disabled] — deshabilitar un botón según una condición (property binding)
html
<button [disabled]="item.stock === 0" (click)="enviarACocina(item.id)">
Enviar a cocina
</button>[disabled]="condición"son corchetes: eso es property binding — le pasa el valor de una expresión de TypeScript (aquí, unboolean) a una propiedad del elemento HTML.- Si
item.stock === 0datrue, el botón queda deshabilitado: ni se puede clicar, ni dispara(click). - Es validación en la interfaz: evita que el usuario intente pedir algo sin stock, aunque el backend también lo valide (por si alguien llama a la API directo, sin pasar por el botón — nunca confíes solo en la validación del frontend).
💡 Regla mental: paréntesis
( )= evento (la vista avisa "pasó algo"); corchetes[ ]= property binding (la clase le manda un valor a una propiedad del HTML).[(ngModel)]combina los dos — por eso se le dice "banana in a box" (ya lo viste en la sección de Two-way Binding).
🔎 Array.find() — buscar un elemento en un array
find() es un método de los arrays de JavaScript/TypeScript: recorre el array y devuelve el primer elemento que cumpla una condición (o undefined si ninguno la cumple).
ts
menu: IPlatillo[] = [
{ id: 1, nombre: 'lomo saltado', precio: 20, disponible: true, categoria: 'segundos' },
{ id: 2, nombre: 'ceviche', precio: 24, disponible: true, categoria: 'entradas' },
];
validarStock(idPlato: number) {
const plato = this.menu.find(p => p.id === idPlato); // busca el platillo con ese id
if (plato) { // si lo encontró (no es undefined)...
plato.disponible = !plato.disponible; // invierte su disponibilidad
}
}| Parte | Qué es |
|---|---|
this.menu.find(...) | Recorre menu buscando el primer elemento que cumpla la condición. |
p => p.id === idPlato | La condición: una función que recibe cada elemento (p) y devuelve true/false. |
if (plato) | find() puede devolver undefined (si no encontró nada) — este if evita un error si intentas usar plato.disponible sobre algo que no existe. |
!plato.disponible | El operador ! (negación) invierte un boolean: si era true pasa a false y viceversa. |
💡 Por qué el
if (plato): comofind()puede no encontrar nada, TypeScript tipa el resultado comoIPlatillo | undefined. Si intentaras hacerplato.disponiblesin comprobar antes que existe, TypeScript marcaría error (no te deja usar propiedades de algo que podría serundefined).
🧪 Tip de entrevista: "¿Qué devuelve
Array.find()?" → el primer elemento que cumple la condición, oundefinedsi ninguno la cumple (a diferencia defilter(), que devuelve todos los que cumplen, en un array nuevo).
🖥️ Estrategias de renderizado (CSR · SSR · SSG)
"Renderizar" = construir el HTML que ve el usuario. La estrategia define dónde y cuándo se arma ese HTML. Angular te lo pregunta al crear el proyecto (ng new).
| Sigla | Nombre | ¿Dónde se arma el HTML? | ¿Cuándo? |
|---|---|---|---|
| CSR | Client Side Rendering | En el navegador (cliente), con JavaScript. | Cada vez que el usuario entra. |
| SSR | Server Side Rendering | En el servidor, antes de enviarlo. | En cada petición. |
| SSG | Static Site Generation | Se genera una sola vez al compilar (build). | Antes de desplegar (páginas ya hechas). |
🔍 Cada una en una frase
- CSR (cliente): el servidor manda un HTML casi vacío + un JS grande; el navegador arma la página. → App rápida al navegar, pero primera carga más lenta y peor SEO.
- SSR (servidor): el servidor manda el HTML ya construido. → Mejor SEO y se ve contenido antes, pero más carga para el servidor.
- SSG (estático): las páginas se generan al hacer el build y se sirven como archivos fijos. → Muy rápido y barato, ideal para contenido que no cambia mucho (blogs, documentación, landing).
💡 Regla rápida para elegir:
- Contenido fijo (blog, landing) → SSG.
- Necesitas SEO + datos que cambian → SSR.
- App privada tipo dashboard (detrás de login, SEO no importa) → CSR.
🧪 Tip de entrevista: "¿Diferencia entre CSR y SSR?" → en CSR el HTML lo arma el navegador; en SSR lo arma el servidor. SSR mejora el SEO y el tiempo hasta ver contenido.
📝 El profe escribió
statis; lo correcto es static (Static Site Generation).
⚡ Reactividad, Zone.js y el problema de rendimiento
Reactividad = que la vista se actualice sola cuando cambian los datos. Tú cambias una variable y la pantalla se refresca sin que tengas que tocar el DOM a mano.
Para saber cuándo refrescar, Angular usa un proceso llamado detección de cambios (change detection).
🌀 Zone.js (el mecanismo "clásico")
Zone.js es una librería que vigila TODO lo que pasa en la app (clics, peticiones HTTP, temporizadores…). Cada vez que ocurre algo, le avisa a Angular: "pasó algo, revisa si hay que actualizar la vista".
⚠️ El problema (lo del dibujo del profe): cuando ocurre un solo evento, Zone.js hace que Angular revise el árbol ENTERO de componentes para ver cuál cambió. Si tu app tiene ~1.000 componentes (
1k c), revisa los 1.000 aunque solo cambió uno. Eso es trabajo desperdiciado → menos rendimiento.
Un clic ──► Zone.js avisa ──► Angular revisa los 1000 componentes 😵
(aunque solo cambió 1)✨ La solución moderna: Signals (desde Angular v17)
Los Signals llegaron a Angular a partir de la versión 17. Son variables "inteligentes" que avisan exactamente qué cambió. Así Angular actualiza solo el componente afectado, no todo el árbol.
📌 Línea de tiempo: Signals = v17. Es la base de la nueva reactividad de Angular y lo que permite el modo zoneless (también v17).
Un clic ──► el signal avisa ──► Angular actualiza SOLO ese componente 🎯- Con Signals, Angular puede funcionar sin Zone.js (modo zoneless) → más rápido.
- Es la dirección hacia la que va Angular (17+).
⚖️ zoneJS vs zoneless (comparativo del profe)
El profe dibujó dos apps iguales con muchos componentes (10k c = 10.000 componentes):
| zoneJS (clásico) | zoneless (moderno, desde V17) | |
|---|---|---|
| ¿Cómo detecta cambios? | Zone.js vigila todo y dispara la revisión. | Los Signals avisan qué cambió. |
| ¿Qué revisa ante un evento? | Todo el árbol de componentes. | Solo el componente que cambió. |
| Rendimiento con muchos componentes | Peor (revisa los 10.000). | Mejor (revisa lo justo). |
| Librería Zone.js | Necesaria. | No se necesita → bundle más pequeño. |
📌
zonelessse introdujo en Angular V17. Es el modo sin Zone.js que se apoya en Signals. Cuantos más componentes tenga la app, más se nota la mejora.
🧪 Tip de entrevista: "¿Qué es zoneless?" → ejecutar Angular sin Zone.js, usando Signals para la detección de cambios. Disponible desde v17.
🧪 Tip de entrevista: "¿Por qué Signals?" → porque Zone.js revisa todo el árbol en cada evento; los Signals permiten actualizar solo lo que cambió (reactividad granular) y prescindir de Zone.js.
⚠️ Caso real (
app-semana02-practica): unng newreciente (Angular 22) crea el proyecto zoneless por defecto (sinzone.jsenpackage.json). Al conectarHttpClienty guardar la respuesta conthis.menu = datos(asignación normal), la petición respondía200 OKpero la pantalla no se actualizaba — sin signal, nada le avisa a Angular que debe repintar. Solución:menu = signal([])+this.menu.set(datos). → ver detalle completo en06-Errores/Angular-TypeScript/2026-07-04-zoneless-menu-no-se-actualiza.md.
🔣 Símbolos de versión ^ y ~ (rangos SemVer)
En el package.json, antes de cada versión hay un símbolo que indica hasta qué actualizaciones automáticas se permiten al hacer npm install. Se basan en SemVer (MAJOR.MINOR.PATCH). → ver detalle práctico en 05-Estructura-proyecto.md.
^ Acento circunflejo (caret)
Permite actualizaciones a versiones menores y de parche (los dos últimos números), siempre que NO cambie el número principal (MAJOR). Sirve para mantener el proyecto actualizado automáticamente con nuevas funciones compatibles.
- Regla: actualiza mientras el primer dígito que no sea cero se mantenga igual.
- Ejemplo:
^1.2.3permite instalar cualquier versión superior a1.2.3pero inferior a2.0.0(p. ej.1.3.0o1.4.5).
~ Tilde
Permite únicamente actualizaciones de parche (el último número) para la versión menor especificada. Se prefiere en proyectos críticos donde la estabilidad es la máxima prioridad y quieres evitar cualquier cambio en las funcionalidades.
- Regla: actualiza solo el último dígito; la versión menor (MINOR) queda bloqueada.
- Ejemplo:
~1.2.3permite de1.2.3hasta< 1.3.0(p. ej.1.2.4,1.2.9).
📊 Resumen visual
^ caret → deja subir MINOR + PATCH (más permisivo)
MAJOR . MINOR . PATCH
│ │ │ ~ tilde → deja subir SOLO PATCH (más estricto)
│ │ │
nunca ^ sí ^ y ~ sí
sube ~ no (ambos)
^1.2.3 → de 1.2.3 hasta < 2.0.0 (1.3.0, 1.4.5… ✅ 2.0.0 ❌)
~1.2.3 → de 1.2.3 hasta < 1.3.0 (1.2.4, 1.2.9… ✅ 1.3.0 ❌)💡 Truco: el caret
^"apunta hacia arriba" → deja subir más (MINOR); la tilde~es más "plana" → casi no deja subir (solo PATCH). Ninguno cruza al siguiente MAJOR.
⚠️ Detalle fino del
^con versiones0.x: la regla real es "el primer dígito distinto de cero no cambia". Por eso^0.2.3solo permite parches (< 0.3.0), porque ahí el dígito que manda es el MINOR, no el MAJOR.
🧪 Tip de entrevista: "¿
^o~para un proyecto crítico?" →~, porque solo acepta parches (bugfixes) y evita cambios de funcionalidad inesperados.
🎯 Ejercicio test: "¿qué versión instala?"
Pregunta típica de examen: dada la definición en el package.json y la última versión disponible ("que hay ahora"), ¿qué versión instala npm?
Con ^ (caret) — sube MINOR y PATCH, nunca el MAJOR:
| Definición | Que hay ahora | Que instala | Por qué |
|---|---|---|---|
^20.3.0 | 20.3.15 | 20.3.15 | Mismo MAJOR (20) → toma la más alta disponible. |
^20.3.0 | 20.5.0 | 20.5.0 | Mismo MAJOR (20), MINOR puede subir → la más alta. |
^20.3.0 | 21.0.0 | 20.x más alta | 21 es otro MAJOR → ❌ no salta; se queda en 20.x. |
Con ~ (tilde) — sube SOLO PATCH:
| Definición | Que hay ahora | Que instala | Por qué |
|---|---|---|---|
~20.3.0 | 20.3.15 | 20.3.15 | Mismo MINOR (20.3) → toma el PATCH más alto. |
~20.3.0 | 20.5.0 | 20.3.x más alta | 20.5 cambia el MINOR → ❌ no sube; se queda en 20.3.x. |
💡 Regla para resolverlo rápido:
- Mira el símbolo:
^deja moverse dentro del MAJOR;~dentro del MINOR.- Dentro de ese rango, npm instala siempre la versión más alta disponible.
⚠️ Caso real: cómo el ^ rompe un despliegue (anécdota del profe)
Historia real que contó el profe y que explica por qué esto importa tanto:
- En el
package.jsontenían una librería con^13.0.1. - En el servidor de despliegue (el pipeline) se ejecutó
npm install. - Como
^permite subir el MINOR, npm instaló13.1.0(más nueva). - Pero
13.1.0traía cambios incompatibles → el proyecto se rompió y hubo que cambiar mucha sintaxis. - Lo peor: en local seguía funcionando (con la versión vieja ya instalada), así que perdieron muchísimo tiempo descubriendo qué se había roto.
local: ^13.0.1 → ya tenía 13.0.1 instalada → todo OK ✅
despliegue: ^13.0.1 → npm install baja 13.1.0 → ¡se rompe! ❌
(13.1.0 está dentro del rango ^, pero no es compatible)📌 Moraleja: el
^puede meter una versión nueva sin que tú cambies nada, y romperte producción. Por eso es clave entender estos símbolos (y, mejor aún, fijar versiones con un lock file → ver abajo).
🔒 package-lock.json y npm ci (la solución al caso anterior)
El package-lock.json es un archivo que npm genera automáticamente y que guarda las versiones EXACTAS que se instalaron (no el rango ^/~, sino el número exacto: 13.0.1). Así todos —tu máquina, la de un compañero, el servidor— instalan exactamente lo mismo.
🆚 package.json vs package-lock.json (la diferencia clave)
En una frase:
package.json= lo que quieres (rango, lo escribes tú);package-lock.json= lo que realmente se instaló (versión exacta, lo genera npm).
package.json | package-lock.json | |
|---|---|---|
| ¿Quién lo escribe? | Tú (a mano o con npm install). | npm, automáticamente. |
| ¿Qué versión guarda? | El rango permitido: ^20.3.0. | La versión exacta: 20.3.25. |
| ¿Para qué sirve? | Declarar qué necesita el proyecto (intención). | Reproducir el mismo node_modules en cualquier máquina. |
| ¿Qué paquetes lista? | Solo los directos (los que tú instalaste). | Todos: los tuyos + los de tus dependencias (árbol completo). |
resolved / integrity | No. | Sí (URL de descarga + hash de seguridad). |
| ¿Se edita a mano? | Sí. | No, lo maneja npm. |
🍪 Analogía:
package.jsones la receta ("necesito harina marca X, v20 o superior");package-lock.jsones el ticket de compra ("compré exactamente harina marca X, lote 20.3.25"). La receta dice qué sirve; el ticket garantiza que todos compren lo idéntico.
🧪 Tip de entrevista: "¿Diferencia entre
package.jsonypackage-lock.json?" → el primero guarda rangos (intención); el segundo, las versiones exactas instaladas (reproducibilidad). El lock incluye todo el árbol de dependencias +integrity.
✅ Lo que dijo el profe: "si quieres que otra persona instale lo mismo que tú, debe usar el
package-lock.json". Ese archivo es el que reproduce el entorno exacto. (Ojo: es el lock, no elpackage.jsona secas — elpackage.jsonsolo tiene el rango.)
🔬 Qué hay dentro del package-lock.json
Por cada paquete instalado (en node_modules), el lock guarda una ficha con su info exacta. Ejemplo real de @angular/compiler:
jsonc
"node_modules/@angular/compiler": {
"version": "20.3.25", // versión EXACTA instalada
"resolved": "https://registry.npmjs.org/...", // de DÓNDE se descargó
"integrity": "sha512-TSh6gVoQ...", // huella para verificar que no se alteró
"license": "MIT", // licencia del paquete
"peer": true, // es una "peer dependency"
"dependencies": { "tslib": "^2.3.0" }, // de qué depende a su vez
"engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } // qué Node necesita
}| Campo | Para qué sirve |
|---|---|
version | La versión exacta que se instaló (lo que fija el lock). |
resolved | La URL del registro npm de donde se bajó el paquete. |
integrity | Un hash de seguridad: npm verifica que el paquete descargado es idéntico al original (no manipulado). |
license | La licencia legal del paquete (MIT, Apache…). |
dependencies | Las dependencias propias de ese paquete. |
engines | Las versiones de Node con las que ese paquete funciona. |
💡 El campo
integrityes por seguridad: si alguien alterara el paquete en el registro, el hash no coincidiría y la instalación fallaría (te protege de paquetes manipulados).
npm install vs npm ci
| Comando | Qué hace | Cuándo usarlo |
|---|---|---|
npm install | Resuelve los rangos y puede actualizar dentro de ^/~. Actualiza el lock. | Cuando desarrollas y quieres añadir/actualizar paquetes. |
npm ci | Instala exactamente lo que dice el package-lock.json, sin actualizar nada. | En despliegue / CI (servidores, pipelines). Reproducible y seguro. |
💡 Lo que debió pasar en la anécdota: si el pipeline hubiera usado
npm ci(en vez denpm install), habría instalado13.0.1exacta (la del lock) y no se habría roto. Por eso en despliegue se usanpm ci.
⚠️ Sube el
package-lock.jsonal repositorio (no lo ignores en.gitignore). Es lo que garantiza que todos instalen las mismas versiones.
🧪 Tip de entrevista: "¿Diferencia entre
npm installynpm ci?" →installresuelve rangos y puede actualizar;ciinstala las versiones exactas del lock file, ideal para builds reproducibles en CI/CD.
🧰 Qué trae Angular ya instalado (vía npm)
Cuando creas un proyecto con ng new, npm instala automáticamente un montón de cosas para que puedas trabajar desde el primer minuto. Dos importantes que mencionó el profe:
nodeJs
└─ npm
├─ angular: TypeScript ← Angular se programa en TypeScript (no JS puro)
└─ servidor web de desarrollo ← el que arranca `ng serve` / `npm start`- TypeScript: Angular no se escribe en JavaScript puro, sino en TypeScript (JS + tipos). Viene incluido; por eso no tienes que instalarlo en cada proyecto.
- Servidor web de desarrollo: un mini-servidor local solo para programar. Es el que levanta
ng serveenhttp://localhost:4200con recarga automática. No es un servidor de producción.
💡 Por eso un proyecto Angular ya funciona apenas lo creas: trae el lenguaje (TypeScript) y el servidor de pruebas listos. Todo eso queda anotado en las dependencias del
package.json. → ver05-Estructura-proyecto.md.
🤖 MCP en Angular (Model Context Protocol)
MCP (Model Context Protocol) es un estándar que permite que un asistente de IA (Cursor, Antigravity, Claude, Copilot…) se conecte a herramientas externas y trabaje con ellas. Piensa en MCP como un "enchufe universal" entre la IA y las herramientas.
🔌 Analogía: MCP es al asistente de IA lo que un puerto USB-C es a tus dispositivos: un conector estándar para que cualquier IA "hable" con cualquier herramienta.
🅰️ El servidor MCP de Angular
El Angular CLI trae su propio servidor MCP. Al conectarlo, tu asistente de IA deja de "adivinar" sobre Angular y trabaja con información oficial y actualizada del CLI.
| Capacidad | Qué hace |
|---|---|
get_best_practices | Le da a la IA las buenas prácticas oficiales de Angular antes de escribir código. |
search_documentation | Busca en angular.dev en tiempo real (docs actualizadas, no de memoria). |
| Generar código | La IA puede pedir al CLI generar componentes, rutas, guards… (ng generate). |
| Analizar el workspace | Lista los proyectos y entiende la estructura para razonar mejor. |
⚙️ Cómo se activa (idea general)
bash
ng mcp # comando del CLI para preparar/configurar el servidor MCPLuego se registra el servidor en el IDE. En VS Code va en .vscode/mcp.json, dentro de una clave servers (así lo confirma la doc oficial de Angular):
json
{
"servers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}💡 Este archivo no lo genera
ng newautomáticamente: lo agregas tú a mano (o VS Code lo crea cuando usas su UI de "Add MCP Server"). Por eso un proyecto recién creado conng newno trae.vscode/mcp.json, aunque sí traeextensions.json,launch.jsonytasks.json.
📌 Versiones: el servidor MCP de Angular apareció como experimental (~v20) y es estable desde Angular v21 — justo la versión del CLI que tienes instalada.
💡 Por qué importa: las IAs entrenan con datos hasta cierta fecha y Angular cambia rápido (Signals, zoneless…). Con el MCP, la IA consulta la documentación viva y sigue las buenas prácticas actuales en vez de sugerir código antiguo.
⚠️ No confundir: "construir una app que use MCP" (tu app es cliente/servidor MCP) es distinto de "usar el servidor MCP del Angular CLI" (para que la IA te ayude a programar en Angular). Aquí hablamos de lo segundo.
📦 Gestores de dependencias de Node
Un gestor de dependencias es la herramienta que descarga, instala y actualiza las librerías (paquetes) que tu proyecto necesita, y las guarda en node_modules/. Lleva la cuenta de qué versión usas en el package.json para que el proyecto sea reproducible en otra máquina.
Vienen con Node: al instalar Node ya tienes
npm. Los otros se instalan aparte.
| Gestor | Qué es |
|---|---|
| npm | El gestor oficial de Node (viene incluido). El más usado por defecto. |
| pnpm | Alternativa más rápida y que ahorra disco: guarda los paquetes una sola vez y los enlaza entre proyectos. |
| yarn | Alternativa creada por Facebook; popular por su velocidad e instalaciones reproducibles. |
💡 Los tres hacen lo mismo (instalar dependencias); cambian en velocidad, uso de disco y algunos comandos. En este curso lo normal es usar npm.
⚠️ Recuerda: tu Angular CLI mostró
Package Manager: yarn, así queng newintentará usar yarn. Si quieres npm:ng new app --package-manager npm. → ver01-Comandos.md.
📝 En la pizarra el profe escribió
npmn; lo más probable es que se refiera a pnpm (el segundo gestor más conocido junto a npm y yarn). Confirmar con el profe.
🔄 Ciclo de vida de un componente (hooks)
Un componente Angular no nace y muere de golpe: pasa por momentos concretos (crearse, recibir datos, pintarse, destruirse), y Angular avisa de cada uno llamando un método con nombre exacto. A esos métodos se les llama hooks del ciclo de vida.
| Momento | Método | Cuándo se dispara |
|---|---|---|
| 0. Nace el objeto | constructor() | Al crear la instancia de la clase — no es un hook de Angular, es JS/TS puro, se ejecuta antes que cualquier otro. |
1. Cambia un @Input | ngOnChanges(changes: SimpleChanges) | Solo si el componente tiene al menos un @Input(). Se dispara antes que ngOnInit (incluida la primera vez que llega el valor inicial) y cada vez que el padre le pasa un valor nuevo. changes['prop'].currentValue/.previousValue dan el valor nuevo/anterior. |
| 2. Listo para usarse | ngOnInit() | Una sola vez, justo después de que Angular termina de armar el componente (ya puede leer sus @Input). Aquí se inicializan datos, no en el constructor. |
| 3. Se destruye | ngOnDestroy() | Justo antes de quitar el componente de la pantalla (p. ej. al salir con @if/@else o cambiar de ruta). Sirve para "limpiar" (cancelar suscripciones, timers, etc.). |
ts
import { Component, OnInit, OnDestroy } from '@angular/core';
export class Saludo implements OnInit, OnDestroy {
constructor() {
console.log('nace (constructor)');
}
ngOnInit(): void {
console.log('listo (ngOnInit)');
}
ngOnDestroy(): void {
console.log('se destruye (ngOnDestroy)');
}
}implements OnInit, OnDestroy: son interfaces de@angular/coreque obligan (por TypeScript) a escribir el método con el nombre exacto si la clase dice que lo implementa. Angular igual llamaría al método por convención de nombre aunque no se declareimplements, pero declarar la interfaz le pide a TypeScript que verifique en compilación que el método existe.- Caso de uso real de
ngOnInit: inicializar una propiedad con un valor (p. ej.this.mensaje = 'Bienvenido al curso') en vez de hacerlo en elconstructor, para no mezclar la creación del objeto con la lógica de arranque del componente.
🧪 Tip de entrevista: ¿Por qué usar
ngOnIniten vez de poner todo en elconstructor? → Elconstructores de JavaScript/TypeScript (crea el objeto);ngOnInites de Angular y se garantiza que corre después de que los@Inputya tienen valor. Poner lógica que depende de esos@Inputen el constructor puede fallar porque todavía no llegaron.
🧪 Tip de entrevista: ¿En qué orden se ejecutan
ngOnChangesyngOnInit? →ngOnChangessiempre antes, y solo si el componente declara al menos un@Input().ngOnInitcorre una sola vez;ngOnChangesse repite cada vez que cambia algún@Input(confirmado en consola:naci→entoy en ngOnChanges→entre al ngOnInit).
→ ver 01-Clases/ciclo-de-vida.md (clase completa, con el código real de saludo y bienvenida y los errores/erratas que salieron).