Tu primer componente: anatomía de un componente standalone

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
La reacción más común al correr ng generate component por primera vez es buscar un app.module.ts para ver dónde quedó declarado el componente nuevo. No hay ninguno: el CLI crea cuatro archivos y listo. En el post anterior (angular-desde-cero) quedó el entorno instalado y el primer proyecto corriendo; ahora toca abrir ese proyecto y entender, en serio, qué es un componente.
Adentro de un proyecto ya creado, esto alcanza para tener un componente nuevo (referencia completa del comando):
ng generate component saludo
# CREATE src/app/saludo/saludo.css
# CREATE src/app/saludo/saludo.html
# CREATE src/app/saludo/saludo.spec.ts
# CREATE src/app/saludo/saludo.tsCuatro archivos: los estilos, el template, los tests y la clase. Nada de saludo.component.ts. Eso no es un recorte para que quepa en la pantalla: desde la versión 20 del Angular CLI el sufijo .component se sacó por default, tanto del nombre de archivo como del nombre de la clase. Antes de la versión 20, este mismo comando creaba saludo.component.ts con la clase SaludoComponent. Si un tutorial viejo muestra esos nombres, es por eso: no está mal, es la convención anterior. Se puede volver a pedir con ng generate component saludo --type=component, pero el default hoy es sin sufijo.
El archivo que importa es saludo.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-saludo',
templateUrl: './saludo.html',
styleUrl: './saludo.css',
})
export class Saludo {}Esas cuatro líneas adentro de @Component({...}) son casi todo lo que hay que entender de un componente (anatomía completa en la documentación oficial). Vamos una por una.
El selector es el nombre de etiqueta que usás para poner el componente en un HTML. Con selector: 'app-saludo', en cualquier template del proyecto podés escribir:
<app-saludo />El prefijo app- no es una regla del lenguaje, es una convención (configurable en angular.json, propiedad prefix) para no chocar con elementos HTML nativos ni con componentes de otras librerías. ng new lo deja en app por defecto.
La barra de cierre en <app-saludo /> tampoco es un capricho de HTML5: hasta la versión 15, un componente sin contenido adentro había que cerrarlo sí o sí con etiqueta completa (<app-saludo></app-saludo>); desde la versión 16, Angular acepta la etiqueta autocerrada para cualquier componente que no proyecte contenido (detalle y guía de migración: angular.dev/reference/migrations/self-closing-tags). El propio CLI, con el lint de angular-eslint, hoy recomienda esta forma.
El template es lo que se renderiza. Puede ir en un archivo aparte (templateUrl, como en saludo.ts de arriba) o directamente inline con template:
import { Component } from '@angular/core';
@Component({
selector: 'app-saludo',
template: `<h1>Hola, {{ nombre }}</h1>`,
styleUrl: './saludo.css',
})
export class Saludo {
nombre = 'mundo';
}No hay una versión "más correcta": para un template de una línea, inline evita saltar de archivo; para algo con más marcado, un archivo aparte se lee mejor. El {{ nombre }} de ahí arriba es interpolación, la forma más básica de mostrar un dato en pantalla, pero cómo se conecta ese dato con la clase (data binding, el nuevo @if/@for) es tema del próximo post: acá solo hace falta saber que existe.
styleUrl (singular) apunta a un único archivo de estilos y es la forma corta que existe desde la versión 17. Si un componente necesita más de una hoja de estilos, ahí sí hace falta el array plural de siempre:
@Component({
selector: 'app-saludo',
templateUrl: './saludo.html',
styleUrls: ['./saludo.css', './saludo-tema-oscuro.css'],
})
export class Saludo {}Lo que no cambió es el encapsulamiento: por default, Angular le agrega un atributo único a cada elemento del template y reescribe el CSS del componente para que solo aplique ahí adentro. Un h1 { color: red; } en saludo.css no se escapa a pintar los h1 de otros componentes. Adentro de ese mismo archivo, el selector :host apunta al propio elemento <app-saludo> que envuelve todo:
:host {
display: block;
padding: 16px;
}Eso pinta el elemento host desde CSS. Para tocarlo desde TypeScript -agregarle una clase condicional, escuchar un click sobre él- hace falta otra cosa.
El "host" de un componente es el elemento que lo contiene, <app-tarjeta> en este caso, no algo que esté adentro del template. Para leerlo o modificarlo desde la clase, la forma recomendada hoy es la propiedad host del decorador:
import { Component } from '@angular/core';
@Component({
selector: 'app-tarjeta',
template: `<ng-content />`,
host: {
class: 'tarjeta',
'[class.activa]': 'activa',
'[attr.aria-expanded]': 'activa',
'(click)': 'alternar()',
},
})
export class Tarjeta {
activa = false;
alternar() {
this.activa = !this.activa;
}
}Cuatro líneas, cuatro cosas distintas: una clase fija (class), una clase condicional atada a una propiedad ([class.activa]), un atributo ARIA que sigue el mismo estado ([attr.aria-expanded]) y un listener de click que lo invierte ((click)). Todo en un solo lugar, con la misma sintaxis de corchetes y paréntesis que ya se usa en cualquier binding de template. (Este activa es una propiedad común, no un signal: los signals tienen su propio post más adelante en la serie.)
Antes de esto, lo mismo se escribía repartido en decoradores de propiedad y de método:
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({ selector: 'app-tarjeta', template: `<ng-content />` })
export class Tarjeta {
activa = false;
@HostBinding('class.activa') get claseActiva() {
return this.activa;
}
@HostListener('click')
alternar() {
this.activa = !this.activa;
}
}@HostBinding y @HostListener siguen andando, no están deprecados con fecha de baja, pero la documentación oficial de Angular es explícita: existen solo por compatibilidad hacia atrás y hay que preferir siempre el objeto host. La diferencia práctica es dónde queda la información: con host, todo lo que le pasa al elemento contenedor se lee de un vistazo arriba de la clase; con los decoradores, queda desparramado entre las propiedades y métodos que los llevan.
Nada de lo que se vio hasta acá mencionó un módulo. Eso es a propósito: los componentes standalone se declaran solos y dicen qué necesitan en su propio imports, en vez de heredarlo de un módulo que los agrupa.
import { Component } from '@angular/core';
@Component({
selector: 'app-saludo',
templateUrl: './saludo.component.html',
styleUrls: ['./saludo.component.css'],
})
export class SaludoComponent {}import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { SaludoComponent } from './saludo/saludo.component';
@NgModule({
declarations: [SaludoComponent],
imports: [BrowserModule],
bootstrap: [SaludoComponent],
})
export class AppModule {}Con NgModule, un componente no existía por sí solo: había que declararlo en algún módulo, y ese módulo importar todo lo que sus componentes necesitaran. Arrancar la aplicación era arrancar el módulo raíz (bootstrapModule(AppModule)), no un componente.
La API de componentes standalone llegó en la versión 14 (en preview) y se estabilizó en la versión 15. Pero durante un tiempo largo seguía haciendo falta escribir standalone: true a mano en cada decorador para usarla. Dos cambios sucesivos la volvieron la forma normal de trabajar:
ng new genera proyectos nuevos ya armados sobre componentes standalone: no hay app.module.ts, y ng generate component no pregunta en qué módulo declarar nada.standalone: true pasó a ser el valor por default del decorador: ningún componente nuevo necesita escribirlo (anuncio oficial del cambio). Si hace falta el modo viejo -por ejemplo, mientras se migra un proyecto grande- se declara explícitamente standalone: false. Para proyectos existentes que todavía usan NgModule, la documentación tiene una guía de migración paso a paso.Lo que arranca la aplicación hoy es esto:
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app';
bootstrapApplication(App);Un componente raíz, sin módulo alrededor. Si el componente necesita algo de afuera -otro componente, una directiva, el router- lo declara en su propio imports, ahí mismo donde va selector y template. Nada de eso vive en un archivo separado a la espera de que alguien se acuerde de actualizarlo.
Un componente standalone no le debe nada a un módulo: sabe solo, con su propio
imports, qué necesita para funcionar. Eso es lo que cambia de verdad respecto deNgModule, no la sintaxis del decorador.
Con selector, template, styles y host ya en su lugar, lo que falta es conectar esa clase con lo que el usuario ve y hace: eso es data binding y el nuevo control de flujo (@if, @for), el tema del próximo post de la serie.