Inicio › Aprender
HTTP, i18n y animaciones
Cliente HTTP con interceptores y resource(), internacionalización con plurales y animaciones de entrada y salida.
Cliente HTTP y datos#
createHttp() (@duvanjamid/angelite/http) es un cliente sobre fetch: URL base, parámetros de consulta, JSON automático, timeout, cancelación con AbortSignal e interceptores. Las respuestas que no son 2xx lanzan un HttpError con status y data.
import { createHttp, retry } from '@duvanjamid/angelite/http';
const http = createHttp({
baseUrl: '/api',
headers: { 'X-App': 'demo' },
interceptors: [
(req, next) => next({ ...req, headers: { ...req.headers, Authorization: `Bearer ${token()}` } }),
retry(2) // reintenta errores de red y 5xx
]
});
const user = await http.get('/users/1', { params: { fields: 'name' }, timeout: 5000 });
await http.post('/users', { name: 'Ada' }); // los objetos se envían como JSONUn interceptor es (petición, next) => respuesta: puede cambiar la petición, la respuesta o contestar sin ir a la red (una caché, por ejemplo). Se encadenan: el primero de la lista es el primero en ver la petición y el último en ver la respuesta.
resource(): datos en señales#
resource(() => promesa) guarda data, loading y error en señales, vuelve a cargar cuando cambia una señal que lea el cargador, e ignora las respuestas tardías de peticiones antiguas (si pasas de la página 2 a la 3, una respuesta lenta de la 2 no pisa a la 3). reload() fuerza otra carga y destroy() la detiene.
Ver el código
const { signal, component, mount, createHttp, resource } = AngeLite;
// un "servidor" de mentira para el ejemplo
const servidor = async (url, init) => {
await new Promise(r => setTimeout(r, 300));
const pagina = new URL(url, 'http://x').searchParams.get('pagina');
return new Response(JSON.stringify({ pagina, auth: init.headers.Authorization, items: ['a' + pagina, 'b' + pagina] }), { status: 200 });
};
const http = createHttp({
baseUrl: '/api',
fetch: servidor,
interceptors: [(req, next) => next({ ...req, headers: { ...req.headers, Authorization: 'Bearer demo' } })]
});
class Lista {
pagina = signal(1);
datos = resource(() => http.get('/items', { params: { pagina: this.pagina() } })); // se recarga solo al cambiar la página
}
component(Lista, {
template: `
<button (click)="pagina.set(pagina() + 1)">Página {{ pagina() }} → siguiente</button>
@if (datos.loading()) {<p>Cargando…</p>}
@else if (datos.error()) {<p class="err">Error</p>}
@else {<p>{{ datos.data().items.join(', ') }} · {{ datos.data().auth }}</p>}`
});
mount(Lista, root);Internacionalización#
createI18n() (@duvanjamid/angelite/i18n) tiene un idioma activo en una señal: cualquier plantilla que llame a t() se repinta al cambiarlo.
- Parámetros:
t('hola', { nombre: 'Ada' })con{nombre}en el mensaje. Un parámetro que falta se queda visible. - Plurales: un mensaje puede ser un objeto con
zero,one,few,many,other…; se elige conIntl.PluralRulessegúncount. - Idiomas regionales:
es-MXcae aesy luego al idioma por defecto. - Carga perezosa:
await i18n.load('fr', () => import('./fr.json')). - Formatos:
number(),date()yrelative()siguen al idioma. - Pipes:
i18n.installPipes()registrat,numydate:{{ 'items' | t:n() }}.
Ver el código
const { signal, component, mount, createI18n } = AngeLite;
const i18n = createI18n({ locale: 'es', fallback: 'en', messages: {
en: { hola: 'Hello', items: { one: '{count} item', other: '{count} items' } },
es: { hola: 'Hola', items: { one: '{count} elemento', other: '{count} elementos' } }
} });
i18n.installPipes();
class App {
i18n = i18n;
n = signal(1);
}
component(App, {
template: `
<button (click)="i18n.setLocale(i18n.locale() === 'es' ? 'en' : 'es')">{{ i18n.locale() }}</button>
<button (click)="n.set(n() + 1)">+1</button>
<p>{{ 'hola' | t }} · {{ 'items' | t:n() }} · {{ 1234.5 | num }}</p>`
});
mount(App, root);Aunque un mensaje o un parámetro contenga HTML, se muestra como texto: no puede inyectar marcado. Si necesitas negritas dentro de una frase, divide el mensaje en partes o usa un componente.
Animaciones de entrada y salida#
Con animate.enter="clase" y animate.leave="clase" un elemento recibe esa clase CSS al crearse, y al desaparecer de un @if o un @for se queda en el DOM hasta que termina su animación o transición (con un tope de seguridad para que nunca se quede atascado). Con prefers-reduced-motion no hay animación. Las clases las escribes tú en CSS:
@keyframes entrar { from { opacity: 0; transform: translateY(-6px); } }
.fade-in { animation: entrar .3s ease both; }
.fade-out { animation: entrar .3s ease reverse both; }Ver el código
const { signal, component, mount } = AngeLite;
class Lista {
items = signal([1, 2, 3]);
next = 4;
add() { this.items.update(l => [...l, this.next++]); }
quitar(n) { this.items.update(l => l.filter(x => x !== n)); }
}
component(Lista, {
template: `
<button (click)="add()">Añadir</button>
<ul>@for (n of items(); track n) {
<li animate.enter="fade-in" animate.leave="fade-out">Fila {{ n }} <button (click)="quitar(n)">×</button></li>
}</ul>`
});
mount(Lista, root);Es lo mínimo útil para entradas y salidas. No hay transiciones entre rutas, animación del reordenado de listas (FLIP) ni orquestación por estados.