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

@schematize/ui.js

v0.3.6

Published

ui library

Downloads

314

Readme

@schematize/ui.js

Schematize UI is a collection of functions for creating UI. (I hesitate to even call it a library...)

It is:

  • reactive
  • small
  • fast
  • easy-to-use
  • intuitive
  • cool
  • ...insert any other adjective describe today's UI libraries... ;)

But WHY? Because:

  • I generally dislike JSX
    • < and > everywhere...but wait, it's not actually html or compatible with html
    • oh, and it's not syntactically js compatible either...need another interpreter/parser/compiler to convert to js
    • oh, and it's also also not sytactically css compatible
  • I like my SPAs to feel like an application...
    • just work with code, not combination of css, html, and js in one file...keep them separate, etc
  • Debugging has always been near impossible in other frameworks to me...debugging a black box feels hard/frustrating/unnecessary.
    • debugging plain js is so much easier...breakpoints actually work, etc.
  • I'm frustrated and confused at all of the new terminology invented by other frameworks
    • controlled/uncontrolled components?
    • hooks?
    • useEffect?
    • to be fair...names I use in this library are probably confusing too... 🙄
  • I was inspired by a svelte presentation about reactivity multiple years ago
  • Virtual DOM felt weird and unneccessary to me...
    • why not just update what needs to be updated?
    • do you really need a copy of a dom that you compare against constantly to know if something needs to change?
  • There are already established patterns for event dispatching and listening...EventTarget...Can't we make use of those patterns for communicating changes or updates for reactivity?
  • I wanted a faster framework...
    • Which this certainly is...potentally...somewhat...faster...💨
  • Plain JS almost always is faster and easier to maintain or upgrade
    • When was the last time a library version was deprecated? Okay, now think about when the last time was that a JS or DOM feature was deprecated?
  • TypeScript is okay, but I prefer just plain JavaScript whenever possible.
  • All of the compiling/transpiling from one form to another hides what is going on underneath and that's frustrating to me
    • I don't want to have to compile it for it to work...can't I just copy and paste it into a browser and have it work?
  • I like having control and visibility into how a framework works
  • Browser APIs are mostly enough...why are we wrapping browser APIs into frameworks to hide the browser APIs?
  • Other frameworks feel mostly like hype
    • And this one isn't? 😜
  • I really like the angularJS version 1 way of dealing with "scopes" and inheritance of scopes
    • Object prototype inheritance
    • state accessible easily everywhere through property access
  • I like reusibilty and components...keep that same concept...but make them easier to understand in terms of parameters, "props", flexibility, etc.
  • In React or Web Components passing props as strings or primitives is not enough...I want to pass down or up any type or even references.
  • I want my state to be referenced, not copied. When I make a change to an object, I don't want to have to also copy that object to all of my other states
  • state management frameworks shouldn't be a thing...firing an event to update your central state across your app requires a ton of coordination...
  • It was fun to re-build my own library and re-think how I might do it starting from scratch if it were my choice

Install

npm install @schematize/ui.js
import {
  // ...functions
} from '@schematize/ui.js';

Hello World

Let's get started with a quick example.

Your index.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Schematize UI Test</title>
  </head>
  <body>
    <script type="importmap">
      {
        "imports": {
          "@schematize/ui.js": "../../src/index.mjs",
          "@schematize/ui.js/": "../../src/",
          "@schematize/refs": "../../node_modules/@schematize/refs/main.mjs",
          "@schematize/refs/": "../../node_modules/@schematize/refs/",
          "@schematize/instance.js": "../../node_modules/@schematize/instance.js/src/Instance.mjs",
          "@schematize/instance.js/": "../../node_modules/@schematize/instance.js/"
        }
      }
    </script>
    <script src="./app.mjs" type="module" ></script>
  </body>
</html>

Your app.mjs file:

import {
  // less typing...
  create as c,
} from '@schematize/ui.js';

// create a section element
const app = c(`section`, null, {}, (el) => [
  // create an h1 element as a child of the section element
  // - whose parent is the section we just created (indicated by the second parameter)
  c(`h1`, el, {}, () => [
    // create a text node as a child of the h1
    // - with a data value of 'Hello World!'
    'Hello World!'
  ])
]);

// append the element to the body
document.body.append(app);

As you can see, all this does is create a section that has a h1 displaying 'Hello World!'.

(See ./examples/hello-world)

Core Concepts

Schematize UI.js is built around several key concepts:

1. Reactive Data Scopes

Every element has a data scope (element._) that can contain reactive data. Scopes inherit from their parent elements, creating a hierarchical data structure.

2. Proxy-based or Object-based Reactivity

Reactivity can be achieved either by using proxies or by using explicit set/deleteProperty/etc.
Data objects are wrapped in proxies that automatically trigger updates when properties change, enabling reactive UI updates.

3. Functional Element Creation

Elements are created using functions that return arrays of child elements, making the code declarative and easy to reason about.

4. Hierarchical Data Scopes

Every element has a data scope (element._) that can contain reactive data. Scopes inherit from their parent elements, creating a hierarchical data structure with prototypal inheritance.

API Reference

See API examples in action

We've provided a way to see the examples in this API reference in action. To follow the examples along:

  1. Start a local server from the root directory (same level as this README.md file)
    • You can run npm serve or
    • Run npx http-server -p 3000
  2. Run the example referenced html file in each of the sections below in a modern browser that supports ES modules.

Element Creation and DOM Manipulation

create(tagName, parent, attributes, children, scope)

Creates a new DOM element with attributes, children, and a data scope.

Parameters:

  • tagName (string): The HTML tag name (e.g., 'div', 'span', 'button')
  • parent (Element|null): The parent element (null for root elements)
  • attributes (Object): HTML attributes and properties
  • children (Function|Array): Child elements (function receives the created element)
  • scope (Object): Data scope for the element

Example:

const button = create('button', null, {
  type: 'button',
  className: 'btn btn-primary',
  onclick: () => console.log('clicked!')
}, (el) => [
  'Click me!'
]);

createNS(namespace, tagName, parent, attributes, children, scope)

Creates an element with a specific namespace (useful for SVG, MathML, etc.).

createSVG(tagName, parent, attributes, children, scope)

Convenience function for creating SVG elements (uses SVG namespace).

creator(tagName)

Creates a reusable element creator function for a specific tag name. This is useful for creating cleaner, more readable code when building complex UIs.

Parameters:

  • tagName (string): The HTML tag name (e.g., 'div', 'span', 'button')

Returns: A function that creates elements of the specified tag type

Example:

// Create element creators
const div = creator('div');
const button = creator('button');
const input = creator('input');

// Use creators to build UI
const myDiv = 
div(null, { className: 'my-class' }, (el) => [
  button(el, {
    style: `
      background-color: red;
      color: white;
      padding: 10px 20px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      font-size: 14px;
      transition: background-color 0.3s;
    `,
    onclick: () => console.log('clicked'),
  }, () => ['Click me']),
  input(el, {
    type: 'text',
    placeholder: 'Enter your name',
  }, () => ['Enter your name'])
]);

Try it live: http://localhost:3000/examples/creator.html

creatorNS(namespace, tagName)

Creates a reusable element creator function for a specific namespace and tag name.

Parameters:

  • namespace (string): The XML namespace (e.g., 'http://www.w3.org/2000/svg')
  • tagName (string): The tag name within the namespace

Returns: A function that creates namespaced elements

creatorSVG(tagName)

Convenience function for creating SVG element creators (uses SVG namespace).

Parameters:

  • tagName (string): The SVG tag name (e.g., 'circle', 'rect', 'path')

Returns: A function that creates SVG elements

Example:

// Create SVG element creators
const svg = creatorSVG('svg');
const circle = creatorSVG('circle');
const rect = creatorSVG('rect');
const line = creatorSVG('line');
const text = creatorSVG('text');

// Build SVG with creators
const mySvg = 
svg(null, { width: 200, height: 200, style: 'border: 1px solid #ddd;' }, (svgEl) => [
  circle(svgEl, { cx: 100, cy: 100, r: 80, fill: '#007bff', opacity: 0.7 }),
  rect(svgEl, { x: 50, y: 50, width: 100, height: 100, fill: '#28a745', opacity: 0.7 }),
  line(svgEl, { x1: 0, y1: 0, x2: 200, y2: 200, stroke: '#dc3545', strokeWidth: 3 }),
  text(svgEl, { x: 100, y: 120, textAnchor: 'middle', fill: 'white', fontSize: 16 }, () => ['SVG'])
]);

createText(text, parent, fn, scope)

Creates a text node with scope assignment and optional callback function. This is useful for creating reactive text content that can be updated when data changes.

Parameters:

  • text (string): The initial text content
  • parent (Element): The parent element (for scope inheritance)
  • fn (Function): Optional callback function that receives the created text node
  • scope (Object): Optional data scope for the text node

Example:

// Basic usage - create text as a child of an element
const paragraph = create('p', null, {}, (el) => [
  createText('Hello, ', el),  // Static text
  createText('World!', el)    // More static text
]);

// Reactive text that updates when data changes
const data = createProxyObject({ name: 'John', count: 0 });

const reactiveParagraph = create('p', null, {}, (el) => [
  createText('Hello, ', el),
  createText(data.name, el, (textNode) => {
    // Listen for name changes and update the text content
    listen(data, ['name'], (newName) => {
      textNode.data = newName;
    });
  }),
  createText('! You have ', el),
  createText(data.count.toString(), el, (textNode) => {
    // Listen for count changes and update the text content
    listen(data, ['count'], (newCount) => {
      textNode.data = newCount.toString();
    });
  }),
  createText(' items.', el)
]);

// Update data to see reactive text changes
data.name = 'Jane';     // Text updates to "Hello, Jane!"
data.count = 5;         // Text updates to "You have 5 items"

Advanced Example with Dynamic Content:

// Create a status message that updates based on multiple data properties
const statusData = createProxyObject({ 
  isOnline: true, 
  lastSeen: new Date(), 
  messageCount: 3 
});

const statusMessage = create('div', null, { className: 'status' }, (el) => [
  createText('', el, (textNode) => {
    // Function to update the status text
    const updateStatus = () => {
      if (statusData.isOnline) {
        textNode.data = `🟢 Online - ${statusData.messageCount} new messages`;
        el.className = 'status online';
      } else {
        const timeAgo = Math.floor((Date.now() - statusData.lastSeen.getTime()) / 1000);
        textNode.data = `🔴 Last seen ${timeAgo} seconds ago`;
        el.className = 'status offline';
      }
    };
    
    // Listen for changes to any status property
    listen(statusData, ['isOnline'], updateStatus);
    listen(statusData, ['messageCount'], updateStatus);
    listen(statusData, ['lastSeen'], updateStatus);
    
    // Initial update
    updateStatus();
  })
]);

// Update status data
statusData.isOnline = false;           // Updates to "Last seen X seconds ago"
statusData.messageCount = 7;           // Updates to "Online - 7 new messages" (if online)
statusData.lastSeen = new Date();      // Updates timestamp
setInterval(() => {
  statusData.lastSeen = new Date();
}, 1000);

Try it live: http://localhost:3000/examples/createText.html

style(styles)

Converts an object of CSS properties into a CSS string that can be used as the value for the style attribute in the create function's attributes parameter.

Parameters:

  • styles (Object): Object containing CSS property-value pairs

Returns: A CSS string that can be used as the style attribute value

Example:

// Basic usage - convert object to CSS string
const buttonStyles = style({
  'background-color': 'black',
  color: 'white',
  padding: '10px 20px',
  border: 'none',
  'border-radius': '8px',
  cursor: 'pointer',
  'font-size': '16px',
  'font-weight': 'bold'
});
console.log(buttonStyles);
// Result: "background-color:blue;color:white;padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-size:16px;font-weight:bold;"

// Use with create function
const basicButton = create('button', null, {
  style: buttonStyles
}, () => ['Click me']);

// Reusable style configuration
const baseStyle = {
  padding: '12px 24px',
  'border-radius': '6px',
  border: 'none',
  cursor: 'pointer',
  'font-size': '16px',
  'font-weight': '500'
};
const styles = {
  primary: style({
    ...baseStyle,
    'background-color': '#007bff',
    color: 'white',
  }),
  secondary: style({
    ...baseStyle,
    'background-color': '#6c757d',
    color: 'white',
  }),
  success: style({
    ...baseStyle,
    'background-color': '#28a745',
    color: 'white',
  }),
  warning: style({
    ...baseStyle,
    'background-color': '#ffc107',
    color: '#212529',
  }),
  danger: style({
    ...baseStyle,
    'background-color': '#dc3545',
    color: 'white',
  })
};
const styleDemo = create('div', null, { className: 'style-demo' }, (el) => [
  create('button', el, { style: styles.primary }, () => ['Primary']),
  create('button', el, { style: styles.secondary }, () => ['Secondary']),
  create('button', el, { style: styles.success }, () => ['Success']),
  create('button', el, { style: styles.warning }, () => ['Warning']),
  create('button', el, { style: styles.danger }, () => ['Danger'])
]);

Advanced Example with Dynamic Styling:

