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

zef-components

v2.0.7

Published

A lightweight library for creating reactive web components with state management, declarative templates, and seamless store integration.

Readme

Zef-components

A lightweight library for creating reactive web components with state management, declarative templates, and seamless store integration. Built on Zustand and Immer for optimal state management.

Think of it as "React-at-home" - if your home is shack in the middle of the forest - or "a localized AlpineJS" that won't fully parse your html.

Features

  • 🔧 Declarative Templates - Use Mustache-like syntax for dynamic content
  • Reactive State - Re-render parts of HTML when state changes
  • 🔄 State Management - Global Zustand-powered store integration with Immer
  • 🎯 Event Handling - Declarative event listener setup
  • 🔁 Loop and Conditional Rendering - Built-in for-loop and if directives
  • 🎨 Shadow DOM - Optional shadow DOM support for style encapsulation
  • 🏷️ TypeScript - Fully typed with comprehensive type definitions and autocomplete
  • 📦 Bind external objects - Bind data source from attributes or global store to your component.
  • 🧩 JSON Support - Serialize/deserialize objects in templates and attributes

Installation

npm install zef-components

Dependencies

Zef-components depends on:

  • Zustand (^5.0.8) for state management
  • Immer (^10.1.3) for immutable state updates

Usage Guide

Basic Component

Define your component:

const counterHTML = `
  <div>
    <h2 re-render="count">Count: {{count}}</h2>
    <button id="inc">Increment</button>
  </div>
`;

Factory("my-counter", counterHTML, {
  state: { count: 0 },
  eventListener: {
    "#inc": [
      {
        event: "click",
        handler() {
          this.state.count++;
        },
      },
    ],
  },
});

You can use all the HTML tags inside your components, including <link> <style> and <svg> !

Build your bundle with esbuild and import it in your html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script type="module" src="./index.js"></script>
  </head>
  <body>
    <my-counter></my-counter>
    ....
  </body>
</html>

Template Syntax

Variable Interpolation

Reference any properties of the component's state or store using Mustache syntax:

{{propertyName}} {{property.nestedProperty}}

Values are rendered as follows:

  • For primitive values: state.propertyName?.toString()
  • For functions: state.propertyName?.call()?.toString()

JSON Serialization

Serialize objects in templates using the json:: prefix:

<div>
  <p>User data: {{json::user}}</p>
  <p>Config: {{json::config}}</p>
</div>

Ternary Expressions

Use ternary operators for conditional rendering:

<div style="display:{{open?:'none'}};"></div>

Notes:

  • True/false values must be strings delimited by ', ", or backticks
  • Both expressions are optional (default to empty strings)
  • Quotes are removed during rendering

Re-render Directive

Specify dependencies for reactive updates:

<div re-render="date,person.age"></div>

This element will re-render when:

  • The modified property path starts with a dependency
  • A dependency starts with the modified property path

If Directive

<div if="open">
  <p>Show content if `open` is true</p>
</div>

The content of the div will only be rendered if open is evaluated to be true. The container will still be present in your html no matter what, but will inherit from a display:none style attribute if the condition is false.

Loop Directive

Generate HTML through iteration:

<div for-loop="item in collection">{{item.name}}</div>
<div for-loop="id,item of collection">{{item.name}}</div>

Supported collection types:

  • Numbers: iterates from 0 to collection-1
  • Arrays: iterates through each item

Elements of you colletion will be alliased as the name you provided (here item), you can also access indexes.

Note: The content of the div will be cloned, you do not need to add any re-render directive as the library handles that for you. A for-loop block will be fully re-rendered everytimes the 'array' changes.

Reactive Props

Component attributes can automatically sync with state:

const userCardHTML = `
  <div>
    <h2>{{userName}}</h2>
    <p>Age: {{userAge}}</p>
    <p>Hobbies: {{userHobbies.name}}</p>
  </div>
`;

Factory("user-card", userCardHTML, {
  props: {
    userName: "name", // Maps to 'name' attribute
    userAge: "age", // Maps to 'age' attribute
    userHobbies: "json::hobbies", // JSON-parsed attribute
  },
});
/*in your html html*/

The props will be added to this.state as a function that return its current value.

Usage in HTML:

<user-card name="john" age="27" hobbies="{name:'soccer'}"></user-card>;

Notice that the hobbies attribute use ' as quote (which is not the way the default JSON.stringify function works). The library exports two functions that will help you deal with this json formatting :

export declare function parseJSON(json: string | null): any;
export declare function marshallJSON(obj: any): string;

Query Selection

Use these methods to query elements within your component:

  • this.$(css) - Alias for querySelector
  • this.$$(css) - Alias for querySelectorAll

These methods are available in all component methods including lifecycle hooks and event handlers.

Lifecycle Callbacks

Hook into component lifecycle events:

Factory("my-component", html, {
  onMount() {
    // Fired after component is inserted into DOM
    // If you need attribute to initialize your
    // component's state this is where you do it.
  },
  onUnmount() {
    // Fired when component is removed from DOM
  },
  onRender(path) {
    // Fired after re-rendering, with the property path that triggered it
    // Warning: Can cause infinite loops if not handled carefully
  },
  onAttributeChanged(name, oldValue, newValue) {
    // Fired when observed attributes change
  },
  observedAttributes: ["data-value"], // Required for attribute change detection
});

Input Value Interface

Components implement an HTMLInput-like interface:

Factory("wc-test", html, {
  state: { count: 15 },
  value() {
    return this.state.count; // Updates component value on state changes
  },
});

The input event is only triggered when the computed value changes.

Shadow DOM and Styling

Components use Shadow DOM by default for style encapsulation:

Factory("my-component", html, {
  noShadowRoot: false, // Default, enables Shadow DOM
});

Factory("inline-component", html, {
  noShadowRoot: true, // Disables Shadow DOM
});

When using Shadow DOM:

  • Styles are encapsulated within the component
  • Use <slot> elements to project light DOM content see Using templates and slots.
  • Set noShadowRoot: true for small reactive HTML blocks

Event Handling

Declare event listeners in options or programmatically:

// Option-based declaration
Factory("my-component", html, {
  eventListener: {
    ".btn": [
      {
        event: "click",
        handler() {
          /* ... */
        },
        options: { once: true },
      },
    ],
  },
});

// Programmatic registration
this.$$on(".btn", {
  event: "click",
  handler() {
    /* ... */
  },
});

// Deregistration
this.$$off(".btn", "click");

Note: Because a re-render will replace the element outer HTML, any event listener attached with the vanilla addEventListener will be lost, please use these method to handle event inside the component root.

Global Store Integration

Access and manage global state with Zustand and Immer:

// External store access
import { globalStore } from "zef-components";
globalStore.setState({ user: { name: "John" } });

const html = `
<h1>{{localUser.name}}</h1>
`;

// Component store binding
Factory("user-display", html, {
  storeListener: {
    localUser: "user", // Maps store.user to component state
  },
  onMount() {
    // Internal store access with Immer
    this.setStore((store) => {
      store.user.name = "Jane"; // Immutable update with Immer
    });
    // Programmatic subscription
    const id = this.subStore("localUser", (s) => s.user);

    // Programmatic unsubscription
    this.unsubStore(id);
  },
});

The store item will be added to this.state as a function that returns it's current value.

API Reference

Factory Options (FactoryOption<T, K, L>)

| Option | Type | Description | | -------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------- | | observedAttributes | string[] | Array of attributes to observe for changes | | value | (this: WebComponent<T, K, L>) => any | Function that returns the component's value | | state | T \| ((this: WebComponent<T, K, L>) => T) | Component state object or factory function | | props | L | Prop binding configuration | | onMount | (this: WebComponent<T, K>) => void | Lifecycle hook called when component mounts | | onUnmount | (this: WebComponent<T, K, L>) => void | Lifecycle hook called when component unmounts | | onRender | (this: WebComponent<T, K, L>, path: string) => void | Hook called after re-rendering | | onAttributeChanged | (this: WebComponent<T, K, L>, name: string, oldValue: string, newValue: string) => void | Called when observed attributes change | | eventListener | EventListenerRecord<T, K, L> | Event listener configuration | | storeListener | K | Store binding configuration | | noShadowRoot | boolean | Disable shadow DOM encapsulation |

Component Instance (WebComponent<T, K, L>)

This object inherits from all the methods of HTMLElement, including remove(), getAttribute()

| Property/Method | Type Signature | Description | | ---------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------ | | state | T & Record<keyof K, () => any> & Record<keyof L, () => string \| object> | Reactive state object | | value | any | Component's current value | | root | HTMLElement \| ShadowRoot | Root element (shadow root or element itself) | | getStore() | () => GlobalStore | Returns current global store state | | setStore(updater) | (updater: (store: GlobalStore) => any) => void | Updates store using Immer producer function | | subStore(key, subscriber) | (key: string, subscriber: (store: GlobalStore) => any) => number | Subscribe to store changes | | unsubStore(index) | (index: number) => void | Unsubscribe from store changes | | $(css) | (css: string) => Element \| null | Query single element | | $$(css) | (css: string) => NodeListOf<Element> | Query multiple elements | | $$on(css, eventHandle) | (css: string, ev: EventHandle<T, K, L>) => void | Register event listener | | $$off(css, event) | (css: string, event: string) => void | Remove event listener | | reRenderProperty(pathname) | (pathname: string) => void | Manually trigger re-render for specific property |

