Mini librería para construir interfaces HTML reactivas con Web Components, peticiones XHR, cola de tareas y mensajes visuales sin depender de frameworks.
Artha JS expone un conjunto pequeño de utilidades y componentes personalizados pensados para:
- enviar formularios por XMLHttpRequest
- renderizar listas o bloques desde respuestas JSON
- mostrar loaders y mensajes de estado
- coordinar eventos globales entre componentes
- ¿Qué incluye?
- Instalación
- Inicio rápido
- Exportaciones
- Componentes
- Core
- Flujo de respuesta esperado
- Eventos útiles
- Desarrollo
- Licencia
La librería exporta estas piezas:
Util: helpers de DOM, formato y utilidades generalesEventBus: bus global de eventosTaskQueue: cola simple para evitar tareas duplicadas y coordinar estadosXHR: wrapper ligero sobreXMLHttpRequestArthaMessage: componente para mostrar mensajes de estadoArthaLoader: componente visual de cargaArthaContainer: componente para cargar y renderizar datosArthaForm: formulario con envío asíncrono
Al importar dist/artha.min.js, la librería registra automáticamente estos custom elements:
artha-containerartha-formartha-messageartha-loader
npm install @dogiloki/artha-jsimport {
XHR,
EventBus,
TaskQueue,
Util,
ArthaForm,
ArthaContainer,
ArthaMessage,
ArthaLoader
} from "@dogiloki/artha-js/dist/artha.min.js";Si tu bundler soporta CSS desde dependencias:
import "@dogiloki/artha-js/dist/artha.min.css";O bien desde HTML:
<link rel="stylesheet" href="./node_modules/@dogiloki/artha-js/dist/artha.min.css">Ejemplo mínimo con un formulario y un contenedor que carga datos remotos:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="csrf-token" content="TOKEN_OPCIONAL">
<link rel="stylesheet" href="./dist/artha.min.css">
</head>
<body>
<artha-form
id="user-form"
action="/api/user"
method="POST">
<input type="text" name="name" placeholder="Nombre">
<input type="email" name="email" placeholder="Correo">
<button type="submit">Guardar</button>
</artha-form>
<artha-container
id="users"
action="/api/users"
method="GET"
template="user-template">
</artha-container>
<template id="user-template">
<article>
<h3 data-wire="name"></h3>
<p data-wire="email"></p>
<small data-wire="id"></small>
</article>
</template>
<script type="module">
import "./dist/artha.min.js";
</script>
</body>
</html>import {
Util,
EventBus,
TaskQueue,
XHR,
ArthaMessage,
ArthaLoader,
ArthaContainer,
ArthaForm
} from "./dist/artha.min.js";Al cargar el módulo también se emiten dos eventos globales:
artha:before-registerartha:after-register
Esto sirve, por ejemplo, para personalizar la transformación de respuestas antes de registrar los componentes:
import { EventBus, XHR } from "./dist/artha.min.js";
EventBus.on("artha:before-register", () => {
XHR.defaults.transformResponse = (xhr) => ({
data: xhr.response,
message: null,
errors: null,
status: "success"
});
});Componente para mostrar mensajes visuales.
infosuccesswarningerror
<artha-message id="feedback"></artha-message>const message = document.getElementById("feedback");
message.info("Cargando información...");
message.success("Guardado correctamente");
message.warning("Faltan campos por revisar");
message.error("Ocurrió un error");
message.hidden();show(message, type)info(message)success(message)warning(message)error(message)hidden()
Loader visual para estados de carga.
type: tipo de loader. Default:ringtext: texto mostrado debajo del loader. Default:Petición en proceso...
ringdotsbarwave
Nota: en la implementación actual bar y wave reutilizan la misma clase visual que dots.
<artha-loader type="ring" text="Cargando usuarios"></artha-loader>
<artha-loader type="dots" text="Procesando"></artha-loader>Formulario asíncrono basado en XMLHttpRequest.
- intercepta el evento
submit - valida los campos con
checkValidity() - envía los datos por XHR
- muestra mensajes con
artha-message - rellena campos automáticamente si la respuesta trae
data
action: endpoint del formulariomethod: método HTTPresponse-type: tipo de respuesta del XHR. Default:jsondisable-submit: impide el envío automático al presionar Entermessage-target: selector interno para localizar el mensaje asociadoid: usado para identificar la tarea enTaskQueue
<artha-form id="profile-form" action="/api/profile" method="POST">
<artha-message></artha-message>
<input type="text" name="name" required>
<input type="email" name="email" required>
<label>
<input type="checkbox" name="active">
Activo
</label>
<button type="submit">Guardar</button>
<button type="reset">Limpiar</button>
</artha-form>submit()reset(resetMessage = true)resetMessage()checkValidity()loadInputs(selector = "input,select,textarea")fillFromJson(json, reset = true)getValue(name)input(name)
load: se dispara cuando XHR termina de cargarresolve: se dispara cuando la respuesta fue aceptada y procesadacomponent-ready: heredado deBaseComponent
Componente para cargar, renderizar y refrescar datos remotos, o actualizar vistas existentes.
- listados
- tarjetas
- tablas simples
- bloques con plantillas HTML
- selección simple o múltiple
- refresco desde eventos globales
action: endpoint a consultarmethod: método HTTP. Default:GETtemplate: id de un<template>o referencia configurada en el componentepagination: valor configurado pero no aplicado directamente en la clase actual. Default:10message: referencia al mensaje asociadomessage-target: selector interno alternativo para localizar unartha-messagesearcher: activa el comportamiento de búsqueda si existe uninput-searchselectable: permite seleccionar itemsmultiple: permite múltiples seleccionesrefresh-on: nombres de eventos delEventBus, separados por comaid: identificador del contenedor
<artha-container
id="users"
action="/api/users"
method="GET"
template="user-card"
selectable
multiple
refresh-on="users:reload,users:updated">
<artha-message></artha-message>
</artha-container>
<template id="user-card">
<article class="user-card">
<h3 data-wire="name"></h3>
<p data-wire="email"></p>
<span data-wire="active:boolean"></span>
</article>
</template>refresh(search = null): vuelve a pedir los datos remotosrefreshWithData(data): actualiza un item ya renderizado pordata.idrender(results, refresh = false, refreshChildren = true)renderItem(data, refreshChildren = true, update = null)reset(): limpia la selecciónselection(): devuelve el store de selección- propiedad
value: ids seleccionados
Si selectable está activo:
container.valuedevuelve el id seleccionado- si también
multipleestá activo, devuelve un arreglo de ids reset()limpia la selección actual
loadresolvedynamic-content-loadeditem-rendereditem-selecteditem-deselectedcomponent-ready
El renderizado se basa en atributos data-wire dentro de la plantilla.
Formato general:
data-wire="ruta"
data-wire="ruta:atributo"
data-wire="ruta:atributo:append"
data-wire="ruta:boolean"
data-wire="ruta:boolean:chooser"Texto simple:
<span data-wire="name"></span>Propiedad anidada:
<span data-wire="user.email"></span>Append sobre contenido actual:
<span data-wire="price:textContent:append">$ </span>Booleano como check o cross:
<span data-wire="active:boolean"></span>Booleano con selección por plantilla:
<div data-wire="status:boolean:chooser">
<template data-chooser-value="approved">
<span>Aprobado</span>
</template>
<template data-chooser-value="rejected">
<span>Rechazado</span>
</template>
<template data-chooser-default>
<span>Pendiente</span>
</template>
</div>El componente soporta rutas terminadas en [] para iterar datos. Internamente, busca elementos marcados con fillable o iterable.
Ejemplo conceptual:
<ul data-wire="tags[]">
<li>
<span fillable></span>
</li>
</ul>Nota: el flujo más sólido en la implementación actual es renderizar texto, booleanos y arreglos. Si quieres usar mapeos más avanzados, conviene probarlos primero en tu caso concreto.
Wrapper de XMLHttpRequest con callbacks y opciones centralizadas.
import { XHR } from "./dist/artha.min.js";
XHR.request({
url: "/api/users",
method: "GET",
headers: {
Accept: "application/json"
},
onData: (xhr, data) => {
console.log("ok", data);
},
onError: (error) => {
console.error("error", error);
}
});method: defaultGETurl: URL finaluri: alternativa para construir"/" + uri"headers: headers adicionalesdata: datos del formularioquery: query params para GETfiles: archivos o listas de archivosresponse_type: defaultjsonwith_credentials: defaultfalsetimeout: default0retry: reintenta enerrorotimeoutretry_delay: default5000transformResponse(xhr): transformaxhr.responseonLoad(xhr)onData(xhr, transformed)onError(transformed)onTimeout(transformed)onProgress(event, loaded, total)onAbort(transformed)onAction(xhr)
- si existe
<meta name="csrf-token">o<meta name="csrf_token">, se envía como headerX-CSRF-Token - para métodos distintos de
GET, la librería envíaFormData - también agrega
_methoddentro delFormData - si hay token CSRF, también agrega
csrf_tokenal cuerpo
Evita ejecutar dos tareas con el mismo id al mismo tiempo y centraliza el cierre de estados.
import { TaskQueue } from "./dist/artha.min.js";
const queue = TaskQueue.singleton();
queue.loadTask("save-user", "Guardando usuario...", (task) => {
setTimeout(() => {
task.resolve({
status: 200,
response: JSON.stringify({
status: "success",
message: "Usuario guardado",
data: { id: 1 }
})
});
}, 500);
}, {
close: true
});TaskQueue.defaults = {
title: "Petición en proceso...",
close: false,
message: null
};- cada tarea necesita un id único
- si se repite el id mientras sigue activa, se cancela la nueva tarea
- si se pasa un
ArthaMessage, la cola actualiza sus estados visuales close: trueintenta cerrar el mensaje automáticamente al finalizar
Bus global basado en EventTarget.
import { EventBus } from "./dist/artha.min.js";
const unsubscribe = EventBus.on("users:reload", (data) => {
console.log("recargar", data);
});
EventBus.emit("users:reload", { source: "manual" });
EventBus.emitAsync("users:reload", { source: "async" });
unsubscribe();EventBus.emit(name, data)EventBus.emitAsync(name, data)EventBus.on(name, callback)EventBus.once(name, callback)EventBus.onAny(callback)EventBus.off(name, callback)EventBus.clean(name)EventBus.clearAll()
EventBus.debug = true;Utilidades generales.
Util.getMeta(name)Util.getValueByPath(obj, path, defaultValue = null)Util.modal(element, visible = -1)Util.modalById(id, visible = -1)Util.formatMoney(value, options = {})Util.numberRandom(min, max)Util.withinRange(value, min, max)Util.createElement(type, value = null, options = {})
Util.getMeta("csrf-token");
Util.getValueByPath({ user: { name: "Ana" } }, "user.name");
Util.modalById("panel", true);
Util.formatMoney("1234.5");
Util.numberRandom(1, 10);
Util.withinRange(204, 200, 299);ArthaForm y ArthaContainer funcionan mejor cuando el backend responde con una estructura parecida a esta:
{
"status": "success",
"message": "Operación completada",
"data": []
}También soporta errores con este formato:
{
"status": "error",
"message": "No se pudo completar la operación",
"errors": {
"email": ["El correo ya existe"]
}
}Si tu API responde con otro formato, puedes adaptar la salida usando XHR.defaults.transformResponse.
component-readyloadresolvedynamic-content-loadeditem-rendereditem-selecteditem-deselected
artha:before-registerartha:after-register
import { EventBus } from "./dist/artha.min.js";
EventBus.emit("users:updated", { id: 3, name: "Nuevo nombre" });
EventBus.emit("users:reload");Y en el contenedor:
<artha-container
action="/api/users"
template="user-template"
refresh-on="users:reload,users:updated">
</artha-container>npm installCompilar CSS en modo watch:
npm run dev:cssLevantar servidor local:
npm run dev:serverEjecutar ambos:
npm run devnpm run buildMIT. Consulta LICENSE.