Angelitedocs ESEN

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 JSON

Un 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.

Interceptores y resource() (con un fetch simulado)en vivo
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 con Intl.PluralRules según count.
  • Idiomas regionales: es-MX cae a es y luego al idioma por defecto.
  • Carga perezosa: await i18n.load('fr', () => import('./fr.json')).
  • Formatos: number(), date() y relative() siguen al idioma.
  • Pipes: i18n.installPipes() registra t, num y date: {{ 'items' | t:n() }}.
Idiomas y pluralesen vivo
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);
Los mensajes son texto

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; }
animate.enter / animate.leaveen vivo
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);
Alcance

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.