Inicio › Aprender
Señales y plantillas
Estado reactivo con señales y plantillas al estilo de Angular: interpolación, enlaces, @if, @for, pipes y @defer.
Señales#
El estado de una app AngeLite vive en señales: contenedores de un valor que avisan a quien los lee cuando cambian. Hay tres piezas:
| Función | Qué es | Cómo se usa |
|---|---|---|
signal(valor) | Un valor que puedes cambiar | s() lee, s.set(x) escribe, s.update(v => …) transforma, s.peek() lee sin suscribirse |
computed(fn) | Un valor derivado: se calcula al leerlo y se recalcula solo cuando cambia algo de lo que lee | total() (solo lectura) |
effect(fn) | Código que se ejecuta ahora y cada vez que cambian las señales que lee | Devuelve una función para detenerlo; fn puede devolver una función de limpieza |
La clave es lo que pasa en una plantilla: al leer count() en una interpolación, esa parte concreta del DOM —un nodo de texto, un atributo, un bloque— se suscribe a count. Cuando cambia, AngeLite vuelve a evaluar solo esas expresiones y escribe en el DOM solo si el valor es distinto. No hay árbol virtual, ni Zone.js, ni un ciclo de detección de cambios que recorra la app.
Ver el código
const { signal, computed, component, mount } = AngeLite;
class Counter {
count = signal(0);
double = computed(() => this.count() * 2);
inc() { this.count.update(n => n + 1); }
reset() { this.count.set(0); }
}
component(Counter, {
template: `
<button (click)="inc()">+1</button>
<button (click)="reset()">reset</button>
<p [class.big]="count() >= 5">
count = {{ count() }} · doble = {{ double() }}
</p>`
});
mount(Counter, root);Varios set() seguidos provocan un único repintado: los cambios se aplican en un microtask. Por eso, justo después de un set() el DOM todavía no refleja el cambio; si necesitas esperar, await Promise.resolve() (o flush() en los tests) basta.
this.nombre = 'Ada' no avisa a nadie. Lo que quieres que reaccione tiene que ser una señal: nombre = signal('Ada') y this.nombre.set('Ada'). Si cambias un objeto o un array, pon uno nuevo: items.update(l => [...l, nuevo]).
Plantillas#
Una plantilla es HTML con unas pocas extensiones. Se compila una sola vez a una lista de instrucciones («este nodo de texto depende de esta expresión», «este elemento tiene este evento»); después, crear una vista es clonar el HTML y aplicar las instrucciones.
| Sintaxis | Qué hace |
|---|---|
{{ expr }} | Interpolación. Siempre texto: nunca se interpreta como HTML. |
[prop]="expr" | Enlaza una propiedad del DOM, o un input() si el elemento es un componente. |
[class.x]="cond" · [style.width.px]="n" · [attr.x]="v" | Una clase, un estilo (con unidad opcional) o un atributo. Con null o false el atributo se quita. |
(evento)="sentencia" | Escucha un evento. $event es el evento (o el valor emitido por un output). Varias sentencias con ;. |
[(ngModel)]="sig" | Enlace bidireccional con una señal escribible. |
{{ x | pipe:arg }} | Pipes, encadenables. |
#nombre | Referencia al elemento, que recibes con viewChild('nombre') (ver Componentes). |
Qué se puede escribir en una expresión#
Las expresiones las ejecuta un intérprete propio, sin eval ni new Function. Soporta literales, operadores aritméticos, de comparación y lógicos, ternario (a ? b : c), ??, acceso con ., ?. y [ ], arrays, y llamadas a métodos del componente. No soporta funciones flecha, literales de objeto, asignaciones, new ni plantillas con comillas invertidas.
| En lugar de… | escribe… |
|---|---|
(click)="x = 1" | (click)="x.set(1)" |
(click)="n++" | un método en la clase: (click)="inc()" |
[style]="{ color: c }" | [style.color]="c" |
items.filter(x => x.ok) | un computed o un método: okItems() |
La plantilla decide qué se ve; la lógica va en la clase (métodos y computed). Si una expresión necesita más de un operador, ponle nombre en la clase.
Control de flujo#
Los bloques @if, @else if, @else, @for y @empty usan la misma sintaxis que Angular 17+. @for reutiliza los nodos del DOM según la clave de track, así que reordenar o actualizar una lista no recrea los elementos que siguen existiendo: solo se mueven.
Ver el código
const { signal, computed, component, mount } = AngeLite;
class Todos {
draft = signal('');
items = signal([{ id: 1, text: 'Probar AngeLite', done: true }]);
pending = computed(() => this.items().filter(t => !t.done).length);
next = 2;
add() {
const text = this.draft().trim();
if (!text) return;
this.items.update(l => [...l, { id: this.next++, text, done: false }]);
this.draft.set('');
}
toggle(id) {
this.items.update(l => l.map(t => t.id === id ? { ...t, done: !t.done } : t));
}
remove(id) { this.items.update(l => l.filter(t => t.id !== id)); }
}
component(Todos, {
template: `
<input [(ngModel)]="draft" placeholder="Nueva tarea">
<button (click)="add()">Añadir</button>
<ul>
@for (t of items(); track t.id) {
<li>
<input type="checkbox" [checked]="t.done" (change)="toggle(t.id)">
<span [style.text-decoration]="t.done ? 'line-through' : ''">{{ $index + 1 }}. {{ t.text }}</span>
<button (click)="remove(t.id)">×</button>
</li>
} @empty {
<li>No hay tareas 🎉</li>
}
</ul>
@if (pending() > 0) {
<p>Te quedan {{ pending() }} por hacer.</p>
} @else {
<p>¡Todo hecho!</p>
}`
});
mount(Todos, root);Variables disponibles dentro de @for: $index, $count, $first, $last, $even y $odd.
track con una clave establetrack t.id le dice a AngeLite qué fila es cuál. Sin track la clave es el propio elemento (vale para textos y números, no para objetos que se recrean). Con claves repetidas, enableDevWarnings() te avisa.
No hay @switch: encadena @if (…) {…} @else if (…) {…} @else {…}.
Pipes#
Un pipe transforma un valor dentro de la plantilla: {{ precio | currency:'EUR' }}. Incluidos: uppercase, lowercase, titlecase, json, number, percent, currency, slice y date. El bundle completo los trae; con el runtime solo (plantillas precompiladas) importa @duvanjamid/angelite/pipes o registra los tuyos.
Ver el código
const { signal, component, mount, registerPipe } = AngeLite;
registerPipe('reverse', s => String(s).split('').reverse().join(''));
registerPipe('repeat', (s, n) => String(s).repeat(n));
class P { word = signal('angelite'); }
component(P, {
template: `<p>{{ word() | reverse }} · {{ word() | slice:0:3 | repeat:3 }}</p>`
});
mount(P, root);Un pipe es solo una función (valor, ...argumentos) => resultado. Si lee señales dentro de una plantilla, la vista se actualiza cuando cambian (así funcionan los pipes de i18n).
Carga diferida con @defer#
@defer no renderiza el bloque hasta que se cumple un disparador, y mientras tanto muestra el @placeholder. Sirve para no pagar el coste de lo que el usuario aún no ve.
| Disparador | Se carga… |
|---|---|
on idle (por defecto) | cuando el navegador está libre |
on viewport | cuando el placeholder entra en pantalla |
on interaction · on hover | al hacer clic o pulsar una tecla en el placeholder · al pasar el ratón o enfocarlo |
on timer(2s) · on immediate | tras un tiempo · en cuanto se pinta lo demás |
when expr | la primera vez que la expresión es verdadera (después no se vuelve atrás) |
Se pueden combinar con ;: @defer (on viewport; on timer(5s)) carga con lo que ocurra primero. viewport, interaction y hover necesitan un elemento en el @placeholder.
Ver el código
const { signal, component, mount } = AngeLite;
class App {
listo = signal(false);
}
component(App, {
template: `
@defer (on interaction) {
<p>Cargado al hacer clic.</p>
} @placeholder {
<button>Haz clic para cargar</button>
}
@defer (on timer(1500ms)) {
<p>Apareció tras 1,5 s.</p>
} @placeholder {
<p><i>Esperando…</i></p>
}
<label><input type="checkbox" [(ngModel)]="listo"> marcar para cargar</label>
@defer (when listo()) {
<p>Cargado porque <code>listo()</code> es verdadero.</p>
}`
});
mount(App, root);@defer retrasa el render. Para dividir el código de una pantalla entera en otro archivo, usa load: () => import('./pantalla.js') en una ruta. En una app precompilada, @defer necesita el bundle runtime-plus (el paquete principal ya lo incluye; enableDevWarnings() avisa si falta).