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 🙏

© 2024 – Pkg Stats / Ryan Hefner

amber-dom

v1.0.3

Published

A modular virtual dom library.

Downloads

22

Readme

Amber-dom

Build Status License: MIT npm version

Amber-dom is yet another virtual dom library, which lets you create and update your DOM easily. It is modularized in architecture, making it easy to extend with modules. Built-in modules are shipped with to cover some of the basic use cases. All of them are optional.

Motivation

Amber-dom started as learning project. At the time I came across an article about the basic idea of virtual dom and a diffing algorithm walking two virtual trees in DFS order. I followed the code in that article and later on I decided to make something that can be used seriously, something scalable. Finally I did make something, but it was rather naive. After wandering through some great projects, I decided to rewrite the whole thing, and this is it.

Amber-dom implements an algorithm that almost works the same as Snabbdom, which compares the children from both from both ways(from the start and end), and then update every child, layer by layer, and finally place them into correct positions. This is great. On top of that, Amber-dom adjusts this algorithm slightly, making it more fit to its implementation.

Another great idea inspired by Snabbdom is its modularity. Separating the core algorithm from its functionality modules makes it easy to extend, configure, or even replace those you don't like. Amber-dom implements a similar modularized architecure with slight difference, though, they both works by registering hooks into lifecycles of DOM nodes. See it below.

Next important feature of Amber-dom is, instead of maintaining two virtual trees in memory, Amber-dom diffs against a real DOM tree directly, freeing you from manually keeping the latest created vtree. It works by keeping neccessary information on the DOM tree for next diffing. This idea was inspired by Preact and morphdom. However, Preact compares the children from only one way, and it uses a different algorithm which, as I'm concerned, might not be the most effective one; morphdom also uses a different diffing algorithm that walks two tree in DFS order, and it requires a rather complicated vnode implementation which is not what I wanted.

But I agree that accessing the DOM data structure is not slow because it's key part of browsers. The slow part is recalculating style, repaint and reflow. As long as we keep the updating of DOM as small as possible, we can avoid such uneccessary overhead. That means if we combine Snabbdom's fast algorithm with the idea of diffing against the real DOM tree, meanwhile keep the virtual node implementation small and simple, we might be able to improve it. And this is Amber-dom.

Simple example

A counter example:

import { h, init } from "amber-dom" // Import `h` for creating vnode, and a `init` function.
import events from "amber-dom/modules/events" // Import the `events` module creator function.


let count = 0
let root = document.createElement('div') // Create an initial element to patch with.
let { patch } = init([events()])         // Init with `events` module. Remember `events` is a function.

function render() {                      // Wire up a render logic.
  return h('div#app', [
    count,
    h('br'),
    h('button', {
      events: {
        click: [incrementCounter, root, 1]  // Add `incrementCounter` as listener,
                                            // and `root`, `1` as arguments to listener when listener is called.
      }
    }, '+')
  ])
}

function incrementCounter(ev, elem, step) { // Event listener
  count += step
  patch(elem, render())
}

document.addEventListener('DOMContentLoaded', () => { // When DOM is loaded
  document.body.appendChild(root)
  patch(root, render()) // Render our logic.
})

API documentation

h(selector, attrs, children)