// Dynamic styling with reactive data
const data = createProxyObject({ 
  theme: 'light',
  size: 'medium',
  isActive: false
});
const dynamicButton = create('button', null, {}, ['Dynamic Button']);
const updateStyle = () => {
  dynamicButton.setAttribute('style', style({
    transition: 'all 0.3s ease',
    'background-color': data.theme === 'light' ? '#ffffff' : '#333333',
    color: data.theme === 'light' ? '#000000' : '#ffffff',
    padding: data.size === 'small' ? '5px 10px' : 
            data.size === 'large' ? '15px 30px' : '10px 20px',
    opacity: data.isActive ? '1' : '0.7',
    transform: data.isActive ? 'scale(1.05)' : 'scale(1)',
    'border-radius': data.size === 'small' ? '6px' : '12px'
  }));
};
listen(data, ['theme'], updateStyle);
listen(data, ['size'], updateStyle);
listen(data, ['isActive'], updateStyle);
updateStyle();

Try it live: http://localhost:3000/examples/style.html

conditional(element, inits)

Creates a conditional element that can be shown or hidden based on a condition.

Parameters:

  • element (Element): The element to conditionally show/hide
  • inits (Array): Array of initialization functions that set up listeners

Returns: The element with conditional behavior

toggle(element1, element2, condition, inits)

Toggles between two elements based on a condition.

Parameters:

  • element1 (Element): First element to show when condition is true
  • element2 (Element): Second element to show when condition is false
  • condition (Function|boolean): Condition function or boolean value
  • inits (Array): Array of initialization functions

onBecameVisible(element, callback)

Uses the Intersection Observer API to call a callback when an element becomes visible in the viewport.

Parameters:

  • element: The DOM element to observe
  • callback (Function): Function to call when the element becomes visible (receives the intersection entry)

Example:

// Lazy load an image when it becomes visible
const image = create('img', null, { src: 'placeholder.jpg' }, () => []);
onBecameVisible(image, (entry) => {
  image.src = 'actual-image.jpg';
});

// Animate elements when they come into view
const animatedDiv = create('div', null, { className: 'fade-in' }, () => ['Animated content']);
onBecameVisible(animatedDiv, (entry) => {
  animatedDiv.style.opacity = '1';
  animatedDiv.style.transform = 'translateY(0)';
});

Data Binding and Reactivity

listen(object, properties, callback, fireImmediately)

Listens for changes to object properties and calls a callback when they change.

Parameters:

  • object: The object to watch
  • properties (Array): Array of property names to watch. Please note this is the "path" to listen to. For example, if you want to listen for the "article.author.name" (the article's author's name) you would specify ['article', 'author', 'name'].
  • callback (Function): Function to call when properties change
  • fireImmediately (boolean): Whether to call callback immediately

Example:

// Create user data
const userData = createProxyObject({
  firstName: 'Jane',
  lastName: 'Doe',
  fullName: 'Jane Doe',
  email: '[email protected]',
  age: 25,
  isActive: true,
  lastLogin: new Date()
});

// Listen for changes to multiple properties
const updateFullName = () => {
  userData.fullName = `${userData.firstName} ${userData.lastName}`;
};
listen(userData, ['firstName'], updateFullName);
listen(userData, ['lastName'], updateFullName);

// Create status display with any property changes
const statusDisplay = create('div', null, { className: 'status' }, (el) => {
  listen(userData, [SYMBOL_ALL_PROPERTIES], () => {
    el.textContent = `User: ${userData.fullName} | Age: ${userData.age} | Email: ${userData.email} | Active: ${userData.isActive ? 'Yes' : 'No'} | Last Login: ${userData.lastLogin.toLocaleTimeString()}`;
  }, 1);
});

// Create controls
const userControls = create('div', null, { className: 'controls' }, (el) => [
  create('div', el, { className: 'input-group' }, (group) => [
    create('label', group, {}, () => ['First Name:']),
    create('input', group, {
      type: 'text',
      value: userData.firstName,
      oninput: (e) => userData.firstName = e.target.value
    })
  ]),
  create('div', el, { className: 'input-group' }, (group) => [
    create('label', group, {}, () => ['Last Name:']),
    create('input', group, {
      type: 'text',
      value: userData.lastName,
      oninput: (e) => userData.lastName = e.target.value
    })
  ]),
  create('div', el, { className: 'input-group' }, (group) => [
    create('label', group, {}, () => ['Age:']),
    create('input', group, {
      type: 'number',
      value: userData.age,
      oninput: (e) => userData.age = parseInt(e.target.value)
    })
  ]),
  create('div', el, { className: 'input-group' }, (group) => [
    create('label', group, {}, () => ['Email:']),
    create('input', group, {
      type: 'text',
      value: userData.email,
      oninput: (e) => userData.email = e.target.value
    })
  ]),
  create('button', el, {
    className: 'btn btn-success',
    onclick: () => userData.isActive = !userData.isActive
  }, () => ['Toggle Active']),
  create('button', el, {
    className: 'btn btn-primary',
    onclick: () => userData.lastLogin = new Date()
  }, () => ['Update Login Time'])
]);

Try it live: http://localhost:3000/examples/listen.html

createHub(connections, inits)

Creates a hub for coordinating multiple data connections. Acts as a communication center that can trigger multiple functions when data changes, making it perfect for updating multiple UI elements or data structures simultaneously.

Parameters:

  • connections (Array): Array of functions that will be called when the hub is triggered
  • inits (Array): Array of initialization functions that determine when and how to call the hub's update function

Returns: An object with:

  • connections: Array of connection functions
  • u(...args): Update function that triggers all connections with the provided arguments

Example:

// Create reactive data
const data = createProxyObject({
  count: 20,
  total: 100,
});

// Create display
const counter = create('h2', null, {
  id: 'counter'
}, () => []);
const progress = create('div', null, {
  id: 'progress',
  style: 'background-color: #007bff; height: 100%; width: 0%;'
}, () => []);
const progressBar = create('div', null, {
  id: 'progress-bar',
  style: 'border: 1px solid #000000; height: 40px; width: 100%;'
}, (el) => [
  progress,
]);
const status = create('h2', null, { id: 'status' }, () => []);
const display = create('div', null, { className: 'status' }, (el) => [
  counter,
  progressBar,
  status,
]);
// Create a hub for updating multiple UI elements
const hub = createHub([
  // Update counter display
  () => {
    counter.textContent = `${data.count} / ${data.total} (${Math.round(data.count / data.total * 100)}%)`;
  },
  // Update progress bar
  () => {
    progress.style.width = `${data.count / data.total * 100}%`;
  },
  // Update progress bar color
  () => {
    progress.style.backgroundColor = 
    (data.count / data.total) > 1 ? '#28a745' :
    (data.count / data.total) > .6 ? '#007bff' : 
    (data.count / data.total) > .3 ? '#ffc107' : 
    '#dc3545';
  },
  // Update status message
  () => {
    const percent = data.count / data.total;
    const statusText = percent > .6 ? 'High' : percent > .3 ? 'Medium' : 'Low';
    status.textContent = `Status: ${statusText}`;
  }
], [
  // Initialize so that when data.count changes, we notify the listeners in the hub
  (update) => listen(data, [`count`], update),
  (update) => listen(data, [`total`], update)
]);
// You can also call it manually
hub.u();

Try it live: http://localhost:3000/examples/createHub.html

Lists and Repetition

repeat(init, childGenerator, scopeProperty, indexProperty)

Creates a reactive list of elements that efficiently updates when data changes. The function returns a nodeList containing comment anchors and dynamically generated child elements.

Parameters:

  • init (Function): Initialization function that receives three parameters:
    • update(items, ev): Function to call when the list should be updated with new items
    • removeItem(item): Function to remove a specific item from the list
    • insertItem(item, index): Function to insert an item at a specific index
  • childGenerator (Function): Function that creates a DOM element for each item: (item, index) => element
  • scopeProperty (string): Property name to set on the element's scope (defaults to 'item')
  • indexProperty (string): Property name to set the index on the element's scope (optional)

Example:

// Create data with an array
const data = createProxyObject({
  items: createProxyObject([
    { name: 'Apple', color: 'red' },
    { name: 'Banana', color: 'yellow' },
    { name: 'Orange', color: 'orange' },
  ])
});

// Create item list using repeat
const itemList = create('div', null, { className: 'item-list' }, (el) => [
  ...repeat(
    // Init function - listen for changes to items array, call update when items change
    (update) => {
      listen(data, ['items', SYMBOL_ALL_ITEMS], update, 1);
    },
    // Child generator function - creates each item
    (item, index) => create('div', null, { 
      className: 'item',
      style: `border-left: 4px solid ${item.color}`
    }, (el) => [
      create('div', el, { className: 'item-number' }, (el) => [
        createText(index + 1, el, (n) => (
          listen(n._, ['index'], (index = 0) => {
            n.data = index + 1;
          }, 1)
        ))
      ]),
      create('div', el, { className: 'item-content' }, (el) => [
        createText(item.name, el, (n) => (
          listen(n._, ['item', 'name'], (name = '') => {
            el.data = name;
          }, 1)
        ))
      ]),
      create('div', el, { className: 'item-actions' }, (el) => [
        create('button', el, {
          className: 'btn btn-danger',
          style: 'padding: 5px 10px; font-size: 12px;',
        }, (el) => (
          el.addEventListener('click', () => {
            const index = data.items.indexOf(el._.item);
            if (index > -1) {
              data.items.splice(index, 1);
            }
          }),
          ['Remove']
        ))
      ])
    ], {
      item: item,
      index: index,
    }),
    'item', // scope property name
    'index' // index property name
  )
]);

// Create controls
const controls = create('div', null, { className: 'controls' }, (el) => [
  create('input', el, {
    type: 'text',
    placeholder: 'Add new item...',
    onkeypress: (e) => {
      if (e.key === 'Enter' && e.target.value.trim()) {
        const colors = ['red', 'blue', 'green', 'purple', 'orange', 'pink'];
        const newItem = createProxyObject({
          name: e.target.value.trim(),
          color: colors[Math.floor(Math.random() * colors.length)]
        });
        data.items.push(newItem);
        e.target.value = '';
      }
    }
  }),
  create('button', el, {
    className: 'btn btn-primary',
    onclick: () => {
      const colors = ['red', 'blue', 'green', 'purple', 'orange', 'pink'];
      const items = ['Apple', 'Banana', 'Orange', 'Grape', 'Strawberry', 'Blueberry'];
      const newItem = createProxyObject({
        name: items[Math.floor(Math.random() * items.length)],
        color: colors[Math.floor(Math.random() * colors.length)]
      });
      data.items.push(newItem);
    }
  }, () => ['Add Random Item'])
]);

Try it live: http://localhost:3000/examples/repeat.html

Utility Functions

resolve(object, properties)

Resolves a property path on an object (e.g., resolve(obj, ['user', 'name'])).

Returns: The appropriate value based on the property path.

set(obj, propertyName, value, skipChange)

Sets a property value on an object and fires appropriate events.

Parameters:

  • obj (Object): The object to set the property on
  • propertyName (string): The name of the property to set
  • value (any): The value to set
  • skipChange (boolean): Whether to skip firing change events

Returns: The value that was set

deleteProperty(obj, propertyName, skipChange)

Deletes a property from an object and fires appropriate events.

Parameters:

  • obj (Object): The object to delete the property from
  • propertyName (string): The name of the property to delete
  • skipChange (boolean): Whether to skip firing change events

Returns: The result of the delete operation

DOM Utility Functions

The library also provides utility functions for common DOM operations:

append(parent, ...children)

Appends children to a parent element.

before(node, ...nodes)

Inserts nodes before a reference node.

after(node, ...nodes)

Inserts nodes after a reference node.

replaceWith(node, ...nodes)

Replaces a node with new nodes.

remove(node)

Removes a node from its parent.

setAttribute(node, attribute, value)

Sets an attribute on a node.

setAttributeNS(node, namespace, attribute, value)

Sets a namespaced attribute on a node.

ClassList Utility Functions

classListToggle(el, token, force)

Toggles a CSS class on an element.

classListAdd(el, ...args)

Adds CSS classes to an element.

classListRemove(el, ...args)

Removes CSS classes from an element.

classListReplace(el, oldToken, newToken)

Replaces one CSS class with another on an element.

EventTarget Utility Functions

addEventListener(target, type, fn, options)

Adds an event listener to a target.

removeEventListener(target, type, fn, options)

Removes an event listener from a target.

dispatchEvent(target, ev)

Dispatches an event on a target.

preventDefault(ev)

Prevents the default action of an event.

stopPropagation(ev)

Stops event propagation.

stopImmediatePropagation(ev)

Stops immediate event propagation.

Custom History Functions

back()

Goes back in browser history.

forward()

Goes forward in browser history.

go(index)

Goes to a specific position in browser history.

pushState(state, title, url)

Pushes a new state to browser history.

replaceState(state, title, url)

Replaces the current state in browser history.

link(linkElement, url)

Creates a navigation link for Single Page Applications (SPAs). Prevents the default link behavior and uses the custom history management to navigate to the specified URL. Should be used in conjuction with history utility functions.

Parameters:

  • linkElement (Element): The anchor (<a>) element to make into a navigation link
  • url (string): The URL to navigate to when clicked

Example:

// Create navigation links for an SPA
const nav = create('nav', null, { className: 'navigation' }, (el) => [
  create('ul', el, {}, (ul) => [
    create('li', ul, {}, () => [
      create('a', ul, { href: '/home' }, (el) => (
        link(el, '/home'),
        window.addEventListener('statechange', () => {
          el.classList.toggle('active', window.location.pathname === '/home');
        }),
        ['Home']
      ))
    ]),
    create('li', ul, {}, () => [
      create('a', ul, { href: '/about' }, (el) => (
        link(el, '/about'),
        window.addEventListener('statechange', () => {
          el.classList.toggle('active', window.location.pathname === '/about');
        }),
        ['About']
      ))
    ]),
    create('li', ul, {}, () => [
      create('a', ul, { href: '/contact' }, (el) => (
        link(el, '/contact'),
        window.addEventListener('statechange', () => {
          el.classList.toggle('active', window.location.pathname === '/contact');
        }),
        ['Contact']
      ))
    ])
  ])
]);
// Create page content
const pageContent = create('div', null, { className: 'page-content' }, (el) => [
  create('div', null, {}, (el) => (
    ((stateChange) => (
      window.addEventListener('statechange', stateChange),
      stateChange()
    ))
    (() => (
      el.style.display = (
        window.location.pathname === '/home' || 
        window.location.pathname === '/examples/link.html'
      ) ? 'block' : 'none'
    )),
    [
      create('h3', null, {}, () => ['Home Page']),
    ]
  )),
  create('div', null, {}, (el) => (
    ((stateChange) => (
      window.addEventListener('statechange', stateChange),
      stateChange()
    ))
    (() => (
      el.style.display = window.location.pathname === '/about' ? 'block' : 'none'
    ), 1),
    [
      create('h3', null, {}, () => ['About Page']),
    ]
  )),
  create('div', null, {}, (el) => (
    ((stateChange) => (
      window.addEventListener('statechange', stateChange),
      stateChange()
    ))
    (() => (
      el.style.display = window.location.pathname === '/contact' ? 'block' : 'none'
    ), 1),
    [
      create('h3', null, {}, () => ['Contact Page']),
    ]
  )),
]);

Try it live: http://localhost:3000/examples/link.html

Understanding Scope

Scope is a fundamental concept in Schematize UI.js that provides hierarchical data access and inheritance. Every element has a data scope accessible via element._ that can contain reactive data and functions.

1. Initializing Scope on Elements

You can initialize scope on an element by passing a scope object as the fifth parameter to the create function:

// Create an element with initial scope
const userData = { name: 'John', email: '[email protected]' };
const userCard = create('div', null, { className: 'user-card' }, (el) => [
  create('h3', el, {}, () => ['User Profile'])
], userData); // <-- Scope object passed here

// Access the scope
console.log(userCard._.name); // 'John'
console.log(userCard._.email); // '[email protected]'

2. Parent-Child Scope Inheritance

When you don't provide a scope object, child elements automatically reference their parent's scope:

// Parent with scope
const parentScope = { theme: 'dark', language: 'en' };
const parent = create('div', null, {}, (el) => [
  create('h1', el, {}, () => ['Title']), // References parentScope
  create('p', el, {}, () => ['Content'])  // References parentScope
], parentScope);

// All children can access parent scope
const h1 = parent.querySelector('h1');
const p = parent.querySelector('p');

console.log(h1._.theme);     // 'dark' (references the "theme" property of the _ scope from the parent div element)
console.log(p._.language);   // 'en' (references the "language" property of the _ scope from the parent div element)

3. Creating Child Scopes with Inheritance

When you provide a scope object to a child element, it creates a new scope that inherits from the parent's scope using prototypal inheritance:

// Parent scope
const parentScope = { 
  theme: 'dark', 
  language: 'en',
  user: { name: 'John', role: 'admin' }
};

const parent = create('div', null, {}, (el) => [
  // Child with its own scope that inherits from parent
  create('div', el, {}, (childEl) => [
    create('span', childEl, {}, () => ['Child content'])
  ], { 
    // Child's own properties
    isActive: true,
    count: 0,
    // Override parent property
    theme: 'light'
  })
], parentScope);

const child = parent.querySelector('div > div');
const grandchild = parent.querySelector('span');

// Child scope properties
console.log(child._.isActive);  // true (child's own property)
console.log(child._.count);     // 0 (child's own property)
console.log(child._.theme);     // 'light' (overrides parent's 'dark')

// Inherited properties from parent
console.log(child._.language);  // 'en' (inherited from parent)
console.log(child._.user.name); // 'John' (inherited from parent)

// Grandchild inherits from child (which inherits from parent)
console.log(grandchild._.isActive); // true (inherited from child)
console.log(grandchild._.theme);    // 'light' (inherited from child)
console.log(grandchild._.language); // 'en' (inherited from parent)

4. Prototypal Inheritance in Action

The scope system uses JavaScript's prototypal inheritance, so you can access properties up the prototype chain:

// Create a hierarchy with different scope levels
const rootScope = { 
  appName: 'MyApp',
  version: '1.0.0',
  config: { apiUrl: 'https://api.example.com' }
};

const root = create('div', null, {}, (el) => [
  create('header', el, {}, (headerEl) => [
    create('h1', headerEl, {}, () => ['App Title'])
  ], { 
    // Header scope
    title: 'Welcome',
    showLogo: true
  }),
  create('main', el, {}, (mainEl) => [
    create('section', mainEl, {}, (sectionEl) => [
      create('p', sectionEl, {}, () => ['Content'])
    ], {
      // Section scope
      content: 'Main content',
      isVisible: true
    })
  ], {
    // Main scope
    currentPage: 'home',
    user: { name: 'Jane', id: 123 }
  })
], rootScope);

// Access properties at different levels
const h1 = root.querySelector('h1');
const p = root.querySelector('p');

// h1 can access: header scope + root scope
console.log(h1._.title);        // 'Welcome' (header scope)
console.log(h1._.appName);      // 'MyApp' (root scope)
console.log(h1._.config.apiUrl); // 'https://api.example.com' (root scope)

// p can access: section scope + main scope + root scope
console.log(p._.content);       // 'Main content' (section scope)
console.log(p._.currentPage);   // 'home' (main scope)
console.log(p._.user.name);     // 'Jane' (main scope)
console.log(p._.version);       // '1.0.0' (root scope)

5. Accessing Parent Scope with _._

Within a child scope, you can directly access the parent's scope using _._ (the underscore property of the current scope):

// Parent scope
const parentScope = { 
  theme: 'dark', 
  language: 'en',
  user: { name: 'John', role: 'admin' }
};