Developer Experience

Building Components

You can use this esbuild config:

// esbuild.config.mjs
import { build, context } from "esbuild";
const arg = process.argv.at(-1);

const config = {
  entryPoints: ["src/index.ts"],
  bundle: true,
  outdir: "dist",
  minify: true,
};

if (arg == "dev") {
  const ctx = await context({
    ...config,
    minify: false,
  });
  await ctx.watch();
} else {
  await build({
    ...config,
  });
}

Usage:

node esbuild.config.mjs
# or dev mode :
node esbuild.config.mjs dev

Recipes

Inter-components communication

The library gives you 3 solution to exchange data between components :

  • Passing data through attribute/props :

Parent component can pass data as attribute to it's child, however note that the following:

<child-component data="{{json:data}}" re-render="{{data}}"></child-component>

Will fully re-render the component each time the data object change. To prevent this, remove the re-render directive (the component will then be initialized with data={{json:data}} but not re-render on change) and interact with the child using :

//in a event listerner or a lifecycle hook of your factory option
this.$("child-component").setAttribute("data", marshallJSON(this.state.data));

The child component can the listen to the data props using :

Factory("child-component",html, {
  ...,
  props : {
    data : "json:data" //parse data from a json attribute
  }
})
  • Input-like interaction

If you only need to get data from the child to the parent you can mobilize the input like beahavior of components by binding the component's value to it's internal state.

Factory("child-component",html, {
  state : {
    name : "John"
  }
  value(){
    return {userName : "John"}
  }
})

Every state change that'll modify value will also dispatch an input event on the component that its parent can listen to.

Factory("parent-component",html, {
  ...,
  eventListener : {
    "child-component" : [
      {
        event : "input",
        handler(ev){
          const {userName} = (ev.target as HTMLInputElement).value
          this.state.userName = userName
        }
      }
    ]
  }
})
  • Global store

The less subtile but most powerfull methods is to use the global store to exchange data no matter they relative hierarchy. You can have several components depending on the same store items but be carefull as this can lead to funky behavior if not handled properly.

Working with children

If you have enabled shadow root (the default), all the "light DOM" children of your component will be casted in any unnamed <slot> tag of your HTML.

You can use named slots as well, see Using templates and slots.

If you want to listen to children you'll need to set up a MutationObserver like so.


Factory("hifi-spectrum", graphHtml, {
  ....,
  onMount() {
    const callback = () => {
      /*updating logic*/
    };
    const observer = new MutationObserver(callback);
    observer.observe(this, { childList: true });
    /*call the callback at least once for initialization*/
    callback();
  },
  ....
});

Also know that when using shadow root :

  • this.$ and this.$$ will query the shadow DOM of the component (meaning the html you provided)
  • this.querySelector and this.querySelectorAll will query the light DOM of the component (which includes childs).
  • this.querySelectorAll([slot=header]) will get all childs placed in the header named slot.

Editor Support

Syntax Highlighting

In VSCode, you can edit your settings to have a shortcut toggling on and off the HTML mode:

// keybindings.json
[
  {
    "key": "ctrl+shift+h",
    "command": "workbench.action.editor.changeLanguageMode",
    "args": "html",
    "when": "editorLangId != 'html'"
  },
  {
    "key": "ctrl+shift+h",
    "command": "workbench.action.editor.changeLanguageMode",
    "args": "ts",
    "when": "editorLangId == 'html'"
  }
]

Alternatively, use the es6-string-html extension which highlights HTML strings when prefixed with /*html*/.

TypeScript Support

All components are fully typed. For optimal TypeScript support:

  1. Define interfaces for your component state
  2. Use generics when calling the Factory function
  3. Type your event handlers and lifecycle methods

Example:

interface MyState {
  count: number;
  items: string[];
}

const MyComponent = Factory<MyState>("my-component", html, {
  state: { count: 0, items: [] },
  props: { index: "index" },
  onMount() {
    // TypeScript knows this.state is contains props and store variables
    this.state.count; //(property) count: number
    this.state.index; //(property) index: () => string | object
  },
});

Performance Tips

  1. Be specific with re-render directives to minimize DOM updates
  2. For complex components, consider breaking them into smaller sub-components
  3. Use store listeners only for data that actually needs to be reactive
  4. Avoid deep nesting in state objects for better performance