Tutorial de Angular

Componentes independientes de Angular 20: entradas de señal, detección de cambios OnPush, plantillas en línea, cero NgModules, cero CSS.

Qué vas a construir

Un pequeño formulario de contacto: campo con etiqueta, botón de envío, enlace bidireccional, comportamiento real de teclado y lector de pantalla; primero desnudo, luego con estilo, en unos 10 minutos.

Antes de empezar

Necesitas git, Node 22 o posterior y pnpm 10 o posterior. Sirve cualquier proyecto de Angular 20: Angular CLI o Analog.

Paso 1: Obtén el código

git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-angular-headless
pnpm install
pnpm test        # vitest + TestBed, all 571 components
pnpm run build-storybook

Los componentes son archivos planos en components/ — Button.ts, TextInput.ts ; cada uno es un componente independiente con plantilla en línea, además de su spec y su story. El barrel index.ts reexporta cada componente; pnpm build produce un paquete en Angular Package Format mediante ng-packagr.

Paso 2: Tu primer componente

import { Component } from "@angular/core";
import { Button } from "@lilydesignsystem/angular-headless";

@Component({
  selector: "app-save-bar",
  standalone: true,
  imports: [Button],
  template: `<lily-button (click)="save()">Save</lily-button>`,
})
export class SaveBar {
  save() { console.log("saved"); }
}

Cada componente declara un selector lily-* y renderiza el elemento semántico con su gancho de clase: <button class="button" type="button"> aquí. Las entradas se basan en señales: label() para un aria-label para anular, className() para añadir clases.

Paso 3: Compón un formulario pequeño

import { Component, signal } from "@angular/core";
import { Form, Field, TextInput, Button } from "@lilydesignsystem/angular-headless";

@Component({
  selector: "app-contact-form",
  standalone: true,
  imports: [Form, Field, TextInput, Button],
  template: `
    <lily-form label="Contact form" (submit)="handleSubmit($event)">
      <lily-field label="Full name" [required]="true">
        <lily-text-input id="name" label="Full name" [(value)]="name" />
      </lily-field>
      <lily-button type="submit">Save</lily-button>
    </lily-form>
  `,
})
export class ContactForm {
  name = signal("");
}

lily-field toma la etiqueta visible como su propia entrada label en lugar de un componente de etiqueta anidado: pásala una vez, en el campo, y el lily-text-inputcorrespondiente de label aporta el nombre accesible. Los componentes similares a entradas exponen valores model() así que el enlace bidireccional banana-box [(value)] funciona de inmediato, compatible con zoneless en todo momento.

Verificado el 2026-08-29: un borrador anterior anidaba un componente Label aparte dentro de lily-field, lo que renderizaba dos etiquetas (una vacía) porque lily-field ya renderiza la suya propia a partir de la entrada label ; se detectó revisando el código fuente del componente. La ruta compuesta del formulario de contacto es el mismo patrón, ejercitado con pruebas e2e y axe reales.

Comprueba tu trabajo: ejecuta la aplicación y el formulario se ve sencillo, tal como se pretendía: el estilo aún está por venir. Pulsa Tab: el foco recorre cada control, se anuncia la etiqueta y el envío funciona. El comportamiento está hecho; el aspecto es tuyo y es el siguiente paso.

Paso 4: Dale estilo

Da estilo a los ganchos de clase: .button, .text-input, .label, .field , en styles.css, o añade un tema ya hecho al arreglo styles en angular.json / enlázalo en index.html:

<link rel="stylesheet" href="/themes/adobe-spectrum.css" />

Paso 5: Míralo todo funcionando

La aplicación de ejemplo Angular + Analog muestra el catálogo completo con enrutamiento basado en archivos y Vite:

git clone https://github.com/LilyDesignSystem/lily-design-system
cd lily-design-system/lily-design-system-angular-examples
pnpm install && pnpm run dev

Próximos pasos

¿Dudas por el camino? La página de ayuda está llena de respuestas, y si algún paso puede ser más claro, cuéntanoslo y con gusto mejoraremos el tutorial.