const parent = create('div', null, {}, (el) => [
  create('div', el, {}, (childEl) => [
    create('button', childEl, {
      onclick: () => {
        // Access current scope properties
        console.log(childEl._.isActive);  // true (current scope)
        console.log(childEl._.count);     // 0 (current scope)
        
        // Access parent scope properties using _._
        console.log(childEl._._.theme);   // 'dark' (parent scope)
        console.log(childEl._._.language); // 'en' (parent scope)
        console.log(childEl._._.user.name); // 'John' (parent scope)
        
        // You can also modify parent scope properties
        childEl._._.theme = 'light'; // Changes parent's theme
        childEl._._.user.role = 'user'; // Changes parent's user role
      }
    }, () => ['Access Parent Scope'])
  ], { 
    // Child scope
    isActive: true,
    count: 0
  })
], parentScope);

// The _._ pattern is especially useful in event handlers and reactive functions
const reactiveChild = create('div', null, {}, (el) => [
  createText('', el, (textNode) => {
    // Listen to both current scope and parent scope
    listen(el._, ['count'], () => {
      textNode.data = `Child count: ${el._.count}, Parent theme: ${el._._.theme}`;
    });
    
    listen(el._._, ['theme'], () => {
      textNode.data = `Child count: ${el._.count}, Parent theme: ${el._._.theme}`;
    });
  })
], { count: 0 });

Practical Example - Component Communication:

// App-level scope
const appScope = {
  currentUser: { name: 'John', permissions: ['read', 'write'] },
  theme: 'dark',
  notifications: []
};

const app = create('div', null, {}, (el) => [
  // Header component
  create('header', el, {}, (headerEl) => [
    create('h1', headerEl, {}, () => ['My App']),
    create('button', headerEl, {
      onclick: () => {
        // Access app-level data from header
        console.log('Current user:', headerEl._._.currentUser.name);
        console.log('Theme:', headerEl._._.theme);
        
        // Add notification to app scope
        headerEl._._.notifications.push({
          message: 'Header button clicked',
          timestamp: new Date()
        });
      }
    }, () => ['Add Notification'])
  ], {
    // Header-specific scope
    isCollapsed: false
  }),
  
  // Main content component
  create('main', el, {}, (mainEl) => [
    create('div', mainEl, {}, (contentEl) => [
      createText('', contentEl, (textNode) => {
        // Display data from both current scope and parent scope
        const updateContent = () => {
          textNode.data = `Welcome ${contentEl._._.currentUser.name}! ` +
                         `Theme: ${contentEl._._.theme}, ` +
                         `Notifications: ${contentEl._._.notifications.length}`;
        };
        
        // Listen to changes in both scopes
        listen(contentEl._, ['isVisible'], updateContent);
        listen(contentEl._._, ['currentUser', 'theme', 'notifications'], updateContent);
        
        updateContent();
      })
    ])
  ], {
    // Main-specific scope
    isVisible: true,
    currentPage: 'dashboard'
  })
], appScope);

Property Shadowing and Accessing Hidden Parent Properties:

// Parent scope with properties that might be shadowed
const parentScope = { 
  theme: 'dark',
  user: { name: 'John', role: 'admin' },
  count: 100,
  settings: { notifications: true }
};

const parent = create('div', null, {}, (el) => [
  create('div', el, {}, (childEl) => [
    create('button', childEl, {
      onclick: () => {
        // Child scope shadows some parent properties
        console.log('Child theme:', childEl._.theme);     // 'light' (child's property)
        console.log('Parent theme:', childEl._._.theme);  // 'dark' (parent's property)
        
        console.log('Child count:', childEl._.count);     // 5 (child's property)
        console.log('Parent count:', childEl._._.count);  // 100 (parent's property)
        
        // Non-shadowed properties are accessible normally
        console.log('User name:', childEl._.user.name);   // 'John' (inherited from parent)
        console.log('Settings:', childEl._.settings);     // { notifications: true } (inherited)
        
        // You can modify the parent's shadowed property
        childEl._._.theme = 'blue'; // Changes parent's theme to 'blue'
        childEl._._.count = 200;    // Changes parent's count to 200
        
        // Child properties remain unchanged
        console.log('Child theme after parent change:', childEl._.theme); // Still 'light'
        console.log('Child count after parent change:', childEl._.count); // Still 5
      }
    }, () => ['Check Shadowed Properties'])
  ], { 
    // Child scope shadows parent properties with same names
    theme: 'light',  // Shadows parent's 'dark' theme
    count: 5,        // Shadows parent's 100 count
    isActive: true   // Child's own property
  })
], parentScope);

// Practical example: Component with local state that shadows global state
const globalState = {
  currentPage: 'home',
  user: { name: 'Jane', isLoggedIn: true },
  theme: 'dark',
  notifications: []
};

const pageComponent = create('div', null, {}, (el) => [
  create('h2', el, {}, () => ['Page Component']),
  create('button', el, {
    onclick: () => {
      // Access local page state
      console.log('Local page:', el._.currentPage);      // 'dashboard' (local)
      console.log('Local theme:', el._.theme);           // 'light' (local)
      
      // Access global state (shadowed by local)
      console.log('Global page:', el._._.currentPage);   // 'home' (global)
      console.log('Global theme:', el._._.theme);        // 'dark' (global)
      
      // Access non-shadowed global properties
      console.log('User:', el._.user.name);              // 'Jane' (inherited)
      console.log('Notifications:', el._.notifications); // [] (inherited)
      
      // Update global state from component
      el._._.currentPage = 'profile';
      el._._.notifications.push('Page component updated global state');
    }
  }, () => ['Update Global State'])
], {
  // Local component state that shadows global state
  currentPage: 'dashboard',  // Shadows global 'home'
  theme: 'light',           // Shadows global 'dark'
  isLoading: false          // Component-specific property
});

6. Reactive Scope Updates

Scope properties can be reactive when using proxy objects:

// Create reactive scope
const reactiveScope = createProxyObject({
  count: 0,
  message: 'Hello',
  items: ['apple', 'banana']
});

const container = create('div', null, {}, (el) => [
  create('button', el, {}, (btnEl) => [
    createText('Count: ', btnEl),
    createText(reactiveScope.count.toString(), btnEl, (textNode) => {
      // Listen for count changes and update text
      listen(reactiveScope, ['count'], (newCount) => {
        textNode.data = newCount.toString();
      });
    })
  ], {
    // Child scope with reactive data
    isClicked: false
  })
], reactiveScope);

// Update reactive scope - all listening elements will update
reactiveScope.count = 5;        // Button text updates to "Count: 5"
reactiveScope.message = 'Hi';   // Available to all child elements
reactiveScope.items.push('orange'); // Array changes trigger updates

Examples

Basic Counter App

Here's a simple counter application that demonstrates reactive data binding:

Try it live: http://localhost:3000/examples/basic-counter.html

import { create as c, listen, createProxyObject } from '@schematize/ui.js';

