@_noobcoder007/rule-graph-editor
v1.0.0
Published
Framework-agnostic web component to read, create, edit and visualize json-rules-engine rule documents as an interactive decision graph.
Maintainers
Readme
verbose-robot — <jre-rule-editor>
Un web component instalable como dependencia npm, agnóstico de framework, para leer, crear y editar documentos de reglas compatibles con el formato de json-rules-engine (facts, condiciones anidadas all/any/not, y el event como resultado final), visualizados como un árbol/grafo de decisión interactivo.
- Añadir un nuevo nodo de decisión (condición o grupo
all/any/not) con un botón "+". - Editar los facts del documento como una lista de filas (una por fact, autoguardado al salir del campo) o, para pegar/importar varios de golpe, con un editor JSON en bloque plegable.
- Una capa de lint semántico resalta en el propio grafo ambigüedades y contradicciones (grupos vacíos,
notcon más de un hijo, condiciones contradictorias, facts sin declarar, tipos de valor incompatibles con el operador,event.typeausente) antes de que rompan la regla. - Construido con Lit + D3 como Custom Elements estándar (Shadow DOM) — funciona en HTML plano, React, Vue, Angular o cualquier otro entorno.
Instalación
npm install @_noobcoder007/rule-graph-editorPublicado en el registro público de npm bajo el scope personal de la cuenta que lo mantiene.
Uso en HTML plano
<script type="module" src="/node_modules/@_noobcoder007/rule-graph-editor/dist/rule-graph-editor.js"></script>
<jre-rule-editor id="editor" layout="horizontal" style="height: 560px; display:block;"></jre-rule-editor>
<script type="module">
const editor = document.getElementById('editor');
editor.setDocument({
$schemaVersion: 1,
facts: { age: 20, country: 'US' },
rules: [
{
name: 'eligibility-rule',
conditions: {
all: [
{ fact: 'age', operator: 'greaterThanInclusive', value: 18 },
{ any: [
{ fact: 'country', operator: 'equal', value: 'US' },
{ fact: 'country', operator: 'equal', value: 'CA' },
] },
],
},
event: { type: 'eligible', params: { message: 'User is eligible' } },
},
],
});
editor.addEventListener('jre:change', (e) => {
console.log('documento actualizado', e.detail.document, 'origen:', e.detail.source);
});
editor.addEventListener('jre:lint', (e) => {
console.log('problemas detectados', e.detail.warnings);
});
</script>Uso en React
import { useEffect, useRef } from 'react';
import '@_noobcoder007/rule-graph-editor';
import type { RuleEditorElement, RuleChangeDetail } from '@_noobcoder007/rule-graph-editor';
function RuleEditor({ document, onChange }: { document: RuleDocument; onChange: (doc: RuleDocument) => void }) {
const ref = useRef<RuleEditorElement>(null);
useEffect(() => {
ref.current?.setDocument(document);
}, [document]);
useEffect(() => {
const el = ref.current;
const handler = (e: Event) => onChange((e as CustomEvent<RuleChangeDetail>).detail.document);
el?.addEventListener('jre:change', handler);
return () => el?.removeEventListener('jre:change', handler);
}, [onChange]);
return <jre-rule-editor ref={ref} style={{ display: 'block', height: 560 }} />;
}Vue: igual que arriba, pero hay que decirle a Vue que jre-* son custom elements nativos y no un componente Vue sin registrar, o el compilador lanzará un warning/error:
// vite.config.js
export default {
plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('jre-') } } })],
};Angular: hay que añadir CUSTOM_ELEMENTS_SCHEMA al módulo/componente que use <jre-rule-editor>:
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@Component({ /* ... */, schemas: [CUSTOM_ELEMENTS_SCHEMA] })En ambos casos, el documento se pasa igual: importar el paquete (registra los custom elements como efecto secundario), usar <jre-rule-editor> en la plantilla, pasar el documento vía .document/setDocument() con una referencia al elemento (ref/@ViewChild), y escuchar los eventos jre:* con addEventListener o el binding de eventos nativo del framework (@jre:change en Vue, (jre:change) no funciona en Angular por el : — usar addEventListener vía ElementRef en su lugar).
El RuleDocument
json-rules-engine no define un esquema para los facts (son valores/funciones en tiempo de ejecución), así que el componente usa un envelope propio, serializable, donde rules es 100% compatible byte a byte con lo que espera Engine.addRule():
interface RuleDocument {
$schemaVersion: 1;
facts: Record<string, unknown>; // valores de muestra usados para visualizar/evaluar
rules: RuleProperties[]; // idéntico al formato de json-rules-engine
}
interface RuleProperties {
conditions: TopLevelCondition; // { all: [...] } | { any: [...] } | { not: ... }
event: { type: string; params?: Record<string, unknown> };
name?: string;
priority?: number;
}Corte de alcance intencional (v1): las referencias a condiciones con nombre de json-rules-engine (
{ condition: 'nombre' }) no están soportadas por el validador, el transformador a grafo, el lint ni la UI.
El editor solo manipula una regla a la vez — rules sigue siendo un array (para que el JSON sea idéntico al de json-rules-engine y no cambie la forma en que lo guardas en tu base de datos), pero siempre tiene longitud 0 (estado vacío, sin regla creada todavía) o 1. setDocument()/loadFromJSON() rechazan con jre:validation-error cualquier documento con 2 o más reglas, en vez de mostrar solo la primera y descartar el resto en silencio.
API pública de <jre-rule-editor>
Propiedades / atributos
| Nombre | Tipo | Descripción |
|---|---|---|
| document | RuleDocument | Getter/setter del documento completo (equivalente a getDocument()/setDocument()) |
| readonly | boolean (atributo reflejado) | Desactiva "+"/editar/eliminar; el grafo queda solo-lectura (pan/zoom siguen activos) |
| layout | 'horizontal' \| 'vertical' | Orientación del árbol |
| theme | 'light' \| 'dark' \| 'auto' (atributo reflejado, por defecto 'auto') | 'auto' sigue el prefers-color-scheme del sistema/navegador; 'light'/'dark' fuerzan una paleta sin importar la preferencia del SO |
Métodos
getDocument(): RuleDocument // clon profundo del estado actual
setDocument(doc: RuleDocument): ValidationResult // valida; si es inválido, dispara jre:validation-error y no cambia nada
loadFromJSON(json: string): ValidationResult // JSON.parse + setDocument
exportJSON(pretty?: boolean): string
createRule(rule?: Partial<RuleProperties>): void // crea la única regla del documento; no-op con jre:validation-error si ya existe una
clearRule(): void // quita la regla y vuelve al estado vacío
validateDocument(doc?: RuleDocument): ValidationResult
lintDocument(doc?: RuleDocument): LintWarning[]Eventos (bubbles: true, composed: true)
| Evento | detail | Cuándo |
|---|---|---|
| jre:change | { document, source } | Tras cualquier edición aceptada (source: condition|fact|event|rule|import) |
| jre:lint | { warnings: LintWarning[] } | Justo después de cada jre:change, con el resultado del análisis semántico |
| jre:node-select | { path?, node } | Al hacer clic en cualquier nodo del grafo |
| jre:validation-error | { errors: ValidationError[] } | Al intentar cargar un JSON con forma inválida (incluye traer más de 1 regla); el documento anterior se conserva |
Theming: variables CSS (--jre-node-fill, --jre-node-stroke, --jre-edge-color, --jre-accent, --jre-severity-error, …) y partes ::part(root|header|graph|side|canvas) para restylear desde fuera del shadow DOM sin ejectar el componente.
El componente hereda el tema de la aplicación anfitriona por dos vías, combinables:
- Automático: con
theme="auto"(el valor por defecto), sigue elprefers-color-schemedel navegador/SO — si el usuario tiene su sistema en modo oscuro, el editor se ve oscuro sin que la app tenga que hacer nada.theme="light"/theme="dark"fuerzan una paleta concreta sin importar el sistema. - Explícito: cualquier app puede sobreescribir variables CSS individuales en
<jre-rule-editor>(o más arriba en la página) — por ejemplojre-rule-editor { --jre-accent: #ff5722; --jre-font: 'Inter', sans-serif; }— y esos valores ganan siempre sobre los del tema claro/oscuro por defecto.--jre-fontpor defecto esinherit, así que si no defines nada el componente toma la tipografía que ya esté heredando de tu página.
Los valores por defecto de los tokens (--jre-*) solo se declaran una vez, en <jre-rule-editor> — los subcomponentes internos (<jre-graph-view>, <jre-facts-panel>, …) únicamente consumen esas variables en vez de redeclararlas, para que la herencia a través de sus shadow roots internos funcione correctamente.
En el demo (npm run dev) hay un selector "Tema" con auto/claro/oscuro y una opción "Marca personalizada" que simula una app con su propio design system, sobreescribiendo varios tokens a la vez (--jre-accent, --jre-font, --jre-node-fill-group, …) con una paleta y tipografía completamente ajenas — ver demo/style.css (.brand-demo) para el ejemplo exacto.
Apps con su propio design system
Si la app anfitriona ya tiene su propia paleta/tema, no hace falta nada especial de nuestro lado — se resuelve igual que el punto 2 de arriba, mapeando los tokens del design system existente a los --jre-*:
/* si el design system ya expone sus propios tokens como variables CSS */
jre-rule-editor {
--jre-accent: var(--brand-primary);
--jre-bg: var(--brand-surface);
--jre-border: var(--brand-border);
--jre-text: var(--brand-foreground);
--jre-severity-error: var(--brand-danger);
--jre-font: var(--brand-font-family);
}Si el design system NO expone variables CSS (p. ej. tokens de Sass compilados o un tema de JS tipo styled-components/MUI), se escriben los valores literales una sola vez en ese mismo bloque — funciona igual, solo que sin el nivel extra de indirección.
Cobertura actual de tokens: colores (fondo/superficie/borde/texto/acento/severidades) y tipografía (--jre-font). Espaciado y radios de borde todavía están fijos en el CSS de cada componente, no tokenizados — si tu design system también necesita controlar eso, se puede añadir.
Nota sobre diálogos nativos: todo diálogo modal del editor (formulario de nodo, confirmaciones de borrado) usa <dialog> propio estilizado con estos mismos tokens — deliberadamente no se usa window.confirm()/alert()/prompt() en ningún punto, porque esos son diálogos nativos del sistema operativo que ignoran cualquier CSS y romperían la consistencia visual con el resto del tema.
Inteligencia del grafo (anti-ambigüedad)
Además de validar la forma del JSON, src/core/lint.ts analiza el documento tras cada cambio y ancla cada problema al nodo exacto del grafo (borde rojo = error, ámbar = warning, azul = info; el resumen de la cabecera muestra el total):
| Código | Severidad | Qué detecta |
|---|---|---|
| not-arity | error | Un grupo not con 0 o más de 1 hijo (red de seguridad para JSON importado/pegado a mano) |
| empty-group | warning | all/any vacío (all([]) es true, any([]) es false) |
| unknown-fact | warning | Una condición referencia un fact no declarado en facts |
| contradictory-conditions | error | Dos condiciones sobre el mismo fact, dentro del mismo all, que nunca pueden cumplirse a la vez |
| duplicate-condition | info | Dos condiciones hermanas idénticas |
| operator-value-mismatch | warning | El tipo de value no encaja con lo que espera el operador |
| missing-event-type | error | Una regla no tiene event.type |
Es una heurística documentada, no un solver de restricciones completo. Además, la UI aplica guardarraíles proactivos: el selector de operador cambia el widget de value según el tipo esperado, un fact nuevo escrito en una condición se crea automáticamente en facts, un grupo not nunca puede tener más de un hijo, y borrar un fact usado por alguna condición pide confirmación.
Desarrollo
npm install
npm run dev # demo en HTML plano (sin framework) en http://localhost:5173
npm test # Vitest: mutaciones, lint, validación, y round-trip contra el Engine real de json-rules-engine
npm run typecheck
npm run build # dist/ ESM + UMD + .d.tsEl demo (demo/) permite: cargar el JSON de ejemplo o un archivo propio, crear un documento nuevo, exportar el JSON resultante, ver en vivo cada jre:change, y evaluar el documento contra el Engine real de json-rules-engine para confirmar que sigue siendo válido para la librería.
Integración en un proyecto externo (verificado)
npm run build && npm pack genera exactamente el tarball que se subiría a npm. Se probó instalando ese tarball (no una copia local del repo) en tres proyectos externos desechables, cada uno representando una forma real de consumir el paquete:
- HTML plano, sin bundler:
<script type="module" src="/node_modules/@_noobcoder007/rule-graph-editor/dist/rule-graph-editor.js">+<jre-rule-editor>en el HTML —npm installtrae Lit y D3 automáticamente (17 paquetes), sin que el proyecto externo tenga que declararlos. - App con bundler (Vite/webpack/React/Vue/Angular):
import '@_noobcoder007/rule-graph-editor'con especificador "pelado" (sin ruta relativa) — Vite lo resuelve y pre-empaqueta correctamente vía el campoexportsdelpackage.json, igual que haría con cualquier paquete de npm. - Proyecto TypeScript:
import { RuleEditorElement, createEmptyDocument, mutations } from '@_noobcoder007/rule-graph-editor'type-checkea correctamente contradist/index.d.ts, con los tipos completos (noany).
Notas de compatibilidad:
- Depende de Custom Elements v1 + Shadow DOM +
<dialog>nativo — navegadores evergreen modernos (Chrome/Edge/Firefox/Safari recientes); no soporta IE11. lity los paquetesd3-*van comodependenciesnormales (se instalan solos);json-rules-engineespeerDependencyopcional (solo hace falta si además quieres ejecutar las reglas, no para leer/editar/visualizar).- El paquete aún no está publicado en el registro público de npm — mientras tanto se consume vía
npm pack+npm install <tarball>.tgz,npm link, o una dependencia de git.
