Ir al contenido

Adapter de Vanilla JS

El adapter de Vanilla es para JavaScript plano: sin framework, sin necesidad de paso de build, sin runtime que cargar. Además es el único adapter construido sobre el contrato compartido createWuAdapter(), lo que lo convierte en el mejor ejemplo práctico si vas a escribir un adapter propio.

Pese al nombre, tiene la mayor superficie de helpers de los adapters ligeros y —a diferencia de Alpine, HTMX y Stimulus— soporta props vivas.

Ventana de terminal
npm install wu-framework

El punto de entrada principal. config describe la app; options es la bolsa de opciones habitual de Wu.

import { wuVanilla } from 'wu-framework/adapters/vanilla';
wuVanilla.register('counter', {
state: { count: 0 },
init: (container, state) => {
console.log('Inicializando…', state);
},
render: (container, state) => {
container.innerHTML = `
<div>
<h1>Cuenta: ${state.count}</h1>
<button id="increment">+</button>
</div>
`;
container.querySelector('#increment').onclick = () => {
state.count++;
// vuelve a renderizar como prefieras — o deja que wu.update() lo dirija
};
},
destroy: (container, state) => {
container.innerHTML = '';
},
});
Clave de config Tipo Obligatoria Firma
render function (container, state) => void
init function no (container, state) => void, antes del primer render
destroy function no (container, state) => void; por defecto vacía el contenedor
state object no Estado inicial, fusionado debajo de las props