// Create reactive data using createProxyObject for proper reactivity
const data = createProxyObject({ count: 0 });

// Create the counter UI
const counter = c('div', null, { className: 'counter' }, (el) => [
  c('h2', el, {}, () => [`Count: ${data.count}`]),
  c('button', el, {
    className: 'btn btn-primary',
    onclick: () => data.count++
  }, () => ['Increment']),
  c('button', el, {
    className: 'btn btn-secondary',
    onclick: () => data.count--
  }, () => ['Decrement'])
]);

// Listen for count changes and update the display
listen(data, ['count'], () => {
  const h2 = counter.querySelector('h2');
  h2.textContent = `Count: ${data.count}`;
});

// Append to document
document.body.appendChild(counter);

Todo List with Conditional Rendering

This example shows how to create a todo list with conditional rendering:

Try it live: http://localhost:3000/examples/todo-list.html

import _set from '@schematize/instance.js/src/Instance/set.mjs';
import _append from '@schematize/instance.js/src/Collection/append.mjs';
import _replace from '@schematize/instance.js/src/Collection/replace.mjs';

import {
  createText as ct,
  creator,
  listen,
  createHub,
  repeat,
  SYMBOL_ALL_ITEMS,
  SYMBOL_ALL_PROPERTIES,
  classListToggle,
} from '@schematize/ui.js';

const button = creator('button');
const input = creator('input');
const form = creator('form');
const label = creator('label');
const ul = creator('ul');
const li = creator('li');
const div = creator('div');
const h1 = creator('h1');

const set = _set.call.bind(_set);
const Collection_append = _append.call.bind(_append);
const Collection_replace = _replace.call.bind(_replace);

const scope = {
  todos: [
    { id: 1, text: 'Learn Schematize UI', completed: true },
    { id: 2, text: 'Build an app', completed: false },
    { id: 3, text: 'Share with community', completed: false }
  ],
  filteredTodos: [],
  newTodo: '',
  filter: 'all' // 'all', 'active', 'completed'
};

// Create the main todo app
const todoApp = 
div(null, { className: 'todo-app' }, (el) => (
  // listen for any changes in the todos or filter
  createHub([
    () => {
      set(el._, 'filteredTodos', el._.todos.filter(todo => {
        if (el._.filter === 'active') return !todo.completed;
        if (el._.filter === 'completed') return todo.completed;
        return true;
      }));
    }
  ], [
    (u) => listen(el._, [
      'todos', [SYMBOL_ALL_ITEMS, 'change'], [SYMBOL_ALL_PROPERTIES, 'change']
    ], u),
    (u) => listen(el._, ['filter'], u)
  ])
  // update the filtered todos immediately
  .u(),
  [
    h1(el, {}, () => ['Todo List']),
    
    // Add new todo form
    form(el, {}, (form) => (
      form.addEventListener('submit', (e) => {
        e.preventDefault();
        let newTodo = el._.newTodo.trim();
        if (newTodo) {
          Collection_append(el._.todos, { id: Date.now(), text: newTodo, completed: false });
          set(el._, 'newTodo', '');
        }
      }),
      [
        input(form, {
          type: 'text',
          placeholder: 'Add a new todo...',
        }, (el) => (
          el.addEventListener('input', (e) => set(el._, 'newTodo', e.target.value)),
          listen(el._, ['newTodo'], (newTodo = '') => (el.value = newTodo), 1)
        )),
        button(form, { type: 'submit', className: 'btn btn-primary' }, () => ['Add'])
      ]
    )),
    
    // Filter buttons
    div(el, { className: 'filters' }, (filters) => [
      button(filters, { className: 'btn' }, (el) => (
        el.addEventListener('click', () => set(el._, 'filter', 'all')),
        listen(el._, ['filter'], filter => (
          classListToggle(el, 'active', filter === 'all')
        ), 1),
        ['All']
      )),
      button(filters, { className: 'btn' }, (el) => (
        el.addEventListener('click', () => set(el._, 'filter', 'active')),
        listen(el._, ['filter'], filter => (
          classListToggle(el, 'active', filter === 'active')
        ), 1),
        ['Active']
      )),
      button(filters, { className: 'btn' }, (el) => (
        el.addEventListener('click', () => set(el._, 'filter', 'completed')),
        listen(el._, ['filter'], filter => (
          classListToggle(el, 'active', filter === 'completed')
        ), 1),
        ['Completed']
      )),
    ]),
    
    // Todo list
    ul(el, { className: 'todo-list', id: 'todo-list' }, (ul, currentId = 0) => [
      ...repeat(
        (u) => {
          listen(ul._, ['filteredTodos'], u, 1);
        },
        (todo) => (
          li(ul, { className: 'todo-item' }, (el, id = ++currentId) => (
            listen(el._, ['todo', 'completed'], (completed) => (
              classListToggle(el, 'completed', completed)
            ), 1),
            [
              input(el, {
                id: id,
                className: 'todo-checkbox',
                type: 'checkbox',
                checked: todo.completed,
              }, (el) => (
                el.addEventListener('change', () => set(el._.todo, 'completed', el.checked)),
                listen(el._, ['todo', 'completed'], (completed) => (
                  el.checked = completed
                ), 1)
              )),
              label(el, { className: 'todo-text', for: id }, (el) => [
                ct(todo.text, el, (node) => (
                  listen(el._, ['todo', 'text'], text => node.data = text, 1)
                )),
              ]),
              button(el, { className: 'delete-btn' }, (el) => (
                el.addEventListener('click', () => {
                  const index = el._.todos.findIndex(t => t.id === el._.todo.id);
                  Collection_replace(el._.todos, index, 1);
                }),
                ['×']
              ))
            ]
          ), {
            todo: todo,
          })
        ),
        'todo'
      )
    ])
  ]
), scope);

// append to document
document.body.appendChild(todoApp);

Form with Two-Way Data Binding

This example demonstrates two-way data binding with form inputs:

Try it live: http://localhost:3000/examples/form-binding.html

import {
  create as c,
  listen,
  createProxyObject,
  SYMBOL_ALL_PROPERTIES
} from '@schematize/ui.js';

// Form data
const formData = createProxyObject({
  name: '',
  email: '',
  age: 25,
  newsletter: true
});

