Inicio › Aprender
Componentes
Cómo se escribe un componente: inputs, outputs, slots, portales, referencias, límites de error y servicios con ámbito.
Un componente#
Un componente es una clase de JavaScript más una llamada a component(Clase, meta). La clase guarda el estado (señales) y los métodos; meta dice cómo se llama la etiqueta (selector), cuál es la plantilla y qué otros componentes usa (imports).
- Inputs: campos creados con
input(valorInicial). Son señales escribibles que el padre rellena con[nombre]="expr"o con un atributo normal (nombre="texto"). - Outputs: campos creados con
output(). El hijo llama aemit(valor)y el padre escucha con(nombre)="..."; el valor llega como$event. - Ciclo de vida:
ngOnInit()(antes de pintar),ngAfterViewInit()(ya en el DOM) yngOnDestroy()(al quitarlo).this.$eles el elemento anfitrión. - Métodos: se pueden pasar sueltos a la plantilla (
(click)="guardar()"): ya están ligados a la instancia.
Ver el código
const { signal, component, mount, input, output } = AngeLite;
class Card {
title = input('');
closed = output();
close() { this.closed.emit('cerrada'); }
}
component(Card, {
selector: 'ui-card',
template: `
<div class="card">
<b>{{ title() }}</b> <button (click)="close()">×</button>
<div><ng-content></ng-content></div>
</div>`
});
class App {
name = signal('Ada');
last = signal('—');
log(e) { this.last.set(e); }
}
component(App, {
imports: [Card],
template: `
<ui-card [title]="'Perfil de ' + name()" (closed)="log($event)">
Contenido proyectado: {{ name() }}
</ui-card>
<p>Último evento: {{ last() }}</p>`
});
mount(App, root);En HTML escribe siempre <ui-card></ui-card>, no <ui-card />: el navegador ignora la barra y se «come» lo que siga.
@Component({ selector, template }) hace lo mismo que component() si tu herramienta admite decoradores. Los tipos (Signal<T>, OutputEmitter<T>, OnInit…) vienen en el paquete: mira la referencia.
Slots con nombre#
El contenido que pones entre las etiquetas de un componente se proyecta donde esté <ng-content>. Si necesitas varias zonas, el componente declara <ng-content select="titulo"> y quien lo usa marca los hijos con slot="titulo". Lo que no lleva slot va a la zona por defecto. El contenido se evalúa en el ámbito del padre y sigue siendo reactivo.
Ver el código
const { signal, component, mount } = AngeLite;
class Panel {}
component(Panel, {
selector: 'ui-panel',
template: `
<div class="card">
<header><ng-content select="titulo"></ng-content></header>
<section><ng-content></ng-content></section>
<footer><small><ng-content select="pie"></ng-content></small></footer>
</div>`
});
class App {
n = signal(1);
}
component(App, {
imports: [Panel],
template: `
<ui-panel>
<b slot="titulo">Pedido #{{ n() }}</b>
Cuerpo del panel
<span slot="pie">{{ n() }} artículo(s)</span>
</ui-panel>
<button (click)="n.set(n() + 1)">+1 artículo</button>`
});
mount(App, root);Portales#
<ng-portal to="#destino"> pinta su contenido dentro de otro elemento (por defecto document.body) pero lo mantiene en el ámbito del componente: sirve para modales, menús y tooltips que no deben quedar atrapados por un overflow o un z-index. Se limpia solo al destruir el bloque. Está en el paquete principal y en runtime-plus.
Ver el código
const { signal, component, mount } = AngeLite;
const target = document.createElement('div');
target.style.cssText = 'margin-top:10px;padding:10px;border:1px dashed currentColor';
target.id = 'portal-destino';
target.textContent = 'Destino del portal: ';
root.after(target);
class App {
open = signal(false);
}
component(App, {
template: `
<button (click)="open.set(!open())">{{ open() ? 'Cerrar' : 'Abrir' }} aviso</button>
@if (open()) {
<ng-portal to="#portal-destino"><b>¡Hola desde otro sitio del DOM!</b></ng-portal>
}`
});
mount(App, root);Referencias a elementos#
Marca un elemento con #nombre y recíbelo con viewChild('nombre'): una señal con el elemento (o la instancia del componente, si lo pones en una etiqueta de componente) que vuelve a null cuando sale del DOM. Es la forma de enfocar un campo, medir un elemento o llamar a un método del hijo.
Ver el código
const { component, mount, viewChild } = AngeLite;
class App {
campo = viewChild('campo');
enfocar() { this.campo().focus(); }
}
component(App, {
template: `
<input #campo placeholder="Escribe aquí">
<button (click)="enfocar()">Enfocar el campo</button>`
});
mount(App, root);Errores controlados#
Si un componente define ngOnError(error, dónde), recibe los errores de sus bindings y handlers y los del contenido que le proyectan. Con eso se hace un límite de error: muestra una alternativa en lugar de dejar un hueco. Sin ngOnError el error se registra en la consola y el resto de la vista sigue funcionando.
Ver el código
const { signal, component, mount } = AngeLite;
class Limite {
fallo = signal(null);
ngOnError(error) { this.fallo.set(error.message); }
}
component(Limite, {
selector: 'error-limite',
template: `
@if (fallo()) {
<p class="card">Algo salió mal: <b>{{ fallo() }}</b></p>
} @else {
<ng-content></ng-content>
}`
});
class App {
roto() { throw new Error('dato inválido'); }
}
component(App, {
imports: [Limite],
template: `
<error-limite><p>{{ roto() }}</p></error-limite>
<p>El resto de la página sigue funcionando.</p>`
});
mount(App, root);Captura errores de expresiones, bindings y handlers de eventos. No captura los que se lanzan en el constructor ni en ngOnInit del propio componente: esos se propagan a quien lo montó.
Servicios e inyección de dependencias#
inject(Clase) devuelve una instancia compartida de la clase (se crea al primer uso): sirve para servicios, estados globales y clientes de API. Con provide(Clase, valor) la sustituyes, por ejemplo en un test.
Ver el código
const { signal, component, mount, inject } = AngeLite;
class CartService {
items = signal(0);
add() { this.items.update(n => n + 1); }
}
class AddButton {
cart = inject(CartService);
}
component(AddButton, {
selector: 'add-button',
template: `<button (click)="cart.add()">Añadir al carrito</button>`
});
class Badge {
cart = inject(CartService);
}
component(Badge, {
selector: 'cart-badge',
template: `<b>🛒 {{ cart.items() }}</b>`
});
class Shop {}
component(Shop, {
imports: [AddButton, Badge],
template: `<add-button></add-button> <cart-badge></cart-badge>`
});
mount(Shop, root);Servicios con ámbito: providers#
Si un componente declara providers, el servicio es suyo y de sus descendientes, y cada instancia del componente tiene el suyo. Un proveedor es la clase, o { provide: Api, useClass | useValue | useFactory }. inject() busca primero en el componente más cercano que lo declare y, si ninguno lo hace, usa la instancia compartida. Funciona dentro del constructor y de los inicializadores de campo.
Ver el código
const { signal, component, mount, inject } = AngeLite;
class Contador {
n = signal(0);
sumar() { this.n.update(v => v + 1); }
}
class Boton {
c = inject(Contador); // el del componente más cercano que lo declare
}
component(Boton, { selector: 'mi-boton', template: `<button (click)="c.sumar()">sumar ({{ c.n() }})</button>` });
class Grupo {}
component(Grupo, {
selector: 'mi-grupo',
providers: [Contador], // cada <mi-grupo> tiene su propio Contador
imports: [Boton],
template: `<mi-boton></mi-boton> <mi-boton></mi-boton>`
});
class App {}
component(App, { imports: [Grupo], template: `<p>Grupo A: <mi-grupo></mi-grupo></p><p>Grupo B: <mi-grupo></mi-grupo></p>` });
mount(App, root);