The h function is provided to let you create a vnode easily.

  • selector can either be a simple CSS selector string(e.g. div#app.content), or a function that returns a vnode.
  • attrs is an optional object describing attributes/properties of the DOM node.
  • children is an array of children vnodes. It is optional, and can be any nested.

Inside the param attrs, you can set any DOM attributes inside it. In fact, Amber-dom will set them as properties on the DOM node as long as possible. If you're confused about DOM properties and attributes, take a look at this article and you'll understand them.

let vnode = h('div#app.content', {style: {color: '#ff4500'}}, [
  h('h1', 'Heading 1'), h('h2', 'Heading 2')
])

// You can skip the `attrs` if it is null
let vnode1 = h('div', 'Some Text')

If you pass multiple strings as children, Amber-dom will concat them into a single string, eventually a single TextNode.

init(modules)

The init function takes a list of modules and return an object containing a .patch() and a .createElement() methods used for patching an element and creating an element from a vnode.

  • modules is an array of modules. It is optional.
import { h, init } from 'amber-dom'
import events from 'amber-dom/modules/events'
import style from 'amber-dom/modules/style'
import dataset from 'amber-dom/modules/dataset'

let { patch, createElement } = init([
  events(),
  style(),
  dataset()
])

patch(elem, vnode)

The patch function returned by init function lets you diff and patch any existing DOM with a vtree efficiently. This frees you from manually keep the last generated vtree in memory, since Amber-dom stores information of the real DOM tree, but keeps it small.

  • elem can be any existing DOM nodes.
  • vnode is a virtual tree rooted at vnode.

createElement(vnode)

Even if patch function can be used to patch any existing DOM trees, it would still be helpful to expose a function to build a DOM tree from a vtree. createElement lets you do that.

let { createElement } = init()
let vnode = h('div', [
  h('h1', 'Hello from ', 'Amber-dom!'),
  h('h2', 'Welcome!')
])
let elem = createElement(vnode) // => <div>
                                //      <h1>Hello from Amber-dom!</h1>
                                //      <h2>Welcom!</h2>
                                //    </div>

Hooks

Lifecycle hooks are powerful because they let you take control of different phazes of Nodes. Modules also work by hooking into the lifecycles of nodes. All hooks are listed as below:

| hook | invoke time | available for | params | |--------|-------------|------------|---------| | init | when a vnode is created. | node | init(vnode) | | creating | when creating the DOM element, before creating any of its children. | node, module| creating(elem) for node, creating(elem, modAttrs) for module | | mounted | when the element is created and mounted to DOM. | node | mounted(elem) | | prepatch | before the whole patching begins. | module | prepatch(elem, vnode) | | updating | when updating the DOM element, before updating any of its children. | node, module| updating(elem) for node, updating(elem, modAttrs) for module | | unmounting | when unmounting the DOM element from its parent. | node, module | unmounting(elem) | | postpatch | after the whole patching ends | module | postpatch(elem, vnode) |

Notice that creating, updating and unmounting are available for both single DOM nodes and modules. To use hooks on a single node, simply specify them inside the hooks inside attrs, when using h function to create a vnode:

let vnode = h('div', {
  hooks: {
    init(vnode) { ... },
    creating(elem) { ... },
    mounted(elem) { ... },
    ...
  }
})

init(vnode) hook

The init hook lets you invoke some function right after a vnode is created(and all children of this vnode are created), and right before the real DOM node is created.

creating(elem) and creating(elem, modAttrs) hook

Invoked when an Element is being created. Available for both modules and single DOM nodes. When used on a single node, only the Element being created will be passed as argument to the hook, while both the Element being created and the corresponding module attributes(see module documentation) will be passed to the hook for modules. Also note that, when this hook is invoked, the childNodes for elem have not been created yet.

mounted(elem) hook

Invoked when an Element is created from a vnode, and mounted to the real DOM. Available for single DOM nodes only. This is because in some case you might want to get some computed DOM properties after they're created and mounted.

prepatch(elem, vnode) hook

Invoked before the whole patching process begins. Available for modules only.

updating(elem) and updating(elem, modAttrs) hook

Invoked when an Element is being updating during the patching process. Available for both modules and single DOM nodes. The arguments passed to the hook are the same as creating hook. Also note that when this hook is invoked, the childNodes of elem have not been created yet.

unmounting(elem) hook

Invoked when an Element is being unmounted from its parent. Available for both modules and single DOM nodes. The params are the same for both. Note that only when an Element is removed from its parent, will this hook be invoked. The elements removed indirectly will not be invoked on this hook.

let elem = createElement(h('div', h('p', h('span'))))

The unmounting hook will be invoked on element p, but not on element span. (However you can traverse the unmounted tree inside the callback to unmounting.)

postpatch(elem, vnode) hook

Invoked after the whole patching process finishes. Available for modules only.

Modules documentation

Modules let you customize the functionality of Amber-dom. It works by hooking into different phazes of lifecycle of the whole tree. This is also the main difference compared to registering hooks on single nodes.

A module is simply an object with a name property. Yes, the name property is the only thing that required to be a module. The value of the name property is a module attribute name. The module attribute is set inside attrs param of the h function. And once you have defined a module, you can use it in init function.

Let's take an example to understand it. Say we'd like to make a foo module:

// 1. define the `foo` module.
let fooMod = {
  name: 'foo',
  creating(elem, fooAttrs) { ... },
  updating(elem, fooAttrs) { ... }
}

// 2. use it in `init` function.
let { createElement, patch } = init([fooMod])

Then we'd be able to define our module attributes like this:

let vnode = h('div', {
  // This is our **module attributes**
  foo: {
    ...
  }
}, h('p'))

let elem = createElement(vnode)

When createElement is creating div element, the div element and the value of foo module attribute will be passed to fooMod.creating() as arguments. You'll be deciding what to do with them inside fooMod.creating(). When elem is being patched with a new vnode, the second argument passed to fooMod.updating() is the new vnode's foo module attribute.

When the vnode doesn't have a module attribute(e.g. the p elem above), undefined will be passed in as the module attributes.

Now you understand what I mean by module attribute. One more important thing to be aware of is, createElement and patch functions returned by init will not do anything with any module attributes. This means you decide what to do with all these module attributes on creating and updating. And the rest attributes will be managed by createElement and patch functions.

Let's continue with the foo module example.

import { h, init } from "amber-dom"

let vnode = h('div', {
  // This is our **module attributes**.
  // You'll decide what to do with it.
  foo: {
    ...
  },

  // This is **not** a module attribute.
  // `createElement` and `patch` will help you deal with it.
  className: 'main content'   // set the CSS class.
}, h('p'))

let elem = createElement(vnode)

Built-in modules

Built-in modules are provided to cover the basic use cases. In fact, they're module creators(a function returns a module).

modules/events creator

This creator creates an events module, which lets you add listeners on events.

import { h, init } from "amber-dom"
import events from "amber-dom/modules/events"

let { createElement } = init([ events() ])
let vnode = h('div', {
  events: {
    click: (ev) => {}
  }
})

Sometimes you might want to pass some additional arguments to the event listeners, just pass an array with the first item as the listener, the rest as arguments. Note that the first argument to the listener will still be the event object.

let vnode = h('div', {
  events: {
    click: [(ev, arg1, arg2) => {}, 'firts arg', 'second arg']
  }
})

modules/style creator

It lets you add inline style to an element easily. The style module attribute can be either a string or an object.

import { init, h } from "amber-dom"
import style from "amber-dom/modules/style"

let { createElement, patch } = init([ style() ])

let vnode = h('div', {
  // An object
  style: {
    // Use camelCase
    fontSize: '12px',
    // Or use a string
    'text-align': 'center'
  }
})

let elem = createElement(vnode) // => <div style="font-size: 12px; text-align: center;"></div>

modules/dataset creator

It allows you to set custom data attributes on elements easily.

let { createElement, patch } = init([ dataset() ])

let vnode = h('div', {
  dataset: {
    id: '123456',
    dateOfBirth: '1960-09-08'
  }
})

let elem = createElement(vnode) // => <div data-id="123456" data-dateOfBirth="1960-09-08"></div>