npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@varbyte/treebound

v1.0.0

Published

Reactive binding engine for Custom Elements using TreeWalker — zero dependencies, no eval()

Downloads

85

Readme

@varbyte/treebound

Motor de binding reactivo para Custom Elements — sin dependencias, sin eval().

npm license coverage

TreeBound añade reactividad declarativa a tus Custom Elements usando TreeWalker nativo del DOM. Sin virtual DOM, sin compilador, sin dependencias externas.


Características

  • Binding de texto{{ expresión }} en cualquier nodo de texto
  • Binding de atributosbind-attr="expr" o :attr="expr"
  • Eventos@click="handler"
  • Two-way bindingbind-value="propiedad" en inputs, textareas y selects
  • Directivas estructurales*if, *for, *ref
  • Reactividad basada en Proxy — sin polling, sin dirty-checking
  • Parser de expresiones seguro — sin eval() ni Function()
  • Zero dependencias en producción
  • TypeScript nativo

Instalación

npm install @varbyte/treebound

Inicio rápido

Con TreeBoundEngine directamente

import { TreeBoundEngine } from '@varbyte/treebound';

const container = document.querySelector('#app')!;
container.innerHTML = `
  <h1>{{ greeting }}</h1>
  <p>Contador: {{ count }}</p>
  <button @click="increment">+1</button>
  <input bind-value="greeting" />
`;

const engine = new TreeBoundEngine(container, {
  greeting: 'Hola mundo',
  count: 0,
  increment() {
    engine.data['count'] = (engine.data['count'] as number) + 1;
  }
});

Con TreeBoundElement (Custom Elements)

import { TreeBoundElement, define } from '@varbyte/treebound';

@define('user-card')
class UserCard extends TreeBoundElement {
  render() {
    this.templateRoot.innerHTML = `
      <div class="card">
        <h2>{{ user.name }}</h2>
        <p>{{ user.age }} años</p>
        <p *if="user.isAdmin">Administrador</p>
        <input bind-value="user.name" placeholder="Nombre" />
      </div>
    `;
  }

  initialState() {
    return {
      user: { name: 'Ana García', age: 30, isAdmin: true }
    };
  }
}
<user-card></user-card>

Lifecycle de TreeBoundElement

El orden de ejecución al conectar el elemento al DOM es:

render()        → escribe el HTML del template en this.templateRoot
initialState()  → retorna los datos reactivos iniciales (sin acceso a this.engine)
setupBindings() → crea el engine con los datos de initialState()
onConnected()   → hook post-engine: this.engine ya existe aquí

| Hook | Acceso a this.engine | Propósito | |---|---|---| | render() | ❌ | Escribir el HTML | | initialState() | ❌ | Declarar datos iniciales | | onConnected() | ✅ | Registrar eventos, lógica post-render | | onDisconnected() | ✅ | Cleanup | | onAttributeChanged() | ✅ | Reaccionar a cambios de atributos |


Sintaxis de binding

Interpolación de texto

<p>Hola, {{ user.name }}!</p>
<span>{{ count === 0 ? 'vacío' : count + ' items' }}</span>

Expresiones soportadas:

  • Variables y propiedades anidadas: user.name, a.b.c
  • Aritmética: +, -, *, /, %
  • Comparación: ===, !==, ==, !=, <, >, <=, >=
  • Lógica: &&, ||, !
  • Ternario: condición ? rama_verdadera : rama_falsa
  • typeof, paréntesis, null, undefined, true, false

Binding de atributos

<input bind-value="name" />        <!-- two-way binding -->
<div :class="cssClass"></div>       <!-- one-way binding de atributo -->
<button :disabled="isLoading"></button>

Eventos

<button @click="handleClick">Enviar</button>
<input @input="onInput" />

Nota: Los handlers deben existir en el contexto del engine (engine.data).

Directiva *if

<div *if="isVisible">
  Este contenido se muestra u oculta con display:none
</div>

Directiva *for

<!-- Forma básica -->
<ul>
  <li *for="item in items">{{ item.name }}</li>
</ul>

<!-- Con índice -->
<ul>
  <li *for="item, i in items">{{ i + 1 }}. {{ item.name }}</li>
</ul>

Directiva *ref

<input *ref="myInput" type="text" />
onConnected() {
  const input = this.engine!.data['myInput'] as HTMLInputElement;
  input.focus();
}

API

TreeBoundEngine

class TreeBoundEngine {
  constructor(
    root: Node,
    initialData?: Record<string, unknown>,
    config?: TreeBoundConfig
  );

  readonly data: Record<string, unknown>;  // Proxy reactivo

  update(): void;    // Fuerza re-evaluación de todos los bindings
  destroy(): void;   // Cancela suscripciones y limpia bindings

  bindInput(
    element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
    property: string
  ): void;  // Registra two-way binding manualmente
}

