Parte 5: Pipes App¶
Una aplicación pequeña para aprender pipes: las piezas de Angular que
cambian cómo se muestra un valor en la plantilla (mayúsculas, números con
decimales, monedas, porcentajes). Al trabajar con listas también aprenderás a
distinguir una copia de una modificación del array original. Es una aplicación
Angular 22 creada con ng new, con Tailwind CSS y daisyUI para el aspecto,
igual que la Parte 4: Country App. Aquí no hay API: cada
pantalla es una prueba de un grupo de pipes. El único servicio es
LocaleService, que guarda el idioma elegido
(Módulo PP3).
Proyecto: 05-pipes-app/. Sitio publicado:
angular-pipes.reskyon.com. Ábrelo
mientras lees. Para correrlo
en local usa npm start dentro de la carpeta (http://localhost:4200/).
Esta página va creciendo: cada módulo nuevo se agrega al final. Por ahora cubre
los pipes de texto, los de números, los de fecha, un pipe propio, el diseño responsive,
los pipes poco comunes, AsyncPipe, un componente compartido, un segundo pipe
propio con argumento, un archivo de constantes, pipes propios sobre enum,
ordenación de arrays y búsqueda con un campo que se puede limpiar.
Qué vas a aprender:
- Texto, números y fechas. Dar formato a los valores, leer el título de la ruta y elegir idioma y zona horaria (Módulo PP1, Módulo PP2, Módulo PP3).
- Pipes propios y presentación. Crear
timeZoneconIntl, adaptar la pantalla al celular y compartir el badge de documentación coninput(),computed()y proyección de contenido (Módulo PP4, Módulo PP5, Módulo PP8). - Otros valores y tareas que terminan después. Elegir frases, cortar listas,
recorrer objetos y mostrar promesas y observables con
AsyncPipe(Módulo PP6, Módulo PP7). - Argumentos, constantes y enums. Crear
toggleCase, separar los datos del componente y traducir booleanos y enums conyesNo,colorMapycreatorMap(Módulo PP9, Módulo PP10, Módulo PP11). - Ordenar y buscar héroes. Entender el comparador de
sort, los empates,localeCompare, el filtrado y las referencias; pasar#searchInputal reset (Módulo PP12, Módulo PP13).
Cada módulo sigue el mismo orden: el problema que resuelve, el código del repositorio, cómo funciona por dentro, errores comunes y preguntas de entrevista con su respuesta.
Esta página es una parte de la guía de estudio; allí están el mapa general y la ruta recomendada. Conviene haber visto antes la Parte 4: Country App, de donde viene la configuración de Tailwind CSS y daisyUI.
Módulo PP1: Pipes de texto y el título de la ruta¶
El problema que resuelve¶
Tienes el texto 'HomErO ThOMPsoN' guardado en una señal. Quieres mostrarlo
como Homero Thompson, pero sin cambiar el dato guardado: otra parte del
programa puede necesitarlo tal cual. Si lo arreglas en la clase tendrías que
crear una segunda variable por cada forma de mostrarlo.
Un pipe (tubería) resuelve eso en la plantilla: toma un valor, lo transforma y entrega el resultado para mostrar. El valor original queda intacto.
Documentación: LowerCasePipe,
UpperCasePipe y
TitleCasePipe.
El código¶
El componente declara los datos y los pipes que usa en imports (recortado
a lo de este módulo; el archivo real importa también DatePipe y el badge de
documentación):
@Component({
selector: 'app-basic-page',
templateUrl: './basic-page.component.html',
imports: [LowerCasePipe, UpperCasePipe, TitleCasePipe, TitleComponent],
})
export default class BasicPageComponent {
route = inject(ActivatedRoute)
title = this.route.snapshot.title
nameLower = signal('Homero')
nameUpper = signal('homero')
nameTitle = signal('HomErO ThOMPsoN')
}
La plantilla pone el pipe después de |. El título va en el componente
compartido app-title (más abajo se explica el !):
<app-title [title]="title!" />
<div class="stat-value">{{ nameLower() | lowercase }}</div>
<div class="stat-value">{{ nameUpper() | uppercase }}</div>
<div class="stat-value">{{ nameTitle() | titlecase }}</div>
Resultado: homero, HOMERO y Homero Thompson. Debajo de cada uno la
pantalla muestra Original: ... para que veas que el dato no cambió.
Cómo funciona por dentro¶
- Un pipe es una clase con un método
transform(valor). Angular lo llama cada vez que el valor cambia y pone el resultado en la pantalla. - Los pipes de este módulo son puros: con el mismo valor devuelven siempre el mismo resultado, así que Angular sólo los vuelve a ejecutar cuando el valor cambia, no en cada revisión de la pantalla.
- Como los componentes son standalone, cada pipe se importa en el
importsdel componente. Si lo olvidas, la plantilla no compila. - Se pueden encadenar:
{{ name() | lowercase | titlecase }}.
Leer el título de la ruta¶
Cada ruta de app.routes.ts tiene su title:
{
path: 'basic',
title: 'Basic Pipes',
loadComponent: () => import('./pages/basic-page/basic-page.component'),
},
Dentro del componente, ActivatedRoute sabe en qué ruta está. Su propiedad
snapshot.title es una foto del título en el momento de crear el
componente: un string (o undefined si la ruta no tiene título), no una
señal. Por eso en la página se escribe title, sin paréntesis. Escribir
title() falla, porque title no es una función.
El h1 no está en la página: está en el componente compartido app-title, y
la página le pasa el título como input:
<app-title [title]="title!" />
// title.component.ts
export class TitleComponent {
title = input.required<string>()
}
<!-- title.component.html -->
<h1 class="text-2xl my-5">{{ title() | uppercase }}</h1>
Dos detalles:
- El
!después detitle. El snapshot esstring | undefined, pero el input deapp-titlees obligatorio y sólo aceptastring. Con!le dices a TypeScript «confía, acá siempre hay valor» (todas las rutas tienentitle). Sin el!, la plantilla no compila. - Dentro de
app-titlesí se escribetitle(), con paréntesis: ahítitlees uninput, y los inputs son señales. Mismo nombre, dos cosas distintas: en la página es unstring; en el componente compartido, una señal.
Si necesitaras que el título se actualice mientras el componente sigue vivo,
usarías toSignal(this.route.title); entonces sí se llamaría title() también
en la página.
Errores comunes¶
- Olvidar el pipe en
imports. La plantilla marca que el pipe no existe. - Tratar el título del snapshot como señal (
title()): no es una función. Sólo dentro deapp-titlese leetitle(), porque ahí es uninput. - Pasar
titleaapp-titlesin el!. El input es obligatorio ysnapshot.titlepuede serundefined: la plantilla no compila. - Pensar que el pipe cambia el dato. Sólo cambia lo que se ve.
Resumen¶
Un pipe transforma un valor sólo para mostrarlo. lowercase, uppercase y
titlecase cubren los casos de texto. El título de la ruta se lee con
inject(ActivatedRoute) y snapshot.title.
Preguntas de entrevista¶
1. ¿Qué es un pipe y por qué usarlo en vez de transformar en la clase?¶
Respuesta:
Es una pieza que transforma un valor al mostrarlo en la plantilla. Se usa porque separa el dato (que queda intacto) de su presentación, y porque la misma transformación se reutiliza en cualquier plantilla sin repetir código en cada clase.
2. ¿Qué significa que un pipe sea «puro»?¶
Respuesta:
Que con la misma entrada siempre da la misma salida y no tiene efectos secundarios. Angular aprovecha eso: sólo lo ejecuta de nuevo cuando cambia el valor de entrada, así que es barato usarlo en la plantilla.
3. ¿Qué diferencia hay entre snapshot.title y toSignal(route.title)?¶
Respuesta:
snapshot.title es una lectura única, hecha cuando se crea el componente: sirve
cuando la ruta no cambia mientras el componente está vivo. route.title es un
Observable; con toSignal se convierte en una señal que se actualiza si el
título cambia, y se lee con title().
4. ¿Por qué el pipe va en imports del componente?¶
Respuesta:
Porque en los componentes standalone no existe un módulo que declare lo disponible: cada componente declara lo que usa su plantilla. Así Angular sabe qué pipes, directivas y componentes puede resolver, y el empaquetado puede descartar lo que nadie importa.
Módulo PP2: Pipes de números: number, currency y percent¶
El problema que resuelve¶
Tienes el número 2433232.5567. En pantalla quieres 2,433,232.56, o
$2,433,232.56 si es dinero, y para 0.8567 quieres 85.67%. Redondear,
poner separadores de miles y agregar el símbolo a mano en la clase es
repetitivo y depende del idioma. Los pipes number, currency y percent lo
hacen según la configuración regional (locale).
Documentación: DecimalPipe (el pipe number),
CurrencyPipe y
PercentPipe.
El código¶
export default class NumbersPageComponent {
route = inject(ActivatedRoute)
title = this.route.snapshot.title
totalSales = signal(2_433_232.5567)
percent = signal(0.8567)
}
El componente importa DecimalPipe, PercentPipe y CurrencyPipe. Ojo: el
pipe se escribe number en la plantilla, pero la clase que se importa es
DecimalPipe.
{{ totalSales() | number: '1.2-2' }}
{{ totalSales() | currency: 'CAD' : 'symbol-narrow' : '1.2-2' }}
{{ percent() | percent: '1.2-2' }}
La app arranca en es-AR; con el botón «Change to English» de la página
basic pasa a en-US, y así se ve (el Módulo PP3
muestra cómo cambia con es-AR):
| Pipe | Entrada | Salida |
|---|---|---|
number: '1.2-2' |
2433232.5567 |
2,433,232.56 |
currency: 'CAD' : 'symbol-narrow' : '1.2-2' |
2433232.5567 |
$2,433,232.56 |
percent: '1.2-2' |
0.8567 |
85.67% |
Cómo funciona por dentro¶
Qué significa '1.2-2'. Se lee mínimoDeEnteros.mínimoDeDecimales-máximoDeDecimales:
1: al menos un dígito en la parte entera (0.5, no.5).2-2: entre 2 y 2 decimales, o sea siempre exactamente 2. Redondea y rellena con ceros si hace falta.
Con '1.0-3' mostrarías entre 0 y 3 decimales, sin ceros de relleno.
currency recibe más argumentos: el código de moneda ('CAD'), cómo
mostrar el símbolo y el formato de dígitos. Con 'symbol' el dólar canadiense
sale como CA$; con 'symbol-narrow' sale como $.
percent multiplica por 100. El valor 0.8567 es el 85,67 %. Si guardas
85.67 y lo pasas por percent, verás 8,567.00%.
Los tres pipes dan el formato del locale activo: en otro idioma cambian los separadores de miles y decimales.
Errores comunes¶
- Pasar a
percentun número ya multiplicado por 100. - Importar
NumberPipe: no existe; la clase esDecimalPipe. - Esperar que el pipe cambie el valor guardado: sólo cambia lo que se ve.
- Confundir el orden de los argumentos de
currency: primero la moneda, luego el símbolo, luego los dígitos.
Resumen¶
number formatea cantidades, currency agrega moneda y percent muestra
proporciones. El texto '1.2-2' controla dígitos enteros y decimales. Los tres
respetan el locale.
Preguntas de entrevista¶
1. ¿Qué significa el formato '1.2-2'?¶
Respuesta:
Al menos 1 dígito entero, y entre 2 y 2 decimales (siempre 2). El primer número es el mínimo de enteros; después del punto van el mínimo y el máximo de decimales separados por un guion.
2. ¿Por qué percent con el valor 85.67 muestra 8,567.00%?¶
Respuesta:
Porque percent espera una proporción: multiplica por 100. El valor correcto
para 85,67 % es 0.8567.
3. ¿Qué diferencia hay entre 'symbol' y 'symbol-narrow' en currency?¶
Respuesta:
'symbol' usa el símbolo completo, que distingue monedas parecidas (CA$ para
el dólar canadiense). 'symbol-narrow' usa el símbolo corto ($). Conviene el
corto cuando el contexto ya deja clara la moneda.
4. ¿Cómo mostrarías el número en español de España?¶
Respuesta:
Registrando los datos del locale (registerLocaleData con
@angular/common/locales/es) y entregando LOCALE_ID con 'es' (o pasando el
locale como último argumento del pipe). Entonces 2433232.5567 se ve como
2.433.232,56: los separadores cambian con el idioma.
Módulo PP3: Pipes de fecha: date¶
El problema que resuelve¶
Una fecha de JavaScript, mostrada tal cual, es un texto enorme e ilegible:
Thu Oct 22 2026 15:09:05 GMT-0300 (...). Cada usuario quiere verla distinta:
22/10/26, «jueves 22 de octubre», solo la hora. El pipe date convierte el
mismo valor en el formato que pidas, sin cambiar el dato.
Documentación: DatePipe.
El código¶
La clase solo guarda la fecha en una señal, y el componente importa DatePipe:
customDate = signal(new Date())
{{ customDate() | date }}
{{ customDate() | date: 'medium' : 'GMT-4' }}
{{ customDate() | date: "d MMM, yyyy 'at' hh:mm a" }}
{{ customDate() | date: 'fullDate' }}
{{ customDate() | date: 'yyyy-MM-dd' }}
Con en-US (la app arranca en es-AR; el botón «Change to English» de la
página la pasa a inglés) y la fecha 22 de octubre de 2026, 15:09:05, se ve:
| Pipe | Resultado |
|---|---|
date |
Oct 22, 2026 |
date: 'medium' : 'GMT-4' |
Oct 22, 2026, 2:09:05 PM (cambia según tu zona) |
date: "d MMM, yyyy 'at' hh:mm a" |
22 Oct, 2026 at 03:09 PM |
date: 'h:mm a z' |
3:09 PM GMT-3 (la zona es la del navegador) |
date: 'fullDate' |
Thursday, October 22, 2026 |
date: 'shortDate' |
10/22/26 |
date: 'shortTime' |
3:09 PM |
date: 'mediumTime' |
3:09:05 PM |
date: 'yyyy-MM-dd' |
2026-10-22 |
date: 'EEEE, MMMM d' |
Thursday, October 22 |
El idioma de toda la app: LOCALE_ID¶
La tabla de arriba es con en-US. La aplicación elige el idioma una sola
vez, para todos los pipes, en app.config.ts. Son dos pasos:
registerLocaleData carga los datos de cada idioma que vas a usar, y
LOCALE_ID le dice a los pipes cuál usar. Esta es la versión simple, con
el idioma fijo:
import localeEsAr from '@angular/common/locales/es-AR'
import localeFr from '@angular/common/locales/fr'
import localeEn from '@angular/common/locales/en'
registerLocaleData(localeEsAr)
registerLocaleData(localeFr)
registerLocaleData(localeEn)
export const appConfig: ApplicationConfig = {
providers: [{ provide: LOCALE_ID, useValue: 'es-AR' }],
}
El app.config.ts real del proyecto no usa useValue: el idioma sale de un
servicio, LocaleService, con una fábrica. Se ve más abajo, en «Cambiar el
idioma mientras la app corre».
Si un idioma no está registrado, los pipes fallan. Es la causa más común de
«el francés no me anda»: con fr-FR hay que registrar fr (el -FR usa los
datos de fr).
Con es-AR, en-US y fr-FR, la misma fecha (22 de octubre de 2026,
15:09:05) y los mismos números se ven así:
| Pipe | en-US |
es-AR |
fr-FR |
|---|---|---|---|
date |
Oct 22, 2026 |
22 oct 2026 |
22 oct. 2026 |
date: 'fullDate' |
Thursday, October 22, 2026 |
jueves, 22 de octubre de 2026 |
jeudi 22 octobre 2026 |
date: 'shortDate' |
10/22/26 |
22/10/26 |
22/10/2026 |
date: 'shortTime' |
3:09 PM |
3:09 p. m. |
15:09 |
date: 'yyyy-MM-dd' |
2026-10-22 |
2026-10-22 |
2026-10-22 |
number: '1.2-2' (2433232.5567) |
2,433,232.56 |
2.433.232,56 |
2 433 232,56 |
percent: '1.2-2' (0.8567) |
85.67% |
85,67% |
85,67 % |
En francés los miles se separan con un espacio y el % lleva un espacio antes.
Tres cosas para notar:
- Los formatos predefinidos (
fullDate,shortTime) cambian de orden y de idioma solos. Es la gran ventaja de usarlos. - Un formato propio cambia sólo en lo que depende del idioma, los nombres:
EEEE, MMMM ddajueves, octubre 22, que en español suena raro; para eso se escribe"EEEE, d 'de' MMMM". El orden de las partes lo pones tú. Y un texto entre comillas, como'at', no se traduce. yyyy-MM-ddno cambia nunca: por eso sirve para formularios y APIs.
Cambiar el idioma mientras la app corre. En vez de un valor fijo, el
proveedor puede usar una fábrica (factory): una función que Angular llama
para obtener el valor. Así LOCALE_ID sale de un servicio que guarda el idioma
elegido. Así queda el app.config.ts real del proyecto:
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
{
provide: LOCALE_ID,
useFactory: (localeService: LocaleService) => localeService.getLocale,
deps: [LocaleService],
},
],
}
// LocaleService
changeLocale(newLocale: AvailableLocale) {
localStorage.setItem('locale', newLocale)
this.currentLocale.set(newLocale)
window.location.reload()
}
La trampa: Angular llama a la fábrica una sola vez, al arrancar, y los
pipes leen LOCALE_ID al crearse. Cambiar la señal del servicio después no
cambia el idioma de los pipes que ya existen. Por eso changeLocale guarda el
idioma en localStorage y recarga la página: al arrancar de nuevo, la
fábrica lee el idioma guardado.
Si quisieras cambiar sin recargar, habría que pasar el idioma como tercer
argumento de cada pipe ({{ fecha | date: 'fullDate' : undefined : idioma() }}).
Una fecha que avanza sola¶
En la página basic la hora se mueve cada segundo. Para lograrlo, un effect
cambia la señal con un temporizador:
customDate = signal(new Date())
tickingDateEffect = effect((onCleanup) => {
const interval = setInterval(() => {
this.customDate.set(new Date())
}, 1000)
onCleanup(() => clearInterval(interval))
})
- Cada segundo se guarda un
Datenuevo conset. Como la señal compara por referencia, un objeto nuevo cuenta como cambio y la plantilla se actualiza. Si modificaras el mismoDate(setSeconds), la señal no avisaría y el pipedateno se volvería a ejecutar. onCleanupapaga el temporizador cuando el componente se destruye. Sin eso, elsetIntervalseguiría vivo al salir de la página.- El
effectsolo escribe en la señal, no la lee, así que no se vuelve a disparar por sí mismo: corre una vez y el temporizador hace el resto.
Cómo funciona por dentro¶
Los argumentos. date: formato : zonaHoraria : locale. Los tres son
opcionales. Sin formato usa mediumDate. Sin zona horaria usa la del
navegador. 'GMT-4' es una zona dada como desfase respecto a GMT.
Los formatos predefinidos (presets). Son nombres ya hechos que además respetan el locale:
- Solo fecha:
shortDate,mediumDate,longDate,fullDate. - Solo hora:
shortTime,mediumTime,longTime,fullTime. - Fecha y hora juntas:
short,medium,long,full.
Los formatos propios. Cada letra es una parte de la fecha:
| Letras | Significan |
|---|---|
d, MMM, MMMM, yyyy |
día, mes corto, mes completo, año |
EEEE |
nombre completo del día de la semana |
hh, mm, a |
hora (12 h), minutos, AM/PM |
z |
zona horaria |
Lo que no sea una letra de formato va entre comillas simples, como 'at'.
Por eso el formato completo se escribe entre comillas dobles: "d MMM, yyyy 'at' hh:mm a".
Cuidado con MM y mm. MM es el mes y mm son los minutos.
Con la misma fecha, 'yyyy-mm-dd' muestra 2026-09-22: los minutos (09) en
lugar del mes (10).
Errores comunes¶
- Confundir
MMconmm. Es el error más frecuente en un formato propio. - Olvidar las comillas en el texto fijo (
atsin comillas se interpreta como letras de formato). - Pensar que
datecambia la zona horaria del dato. Solo cambia cómo se muestra; el valor sigue siendo el mismo instante. - Esperar otro idioma sin registrarlo. Para
eshay que registrar sus datos conregisterLocaleData, igual que en el Módulo PP2.
Resumen¶
date formatea fechas con un nombre predefinido (fullDate, shortTime) o con
letras propias (yyyy-MM-dd). Acepta una zona horaria y un locale como
segundo y tercer argumento. Recuerda: MM mes, mm minutos.
Preguntas de entrevista¶
1. ¿Qué diferencia hay entre 'MM' y 'mm' en el formato de date?¶
Respuesta:
MM es el mes y mm son los minutos. Si escribes 'yyyy-mm-dd' obtienes los
minutos donde esperabas el mes: el 22 de octubre de 2026 a las 15:09 se ve
2026-09-22.
2. ¿Cuándo usar un formato predefinido y cuándo uno propio?¶
Respuesta:
El predefinido (shortDate, fullDate) se adapta solo al locale: el orden
día/mes cambia según el país. Es lo mejor para mostrar fechas a personas. El
propio (yyyy-MM-dd) da el mismo resultado en cualquier idioma, y sirve para
entradas de formulario o APIs.
3. ¿Qué hace el tercer argumento de date?¶
Respuesta:
Es el locale: el idioma y la región con que se escriben nombres de meses y
días y el orden de las partes. Primero hay que registrar los datos de ese
idioma (registerLocaleData con @angular/common/locales/es).
4. ¿Cambia el pipe date el valor de la fecha?¶
Respuesta:
No. Solo cambia el texto que se muestra. Con una zona como 'GMT-4' el
instante es el mismo; solo se escribe con la hora de esa zona.
5. ¿Por qué hay que guardar un Date nuevo en la señal en vez de modificar el existente?¶
Respuesta:
Porque la señal avisa del cambio comparando por referencia. Si cambias el mismo
objeto Date por dentro, la referencia es la misma y nadie se entera: la
plantilla no se actualiza y el pipe puro tampoco se vuelve a ejecutar. Con
set(new Date()) la referencia cambia y todo se actualiza.
6. ¿Para qué sirve onCleanup en el effect del reloj?¶
Respuesta:
Para apagar el setInterval cuando el effect se destruye (al salir de la
página) o antes de volver a ejecutarse. Sin eso, el temporizador seguiría
corriendo en segundo plano, cambiando una señal que ya nadie usa: una fuga de
memoria.
7. ¿Cómo cambias el idioma de todos los pipes de una app, y qué dos pasos hacen falta?¶
Respuesta:
En app.config.ts: primero registerLocaleData con los datos del idioma
(@angular/common/locales/es-AR), y luego el proveedor de LOCALE_ID. En la
versión simple es { provide: LOCALE_ID, useValue: 'es-AR' }; en el proyecto
es una fábrica (useFactory) que le pide el idioma a LocaleService. Sin el
primero, los pipes fallan porque faltan los datos; sin el segundo, los datos
están pero nadie los usa.
8. ¿Por qué cambiar la señal del servicio no cambia el idioma de los pipes?¶
Respuesta:
Porque LOCALE_ID se calcula una sola vez, cuando arranca la app: Angular llama
a la fábrica una vez y los pipes toman ese valor al crearse. La señal sí cambia,
pero nadie vuelve a pedirle el valor a la fábrica. Por eso el servicio guarda el
idioma en localStorage y recarga la página.
9. Cambio el idioma a francés y la página falla. ¿Qué reviso primero?¶
Respuesta:
Que el idioma esté registrado: registerLocaleData con
@angular/common/locales/fr. Si solo se registró es-AR, los pipes no
encuentran los datos de fr-FR y fallan. Con fr registrado, fr-FR funciona,
porque usa los datos de fr.
Módulo PP4: Un pipe propio: zona horaria con Intl¶
El problema que resuelve¶
Quieres mostrar la hora de Caracas, de Houston y de Madrid a la vez. El pipe
date recibe la zona como un desfase fijo ('GMT-4'), y eso falla donde
hay horario de verano: Houston es UTC−5 ahora y UTC−6 desde el 1 de noviembre;
Madrid es UTC+2 y pasa a UTC+1 el 25 de octubre. Con un desfase fijo, la hora
queda mal la mitad del año.
Además, date no entiende nombres de zona como America/Chicago. La
herramienta del navegador que sí los entiende es Intl.DateTimeFormat, que
conoce las reglas reales de cada zona. Como Angular no trae un pipe que la use,
se escribe uno propio.
Documentación: pipes personalizados en la guía de Angular. Un pipe propio no tiene página en la referencia de la API, porque lo escribes tú.
Qué es Intl¶
Intl es una herramienta que ya viene dentro del navegador (y de Node). No
es de Angular y no hay que instalarla. Su trabajo es que un mismo dato se
escriba como lo espera cada persona, según su idioma, su país y su zona
horaria.
El problema que resuelve: la misma fecha se escribe 22/10/26 en Argentina,
10/22/26 en Estados Unidos y 22 oct 2026 en otros formatos. Con los números
pasa igual: 2.433.232,56 en español y 2,433,232.56 en inglés. Hacerlo a
mano obliga a conocer las reglas de cada país; Intl ya las conoce.
Siempre se usa en dos pasos: primero se crea un formateador con el idioma y las opciones, y después se le da el valor:
const formato = new Intl.DateTimeFormat('es-AR', { dateStyle: 'full' })
formato.format(new Date())
Trae varios formateadores:
| Formateador | Para qué | Ejemplo |
|---|---|---|
DateTimeFormat |
fechas y horas | jueves, 22 de octubre de 2026 |
NumberFormat |
números y monedas | $ 2.433.232,56 (es-AR) o $2,433,232.56 (en-US) |
RelativeTimeFormat |
tiempo relativo | ayer, dentro de 3 horas |
ListFormat |
listas con «y» | Caracas, Houston y Madrid |
DisplayNames |
nombres de países o idiomas | Venezuela (de 'VE') |
Por qué sirve para las zonas horarias. DateTimeFormat tiene la opción
timeZone, que entiende nombres como America/Chicago. Por dentro trae la base
de datos oficial de zonas horarias, con las reglas del horario de verano de
cada lugar. Con la misma zona y dos fechas se ve el cambio:
1 de julio: 1:00:00 PM CDT (UTC-5)
1 de diciembre: 12:00:00 PM CST (UTC-6)
El mismo mediodía UTC da horas distintas porque Houston cambió de horario. Con
un 'GMT-5' fijo, diciembre saldría mal.
En qué se diferencia de los pipes de Angular:
Pipe date de Angular |
Intl |
|
|---|---|---|
| Quién tiene los datos del idioma | Tu app, tras registerLocaleData |
El navegador |
| Zona horaria | Solo desfases ('GMT-4') |
Nombres (America/Chicago) |
| Idiomas | Solo los que registraste | Casi todos, sin registrar |
Para tener en cuenta: el texto exacto puede variar un poco entre navegadores y versiones (espacios, puntos, la forma de «p. m.»), y crear un formateador cuesta algo, así que si formatearas miles de valores conviene crearlo una vez y reutilizarlo.
El código¶
El pipe vive en src/app/pipes/time-zone.pipe.ts:
@Pipe({ name: 'timeZone' })
export class TimeZonePipe implements PipeTransform {
private localeId = inject(LOCALE_ID)
transform(
value: Date,
timeZone?: string,
{ locale = this.localeId, dateStyle = 'short', timeStyle = 'medium' }: TimeZoneOptions = {},
): string {
return new Intl.DateTimeFormat(locale, {
dateStyle: dateStyle ?? undefined,
timeStyle: timeStyle ?? undefined,
timeZone,
}).format(value)
}
}
Y las opciones, en src/app/interfaces/time-zone.interfaces.ts:
type Style = 'short' | 'medium' | 'long' | 'full'
export interface TimeZoneOptions {
locale?: string
dateStyle?: Style | null
timeStyle?: Style | null
}
En la plantilla de custom-page (con customDate actualizándose cada segundo,
como en el Módulo PP3):
{{ customDate() | timeZone: 'America/Caracas' }}
{{ customDate() | timeZone: 'America/Chicago' }}
{{ customDate() | timeZone: 'Europe/Madrid' }}
{{ customDate() | timeZone: 'America/Argentina/Buenos_Aires' : { dateStyle: null, timeStyle: 'medium' } }}
Con el 22 de octubre de 2026, 18:09:05 UTC, se ve:
| Zona | Resultado |
|---|---|
America/Caracas |
22/10/26, 2:09:05 p. m. |
America/Chicago (Houston) |
22/10/26, 1:09:05 p. m. |
Europe/Madrid |
22/10/26, 8:09:05 p. m. |
Buenos Aires, con { dateStyle: null } |
3:09:05 p. m. (solo la hora) |
Houston no tiene zona propia: usa America/Chicago, la zona de la hora central
de Estados Unidos.
Cómo funciona por dentro¶
El valor y los argumentos. En {{ fecha | timeZone: 'zona' : opciones }},
lo que está a la izquierda del | llega como primer parámetro de
transform, y cada argumento después de : llega como el siguiente. Por eso
la firma es (value, timeZone, options) y no un solo objeto con todo adentro.
Si transform recibe un solo objeto, Angular no compila y avisa de que se
esperaba un argumento y llegaron dos.
El idioma por defecto sale de LOCALE_ID. El pipe pide LOCALE_ID con
inject y lo usa si no le pasas otro idioma. Así habla el mismo idioma que los
pipes de Angular (date, number...) sin repetirlo. Si el idioma estuviera
escrito a mano ('es-AR'), esta página seguiría en español aunque el resto
cambiara a francés.
Solo fecha o solo hora. Intl muestra la fecha con dateStyle y la hora
con timeStyle. Si omites uno, esa parte no aparece. Pero Intl no acepta
null (falla con un error de rango), así que el pipe usa null como señal
propia de «no mostrar esto» y lo convierte con ?? undefined:
{ dateStyle: null }: solo la hora.{ timeStyle: null }: solo la fecha.
Por qué null y no undefined para apagar. Los valores por defecto de la
desestructuración (dateStyle = 'short') solo se aplican cuando la opción
falta (undefined). Si pasas null, el valor se respeta y el pipe lo
interpreta como «apagado». Con undefined volvería el valor por defecto.
La zona sí importa aunque muestres solo la fecha. A las 23:30 del 22 en Caracas, en Madrid ya es el 23. Por eso el pipe siempre recibe la zona.
Errores comunes¶
- Un pipe con un solo parámetro objeto: la plantilla no compila. El valor va primero.
- Pasar
nulldirectamente aIntl: falla. Hay que convertirlo antes. - Pasar
'GMT-4'aIntl: no lo entiende (da un error de rango); usa el nombre de la zona, comoAmerica/Caracas. - Escribir mal el nombre de la zona: da un error de rango. Los nombres
siguen el formato
Continente/Ciudad. - Esperar que el pipe cambie el dato: solo cambia cómo se ve.
Resumen¶
Un pipe propio es una clase con @Pipe y un método transform. El valor a
formatear va primero y los argumentos después. Para zonas horarias con horario
de verano se usa Intl.DateTimeFormat con el nombre de la zona, y el pipe
convierte null en «no mostrar esta parte».
Preguntas de entrevista¶
1. ¿Por qué no basta el pipe date con 'GMT-5' para mostrar la hora de Houston?¶
Respuesta:
Porque 'GMT-5' es un desfase fijo y Houston cambia con el horario de verano:
UTC−5 en verano y UTC−6 en invierno. Durante la mitad del año la hora saldría
una hora corrida. Con el nombre America/Chicago, Intl aplica la regla
correcta de cada fecha.
2. ¿Cómo recibe transform el valor y los argumentos de un pipe?¶
Respuesta:
El valor de la izquierda del | es el primer parámetro; cada argumento después
de : es el siguiente. En {{ fecha | timeZone: 'America/Caracas' : { ... } }}
llegan (fecha, 'America/Caracas', { ... }). Un transform con un solo
objeto no se corresponde con esa llamada y Angular lo marca como error de
compilación.
3. ¿Cómo permites mostrar solo la hora o solo la fecha?¶
Respuesta:
Intl.DateTimeFormat muestra cada parte solo si pasas su estilo
(dateStyle o timeStyle). Como Intl no acepta null, el pipe lo recibe
como opción propia y lo convierte con ?? undefined. Así, { dateStyle: null }
da solo la hora y { timeStyle: null } da solo la fecha.
4. ¿Por qué el valor por defecto no se aplica si pasas null?¶
Respuesta:
Porque los valores por defecto de la desestructuración solo se usan cuando la
propiedad es undefined. null es un valor, así que se conserva. Eso permite
distinguir «no dije nada» (usa el defecto) de «apágalo» (null).
5. ¿Es puro este pipe, y qué implica en una fecha que cambia cada segundo?¶
Respuesta:
Sí: con los mismos argumentos devuelve siempre lo mismo. Angular lo vuelve a
ejecutar cuando cambia la entrada. Por eso el reloj guarda un Date nuevo
cada segundo con set: una referencia nueva activa el pipe, mientras que
modificar el mismo Date por dentro no lo haría.
Módulo PP5: Que se vea bien en el celular: responsive con Tailwind y daisyUI¶
El problema que resuelve¶
Abres la aplicación en el celular y hay dos cosas rotas. Las tarjetas de los
pipes van en fila y se salen de la pantalla, con títulos tan largos como
Currency Pipe: currency: 'CAD' : 'symbol-narrow' : '1.2-2' cortados. Y el
navbar tiene el nombre de la app y cinco botones (las cuatro páginas y la
wiki) en una sola línea, que no caben.
Diseñar para que una página se adapte a cualquier ancho se llama responsive. La idea es que cada pantalla tenga la distribución que le conviene: en el celular, todo apilado; en una pantalla grande, en fila.
El código¶
Las tarjetas (stats). daisyUI las pone en fila por defecto. Se les indica
que sean verticales y que pasen a horizontales en pantallas grandes:
<div
class="stats stats-vertical lg:stats-horizontal bg-base-200 w-full border-b border-dashed border-base-400 mb-5"
>
<div class="stat place-items-center">
<div class="stat-title whitespace-normal text-center">Decimal Pipe: number: '1.2-2'</div>
<div class="stat-value whitespace-normal text-center">{{ totalSales() | number: '1.2-2' }}</div>
</div>
</div>
El navbar. En el celular, los links van dentro de un menú desplegable con un botón de hamburguesa; desde pantallas medianas, vuelven a verse en la barra:
<div class="navbar-start">
<div class="dropdown md:hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-square" aria-label="Open menu">
<!-- icono de tres rayas -->
</div>
<ul tabindex="0" class="menu dropdown-content bg-base-200 rounded-box z-10 mt-3 w-52 p-2 shadow">
@for (route of routes; track route.path) {
<li>
<a routerLinkActive="menu-active" [routerLink]="route.path" (click)="closeMenu()">{{ route.title }}</a>
</li>
}
</ul>
</div>
<a class="btn btn-ghost text-xl md:text-3xl" routerLink="/">{{ title() }}</a>
</div>
<div class="navbar-end hidden md:flex">
<!-- el menú horizontal de siempre -->
</div>
Y en la clase del navbar:
private document = inject(DOCUMENT)
routes = routes
.filter((route) => route.title)
.map((route) => ({ path: route.path ?? '', title: route.title as string }))
closeMenu() {
;(this.document.activeElement as HTMLElement | null)?.blur()
}
Cómo funciona por dentro¶
Puntos de corte (breakpoints). Tailwind usa prefijos que significan «a partir de este ancho»:
| Prefijo | Desde |
|---|---|
| (ninguno) | cualquier ancho, incluido el celular |
sm: |
640 px |
md: |
768 px |
lg: |
1024 px |
Se escribe primero lo del celular y luego lo que cambia en pantallas más
grandes. Por eso stats-vertical lg:stats-horizontal se lee: «vertical, y desde
1024 px, horizontal».
Mostrar una cosa en el celular y otra en pantallas grandes. Son dos bloques distintos, cada uno oculto donde no corresponde:
md:hidden: visible en el celular, oculto desde 768 px (el menú de hamburguesa).hidden md:flex: oculto en el celular, visible desde 768 px (los botones de la barra).
Por qué whitespace-normal. daisyUI pone a stat-title y stat-value la
regla «no partir líneas». Eso corta un texto largo en una pantalla angosta.
Con whitespace-normal el texto se parte en varias líneas, y text-center
lo centra.
Cómo se abre el menú sin JavaScript. El dropdown de daisyUI se abre cuando
el botón tiene el foco (tabindex="0") y se cierra cuando lo pierde. Es
cómodo, pero al elegir un link el foco sigue en el menú y se queda abierto.
Por eso closeMenu() le quita el foco al elemento activo con blur(). El
DOCUMENT se inyecta, en lugar de usar document directamente, para no
depender de una variable global.
Rutas sin título. La ruta ** (la redirección) no tiene title. Sin el
filter, el menú mostraba un botón vacío. La barra horizontal tenía el mismo
problema, solo que menos visible.
Errores comunes¶
- Escribir primero lo de pantalla grande. Los prefijos son «desde este ancho hacia arriba»: lo del celular va sin prefijo.
- Olvidar el
hiddenen el bloque que no corresponde: se ven los dos menús a la vez. - Un botón de menú sin etiqueta: un lector de pantalla solo dice «botón».
Por eso lleva
aria-label, y el icono,aria-hidden. - Probar solo en un monitor grande: abre las herramientas de desarrollo y activa el modo celular.
- Dejar que el menú siga abierto tras navegar: hay que quitarle el foco.
Resumen¶
Responsive es escribir la versión del celular primero y agregar cambios con
prefijos (md:, lg:) para pantallas más grandes. En el navbar, dos bloques
se ocultan y se muestran según el ancho; en las tarjetas, una sola clase cambia
de vertical a horizontal.
Preguntas de entrevista¶
1. ¿Qué significa «mobile first» en Tailwind?¶
Respuesta:
Que las clases sin prefijo valen para todos los anchos, empezando por el
celular, y los prefijos (md:, lg:) solo agregan cambios a partir de cierto
ancho. Así se parte de la versión más simple y se va enriqueciendo.
2. ¿Qué diferencia hay entre md:hidden y hidden md:flex?¶
Respuesta:
md:hidden muestra el elemento en el celular y lo oculta desde 768 px.
hidden md:flex hace lo contrario: lo oculta en el celular y desde 768 px lo
muestra como flex. Juntos permiten tener un menú para cada tamaño.
3. ¿Por qué se oculta un bloque con CSS en lugar de crear uno distinto con @if?¶
Respuesta:
Porque el ancho de la pantalla lo conoce el CSS sin escribir código, y se
adapta solo al girar el celular. Con @if habría que medir la pantalla y
escuchar los cambios. La contrapartida es que ambos menús existen en el HTML,
aunque uno esté oculto.
4. ¿Cómo se abre y se cierra el menú de daisyUI sin JavaScript?¶
Respuesta:
Con el foco: el dropdown se muestra mientras el botón (con tabindex="0")
tiene el foco y se oculta al perderlo. Por eso, tras elegir un link, se llama a
blur() para quitar el foco y cerrarlo.
5. ¿Qué se necesita para que el botón de hamburguesa sea accesible?¶
Respuesta:
Un nombre que lea el lector de pantalla (aria-label="Open menu"), que sea
alcanzable con el teclado (tabindex="0" y role="button") y que el icono
decorativo se marque aria-hidden, para que no lo anuncie como una imagen sin
nombre.
Módulo PP6: Pipes poco comunes: i18nSelect, i18nPlural, slice, json y keyvalue¶
El problema que resuelve¶
Con una lista de personajes en pantalla aparecen preguntas que uppercase y
date no resuelven:
- Quieres decir «invitarlo» o «invitarla» según el género, sin escribir un
@ifpor cada caso. - Quieres decir «no hay nadie», «hay una persona» o «hay 5 personas» sin armar la frase a mano.
- Quieres mostrar solo una parte de la lista, ver un objeto como texto o recorrer sus propiedades.
Para cada una hay un pipe. Todos están en la página uncommon de la app, y por
eso se llaman «poco comunes»: no se usan todos los días, pero ahorran código.
i18nSelect: elegir una frase según un valor¶
Documentación: I18nSelectPipe.
Recibe un texto y un mapa. Busca en el mapa la propiedad que se llama igual que el texto y devuelve su valor:
invitationMap = {
male: 'invitarlo',
female: 'invitarla',
}
{{ client().gender | i18nSelect: invitationMap }}
Si gender es 'male' muestra invitarlo. Si el valor no está en el mapa,
usa la propiedad other, y si tampoco existe, muestra un texto vacío.
i18nPlural: elegir según una cantidad¶
Documentación: I18nPluralPipe.
Es parecido, pero recibe un número. Las claves del mapa que empiezan con
= son coincidencias exactas, y other es el respaldo:
listMap = {
'=0': 'there is no one',
'=1': 'there is one person',
other: 'there are # people',
}
Right now
<strong>{{ characterList().length | i18nPlural: listMap }}</strong>
waiting.
- Con 0 personajes sale «there is no one» y con 1, «there is one person».
- Con cualquier otro número se usa
other, y el#se reemplaza por el número: con 5 sale «there are 5 people». - El
#solo funciona dentro del mensaje, no en la clave. Por eso=0y=1no lo llevan: ya sabes cuál es el número. - El pipe devuelve la frase completa. El texto alrededor no debe repetirla: por eso el template solo pone «Right now» antes y «waiting.» después.
characterList es una señal. Hay que escribir characterList().length: sin
los paréntesis, .length mide la función y siempre da 0.
slice, json y keyvalue¶
Documentación: SlicePipe,
JsonPipe y
KeyValuePipe.
slice corta una parte de un arreglo (o de un texto), igual que el slice de
JavaScript: el primer número es dónde empieza, el segundo dónde termina, y un
número negativo cuenta desde el final. No modifica el arreglo original.
{{ characterList() | slice: 0 : 3 }} <!-- los primeros tres -->
{{ characterList() | slice: -3 }} <!-- los últimos tres -->
{{ characterList() | slice: 1 : 4 }} <!-- del segundo al cuarto -->
json convierte un valor en texto con forma de JSON, con sangría. Sirve para
ver qué trae un objeto o un arreglo mientras desarrollas. Se pone dentro de un
<pre> para que se conserven los saltos de línea:
<pre>{{ characterList() | slice: 0 : 4 | json }}</pre>
keyvalue convierte un objeto en una lista de pares { key, value }, que se
puede recorrer con @for:
@for (item of personProfile | keyvalue; track $index) {
<p>
<strong>{{ item.key }}:</strong> {{ item.value.toString() | titlecase }}
</p>
}
- Por defecto ordena por la clave, de la A a la Z: aunque el objeto se escriba
name, age, gender, address, en pantalla saleaddress, age, gender, name. item.valuepuede ser texto o número, por eso se llamatoString()antes detitlecase, que solo acepta texto.
Cómo funciona por dentro¶
- Los tres primeros reciben un valor y devuelven otro; no cambian el dato original, igual que los pipes de los módulos anteriores.
i18nSelectei18nPluralson una forma de escribir un@switchdentro de una expresión: el mapa hace de lista de casos.- Se pueden encadenar:
characterList() | slice: 0 : 4 | jsonprimero corta y luego convierte. - Cada pipe se importa en
importsdel componente (I18nSelectPipe,I18nPluralPipe,SlicePipe,JsonPipe,KeyValuePipe,TitleCasePipe).
Errores comunes¶
- Olvidar los paréntesis de la señal:
characterList.lengthmide la función; hay que escribircharacterList().length. - Poner el
#en la clave del mapa ('#'): no funciona, va en el mensaje. - Repetir en el template lo que ya dice la frase del mapa: sale «hay hay 5 personas».
- Cambiar un arreglo con
pop()y devolver el mismo arreglo en una señal: la señal no ve el cambio porque es el mismo objeto. Hay que devolver uno nuevo, por ejemploprev.slice(0, -1). - Esperar que
keyvaluerespete el orden del objeto: ordena por clave.
Resumen¶
i18nSelect elige una frase según un texto e i18nPlural según un número
(con # para poner ese número en la frase). slice corta un arreglo sin
tocarlo, json lo muestra como texto y keyvalue convierte un objeto en una
lista que se puede recorrer.
Preguntas de entrevista¶
1. ¿Para qué sirven i18nSelect e i18nPlural, y en qué se parecen?¶
Respuesta:
Los dos eligen una frase de un mapa. i18nSelect busca por un texto (por
ejemplo el género) e i18nPlural por una cantidad. En los dos, la clave
other es el respaldo cuando nada coincide. Evitan escribir @if o @switch
para cada caso en la plantilla.
2. ¿Qué hace el # en el mapa de i18nPlural?¶
Respuesta:
Es un marcador dentro del mensaje: el pipe lo reemplaza por el número que
recibió. Con 5, 'there are # people' queda 'there are 5 people'. No sirve
en las claves, y los casos =0 y =1 no lo necesitan porque el número ya se
conoce.
3. ¿Qué diferencia hay entre la clave '=1' y la clave other?¶
Respuesta:
'=1' coincide solo cuando el número es exactamente 1. other coincide con
cualquier número que no tenga una clave exacta. El pipe revisa primero las
claves exactas y, si ninguna calza, usa other.
4. ¿Modifica slice el arreglo original?¶
Respuesta:
No. Devuelve un arreglo nuevo con la parte pedida y deja el original igual. Por eso en la pantalla se puede mostrar la lista completa y, al lado, solo sus tres primeros elementos.
5. ¿En qué orden recorre keyvalue las propiedades de un objeto?¶
Respuesta:
Por defecto ordena por clave, de la A a la Z, sin importar el orden en que se escribió el objeto. Si necesitas otro orden, el pipe acepta una función de comparación como argumento.
6. ¿Por qué characterList.length daba siempre 0 si characterList es una señal?¶
Respuesta:
Porque una señal es una función, y .length de una función es la cantidad de
parámetros que recibe: cero. Para leer el arreglo hay que llamar a la señal,
characterList(), y recién entonces pedir .length.
Módulo PP7: AsyncPipe: promesas y observables¶
El problema que resuelve¶
Una promesa o un observable entregan su valor más tarde: la promesa a los 2
segundos, el observable cada segundo. Normalmente tendrías que escribir
subscribe() o .then() en la clase, guardar el resultado en una variable y,
para observables, acordarte de cancelar la suscripción al salir.
El pipe async hace todo eso solo: se suscribe, entrega el último valor que
llegó y cancela la suscripción cuando ya no hace falta.
Documentación: AsyncPipe.
El código con una promesa¶
isPromiseActive = signal(true)
promiseValue = signal(this.createPromise())
createPromise(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve('Promise resolved successfully!'), 2000)
})
}
@if (isPromiseActive()) {
@if (promiseValue() | async; as value) {
<p>{{ value }}</p>
} @else {
<p>Loading...</p>
}
}
- Mientras no hay valor,
asyncdevuelvenull, así que el@ifmuestra el@else(«Loading...»). A los 2 segundos llega el texto y el@iflo muestra. ; as valueguarda el valor en una variable para usarla dentro del bloque. Como ya es el texto, no lleva otro| async.promiseValuees una señal, por eso se lee conpromiseValue().
El código con un observable¶
myObservable = interval(1000).pipe(tap((value) => console.log(value)))
isSubscribed = signal(false)
@if (isSubscribed()) {
@if (myObservable | async; as value) {
<p>Observable: <strong>{{ value }}</strong></p>
} @else {
<p>Loading...</p>
}
}
interval(1000) emite 0, 1, 2, 3… un número por segundo y nunca termina. El
tap solo escribe cada valor en la consola, para que veas si la suscripción
sigue viva. Un observable puede emitir muchas veces, y el pipe actualiza la
pantalla con cada valor nuevo. isSubscribed arranca en false: el contador
no corre hasta que pulsas «Subscribe».
Encender y apagar con una señal¶
Los botones no llaman a subscribe() ni a unsubscribe(). Solo cambian una
señal, y el @if pone o quita el pipe de la plantilla:
subscribeToObservable(): void {
this.isSubscribed.set(true)
}
cancelSubscription(): void {
this.isSubscribed.set(false)
}
<button [disabled]="isSubscribed()" (click)="subscribeToObservable()">Subscribe</button>
<button [disabled]="!isSubscribed()" (click)="cancelSubscription()">Cancel Subscription</button>
Cuando el @if quita el pipe, el pipe se destruye y cancela la suscripción
por su cuenta. Al volver a ponerlo, se suscribe de nuevo y el contador
empieza otra vez desde 0. Cada botón se deshabilita cuando no tiene sentido
usarlo.
Con la promesa se hace igual, con una diferencia: una promesa se resuelve una sola vez y no se puede cancelar. Si al volver a activar la card reutilizaras la misma promesa, ya estaría resuelta y el valor aparecería de golpe, sin esperar los 2 segundos. Por eso el botón crea una promesa nueva:
triggerPromise(): void {
this.promiseValue.set(this.createPromise())
this.isPromiseActive.set(true)
}
resetPromise(): void {
this.isPromiseActive.set(false)
}
Al apagar la card, la promesa sigue corriendo en segundo plano, pero el pipe ya no está y nadie escucha el resultado.
Cómo funciona por dentro¶
asyncse suscribe en cuanto aparece en la plantilla y avisa a Angular cuando llega un valor, para que la pantalla se actualice.- Antes del primer valor devuelve
null. - Cuando se destruye (porque el
@iflo quitó o porque se cerró el componente), cancela la suscripción. Por eso no hay que escribirngOnDestroypara esto. intervales un observable frío (cold): cada suscripción empieza su propia cuenta desde 0.
Errores comunes¶
- Poner un segundo
| asyncsobre el valor ya resuelto ({{ value | async }}):valueya es el texto, no una promesa; la compilación falla. - Usar
| asyncvarias veces sobre el mismo observable frío: cada uso crea su propia suscripción, y conintervalserían varios contadores y variosconsole.log. Si necesitas el valor en varios lugares, guárdalo conas. - Esperar que el
@ifmuestre el 0:@iftrata el0como falso, así que el contador muestra «Loading...» los dos primeros segundos (el primero porque no llegó nada, el segundo porque llegó el 0). - Reutilizar una promesa ya resuelta para «volver a cargar»: no vuelve a esperar. Hay que crear una nueva.
- Llamar
subscribe()a mano y no guardar la suscripción: no podrás cancelarla, y cada clic crea otra.
Resumen¶
async se suscribe a una promesa o a un observable, muestra el último valor
(null mientras no hay ninguno) y cancela la suscripción al desaparecer. Para
encender y apagar, basta una señal y un @if. Con promesas hay que crear una
nueva cada vez, porque solo se resuelven una vez.
Preguntas de entrevista¶
1. ¿Qué ventajas tiene async frente a hacer subscribe() en la clase?¶
Respuesta:
Se suscribe y cancela la suscripción solo, así que no hay fugas por olvidar el
unsubscribe(). Además actualiza la pantalla cada vez que llega un valor y
evita guardar el resultado en una variable de la clase.
2. ¿Qué devuelve async antes de que llegue el primer valor?¶
Respuesta:
null. Por eso se combina con @if (… | async; as value) y un @else: el
@else cubre la espera y el bloque principal se muestra cuando ya hay valor.
3. ¿Por qué el contador muestra «Loading...» durante dos segundos y no uno?¶
Respuesta:
Porque interval emite el primer valor, 0, al pasar un segundo, y @if lo
trata como falso, igual que a null. El primer valor «verdadero» (el 1) llega
al segundo 2.
4. ¿Cómo cancelas la suscripción de un async sin llamar a unsubscribe()?¶
Respuesta:
Quitando el pipe de la plantilla, por ejemplo con un @if que depende de una
señal. Al destruirse, el pipe cancela la suscripción por su cuenta. Al volver a
mostrarlo, se suscribe de nuevo.
5. ¿Por qué hay que crear una promesa nueva para «volver a ejecutarla»?¶
Respuesta:
Porque una promesa se resuelve una sola vez y recuerda su resultado. Si el pipe se suscribe a una promesa ya resuelta, recibe el valor de inmediato, sin esperar. Una promesa nueva vuelve a empezar la espera.
6. ¿Qué pasa si usas | async tres veces sobre interval(1000)?¶
Respuesta:
Se crean tres suscripciones, y como interval es frío, cada una tiene su propio
contador y escribe en la consola por separado. Para compartir una sola, guarda
el valor con as en un @if, o convierte el observable en una señal.
Módulo PP8: Un componente compartido: el badge de documentación¶
El problema que resuelve¶
Cada sección de la app tiene un link a la documentación de Angular de su pipe.
Si escribes el <a> con sus clases en cada sección, el mismo bloque queda
copiado en una docena de lugares: para cambiar el color, el texto o la forma de
abrir el link tendrías que tocarlos todos, y es fácil que uno quede distinto.
Un componente compartido resuelve eso: el link se escribe una vez
(app-docs-link) y cada sección solo dice qué pipe quiere.
El código¶
El componente recibe cuatro datos con input():
export class DocsLinkComponent {
name = input.required<string>()
label = input('Docs')
color = input<BadgeColor>('primary')
url = input<string>()
href = computed(() => this.url() ?? `https://angular.dev/api/common/${this.name()}`)
colorClass = computed(() => BADGE_COLORS[this.color()])
}
<a
class="badge"
[class]="colorClass()"
[href]="href()"
[attr.title]="name()"
target="_blank"
rel="noopener noreferrer"
>
{{ label() }}<span class="sr-only"> ({{ name() }}, opens in a new tab)</span>
</a>
namees obligatorio: es la clase del pipe (DatePipe) y de ahí sale la direcciónangular.dev/api/common/DatePipe.labeles el texto del badge. Por defecto dice «Docs»; en «Text Pipes» y «Number Pipes» hay tres badges juntos, así que cada uno muestra el nombre de su pipe.colores uno de los colores de badge de daisyUI (primary,secondary,accent,neutral,info,success,warning,error). Si no se pasa, esprimary.urles opcional: sirve para algo que no está en la referencia de la API, como la guía de pipes propios, donde no hay una página por pipe.
Y así se usa en una sección:
<div class="flex flex-wrap items-center gap-2 my-5">
<h2 class="text-xl">Text Pipes</h2>
<div class="ml-auto flex flex-wrap gap-2">
<app-docs-link name="LowerCasePipe" label="LowerCasePipe" />
<app-docs-link color="accent" name="UpperCasePipe" label="UpperCasePipe" />
<app-docs-link color="info" name="TitleCasePipe" label="TitleCasePipe" />
</div>
</div>
Por qué los colores están en una lista¶
Podrías escribir [class]="'badge-' + color()", pero no funciona. Tailwind
busca en el código las clases escritas completas y solo genera el CSS de
esas. Si badge-accent nunca aparece escrita, no se genera y el badge se ve
sin color. Por eso los colores están en un objeto:
const BADGE_COLORS = {
primary: 'badge-primary',
accent: 'badge-accent',
// ...
} as const
export type BadgeColor = keyof typeof BADGE_COLORS
Así cada clase aparece completa en el código, y el tipo BadgeColor hace que
un color mal escrito falle al compilar en vez de verse mal en pantalla.
Poner el badge junto al título de una card¶
La card (app-card) no sabe nada de documentación. Solo reserva un lugar junto
al título para lo que sea un app-docs-link, con select en ng-content:
<div class="flex flex-wrap items-center gap-2 mb-2">
<h2 class="card-title">{{ title() }}</h2>
<div class="ml-auto flex flex-wrap gap-2">
<ng-content select="app-docs-link"></ng-content>
</div>
</div>
<ng-content></ng-content>
Todo lo que se escribe dentro de <app-card> va al <ng-content> normal, salvo
los <app-docs-link>, que van al lugar junto al título. El ml-auto (margen
izquierdo automático) empuja los badges a la derecha de la fila.
Cómo funciona por dentro¶
input()define datos que el padre pasa por la plantilla. Coninput.requiredAngular exige que el padre lo pase; con un valor inicial (input('Docs')) es opcional.computed()calculahrefycolorClassa partir de los inputs y se recalcula solo si alguno cambia.- El link abre una pestaña nueva, así que lleva
rel="noopener noreferrer": la página nueva no puede controlar la pestaña de origen. - Para accesibilidad: el texto visible es corto («Docs»), y un texto oculto
(
sr-only) le dice al lector de pantalla a qué pipe corresponde y que se abre una pestaña nueva. El nombre también sale como tooltip (title).
Errores comunes¶
- Armar la clase con un texto (
'badge-' + color): Tailwind no la ve y el color no se aplica. - Varios badges iguales en una sección: si todos dicen «Docs», nadie sabe
cuál es cuál. Usa
labelcon el nombre de cada pipe. - Abrir una pestaña nueva sin
rel="noopener noreferrer". - Un link que solo dice «Docs» para un lector de pantalla: no dice a qué pertenece. Hace falta el texto oculto con el nombre.
- Copiar el
<a>en cada página en vez de usar el componente.
Resumen¶
Un componente compartido escribe una vez lo que se repite y lo configura con
input(): name (obligatorio), label, color y url. Los colores van en un
objeto para que Tailwind genere sus clases, y la card reserva un lugar junto al
título con ng-content select.
Preguntas de entrevista¶
1. ¿Qué diferencia hay entre input() e input.required()?¶
Respuesta:
input.required() obliga al padre a pasar el dato: si no lo pasa, la plantilla
no compila. input() con un valor inicial es opcional y usa ese valor cuando el
padre no dice nada. En el badge, name es obligatorio y label y color tienen
valor por defecto.
2. ¿Por qué no funciona [class]="'badge-' + color()" con Tailwind?¶
Respuesta:
Porque Tailwind genera el CSS leyendo el código fuente como texto y buscando
clases escritas completas. Una clase armada al ejecutar (badge- más el color)
nunca aparece completa en el código, así que no se genera. La solución es tener
las clases completas en un objeto y elegir una con el valor.
3. ¿Para qué sirve select en ng-content?¶
Respuesta:
Para repartir el contenido que el padre escribe dentro del componente en varios
lugares. Con select="app-docs-link", los <app-docs-link> van a ese lugar y
todo lo demás va al <ng-content> sin select.
4. ¿Por qué el link lleva rel="noopener noreferrer"?¶
Respuesta:
Porque abre una pestaña nueva (target="_blank"). Sin noopener, la página
nueva podría controlar la pestaña original; noreferrer además evita enviarle
la dirección de origen.
5. ¿Para qué sirve un type como BadgeColor en lugar de un string?¶
Respuesta:
Con string se podría escribir cualquier cosa, incluso color="rojo", y el
error se vería recién en pantalla. Con un tipo de unión ('primary' | 'accent' |
...), un valor que no está en la lista falla al compilar.
Módulo PP9: Un pipe propio con argumento: toggleCase¶
El problema que resuelve¶
En la página custom quieres mostrar un nombre en mayúsculas o en minúsculas, y
que un botón cambie entre las dos. Con uppercase y lowercase tendrías que
escribir dos pipes distintos en la plantilla y decidir cuál mostrar con un @if.
Un pipe propio con argumento lo resuelve: es un solo pipe, y el argumento
dice cómo debe transformar. Ya viste uno en el Módulo PP4
(timeZone recibe la zona); este es el más pequeño posible para ver la idea.
El código¶
El pipe vive en src/app/pipes/toggle-case.pipe.ts:
@Pipe({
name: 'toggleCase',
})
export class ToggleCasePipe implements PipeTransform {
transform(value: any, CaseType: CaseType = 'uppercase'): string {
const name = signal(value)
return CaseType === 'uppercase' ? name().toUpperCase() : name().toLowerCase()
}
}
Y el tipo del argumento está en src/app/interfaces/toggle-case.interfaces.ts:
export type CaseType = 'uppercase' | 'lowercase'
En la plantilla se usa sin argumento (toma el valor por defecto) y con argumento:
{{ name() | toggleCase }}
{{ name() | toggleCase: toggleCaseValue() }}
Y en el componente, una señal guarda cuál de los dos modos está activo y el botón la cambia:
toggleCaseValue = signal<'uppercase' | 'lowercase'>('uppercase')
handleToggleCase() {
this.toggleCaseValue.set(this.toggleCaseValue() === 'uppercase' ? 'lowercase' : 'uppercase')
}
Cómo funciona por dentro¶
@Pipe({ name: 'toggleCase' })es el nombre que escribes después de la barra|. Es parecido alselectorde un componente.implements PipeTransformobliga a tener un métodotransform. Angular lo llama cada vez que cambia el valor de la izquierda.- El primer parámetro de
transformes el valor de la izquierda de|. Los demás son los argumentos que se escriben después de:. - El segundo parámetro tiene un valor por defecto (
'uppercase'), así que el argumento es opcional:toggleCasesolo equivale atoggleCase: 'uppercase'. - El tipo
CaseTypees una unión de dos textos. Si escribestoggleCase: 'titlecase', el compilador avisa. - El pipe es standalone (como los componentes de este proyecto), así que no
se declara en ningún módulo: cada componente que lo usa lo pone en su
imports, igual queTimeZonePipe. - Un pipe puro (el que Angular crea por defecto) solo se vuelve a ejecutar
si cambia el valor o algún argumento. Por eso el botón funciona: al cambiar
toggleCaseValue(), cambia el argumento y el pipe se ejecuta de nuevo.
Errores comunes¶
- Olvidar poner el pipe en
importsdel componente: la plantilla da error de que no existe el pipe. - Llamar
transformdesde la plantilla ({{ pipe.transform(...) }}) en vez de usar|. - Pasar el argumento sin los
:(toggleCase 'lowercase'): la sintaxis estoggleCase: 'lowercase'. - Cambiar el valor por dentro (por ejemplo, modificar un objeto y esperar que el pipe se entere): un pipe puro mira si el valor es otro, no si cambió por dentro.
- Usar
anyenvaluecuando solo tiene sentido con texto:stringes más claro y el compilador avisa si pasas otra cosa.
Resumen¶
Un pipe propio es una clase con @Pipe y un método transform. El primer
parámetro es el valor; los siguientes son los argumentos, y pueden tener valor
por defecto. Se importa en cada componente que lo usa y, al ser puro, se vuelve
a ejecutar solo cuando cambia el valor o un argumento.
Preguntas de entrevista¶
1. ¿Qué hace falta para crear un pipe propio?¶
Respuesta:
Una clase con el decorador @Pipe({ name: '...' }) que implemente
PipeTransform, es decir, que tenga un método transform. El name es lo que
se escribe en la plantilla después de |. Como es standalone, se agrega al
imports del componente que lo usa.
2. ¿Cómo recibe argumentos un pipe?¶
Respuesta:
Como parámetros extra de transform. El primero es el valor de la izquierda de
| y el resto son los argumentos que se escriben después de :, en el mismo
orden. Si el parámetro tiene valor por defecto, el argumento es opcional.
3. ¿Qué es un pipe puro y por qué importa?¶
Respuesta:
Es un pipe que Angular solo vuelve a ejecutar cuando cambia el valor de entrada
o alguno de sus argumentos (por referencia). Importa para el rendimiento:
transform no se llama en cada ciclo de detección de cambios. Pero si le pasas
un objeto y lo modificas por dentro, el pipe no se entera. Hay pipes impuros
(pure: false), que se ejecutan siempre, pero son más lentos y casi nunca
hacen falta.
4. ¿Por qué usar un pipe y no un método del componente en la plantilla?¶
Respuesta:
Un método se vuelve a llamar en cada ciclo de detección de cambios, aunque nada haya cambiado. Un pipe puro guarda el resultado y solo recalcula cuando cambia su entrada. Además, el pipe se puede reutilizar en cualquier plantilla sin copiar código.
Módulo PP10: Constantes: los datos fuera del componente¶
El problema que resuelve¶
Al principio, los datos de ejemplo viven dentro del componente: el nombre
'Geralt of Rivia', la lista de personajes, la dirección de la wiki. Cuando
otra página necesita el mismo dato lo copias, y ahora hay dos lugares que
cambiar. Además, el componente se llena de datos y cuesta ver su lógica.
La solución es sacarlos a un archivo de constantes: se escriben una vez y cada componente los importa.
El código¶
Todo está en src/app/constants/app.constants.ts (recortado: el archivo real
tiene tres personajes, veinte nombres y veinticinco héroes):
import { Color, Character, Creator, Hero } from '../interfaces/pages.interfaces'
export const EXTERNAL_WIKI_URL = 'https://angular-wiki.reskyon.com/05-pipes-app/'
export const EXTERNAL_ORGANIZATION_URL = 'https://github.com/Reskyon'
export const characterOne: Character = {
name: 'Geralt of Rivia',
age: 100,
gender: 'male',
address: 'Kaer Morhen',
}
export const characterList = ['Geralt of Rivia', 'Cirilla Fiona Elen Riannon', /* ... */]
export const ColorMap = {
[Color.red]: '#E57373',
[Color.black]: '#424242',
[Color.blue]: '#64B5F6',
[Color.green]: '#81C784',
}
export const heroes: Hero[] = [
{ id: 1, name: 'Superman', canFly: true, color: Color.blue, creator: Creator.DC },
{ id: 2, name: 'Batman', canFly: false, color: Color.black, creator: Creator.DC },
/* ... */
]
Character es una interface en src/app/interfaces/pages.interfaces.ts:
export interface Character {
name: string
age: number
gender: string
address: string
}
Hay dos clases de datos en el archivo:
- Direcciones (
EXTERNAL_WIKI_URL,EXTERNAL_ORGANIZATION_URL): las usan el navbar y el footer para sus links. - Datos de ejemplo (
characterOne,characterTwo,characterThree,characterList,heroes): los usan las páginas para probar los pipes.heroeses la lista de la tabla de la páginacustom. ColorMap: el código hexadecimal de cadaColor; lo usa el pipecolorMap(Módulo PP11).
Y así se usan. La página custom toma el nombre del personaje y lo guarda en
una señal para pasárselo al pipe:
import { characterOne } from '../../constants/app.constants'
name = signal(characterOne.name)
El navbar y el footer solo importan la dirección que necesitan:
import { EXTERNAL_WIKI_URL } from '../../constants/app.constants'
Cómo funciona por dentro¶
- Un
export consten un archivo.tsnormal es un valor que cualquier otro archivo puede importar. No hay nada de Angular: es TypeScript común. - Cada componente importa solo lo que usa (
{ characterOne }), no todo el archivo. - Poner el tipo (
: Character) a cada objeto hace que el compilador avise si falta un campo o si escribes mal uno. Lo ves al guardar, no en pantalla. - Las constantes son datos que no cambian. Si un dato debe cambiar y la
pantalla debe enterarse, se mete en una señal:
signal(characterOne.name). La constante es el valor de partida; la señal es la que se actualiza. - Los tipos (
Character,CaseType) van eninterfaces/, separados de los datos, que van enconstants/. Así cada carpeta tiene una sola función.
Errores comunes¶
- Modificar una constante desde un componente (
characterOne.name = '...'): como el objeto lo comparten todas las páginas, el cambio se ve en todas. Si necesitas cambiarlo, haz una copia ({ ...characterOne }) o usa una señal. - Meter claves secretas en el archivo de constantes: va al navegador y cualquiera la ve. Una clave secreta va siempre por el proxy (ver la Parte 3: Gifs App). Una dirección pública como la de la wiki sí puede estar aquí.
- Copiar el mismo dato en dos componentes en vez de importarlo.
- Un solo archivo gigante cuando el proyecto crece: conviene separar por tema (direcciones, personajes) si el archivo ya no se lee de un vistazo.
Resumen¶
Los datos que se repiten o que ensucian el componente van a
constants/app.constants.ts y se importan donde se necesitan. Los tipos van
aparte, en interfaces/. Una constante no cambia; si algo debe cambiar en
pantalla, se parte de la constante y se guarda en una señal.
Preguntas de entrevista¶
1. ¿Por qué sacar los datos a un archivo de constantes?¶
Respuesta:
Para tener cada dato en un solo lugar: si cambia, lo cambias una vez y todos los componentes lo ven. Además el componente queda con su lógica, sin datos de ejemplo en medio, y es más fácil de leer y de probar.
2. ¿Cuál es la diferencia entre constants/ e interfaces/?¶
Respuesta:
constants/ guarda valores que existen en tiempo de ejecución (un texto, un
objeto, una lista). interfaces/ guarda tipos, que solo existen mientras
compilas y desaparecen del código final. Separarlos deja claro qué es un dato y
qué es la forma que debe tener.
3. Una constante es un objeto y dos componentes lo importan. Si uno lo modifica, ¿qué pasa?¶
Respuesta:
El otro también ve el cambio, porque los dos tienen una referencia al mismo
objeto (un módulo se carga una sola vez). const solo impide reasignar la
variable, no cambiar las propiedades del objeto. Para evitarlo se hace una copia
({ ...characterOne }), se usa una señal, o se marca el tipo como Readonly.
4. ¿Cuándo una constante debe pasar a ser una señal?¶
Respuesta:
Cuando el valor debe cambiar y la pantalla debe enterarse. La constante sigue
siendo el valor de partida (signal(characterOne.name)), y la señal es la que
se actualiza con set(). Así los datos fijos quedan en un lugar y el estado
que cambia, en el componente.
Módulo PP11: Pipes propios con enums: yesNo, colorMap y creatorMap¶
El problema que resuelve¶
En la página custom hay una tabla de héroes. Cada héroe trae datos que no se
pueden mostrar tal cual: canFly es true o false, y color y creator
son números (0, 1, 2…). Si los pones en la tabla, el usuario lee «true» y
«2», que no dicen nada.
Hace falta traducir cada dato a algo que se entienda: «Yes» o «No», el nombre
del color, el nombre de la editorial. Tres pipes propios lo hacen, y la
plantilla queda limpia, sin @if ni funciones.
Qué es un enum¶
Un enum es una lista de nombres que TypeScript guarda como números. Sirve
para no escribir números sueltos en el código:
export enum Color {
red, // 0
black, // 1
blue, // 2
green, // 3
}
export enum Creator {
DC, // 0
Marvel, // 1
}
Así escribes Color.blue en vez de 2, y el código se lee solo. Un detalle
útil de los enums numéricos: funcionan en los dos sentidos.
Color.blue // 2 (del nombre al número)
Color[2] // 'blue' (del número al nombre: el «mapeo inverso»)
Eso es justo lo que necesitan los pipes. Solo funciona con enums numéricos: si
le pones valores de texto (red = 'red'), el camino inverso no existe.
El código¶
El héroe y el tipo del resultado de yesNo están en
src/app/interfaces/pages.interfaces.ts:
export interface Hero {
id: number
name: string
canFly: boolean
color: Color
creator: Creator
}
export type YesNo = 'Yes' | 'No'
yesNo: de un booleano a texto.
@Pipe({ name: 'yesNo' })
export class YesNoPipe implements PipeTransform {
transform(value: boolean): YesNo {
return value ? 'Yes' : 'No'
}
}
creatorMap: de un número a su nombre, con el mapeo inverso.
@Pipe({ name: 'creatorMap' })
export class CreatorMapPipe implements PipeTransform {
transform(value: Creator): string {
return Creator[value]
}
}
colorMap: devuelve el nombre del color o su código hexadecimal, según un
argumento opcional.
@Pipe({ name: 'colorMap' })
export class ColorMapPipe implements PipeTransform {
transform(value: Color, color: 'name' | 'value' = 'name'): string {
return color === 'name' ? Color[value] : ColorMap[value]
}
}
ColorMap es un objeto de constants/app.constants.ts que guarda el código de
cada color (ver Módulo PP10):
export const ColorMap = {
[Color.red]: '#E57373',
[Color.black]: '#424242',
[Color.blue]: '#64B5F6',
[Color.green]: '#81C784',
}
Y así se usan en la tabla. El componente guarda la lista de héroes en una
señal y expone el enum Creator como una propiedad, porque la plantilla no
puede leer un enum directamente:
import { characterOne, heroes } from '../../constants/app.constants'
import { Creator, Hero } from '../../interfaces/pages.interfaces'
heroes = signal<Hero[]>(heroes)
creator = Creator
En esta etapa anterior a la búsqueda y la ordenación, la plantilla recorre la
señal con un @for, y colorea la editorial comparando
con creator.DC y creator.Marvel, no con los números:
@for (hero of heroes(); track hero.id; let i = $index) {
<tr>
<td>{{ hero.name }}</td>
<td>{{ hero.canFly | yesNo }}</td>
<td>
<div class="badge" [style.backgroundColor]="hero.color | colorMap: 'value'">
{{ hero.color | colorMap | titlecase }}
</div>
</td>
<td
[class.text-info]="hero.creator === creator.DC"
[class.text-error]="hero.creator === creator.Marvel"
>
{{ hero.creator | creatorMap | titlecase }}
</td>
</tr>
}
Asociar un enum con un valor: el objeto ColorMap¶
Un enum te da el nombre (Color[2] es 'blue'). Pero a veces necesitas un
valor propio para cada opción: el código hexadecimal de cada color, un
texto en español, un icono. Eso el enum no lo guarda. Para eso se hace un
objeto que asocia cada opción con su valor:
export const ColorMap = {
[Color.red]: '#E57373',
[Color.black]: '#424242',
[Color.blue]: '#64B5F6',
[Color.green]: '#81C784',
}
Se lee como una tabla de dos columnas: «cuando la opción es red, el valor es
#E57373».
| Opción del enum | Número | Valor en ColorMap |
|---|---|---|
Color.red |
0 |
'#E57373' |
Color.black |
1 |
'#424242' |
Color.blue |
2 |
'#64B5F6' |
Color.green |
3 |
'#81C784' |
Los corchetes [Color.red]. En un objeto normal, la clave se escribe como
texto: { red: '#E57373' }. Con corchetes, JavaScript primero
calcula lo que hay dentro y usa el resultado como clave. [Color.red] vale 0,
así que el objeto queda { 0: '#E57373', 1: '#424242', ... }. Se llaman
claves calculadas (computed property names).
¿Por qué no escribir { red: '#E57373' } y ya? Porque el pipe recibe el
número (hero.color vale 2), no el nombre. Con las claves hechas con el
enum, la búsqueda es directa: ColorMap[2] da '#64B5F6'. Y como las claves
salen del enum, el compilador te ayuda: si agregas yellow al enum y te olvidas
de ponerle su valor, el pipe deja de compilar donde lo usas con ese color.
Dos búsquedas distintas sobre el mismo número. El pipe colorMap usa las
dos y el argumento elige cuál:
Color[value] // 'blue' -> el enum devuelve el NOMBRE
ColorMap[value] // '#64B5F6' -> el objeto devuelve el VALOR PROPIO
Para la hora de elegir dónde guardar un dato nuevo:
- Si solo necesitas el nombre de la opción, ya lo tienes en el enum
(
Creator[value]encreatorMap). No hace falta ningún objeto. - Si cada opción necesita un dato propio (el color hexadecimal), haz un objeto
como
ColorMap.
¿Por qué no usar keyvalue aquí?¶
En el Módulo PP6
viste keyvalue, que convierte un objeto en una lista de pares
{ key, value }. Parece buen candidato para ColorMap, pero no encaja, por
dos razones.
1. keyvalue sirve para recorrer, y aquí hay que buscar.
keyvalue es para cuando quieres mostrar todas las entradas de un objeto
(por ejemplo con un @for). Aquí pasa lo contrario: la tabla de héroes ya
tiene el número de un héroe y necesita un solo valor. Eso es una búsqueda
directa por clave, ColorMap[value], que es inmediata. Con keyvalue tendrías
que armar la lista de todos los pares y luego buscar dentro de ella cuál es el
que corresponde: más trabajo para el mismo resultado.
<!-- keyvalue: mostrar TODAS las entradas -->
@for (item of ColorMap | keyvalue; track item.key) {
<li>{{ item.key }}: {{ item.value }}</li>
}
<!-- búsqueda: UN valor, el que corresponde a este héroe -->
{{ hero.color | colorMap: 'value' }}
2. Con un enum, keyvalue mezcla los dos sentidos. Un enum numérico guarda
los datos en las dos direcciones (el mapeo inverso del que hablamos arriba), así
que Color tiene ocho entradas, no cuatro:
// Color por dentro
{ 0: 'red', 1: 'black', 2: 'blue', 3: 'green',
red: 0, black: 1, blue: 2, green: 3 }
Si le pasas el enum a keyvalue, recibes las ocho: cuatro con el número como
clave y cuatro con el nombre como clave. Para listar las opciones tendrías que
filtrar las que sobran. Y las claves de un objeto son siempre texto, así que
item.key te llega como '2', no como el número 2.
Resumen de cuándo usar cada cosa:
| Necesito... | Uso |
|---|---|
| El valor de una opción que ya tengo | ColorMap[value] (pipe propio) |
| El nombre de una opción | Color[value] (mapeo inverso) |
| Listar todas las entradas de un objeto | keyvalue con @for |
| Listar las opciones de un enum | keyvalue no, mejor un arreglo con los valores que quieras mostrar |
Cómo funciona por dentro¶
- Cada pipe recibe el dato crudo y devuelve texto. El dato original (
hero) no cambia: el pipe solo cambia cómo se muestra. colorMapse usa de dos maneras con el mismo nombre: sin argumento da el nombre ('red'), y con: 'value'da el código ('#E57373'). El argumento es opcional porque tiene un valor por defecto.- Los pipes se pueden encadenar:
hero.color | colorMap | titlecaseprimero pasa de número a nombre y después pone la primera letra en mayúscula.titlecasees un pipe de Angular que ya conoces del Módulo PP1. - El código hexadecimal va a
[style.backgroundColor], que cambia un estilo del elemento. No se puede usar una clase de Tailwind, porque el color sale de un dato y Tailwind solo genera las clases que aparecen escritas completas (ver Módulo PP8). [class.text-success]="hero.canFly"agrega la clase solo si la condición es verdadera. Con eso la columna «Flight?» se ve verde para «Yes» y de otro color para «No».@fornecesitatrack hero.id: le dice a Angular cómo reconocer cada fila, para no repintar toda la tabla cuando cambia la lista.- Los tres pipes son standalone y puros: cada uno va en el
importsdel componente y solo se ejecuta cuando cambia su valor o su argumento (ver Módulo PP9).
Errores comunes¶
- Usar
Color[value]con un enum de texto: el mapeo inverso solo existe en los enums numéricos. - Comparar con el número (
hero.creator === 1) en vez de con el enum (Creator.Marvel): funciona, pero nadie recuerda qué es el 1. En la plantilla no se puede escribirCreator.Marveldirectamente: hay que guardar el enum en una propiedad del componente. Así lo hace la páginacustom:creator = Creator, y en la plantillahero.creator === creator.Marvel. - Armar la clase de Tailwind con el nombre del color
(
'bg-' + colorName): Tailwind no la genera. Para colores que vienen de un dato, usa el código hexadecimal con[style.backgroundColor]. - Escribir
{ red: '#E57373' }enColorMapcuando el pipe recibe el número:ColorMap[2]no encuentra nada. Las claves deben ser[Color.red], que valen0,1,2,3. - Usar
keyvaluepara buscar un solo valor:keyvaluees para recorrer todas las entradas. Para una sola, se busca por clave:ColorMap[value]. - Pasarle un enum a
keyvaluey esperar cuatro entradas: llegan ocho, porque el enum guarda el número y el nombre. - Dejar un
console.logdentro detransform: se ejecuta cada vez que cambia el valor y llena la consola. - Olvidar agregar
TitleCasePipealimports: es un pipe de Angular, pero en un componente standalone también hay que importarlo.
Resumen¶
Un enum guarda nombres como números, y el mapeo inverso (Color[2]) devuelve
el nombre. Tres pipes propios traducen datos crudos a texto legible: yesNo
(booleano a «Yes»/«No»), creatorMap (número a nombre) y colorMap (número a
nombre o a código hexadecimal, según un argumento). Para darle a cada opción un valor propio (como el código hexadecimal) se usa un
objeto con claves calculadas, { [Color.red]: '#E57373' }, y se busca por
clave: ColorMap[value]. No se usa keyvalue porque ese pipe sirve para
recorrer todas las entradas, no para buscar una, y con un enum mezcla el
número y el nombre. Los pipes se pueden encadenar con pipes de Angular como
titlecase.
Preguntas de entrevista¶
1. ¿Qué es el mapeo inverso de un enum?¶
Respuesta:
Es que un enum numérico se puede leer en los dos sentidos. Color.blue da 2,
y Color[2] da 'blue'. TypeScript lo crea solo para los enums numéricos; en
los enums de texto no existe.
2. ¿Por qué usar un pipe y no preguntar en la plantilla con @if?¶
Respuesta:
Porque la traducción queda en un solo lugar y se reutiliza. Con @if tendrías
que repetir la condición en cada tabla o página que muestre el dato. Además, un
pipe puro no se vuelve a calcular si su valor no cambió.
3. ¿Cómo se encadenan pipes y en qué orden se ejecutan?¶
Respuesta:
Se escriben uno tras otro con |, y se ejecutan de izquierda a derecha: la
salida de uno es la entrada del siguiente. En hero.color | colorMap |
titlecase, colorMap convierte el número en 'red' y titlecase lo deja en
'Red'.
4. ¿Para qué sirve track en un @for?¶
Respuesta:
Para que Angular reconozca cada elemento de la lista. Si la lista cambia, Angular
sabe qué filas son las mismas, cuáles son nuevas y cuáles se fueron, y solo
toca esas en vez de repintar todo. Se usa algo único, como el id.
5. ¿Por qué el color se pone con [style.backgroundColor] y no con una clase de Tailwind?¶
Respuesta:
Porque el color viene de un dato y se conoce al ejecutar. Tailwind genera el CSS leyendo el código fuente y solo crea las clases que están escritas completas; una clase armada con el nombre del color no existe. Un estilo en línea con el código hexadecimal no depende de eso.
6. ¿Cómo se asocia cada opción de un enum con un valor propio?¶
Respuesta:
Con un objeto cuyas claves salen del enum: { [Color.red]: '#E57373', ... }.
Los corchetes hacen que JavaScript calcule la clave (Color.red vale 0), y
así ColorMap[value] encuentra el valor a partir del número. El enum solo
guarda el nombre; si cada opción necesita otro dato (un color, un texto, un
icono), ese dato va en un objeto aparte.
7. ¿Por qué no se usó keyvalue para ColorMap?¶
Respuesta:
Porque keyvalue convierte un objeto en una lista de pares para recorrerla
(por ejemplo con @for), y aquí se necesita un solo valor: el del héroe
que se está mostrando. Eso es una búsqueda directa por clave, ColorMap[value],
que no necesita armar ninguna lista. keyvalue sí sirve cuando quieres
mostrar todas las entradas de un objeto.
8. ¿Qué pasa si le pasas un enum numérico a keyvalue?¶
Respuesta:
Llegan el doble de entradas: un enum numérico guarda el número y el nombre
({ 0: 'red', red: 0, ... }), así que Color da ocho pares en vez de
cuatro. Además las claves de un objeto son texto, así que la clave llega como
'2' y no como el número 2. Por eso, para listar las opciones de un enum
conviene un arreglo con las que quieres mostrar.
9. ¿Qué ventaja tiene crear las claves con el enum en vez de escribirlas a mano?¶
Respuesta:
Que el compilador te protege. Si agregas una opción nueva al enum y no le pones su valor en el objeto, el código que busca por esa opción deja de compilar y te avisa. Con claves escritas a mano, el error aparecería recién en pantalla, como un valor vacío.
Módulo PP12: Ordenar héroes con sort y switch¶
El problema que resuelve¶
La tabla de custom ya muestra héroes. Ahora quieres que un botón los ordene
por nombre y otro ponga primero a quienes vuelan. No puedes comparar un nombre,
un booleano y un enum de la misma manera: cada propiedad necesita su regla.
La ordenación está en hero-sort-by.pipe.ts. La construcción de un pipe ya está en el
Módulo PP9; aquí el foco
es entender y poder repetir la ordenación de un array (sorting).
El código¶
Este es el método actual de src/app/pipes/hero-sort-by.pipe.ts, sin los
comentarios que se desarrollan debajo:
transform(value: Hero[], sortBy: keyof Hero | null): Hero[] {
if (!sortBy) {
return value
}
const sorted = [...value]
switch (sortBy) {
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name))
case 'canFly':
return sorted.sort((a, b) => Number(b.canFly) - Number(a.canFly))
case 'color':
return sorted.sort((a, b) => a.color - b.color)
case 'creator':
return sorted.sort((a, b) => a.creator - b.creator)
default:
break
}
return value
}
Elegir una propiedad con keyof y switch¶
El componente guarda la elección en una señal:
sortBy = signal<keyof Hero | null>(null)
handleSort(sortBy: keyof Hero | null) {
this.sortBy.set(sortBy)
}
keyof Hero representa los nombres de las propiedades de Hero:
'id' | 'name' | 'canFly' | 'color' | 'creator'. No contiene los valores de un
héroe concreto. null significa que no hay criterio elegido.
switch (sortBy) compara la elección con cada case usando igualdad estricta
(===, sin convertir tipos). Si vale 'name', entra en ese caso; si vale
'color', entra en el del color. default recoge las opciones sin caso propio:
por ahora, 'id' pertenece al tipo, pero no tiene ordenación implementada.
En cada caso hay un return: termina todo el método y devuelve la lista.
Por eso no hace falta un break después. En cambio, el break de default
solo sale del switch; después se ejecuta el return value final. Cuando la
elección es null, el primer if devuelve la lista sin entrar al switch.
Qué devuelve el comparador de sort¶
En sort((a, b) => ...), a y b son dos elementos del array. La función que
recibes dentro de otra función se llama callback. Aquí actúa como
comparador (comparator): devuelve un número que decide el orden de esa
pareja. sort lo llama tantas veces como necesita; tú no eliges las parejas ni
debes depender del orden en que las compara.
| Resultado | Dónde debe quedar a respecto de b |
|---|---|
| Negativo | Antes de b |
| Positivo | Después de b |
| Cero | Empatan para ese criterio; conservan su orden relativo |
Negativo no significa descendente, ni positivo significa ascendente. El signo decide dónde va una pareja. La fórmula del comparador decide el sentido del orden de toda la lista.
Ejemplo independiente para practicar con números:
[10, 2, 5].sort((a, b) => a - b) // [2, 5, 10]
[10, 2, 5].sort((a, b) => b - a) // [10, 5, 2]
En el primero, si a vale 10 y b vale 2, 10 - 2 da 8. Es positivo:
10 debe ir después de 2. En el segundo, para esa misma pareja,
2 - 10 da -8: 10 debe ir antes de 2.
Sin comparador, sort() compara representaciones de texto: [2, 10].sort()
produce [10, 2]. Para números, pasa una resta; no devuelvas solo
a > b, porque da un booleano y no distingue los tres resultados.
Cero significa empate en la propiedad elegida¶
Superman y Batman tienen creator: Creator.DC. Al compararlos por editorial,
a.creator - b.creator da 0 - 0, es decir, 0.
Eso no los convierte en el mismo héroe: solo empatan en editorial. Si Superman aparecía antes que Batman en el array recibido, seguirá antes. Conservar ese orden en los empates se llama ordenación estable (stable sort).
No aparece un desempate alfabético automáticamente. Si lo necesitas, debes escribirlo. Esta es una variante didáctica, no el comparador actual:
value.sort((a, b) => a.creator - b.creator || a.name.localeCompare(b.name))
Si la resta es distinta de cero, se usa ese resultado. Si da cero, || evalúa
la comparación de nombres. Así ordenas primero por editorial y, dentro de
cada editorial, por nombre.
Nombres: localeCompare¶
En a.name.localeCompare(b.name), el primer nombre se compara con el segundo
siguiendo reglas de ordenación de texto de un idioma (locale), como el
tratamiento de acentos y mayúsculas. Devuelve un número negativo, cero o
positivo: justo el contrato que necesita sort.
'Batman'.localeCompare('Superman') < 0 // true
'Superman'.localeCompare('Batman') > 0 // true
'Batman'.localeCompare('Batman') === 0 // true
No compruebes si devuelve exactamente -1 o 1: lo que importa es el signo.
Con a.name.localeCompare(b.name), Batman va antes que Superman. Intercambiar
los nombres, b.name.localeCompare(a.name), invierte el sentido.
El código actual no pasa un idioma, así que usa el predeterminado del entorno
de JavaScript. El LOCALE_ID de Angular no configura este método. Si quieres
elegirlo expresamente, esta es una variante, no el código actual:
value.sort((a, b) => a.name.localeCompare(b.name, 'es-AR'))
Booleanos y enums: convertir o restar¶
Cada botón usa estos criterios del archivo actual:
| Propiedad | Comparador | Resultado |
|---|---|---|
name |
a.name.localeCompare(b.name) |
Orden de nombres según el idioma del entorno |
canFly |
Number(b.canFly) - Number(a.canFly) |
Primero true, después false |
color |
a.color - b.color |
red, black, blue, green |
creator |
a.creator - b.creator |
DC antes que Marvel |
Number(true) es 1 y Number(false) es 0. La resta b - a ordena de mayor
a menor: si a vuela y b no, 0 - 1 da -1, así que a va primero. Si
los dos vuelan o ninguno vuela, da cero. Con a - b irían primero quienes no
vuelan.
Color y Creator ya son enums numéricos, explicados en el
Módulo PP11.
Color.red vale 0, black vale 1, blue vale 2 y green vale 3.
Creator.DC vale 0 y Marvel vale 1. La resta a - b los ordena de menor
a mayor. No ordena sus nombres alfabéticamente: sigue los números del enum.
Cómo funciona por dentro¶
sort() cambia el mismo array que recibe y devuelve esa misma referencia.
Una referencia es el acceso a un objeto concreto en memoria: dos variables
pueden apuntar al mismo array y ver ambas el cambio.
Ejemplo independiente:
const numbers = [10, 2, 5]
const sortedNumbers = numbers.sort((a, b) => a - b)
numbers // [2, 5, 10]
sortedNumbers === numbers // true
Por eso el pipe no ordena el array que recibe: primero hace una copia con
const sorted = [...value] y ordena esa copia. [...value] crea un array
nuevo con los mismos elementos; sort modifica la copia y el array de partida
conserva su orden. El Módulo PP13
explica qué pasaba en la tabla sin esa copia.
const original = [10, 2, 5]
const sorted = [...original].sort((a, b) => a - b)
original // [10, 2, 5]
sorted // [2, 5, 10]
toSorted devuelve directamente otro array ordenado, pero necesita la
biblioteca de tipos de ES2023 y este proyecto compila con ES2022; por eso se
usa la copia.
Con héroes es una copia superficial (shallow copy): cambia el contenedor,
pero los objetos de dentro siguen compartidos. Ordenar la copia no reordena el
original; cambiar sorted[0].name sí modifica ese héroe compartido.
Errores comunes¶
- Confundir el signo con ascendente o descendente. El signo habla de
arespecto deb; el sentido lo establece la fórmula completa. - Interpretar cero como igualdad de objetos. Solo significa empate según el comparador, y no añade otro criterio por sí solo.
- Esperar orden alfabético al restar enums. La resta compara sus números.
- Creer que
sortdevuelve una copia. Cambia el array que recibe. Por eso el pipe copia primero con[...value]. - Suponer que
keyof Heroimplementa todos los casos. Acepta'id', pero elswitchactual no tiene uncase 'id'.
Resumen¶
El switch elige una regla por propiedad. El comparador devuelve un número:
negativo coloca a antes, positivo después y cero conserva el orden relativo
del empate. localeCompare compara texto, la conversión con Number permite
ordenar booleanos y una resta compara enums numéricos. sort modifica su array,
así que el pipe ordena una copia hecha con [...value].
Preguntas de entrevista¶
1. ¿Un comparador negativo ordena de forma descendente?¶
Respuesta:
No. Solo indica que a debe ir antes que b. Tanto a - b como b - a
pueden devolver negativos; la primera fórmula produce orden ascendente y la
segunda descendente para estos números.
2. ¿Qué significa devolver cero y cómo añades un desempate?¶
Respuesta:
Significa que los elementos empatan según ese criterio y conservan su orden
relativo en el array recibido. Para desempatar por nombre, compara los nombres
cuando el primer resultado sea cero, como en
a.creator - b.creator || a.name.localeCompare(b.name).
3. ¿Qué garantiza localeCompare y cómo eliges el idioma?¶
Respuesta:
Devuelve un número cuyo signo expresa el orden entre dos textos según reglas
de idioma; no garantiza valores exactos de -1 y 1. Puedes pasar 'es-AR'
como segundo argumento. LOCALE_ID no elige ese idioma por ti.
4. ¿Una copia con spread evita todas las modificaciones compartidas?¶
Respuesta:
No. Evita compartir el contenedor del array, pero sigue compartiendo los objetos que contiene. Reordenar la copia no cambia el orden del original; editar una propiedad de un héroe compartido sí afecta al mismo objeto.
5. ¿Por qué estos case no llevan break?¶
Respuesta:
Porque el return ya sale de todo el método. break solo saldría del
switch. Si no hay ninguno de los dos, la ejecución continúa por el siguiente
caso aunque su etiqueta no coincida.
Para ampliar: operador keyof de TypeScript.
Módulo PP13: Filtrar héroes y limpiar la búsqueda¶
El problema que resuelve¶
Al escribir «man» quieres ver solo los héroes cuyo nombre contiene ese texto. Después quieres ordenarlos y poder vaciar el campo con Reset. Son tres tareas: seleccionar elementos del array, ordenar el resultado y mantener el campo y el estado del componente de acuerdo.
Los ejemplos están en hero-filter.pipe.ts y custom-page.component. El
reset quita el criterio y el texto de búsqueda.
El código¶
El método de src/app/pipes/hero-filter.pipe.ts recibe la lista y el texto:
transform(value: Hero[], filterBy: string): Hero[] {
if (!value) return []
// Receives an array of heroes and a filter string, returns heroes whose names include the filter string
return value.filter((hero) => hero.name.toLowerCase().includes(filterBy.toLowerCase()))
}
filter recorre la lista y conserva cada elemento cuyo callback devuelve
true. Devuelve otro array, sin quitar elementos del original. Como en
una copia superficial, los héroes de dentro siguen siendo los mismos objetos.
toLowerCase() convierte el texto a minúsculas. Se aplica a ambos lados para
que buscar 'MAN' y 'man' dé el mismo resultado. includes() responde si
un texto contiene otro. No elimina acentos ni espacios: buscar ' man '
también busca esos espacios.
Ejemplo independiente con nombres del proyecto:
const names = ['Superman', 'Batman', 'Thor']
const matches = names.filter((name) => name.toLowerCase().includes('MAN'.toLowerCase()))
matches // ['Superman', 'Batman']
names // ['Superman', 'Batman', 'Thor']
Todo texto contiene '', así que un filtro vacío devuelve todos los héroes,
en un array nuevo cuando se ejecuta transform. Si Angular reutiliza el
resultado de un pipe puro, no ejecuta de nuevo filter en cada renderizado.
De la plantilla al componente¶
La página guarda el texto y el criterio en señales, y los actualiza con estos métodos actuales:
heroes = signal<Hero[]>(heroes)
sortBy = signal<keyof Hero | null>(null)
searchQuery = signal<string>('')
handleSort(sortBy: keyof Hero | null) {
this.sortBy.set(sortBy)
}
handleSearch(query: string) {
this.searchQuery.set(query)
}
El input y el botón Reset de custom-page.component.html son:
<input
type="text"
class="input input-bordered mr-4"
placeholder="Search by hero name..."
(input)="handleSearch(searchInput.value)"
#searchInput
/>
<button class="btn btn-neutral" (click)="reset(searchInput)">Reset</button>
#searchInput crea una referencia de plantilla (template reference
variable). En este input nativo representa el elemento del navegador, un
HTMLInputElement; no es el texto escrito ni una signal.
Al escribir, el evento input llama a handleSearch con searchInput.value,
que sí es un string. Al pulsar Reset, pasas searchInput completo porque el
método necesita acceder al campo para vaciarlo.
Filtrar antes de ordenar¶
La expresión actual del @for es:
@for (
hero of heroes() | heroFilter: searchQuery() | heroSortBy: sortBy();
track hero.id;
let i = $index
) {
Es la apertura del bloque; el cuerpo con las filas se explicó en el Módulo PP11. Los pipes se aplican de izquierda a derecha:
heroes() -> heroFilter(searchQuery()) -> heroSortBy(sortBy()) -> filas
Primero filter produce la lista de coincidencias. Después heroSortBy
ordena una copia de esa lista. Cuando cambia el criterio, el argumento
sortBy() cambia y Angular vuelve a ejecutar el pipe de ordenación aunque la
búsqueda siga igual.
Aquí importa que heroSortBy copie antes de ordenar. Los pipes son puros por
defecto: Angular guarda el último resultado de heroFilter y lo reutiliza
mientras reciba el mismo array y el mismo texto. Si heroSortBy ordenara ese
array en el lugar, estaría reordenando el resultado guardado del filtro. Al
pulsar Reset con la búsqueda vacía, el filtro no se volvería a ejecutar y
heroSortBy, al recibir null, devolvería la lista ya ordenada: la tabla no
volvería a su orden inicial. Con la copia, el array guardado por el filtro
nunca cambia.
Limpiar el campo y el estado¶
El reset actual del componente recibe el elemento:
reset(searchInput: HTMLInputElement) {
this.sortBy.set(null)
this.searchQuery.set('')
searchInput.value = ''
}
sortBy.set(null)quita el criterio de ordenación.searchQuery.set('')quita el filtro de los datos.searchInput.value = ''borra el texto visible. Asignar.valuedesde TypeScript no dispara por sí solo el eventoinput, por eso también se actualiza la signal.
No hace falta tocar la lista de héroes: como heroSortBy nunca modificó el
array del filtro, al quitar el criterio la tabla vuelve a su orden de partida.
También puedes evitar pasar el elemento: como alternativa al enfoque
actual, añade [value]="searchQuery()" al input y deja que la signal
controle su texto. En ese caso, vaciarla desde reset() actualiza el campo y
ya no hace falta asignar searchInput.value.
Agrupar los imports con index.ts¶
El nuevo src/app/pipes/index.ts importa y exporta los pipes de esa carpeta.
Este archivo de entrada común se suele llamar barrel. El componente los
importa así:
import {
ColorMapPipe,
CreatorMapPipe,
HeroFilterPipe,
HeroSortByPipe,
TimeZonePipe,
ToggleCasePipe,
YesNoPipe,
} from '../../pipes'
Eso organiza los imports de TypeScript. No registra los pipes en Angular:
siguen apareciendo en imports: [...] del componente standalone.
Cómo funciona por dentro¶
El input actualiza searchQuery, el filtro selecciona héroes y el ordenador
aplica el criterio elegido. Angular vuelve a ejecutar cada pipe puro cuando
cambia alguno de sus argumentos: un valor primitivo se compara por su valor
y un array por su referencia. track hero.id identifica las filas aunque
cambie su posición; no decide cuándo se ejecutan los pipes.
Ordenar no cambia los campos de los héroes. Filtrar tampoco crea héroes nuevos: crea otro array que contiene referencias a los que coinciden.
Errores comunes¶
- Pasar
searchInput.valuea un reset que esperaHTMLInputElement. Estarías pasando un texto donde se necesita el elemento. - Vaciar solo el input. La búsqueda guardada en la signal seguiría activa.
- Vaciar solo la signal con el input actual. Sin un enlace
[value], el texto visible no se limpia automáticamente. - Ordenar en el lugar dentro de un pipe puro. Reordena el resultado que Angular tiene guardado del pipe anterior, y Reset deja la tabla ordenada.
- Esperar que la búsqueda ignore acentos o recorte espacios. El código
actual solo convierte a minúsculas y usa
includes.
Resumen¶
filter devuelve otro array con las coincidencias. La cadena actual filtra
primero y ordena después para trabajar sobre ese resultado. #searchInput
permite pasar el elemento al reset y vaciar su .value, además de limpiar la
signal. Como heroSortBy ordena una copia, al quitar el criterio el filtro
entrega la lista con su orden de partida.
Preguntas de entrevista¶
1. ¿Por qué heroSortBy ordena una copia y no el array que recibe?¶
Respuesta:
Porque ese array es el último resultado de heroFilter, que Angular guarda y
reutiliza mientras no cambien sus entradas. Ordenarlo en el lugar modificaría
el resultado guardado, y el filtro entregaría una lista ya reordenada incluso
después de quitar el criterio. Con [...value] el pipe ordena otro array y el
guardado queda intacto.
2. ¿filter con una búsqueda vacía devuelve el mismo array?¶
Respuesta:
No. Cuando se ejecuta, devuelve otro array con todos los elementos porque
todos los nombres contienen ''. Los objetos de dentro siguen compartidos.
Angular sí puede reutilizar ese resultado si no cambian las entradas del pipe.
3. ¿Qué diferencia hay entre searchInput y searchInput.value?¶
Respuesta:
searchInput es el elemento del navegador; .value es su texto. Pasa el
texto para buscar y el elemento para que un método pueda manipular el campo,
como vaciarlo con .value = ''.
4. ¿Qué hace falta para que Reset deje la tabla como al principio?¶
Respuesta:
Tres cosas: poner sortBy en null, vaciar searchQuery y borrar el texto
visible con searchInput.value = ''. El orden vuelve solo, porque
heroSortBy nunca tocó el array del filtro; no hace falta reasignar la lista
de héroes.
5. ¿Un archivo index.ts sustituye los imports de Angular?¶
Respuesta:
No. Solo concentra las exportaciones de TypeScript en una ruta. Cada pipe que
usa una plantilla standalone debe seguir en imports: [...] del componente.
Para ampliar: pipes y detección de cambios y referencias de plantilla.