// Create form with bound inputs
const form = c('form', null, { className: 'user-form' }, (el) => [
  c('h2', el, {}, () => ['User Registration']),
  
  // Name input
  c('div', el, { className: 'form-group' }, (group) => [
    c('label', group, { for: 'name' }, () => ['Name:']),
    c('input', group, {
      type: 'text',
      value: formData.name,
      oninput: (e) => formData.name = e.target.value
    })
  ]),
  
  // Email input
  c('div', el, { className: 'form-group' }, (group) => [
    c('label', group, { for: 'email' }, () => ['Email:']),
    c('input', group, {
      type: 'email',
      value: formData.email,
      oninput: (e) => formData.email = e.target.value
    })
  ]),
  
  // Age input
  c('div', el, { className: 'form-group' }, (group) => [
    c('label', group, { for: 'age' }, () => ['Age:']),
    c('input', group, {
      type: 'number',
      value: formData.age,
      oninput: (e) => formData.age = parseInt(e.target.value)
    })
  ]),
  
  // Newsletter checkbox
  c('div', el, { className: 'form-group' }, (group) => [
    c('label', group, {}, () => [
      c('input', group, {
        type: 'checkbox',
        checked: formData.newsletter,
        onchange: (e) => formData.newsletter = e.target.checked
      }),
      ' Subscribe to newsletter'
    ])
  ]),
  
  // Submit button
  c('button', el, {
    type: 'submit',
    className: 'btn btn-primary'
  }, () => ['Submit'])
]);

// Display current form data
const dataDisplay = c('div', null, { className: 'data-display' }, (el) => [
  c('h3', el, {}, () => ['Current Form Data:']),
  c('pre', el, {}, (el) => (
    listen(formData, [[SYMBOL_ALL_PROPERTIES, 'change']], () => (
      el.textContent = `{\n${
        ['name', 'email', 'age', 'newsletter']
        .map((key) => `  ${key}: ${JSON.stringify(formData[key])}`)
        .join(',\n')
      }\n}`
    ), 1),
    []
  ))
]);

// Handle form submission
form.addEventListener('submit', (e) => {
  e.preventDefault();
  console.log('Form submitted:', formData);
  alert('Form submitted! Check console for data.');
});

// Append to document
document.body.appendChild(form);
document.body.appendChild(dataDisplay);

SVG Graphics Example

This example shows how to create interactive SVG graphics:

Try it live: http://localhost:3000/examples/svg-graphics.html

import {
  create as c,
  createSVG as svg,
  listen,
  repeat,
  createProxyObject,
  SYMBOL_ALL_PROPERTIES
} from '@schematize/ui.js';

// SVG data
const data = createProxyObject({
  circles: createProxyObject([
    createProxyObject({ id: 1, cx: 100, cy: 100, r: 30, fill: 'red' }),
    createProxyObject({ id: 2, cx: 200, cy: 150, r: 25, fill: 'blue' }),
    createProxyObject({ id: 3, cx: 150, cy: 200, r: 35, fill: 'green' }),
  ]),
  selected: null
});

// Instructions
const instructions = c('div', null, { className: 'instructions' }, (el) => [
  c('h4', el, {}, () => ['Instructions:']),
  c('ul', el, {}, () => [
    c('li', el, {}, () => ['Click on circles to select them (they will get a black border)']),
    c('li', el, {}, () => ['Click "Add Random Circle" to add new circles']),
    c('li', el, {}, () => ['Click "Delete Selected" to remove the selected circle']),
    c('li', el, {}, () => ['All changes are reactive and update automatically'])
  ])
]);

// Create SVG canvas
const svgCanvas = svg('svg', null, {
  width: 400,
  height: 300,
  viewBox: '0 0 400 300',
  style: 'border: 1px solid #ccc;'
}, (svgEl) => [
  // Background
  svg('rect', svgEl, {
    width: 400,
    height: 300,
    fill: '#f0f0f0'
  }),
  // Circles
  ...repeat(
    (update) => listen(data, ['circles', SYMBOL_ALL_PROPERTIES], (circles) => update(circles), 1),
    (circle) => {
      return svg('circle', svgEl, {
        stroke: 'none',
        'stroke-width': 2,
        style: 'cursor: pointer;',
      }, (el) => (
        // when selected changes or the circle changes, update the class
        ((u) => (
          listen(data, ['selected'], u),
          listen(el._, ['circle'], u),
          u()
        ))
        (() => {
          // stroke if selected
          el.setAttribute('stroke', data.selected === el._.circle ? 'black' : 'none');
          // assign properties to the element
          el._.circle &&
          ['cx', 'cy', 'r', 'fill'].forEach(name => {
            el.setAttribute(name, el._.circle[name]);
          });
        }),
        // when the circle is clicked, set the selected to the circle
        el.addEventListener('click', () => data.selected = el._.circle),
        []
      ), {
        circle: circle
      });
    },
    'circle',
  ),
]);

// Control panel
const controls = c('div', null, { className: 'svg-controls' }, (el) => [
  c('h3', el, {}, () => ['SVG Controls']),
  c('button', el, {
    className: 'btn btn-primary',
    onclick: () => {
      const newCircle = createProxyObject({
        id: Date.now(),
        cx: Math.random() * 300 + 50,
        cy: Math.random() * 200 + 50,
        r: Math.random() * 20 + 15,
        fill: ['red', 'blue', 'green', 'yellow', 'purple'][Math.floor(Math.random() * 5)]
      });
      data.circles.push(newCircle);
    }
  }, () => ['Add Random Circle']),
  c('button', el, {
    className: 'btn btn-danger',
    onclick: () => {
      if (data.selected) {
        const index = data.circles.indexOf(data.selected);
        if (index !== -1) {
          data.circles.splice(index, 1);
          data.selected = null;
        }
      }
    }
  }, () => ['Delete Selected'])
]);

// Append to document
document.body.appendChild(instructions);
document.body.appendChild(svgCanvas);
document.body.appendChild(controls);

Advanced Features

Custom History Management

The library includes custom history management functions:

import { pushState, replaceState, back, forward, go } from '@schematize/ui.js';

// Push a new state
pushState({ page: 'home' }, 'Home', '/home');

// Replace current state
replaceState({ page: 'about' }, 'About', '/about');

// Navigate back/forward
back();
forward();
go(-2); // Go back 2 steps

Event Target Interface

All elements support the EventTarget interface:

import { create as c } from '@schematize/ui.js';

const element = c('div', null, {}, () => []);

// Add event listeners
element.addEventListener('customEvent', (e) => {
  console.log('Custom event received:', e.detail);
});

// Dispatch custom events
element.dispatchEvent(new CustomEvent('customEvent', {
  detail: { message: 'Hello from custom event!' }
}));

Browser Support

Schematize UI.js works in all modern browsers that support:

  • ES6 Proxies
  • ES6 Classes
  • ES6 Arrow Functions
  • ES6 Template Literals

This includes:

  • Chrome 49+
  • Firefox 18+
  • Safari 10+
  • Edge 12+

License

MIT License - see LICENSE file for details.

Want to explore further?

  • Check out the examples in the ./examples/ directory
  • Look at the source code in ./src/ to understand how things work
  • The library is intentionally simple and transparent - you can read and understand the entire codebase