TreeBoundElement

abstract class TreeBoundElement extends HTMLElement {
  protected engine?: TreeBoundEngine;

  // Hooks
  protected abstract render(): void;
  protected initialState(): Record<string, unknown>;
  protected onConnected(): void;
  protected onDisconnected(): void;
  protected onAttributeChanged(name, oldValue, newValue): void;

  // Acceso al DOM interno (shadow o light)
  public querySelector<E>(selector: string): E | null;
  public querySelectorAll<E>(selector: string): NodeListOf<E>;
  protected get templateRoot(): ShadowRoot | HTMLElement;

  // Datos reactivos
  get data(): Record<string, unknown>;
  set data(value: Record<string, unknown>);
}

TreeBoundConfig

interface TreeBoundConfig {
  prefix?: string;           // Prefijo de interpolación, por defecto '{{'
  attributePrefix?: string;  // Prefijo de atributos, por defecto 'bind-'
  eventPrefix?: string;      // Prefijo de eventos, por defecto '@'
  directivePrefix?: string;  // Prefijo de directivas, por defecto '*'
}

define(tagName) — Decorador

@define('my-component')
class MyComponent extends TreeBoundElement {
  // ...
}

Equivalente funcional sin decorador:

class MyComponent extends TreeBoundElement { /* ... */ }
customElements.define('my-component', MyComponent);

DirectiveRegistry

import { directives } from '@varbyte/treebound';

directives.register({
  name: 'mi-directiva',
  bind(binding, context) { /* setup */ },
  update(binding, context) { /* re-evaluación */ },
  unbind?(binding) { /* cleanup */ },
});

Shadow DOM vs Light DOM

Por defecto los componentes usan shadow DOM (mode: 'open'). Para usar light DOM:

class MyEl extends TreeBoundElement {
  constructor() {
    super({ shadow: false });
  }
  // ...
}

Con light DOM (shadow: false):

  • Los estilos del host aplican directamente al contenido
  • querySelector y querySelectorAll buscan en el propio elemento

Rendimiento

Benchmarks medidos en Node.js v22 con jsdom en Apple M-series.
Ejecutar localmente: npm run benchmark

Los tiempos en browser real son menores — jsdom es significativamente más lento que los motores nativos de los navegadores.

Inicialización del engine

Tiempo en crear el engine, hacer el walk del DOM y registrar todos los bindings:

| Bindings | ops/seg | media | p95 | |---|---|---|---| | 10 | 11,258 | 88 µs | 100 µs | | 50 | 2,367 | 422 µs | 545 µs | | 100 | 1,113 | 898 µs | 1.14 ms | | 200 | 588 | 1.70 ms | 1.89 ms |

Actualización reactiva (dato → DOM)

Tiempo desde engine.data['x'] = valor hasta que el DOM refleja el cambio:

| Escenario | ops/seg | media | p95 | |---|---|---|---| | 1 binding — escalar simple | 432,420 | 2.3 µs | 2.4 µs | | 1 binding — propiedad anidada (user.name) | 408,981 | 2.4 µs | 2.6 µs | | 10 bindings dependientes de una variable | 50,115 | 20 µs | 20 µs | | 50 bindings dependientes de una variable | 10,015 | 99 µs | 104 µs |

Parser de expresiones

| Escenario | ops/seg | media | |---|---|---| | Evaluación con caché (hit) | 540,854 | 1.8 µs | | Evaluación sin caché (miss) | 281,764 | 3.5 µs |

El caché del parser persiste durante la vida del engine. En uso normal todas las expresiones se cachean tras la primera evaluación.

Escalabilidad del walk

| Tamaño DOM | Bindings | ops/seg | media | Bindings/ms | |---|---|---|---|---| | 50 elementos | 100 | 738 | 1.35 ms | 74 | | 200 elementos | 400 | 103 | 9.70 ms | 41 | | 500 elementos | 1,000 | 22 | 46 ms | 22 | | 1,000 elementos | 2,000 | 6 | 154 ms | 13 |

Nota de diseño: TreeBound está optimizado para componentes con menos de 100 bindings — el caso habitual de un Custom Element. Para listas largas, delega el renderizado a *for que evalúa cada ítem en contexto local sin pasar por el engine global.


npm test              # Ejecutar tests
npm run test:coverage # Tests con reporte de cobertura

Cobertura actual:

| Métrica | Cobertura | |---|---| | Statements | 96.74% | | Branches | 89.76% | | Functions | 96.07% | | Lines | 97.07% |


Build

npm run build          # Compila a dist/
npm run playground     # Servidor de desarrollo con ejemplos interactivos

Compatibilidad

Requiere un entorno que soporte:

  • Custom Elements v1
  • Proxy
  • document.createTreeWalker
  • ES2020+

Compatible con todos los navegadores modernos. No compatible con IE11.


Licencia

MIT © VarByte