Opciones: las estándar props, standalone, standaloneContainer (por defecto #app), onMount y onUnmount.

render es obligatorio; omitirlo lanza:

[WuVanilla] render function is required for <appName>

El estado es config.state fusionado con las props

Sección titulada «El estado es config.state fusionado con las props»

Al montar, el adapter calcula { ...config.state, ...props }. Las props de registro y las que envía el shell aterrizan en el mismo objeto que recibe tu render. Eso es lo que hace que las props vivas funcionen sin un segundo concepto.

await wu.update('counter', { count: 42 });

El slot update fusiona las props nuevas en el estado guardado y vuelve a llamar a render(container, state) sobre el mismo contenedor: en el sitio, sin desmontar. Como render controla por completo el contenido del contenedor, esto es lo más cerca de un re-render que llega el JavaScript plano.

Para apps organizadas como una clase con constructor(container) y métodos render() / destroy():

class TodoApp {
constructor(container) {
this.container = container;
this.todos = [];
}
render() {
this.container.innerHTML = `
<ul>${this.todos.map((t) => `<li>${t}</li>`).join('')}</ul>
`;
}
addTodo(text) {
this.todos.push(text);
this.render();
}
destroy() {
this.container.innerHTML = '';
this.todos = [];
}
}
wuVanilla.registerClass('todo-app', TodoApp);

El adapter construye una instancia por montaje y llama a render(); al desmontar llama a destroy() si existe y luego vacía el contenedor.

registerTemplate(appName, template, options)

Sección titulada «registerTemplate(appName, template, options)»

Para HTML estático o dirigido por datos sin comportamiento propio:

// Estático
wuVanilla.registerTemplate('header', '<header><h1>Mi cabecera</h1></header>');
// Dinámico
wuVanilla.registerTemplate('greeting', (data) => `<h1>¡Hola ${data.name}!</h1>`, {
data: { name: 'Mundo' },
styles: ['h1 { color: rebeccapurple; }'],
scripts: [],
});

template es un string de HTML, o una función que recibe data.

Opción Tipo Por defecto Notas
data object {} Se pasa a template cuando es una función.
styles string[] [] Se unen en un único <style data-wu-app="<appName>"> antepuesto al contenedor.
scripts string[] [] Fuentes de script inyectadas tras el markup.
standalone boolean true Plan B standalone.
standaloneContainer string '#app' Selector usado en modo standalone.
onMount / onUnmount function null Callbacks de ciclo de vida.

Como los estilos van dentro del contenedor, aterrizan dentro del Shadow root cuando Wu está aislando la app, que es exactamente donde los quieres.

Un componente en miniatura de estado y acciones, pensado para pasárselo directamente a register():

import { wuVanilla, createComponent } from 'wu-framework/adapters/vanilla';
const Counter = createComponent({
state: { count: 0 },
template: (state) => `
<div>
<h1>${state.count}</h1>
<button data-action="increment">+</button>
<button data-action="decrement">−</button>
</div>
`,
actions: {
increment: (state) => ({ count: state.count + 1 }),
decrement: (state) => ({ count: state.count - 1 }),
},
onInit: (container, state) => console.log('init', state),
onDestroy: (container, state) => console.log('adiós'),
});
wuVanilla.register('counter', Counter);

El objeto devuelto ya tiene la forma { state, init, render, destroy } que espera register(), más setState(partial) y getState() para uso imperativo.

Cómo funciona:

  • template(state) produce el HTML; render lo escribe y después enlaza un listener de click a cada elemento [data-action].
  • Una acción recibe (state, element). Devolver un objeto llama a setState() con él; no devolver nada significa “sin cambio de estado” y se salta el re-render.
  • El foco se conserva entre re-renders cuando el elemento enfocado tiene un id: el adapter guarda document.activeElement.id y vuelve a enfocar el elemento correspondiente tras escribir el HTML nuevo. Sin esto, escribir en un input dentro de un componente con data-action perdería el cursor en cada pulsación.

Lado del shell, imperativo: sin JSX, sin compilador de templates.

import { createWuSlot } from 'wu-framework/adapters/vanilla';
const slot = createWuSlot(document.querySelector('#main'), {
name: 'catalog',
url: 'http://localhost:3001',
fallbackText: 'Cargando catálogo…',
onLoad: ({ name }) => console.log(name, 'listo'),
onError: (err) => console.error(err),
});

Crea un div.wu-slot con data-wu-app / data-wu-url, lo añade a target, pinta un estado de carga y monta la micro-app en un contenedor interno con id único.

El texto del fallback pasa por escapeHtml() antes de llegar a innerHTML: este slot renderiza los mensajes de error de la misma forma, y esos mensajes pueden originarse en un módulo remoto. Consulta Escribir un adapter.

Funciones simples, sin ciclo de vida de framework:

import { useWuEvents, useWuStore } from 'wu-framework/adapters/vanilla';
const { emit, on, once, off, cleanup } = useWuEvents();
on('user:login', (e) => console.log(e.data));
const { state, setState, getState, cleanup: storeCleanup } = useWuStore('user');
setState('name', 'Ada'); // escribe en user.name
import { useWuAI } from 'wu-framework/adapters/vanilla';
const ai = useWuAI({ namespace: 'support' });
await ai.send('Muéstrame los pedidos pendientes');
console.log(ai.getMessages()); // copia del array
console.log(ai.isStreaming()); // boolean
console.log(ai.getError()); // string | null
ai.clear();

Sin streaming: envuelve wu.ai.send(). Los getters son métodos, no propiedades, porque no hay sistema de reactividad que haga significativos los valores vivos. Consulta Visión general de IA.

register() es el único punto de entrada de adapter en el código base que delega en el contrato compartido:

const registerFn = createWuAdapter({
name: 'WuVanilla',
mount(container, { props }) { /* … */ return { container, state }; },
unmount(container) { /* … */ },
update(container, { props }) { /* … */ },
});
return registerFn(appName, config, { standaloneContainer: '#app', ...rest });

Todo lo demás —waitForWu, wu.define, la contabilidad por app, el plan B standalone, el slot condicional update— viene de createWuAdapter(). Si vas a escribir un adapter para un framework que Wu no incluye, lee este archivo junto con Escribir un adapter.

import {
wuVanilla, // export por defecto
register,
registerClass,
registerTemplate,
createComponent,
createWuSlot,
useWuEvents,
useWuStore,
useWuAI,
getWuInstance,
waitForWu,
} from 'wu-framework/adapters/vanilla';

Desde el barrel: registerVanilla, createVanillaWuSlot, useVanillaWuEvents, useVanillaWuStore, useVanillaWuAI.