Skip to content

magnumjs/micro-ui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“© Micro UI - Reactive Component System

Docs: GitHub Pages npm version Build Status npm package minimized gzipped size npm downloads License: MIT Known Vulnerabilities

A minimalist reactive component library with support for state, props, named slots, refs, DOM diffing, shared context, and declarative event bindings.

✨ Features

  • createComponent() with internal state, setState, and refs
  • Props are read-only (passed in by parent)
  • Declarative rendering using template strings or render functions
  • Named and default slots (<slot name="..."> and data-slot="..." support)
  • Support for this.refs and lazy this.ref(name) inside components
  • Lifecycle hooks: onMount, onUnmount, onBeforeMount, onBeforeUnmount, onUpdate
  • Diffing DOM updates for performance with data-key="..."
  • Keyed list rendering with renderList() for efficient updates
  • DOM caching when render() returns null
  • Declarative event binding via on option (e.g. "click .btn") and data-action
  • Arguments support via data-args for cleaner templates
  • Built-in context pub/sub with shared() stores
  • Full unit test coverage

πŸš€ Getting Started

npm i @magnumjs/micro-ui

πŸ“„ PDF Guides

import { createComponent } from "@magnumjs/micro-ui";

const ClickCounter = createComponent({
  state: {
    count: 0,
  },
  render() {
    this.handleClick = () => {
      this.setState({ count: ++this.state.count });
    };

    return `
      <button data-action-click="handleClick">
        Count: ${this.state.count}
      </button>`;
  },
});

ClickCounter.mount("#app");

JSBin

🧩 Composability for Components

const Parent = createComponent(
  ({ props }) => `
  <div>
    <slot></slot> <!-- Will auto-map to props.children.default -->
  </div>
`
);

const Child = createComponent(() => `<p>Hello</p>`);

Parent.mount({ children: Child });

JSBin

πŸ“‘ Global Shared State with shared()

Create a shared state store with event-based updates:

import { shared } from "@magnumjs/micro-ui/utils";

const auth = shared("auth", { user: null });

auth.subscribe(console.log); // Logs current and future state

auth.emit("login", { user: "Tova" }); // auto-merges into state

You can on(event, fn) to subscribe to specific events (e.g. "login", "logout").

auth.on("logout", () => console.log("logged out"));
auth.emit("logout", { user: null });

⚑ Inline Actions with data-action and data-args

You can declaratively bind handlers in your template:

const Demo = createComponent(
  () => `
  <button data-action="sayHello" data-args='{"name":"Tova"}'>Hi</button>
`,
  {
    on: {
      "click:sayHello"({ args }) {
        alert(`Hello, ${args[0]}!`);
      },
    },
  }
);

🧬 Component Example

const MyCard = createComponent(
  ({ props: { title = "", children } }) => `
  <div class="card">
    <header data-slot="header">${title}</header>
    <main><slot></slot></main>
    <footer data-slot="footer">Default Footer</footer>
  </div>
`
);

βœ… Mounting & Updating

MyCard.mount("#demo");
MyCard.update({
  children: {
    default: "<p>Hello world!</p>",
    footer: "<p>Custom footer here</p>",
  },
});

πŸ” Internal State (DX)

Each component automatically has this.state and this.setState. Usage:

const Counter = createComponent(
  function () {
    const count = this.state.count ?? 0;

    return `<button>Count: ${count}</button>`;
  },
  {
    onMount() {
      this.setState({ count: 0 });
    },
    on: {
      "click button"(e) {
        this.setState((s) => ({ count: s.count + 1 }));
      },
    },
  }
);

πŸ”Œ Slots with Fallbacks

Named slots work with both <slot name="x"> and <div data-slot="x">.

const Card = createComponent(
  () => `
  <section>
    <header data-slot="title">Default Title</header>
    <main><slot></slot></main>
    <footer data-slot="footer">Default Footer</footer>
  </section>
`
);

πŸ” Lifecycle Hooks

createComponent(() => "<p>Lifecycle</p>", {
  onBeforeMount() {
    // Called before initial mount (async supported)
  },
  onMount() {
    // Called after initial mount
  },
  onUpdate(prevProps) {
    // Called after update render
  },
  onBeforeUnmount(next) {
    // Delay unmount with callback or Promise or just sync
    setTimeout(() => next(), 100);
  },
  onUnmount() {
    // Final cleanup logic
  },
});

πŸ“– Core API Docs

🧱 API

createComponent(renderFn, options)

// Object instance style: createComponent returns a component instance
const Comp = createComponent({
  render({ state }) {
    return state.show ? `
      <div data-ref="container">
        <span>${state.count}</span>
        <button data-ref="inc">+</button>
      </div>
    ` : null;
  },
  state: { count: 0, show: true },
  on: {
    "click [data-ref='inc']": ({ setState, state }) => {
      setState({ count: state.count + 1 });
    }
  },
  // Add lifecycle handlers directly on the instance
  onMount() {
    console.log("Mounted!");
  },
  onUpdate(prevProps) {
    console.log("Updated!");
  },
  onUnmount() {
    console.log("Unmounted!");
  }
});

// Instance can be called in literals, with toString override:
const html = `<section>${Comp({ show: true })}</section>`;

// Inline actions with data-action-event:
const Demo = createComponent({
  render() {
    return `<button data-action-click="sayHello" data-name="${}">Say Hi</button>`;
  },
  sayHello() {
    alert("Hello!");
  }
});

// Hooks: effect, state, context ..
// Compose your own hooks, e.g. useFetch
import { useEffect, useState, useContext } from "@magnumjs/micro-ui/hooks";

function useFetch(url) {
  const data = useState(null);
  useEffect(() => {
    fetch(url).then(res => res.json()).then(data.set);
  }, [url]);
  return data;
}

Instance Methods

  • Comp.mount(target) β€” Mount to target container
  • Comp.update(nextProps) β€” Update props and re-render
  • Comp.setState(nextState) β€” Trigger state update
  • Comp.unmount() β€” Cleanly unmount component
  • Comp.renderFn() β€” Returns the original component as String

Comp.el

Auto-populated with the Parent Node after mount.

Comp.refs

Auto-populated with [data-ref="name"] nodes after mount.

Comp.ref(name)

Lazy accessor for a single ref. Returns null after unmount.

Comp.props

Auto-populated with props from Comp.update(nextProps) before each render.

Comp.state

Auto-populated with state after setState.

DOM Caching on null

If render() returns null, the previous DOM is cached and restored if render() returns content again.

πŸ” renderList(array, renderFn, keyFn?)

Renders keyed list efficiently:

import { renderList } from "@magnumjs/micro-ui/utils";

renderList(
  data,
  (item) => `<li>${item.label}</li>`,
  (item) => item.id
);

Auto-wraps each root tag with data-key for DOM diffing.

πŸ§ͺ Testing

βœ… Example Test Case

const Counter = createComponent(
  ({ state, setState }) => {
    return `
    <button data-ref="btn">${state.count}</button>
  `;
  },
  {
    state: { count: 0 },
    on: {
      "click [data-ref='btn']": ({ state, setState }) => {
        setState({ count: state.count + 1 });
      },
    },
  }
);

Mount and assert changes after click.


🀝 Contributing

Pull requests are welcome!


Built with ❀️ by developers who love simplicity.

About

A tiny reactive component system with declarative slots, keyed lists, reactive props, and DOM diffing.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •