DOM ViewModel - A thin, fast, dependency-free vdom view layer

Overview

domvm (DOM ViewModel)domvm logo

A thin, fast, dependency-free vdom view layer (MIT Licensed)


Introduction

domvm is a flexible, pure-js view layer for building high performance web applications. Like jQuery, it'll happily fit into any existing codebase without introducing new tooling or requiring major architectural changes.

  • It's zero-dependency and requires no compilation or tooling; one <script> tag is all that's needed.
  • It's small: ~6k gz, fast: just 20% slower vs painfully imperative vanilla DOM code. 2x faster SSR vs React v16.
  • Its entire, practical API can be mastered in under 1 hour by both, OO graybeards and FRP hipsters. Obvious explicit behavior, debuggable plain JS templates, optional statefulness and interchangable imperative/declarative components.
  • It's well-suited for building simple widgets and complex, fault-tolerant applications.
  • Supports down to IE11 with a tiny Promise shim.

To use domvm you should be comfortable with JavaScript and the DOM; the following code should be fairly self-explanatory:

var el = domvm.defineElement,
    cv = domvm.createView;

var HelloView = {
    render: function(vm, data) {
        return el("h1", {style: "color: red;"}, "Hello " + data.name);
    }
};

var data = {name: "Leon"};

var vm = cv(HelloView, data).mount(document.body);

Demo Playground

demo playground


Documentation


What domvm Is Not

As a view layer, domvm does not include some things you would find in a larger framework. This gives you the freedom to choose libs you already know or prefer for common tasks. domvm provides a small, common surface for integration of routers, streams and immutable libs. Some minimalist libs that work well:

Many /demos are examples of how to use these libs in your apps.


Builds

domvm comes in several builds of increasing size and features. The nano build is a good starting point and is sufficient for most cases.


Changelog

Changes between versions are documented in Releases.


Tests


Installation

Browser

<script src="dist/nano/domvm.nano.iife.min.js"></script>

Node

var domvm = require("domvm");   // the "full" build

DEVMODE

If you're new to domvm, the dev build is recommended for development & learning to avoid common mistakes; watch the console for warnings and advice.

There are a couple config options:

  • domvm.DEVMODE.mutations = false will disable DOM mutation logging.
  • domvm.DEVMODE.warnings = false will disable all warnings.
  • domvm.DEVMODE.verbose = false will suppress the explanations, but still leave the error names & object info.
  • domvm.DEVMODE.UNKEYED_INPUT = false will disable only these warnings. The full list can be found in devmode.js.

Due to the runtime nature of DEVMODE heuristics, some warnings may be false positives (where the observed behavior is intentional). If you feel an error message can be improved, open an issue!

While not DEVMODE-specific, you may find it useful to toggle always-sychronous redraw during testing and benchmarks:

domvm.cfg({
    syncRedraw: true
});

Templates

Most of your domvm code will consist of templates for creating virtual-dom trees, which in turn are used to render and redraw the DOM. domvm exposes several factory functions to get this done. Commonly this is called hyperscript.

For convenience, we'll alias each factory function with a short variable:

var el = domvm.defineElement,
    tx = domvm.defineText,
    cm = domvm.defineComment,
    sv = domvm.defineSvgElement,
    vw = domvm.defineView,
    iv = domvm.injectView,
    ie = domvm.injectElement,
    cv = domvm.createView;

Using defineText is not required since domvm will convert all numbers and strings into defineText vnodes automatically.

Below is a dense reference of most template semantics.

el("p", "Hello")                                            // plain tags
el("textarea[rows=10]#foo.bar.baz", "Hello")                // attr, id & class shorthands
el(".kitty", "Hello")                                       // "div" can be omitted from tags

el("input",  {type: "checkbox",    checked: true})          // boolean attrs
el("input",  {type: "checkbox", ".checked": true})          // set property instead of attr

el("button", {onclick: myFn}, "Hello")                      // event handlers
el("button", {onclick: [myFn, arg1, arg2]}, "Hello")        // parameterized

el("p",      {style: "font-size: 10pt;"}, "Hello")          // style can be a string
el("p",      {style: {fontSize: "10pt"}}, "Hello")          // or an object (camelCase only)
el("div",    {style: {width: 35}},        "Hello")          // "px" will be added when needed

el("h1", [                                                  // attrs object is optional
    el("em", "Important!"),
    "foo", 123,                                             // plain values
    ie(myElement),                                          // inject existing DOM nodes
    el("br"),                                               // void tags without content
    "", [], null, undefined, false,                         // these will be auto-removed
    NaN, true, {}, Infinity,                                // these will be coerced to strings
    [                                                       // nested arrays will get flattened
        el(".foo", {class: "bar"}, [                        // short & attr class get merged: .foo.bar
            "Baz",
            el("hr"),
        ])
    ],
])

el("#ui", [
    vw(NavBarView, navbar),                                 // sub-view w/data
    vw(PanelView, panel, "panelA"),                         // sub-view w/data & key
    iv(someOtherVM, newData),                               // injected external ViewModel
])

// special _* props

el("p", {_key: "myParag"}, "Some text")                     // keyed nodes
el("p", {_data: {foo: 123}}, "Some text")                   // per-node data (faster than attr)

el("p", {_ref: "myParag"}, "Some text")                     // named refs (vm.refs.myParag)
el("p", {_ref: "pets.james"}, "Some text")                  // namespaced (vm.refs.pets.james)

el("p", {_hooks: {willRemove: ...}}, "Some text")           // lifecycle hooks

el("div", {_flags: ...}, "Some text")                       // optimization flags

Spread children

micro+ builds additionally provide two factories for defining child elements using a ...children spread rather than an explicit array.

var el = domvm.defineElementSpread,
    sv = domvm.defineSvgElementSpread;

el("ul",
    el("li", 1),
    el("li", 2),
    el("li", 3)
);

JSX

While not all of domvm's features can be accommodated by JSX syntax, it's possible to cover a fairly large subset via a defineElementSpread pragma. Please refer to demos and examples in the JSX wiki.


Views

What React calls "components", domvm calls "views". A view definition can be a plain object or a named closure (for isolated working scope, internal view state or helper functions). The closure must return a template-generating render function or an object containing the same:

var el = domvm.defineElement;

function MyView(vm) {                                       // named view closure
    return function() {                                         // render()
        return el("div", "Hello World!");                           // template
    };
}

function YourView(vm) {
    return {
        render: function() {
            return el("div", "Hello World!");
        }
    };
}

var SomeView = {
    init: function(vm) {
        // ...
    },
    render: function() {
        return el("div", "Hello World!");
    }
};

Views can accept external data to render (à la React's props):

function MyView(vm) {
    return function(vm, data) {
        return el("div", "Hello " + data.firstName + "!");
    };
}

vm is this views's ViewModel; it's the created instance of MyView and serves the same purpose as this within an ES6 React component. The vm provides the control surface/API to this view and can expose a user-defined API for external view manipulation.

Rendering a view to the DOM is called mounting. To mount a top-level view, we create it from a view definition:

var data = {
    firstName: "Leon"
};

var vm = cv(MyView, data);

vm.mount(document.body);            // appends into target

By default, .mount(container) will append the view into the container. Alternatively, to use an existing placeholder element:

var placeholder = document.getElementById("widget");

vm.mount(placeholder, true);        // empties & assimilates placeholder

When your data changes, you can request to redraw the view, optionally passing a boolean sync flag to force a synchronous redraw.

vm.redraw(sync);

If you need to replace a view's data (as with immutable structures), you should use vm.update, which will also redraw.

vm.update(newData, sync);

Views can be nested either declaratively or by injecting an already-initialized view:

var el = domvm.defineElement,
    vw = domvm.defineView,
    iv = domvm.injectView;

function ViewA(vm) {
    return function(vm, dataA) {
        return el("div", [
            el("strong", dataA.test),
            vw(ViewB, dataA.dataB),               // implicit/declarative view
            iv(data.viewC),                       // injected explicit view
        ]);
    };
}

function ViewB(vm) {
    return function(vm, dataB) {
        return el("em", dataB.test2);
    };
}

function ViewC(vm) {
    return function(vm, dataC) {
        return el("em", dataC.test3);
    };
}

var dataC = {
    test3: 789,
};

var dataA = {
    test: 123,
    dataB: {
        test2: 456,
    },
    viewC: cv(ViewC, dataC),
};

var vmA = cv(ViewA, dataA).mount(document.body);

Notes:

  • render() must return a single dom vnode. There is no support yet for views returning fragments/arrays, other views or null. These capabilities do not add much value to domvm's API (see Issue #207).

Options

cv and vw have four arguments: (view, data, key, opts). The fourth opts arg can be used to pass in any additional data into the view constructor/init without having to cram it into data. Several reserved options are handled automatically by domvm that correspond to existing vm.cfg({...}) options (documented in other sections):

  • init (same as using {init:...} in views defs)
  • diff
  • hooks
  • onevent
  • onemit

This can simplify sub-view internals when externally-defined opts are passed in, avoiding some boilerplate inside views, eg. vm.cfg({hooks: opts.hooks}).

ES6/ES2015 Classes

Class views are not supported because domvm avoids use of this in its public APIs. To keep all functions pure, each is invoked with a vm argument. Not only does this compress better, but also avoids much ambiguity. Everything that can be done with classes can be done better with domvm's plain object views, ES6 modules, Object.assign() and/or Object.create(). See #194 & #147 for more details.

TODO: create Wiki page showing ES6 class equivalents:

  • extend via ES6 module import & Object.assign({}, base, current)
  • super() via ES6 module import and passing vm instance
  • invoking additional "methods" via vm.view.* from handlers or view closure

Parents & Roots

You can access any view's parent view via vm.parent() and the great granddaddy of any view hierarchy via vm.root() shortcut. So, logically, to redraw the entire UI tree from any subview, invoke vm.root().redraw(). For traversing the vtree, there's also vm.body() which gets the next level of descendant views (not necessarily direct children). vnode.body and vnode.parent complete the picture.


Sub-views vs Sub-templates

A core benefit of template composition is code reusability (DRY, component architecture). In domvm composition can be realized using either sub-views or sub-templates, often interchangeably. Sub-templates should generally be preferred over sub-views for the purposes of code reuse, keeping in mind that like sub-views, normal vnodes:

  • Can be keyed to prevent undesirable DOM reuse
  • Can subscribe to numerous lifecycle hooks
  • Can hold data, which can then be accessed from event handlers

Sub-views carry a bit of performance overhead and should be used when the following are needed:

  • Large building blocks
  • Complex private state
  • Numerous specific helper functions
  • Isolated redraw (as a perf optimization)
  • Synchronized redraw of disjoint views

As an example, the distinction can be discussed in terms of the calendar demo. Its implementation is a single monolithic view with internal sub-template generating functions. Some may prefer to split up the months into a sub-view called MonthView, which would bring the total view count to 13. Others may be tempted to split each day into a DayView, but this would be a mistake as it would create 504 + 12 + 1 views, each incuring a slight performance hit for no reason. On the other hand, if you have a full-page month view with 31 days and multiple interactive events in the day cells, then 31 sub-views are well-justified.

The general advice is, restrict your views to complex, building-block-level, stateful components and use sub-template generators for readability and DRY purposes; a button should not be a view.


Event Listeners

Basic listeners are bound directly and are defined by plain functions. Like vanilla DOM, they receive only the event as an argument. If you need high performance such as mousemove, drag, scroll or other events, use basic listeners.

function filter(e) {
    // ...
}

el("input", {oninput: filter});

Parameterized listeners are defined using arrays and executed by a single, document-level, capturing proxy handler. They:

  • Can pass through additional args and receive (...args, e, node, vm, data)
  • Will invoke global and vm-level onevent callbacks
  • Will call e.preventDefault() & e.stopPropagation() if false is returned
function cellClick(foo, bar, e, node, vm, data) {}

el("td", {onclick: [cellClick, "foo", "bar"]}, "moo");

View-level and global onevent callbacks:

// global
domvm.cfg({
    onevent: function(e, node, vm, data, args) {
        // ...
    }
});

// vm-level
vm.cfg({
    onevent: function(e, node, vm, data, args) {
        // ...
    }
});

Autoredraw

Is calling vm.redraw() everywhere a nuisance to you?

There's an easy way to implement autoredraw yourself via a global or vm-level onevent which fires after all parameterized event listeners. The onevent demo demonstrates a basic full app autoredraw:

domvm.cfg({
    onevent: function(e, node, vm, data, args) {
        vm.root().redraw();
    }
});

You can get as creative as you want, including adding your own semantics to prevent redraw on a case-by-case basis by setting and checking for e.redraw = false. Or maybe having a Promise piggyback on e.redraw = new Promise(...) that will resolve upon deep data being fetched. You can maybe implement filtering by event type so that a flood of mousemove events, doesnt result in a redraw flood. Etc..


Streams

Another way to implement view reactivity and autoredraw is by using streams. By providing streams to your templates rather than values, views will autoredraw whenever streams change. domvm does not provide its own stream implementation but instead exposes a simple adapter to plug in your favorite stream lib.

domvm's templates support streams in the following contexts:

  • view data: vw(MyView, dataStream...) and cv(MyView, dataStream...)
  • simple body: el("#total", cartTotalStream)
  • attr value: el("input[type=checkbox]", {checked: checkedStream})
  • css value: el("div", {style: {background: colorStream}})

A stream adapter for flyd looks like this:

domvm.cfg({
    stream: {
        val: function(v, accum) {
            if (flyd.isStream(v)) {
                accum.push(v);
                return v();
            }
            else
                return v;
        },
        on: function(accum, vm) {
            let calls = 0;

            const s = flyd.combine(function() {
                if (++calls == 2) {
                    vm.redraw();
                    s.end(true);
                }
            }, accum);

            return s;
        },
        off: function(s) {
            s.end(true);
        }
    }
});
  • val accepts any value and, if that value is a stream, appends it to the provided accumulator array; then returns the stream's current value, else the original object. called multiple times per redraw.
  • on accepts the accumulater array (now filled with streams) and returns a dependent stream that will invoke vm.redraw() once and end (ignoring initial stream creation). this can also be implemented via a .drop(1).take(1).map(...) pattern, if supported by your stream lib (see https://github.com/paldepind/flyd/issues/176#issuecomment-385141469). called once per redraw.
  • off accepts the dependent stream created by on and ends it. called once per unmount.

An extensive demo can be found in the streams playground.

Notes:

  • Streams must never create, cache or reuse domvm's vnodes (defineElement(), etc.) since this will cause memory leaks and major bugs.

Refs & Data

Like React, it's possible to access the live DOM from event listeners, etc via refs. In addition, domvm's refs can be namespaced:

function View(vm) {
    function sayPet(e) {
        var vnode = vm.refs.pets.fluffy;
        alert(fluffy.el.value);
    }

    return function() {
        return el("form", [
            el("button", {onclick: sayPet}, "Say Pet!"),
            el("input", {_ref: "pets.fluffy"}),
        ]);
    };
}

VNodes can hold arbitrary data, which obviates the need for slow data-* attributes and keeps your DOM clean:

function View(vm) {
    function clickMe(e, node) {
        console.log(node.data.myVal);
    }

    return function() {
        return el("form", [
            el("button", {onclick: [clickMe], _data: {myVal: 123}}, "Click!"),
        ]);
    };
}

Notes:

vm.state & vm.api are userspace-reserved and initialized to null. You may use them to expose view state or view methods as you see fit without fear of collisions with internal domvm properties & methods (present or future).


Keys & DOM Recycling

Like React [and any dom-reusing lib worth its salt], domvm sometimes needs keys to assure you of deterministic DOM recycling - ensuring similar sibling DOM elements are not reused in unpredictable ways during mutation. In contrast to other libs, keys in domvm are more flexible and often already implicit.

  • Both vnodes and views may be keyed: el('div', {_key: "a"}), vw(MyView, {...}, "a")
  • Keys do not need to be strings; they can be numbers, objects or functions
  • Not all siblings need to be keyed - just those you need determinism for
  • Attrs and special attrs that should be unique anyhow will establish keys:
    • _key (explicit)
    • _ref (must be unique within a view)
    • id (should already be unique per document)
    • name or name+value for radios and checkboxes (should already be unique per form)

Hello World++

Try it: https://domvm.github.io/domvm/demos/playground/#stepper1

var el = domvm.defineElement;                       // element VNode creator

function StepperView(vm, stepper) {                 // view closure (called once during init)
    function add(num) {
        stepper.value += num;
        vm.redraw();
    }

    function set(e) {
        stepper.value = +e.target.value;
    }

    return function() {                             // template renderer (called on each redraw)
        return el("#stepper", [
            el("button", {onclick: [add, -1]}, "-"),
            el("input[type=number]", {value: stepper.value, oninput: set}),
            el("button", {onclick: [add, +1]}, "+"),
        ]);
    };
}

var stepper = {                                     // some external model/data/state
    value: 1
};

var vm = cv(StepperView, stepper);    // create ViewModel, passing model

vm.mount(document.body);                            // mount into document

The above example is simple and decoupled. It provides a UI to modify our stepper object which itself needs no awareness of any visual representation. But what if we want to modify the stepper using an API and still have the UI reflect these changes. For this we need to add some coupling. One way to accomplish this is to beef up our stepper with an API and give it awareness of its view(s) which it will redraw. The end result is a lightly-coupled domain model that:

  1. Holds state, as needed.
  2. Exposes an API that can be used programmatically and is UI-consistent.
  3. Exposes view(s) which utilize the API and can be composed within other views.

It is this fully capable, view-augmented domain model that domvm's author considers a truely reusable "component".

Try it: https://domvm.github.io/domvm/demos/playground/#stepper2

var el = domvm.defineElement;

function Stepper() {
    this.value = 1;

    this.add = function(num) {
        this.value += num;
        this.view.redraw();
    };

    this.set = function(num) {
        this.value = +num;
        this.view.redraw();
    };

    this.view = cv(StepperView, this);
}

function StepperView(vm, stepper) {
    function add(val) {
        stepper.add(val);
    }

    function set(e) {
        stepper.set(e.target.value);
    }

    return function() {
        return el("#stepper", [
            el("button", {onclick: [add, -1]}, "-"),
            el("input[type=number]", {value: stepper.value, oninput: set}),
            el("button", {onclick: [add, +1]}, "+"),
        ]);
    };
}

var stepper = new Stepper();

stepper.view.mount(document.body);

// now let's use the stepper's API to increment
var i = 0;
var it = setInterval(function() {
    stepper.add(1);

    if (i++ == 20)
        clearInterval(it);
}, 250);

Emit System

Emit is similar to DOM events, but works explicitly within the vdom tree and is user-triggerd. Calling vm.emit(evName, ...args) on a view will trigger an event that bubbles up through the view hierarchy. When an emit listener is matched, it is invoked and the bubbling stops. Like parameterized events, the vm and data args reflect the originating view of the event.

// listen
vm.cfg({
    onemit: {
        myEvent: function(arg1, arg2, vm, data) {
            // ... do stuff
        }
    }
});

// trigger
vm.emit("myEvent", arg1, arg2);

There is also a global emit listener which fires for all emit events.

domvm.cfg({
    onemit: {
        myEvent: function(arg1, arg2, vm, data) {
            // ... do stuff
        }
    }
});

Lifecycle Hooks

Demo: lifecycle-hooks different hooks animate in/out with different colors.

Node-level

Usage: el("div", {_key: "...", _hooks: {...}}, "Hello")

  • will/didInsert(newNode) - initial insert
  • will/didRecycle(oldNode, newNode) - reuse & patch
  • will/didReinsert(newNode) - detach & move
  • will/didRemove(oldNode)

While not required, it is strongly advised that your hook-handling vnodes are uniquely keyed as shown above, to ensure deterministic DOM recycling and hook invocation.

View-level

Usage: vm.cfg({hooks: {willMount: ...}}) or return {render: ..., hooks: {willMount: ...}}

  • willUpdate(vm, data) - before views's data is replaced
  • will/didRedraw(vm, data)
  • will/didMount(vm, data) - dom insertion
  • will/didUnmount(vm, data) - dom removal

Notes:

  • did* hooks fire after a forced DOM repaint.
  • willRemove & willUnmount hooks can return a Promise to delay the removal/unmounting allowing you to CSS transition, etc.

Third-Party Integration

Several facilities exist to interoperate with third-party libraries.

Non-interference

First, domvm will not touch attrs that are not specified or managed in your templates. In addition, elements not created by domvm will be ignored by the reconciler, as long as their ancestors continue to remain in the DOM. However, the position of any inserted third-party DOM element amongst its siblings cannot be guaranteed.

will/didInsert Hooks

You can use normal DOM methods to insert elements into elements managed by domvm by using will/didInsert hooks. See the Embed Tweets demo.

injectElement

domvm.injectElement(elem) allows you to insert any already-created third-party element into a template, deterministically manage its position and fire lifecycle hooks.

innerHTML

You can set the innerHTML of an element created by domvm using a normal .-prefixed property attribute:

el("div", {".innerHTML": "<p>Foo</p>"});

However, it's strongly recommended for security reasons to use domvm.injectElement() after parsing the html string via the browser's native DOMParser API.


Extending ViewModel & VNode

If needed, you may extend some of domvm's internal class prototypes in your app to add helper methods, etc. The following are available:

  • domvm.ViewModel.prototype
  • domvm.VNode.prototype

createContext

This demo in the playground shows how to implement VNode.prototype.pull() - a close analog to React's createContext - a feature designed to alleviate prop drilling without resorting to globals.


Isomorphism & SSR

Like React's renderToString, domvm can generate html and then hydrate it on the client. In server & full builds, vm.html() can generate html. In client & full builds, vm.attach(target) should be used to hydrate the rendered DOM.

var el = domvm.defineElement;

function View() {
    function sayHi(e) {
        alert("Hi!");
    }

    return function(vm, data) {
        return el("body", {onclick: sayHi}, "Hello " + data.name);
    }
}

var data = {name: "Leon"};

// return this generated <body>Hello Leon</body> from the server
var html = cv(View, data).html();

// then hydrate on the client to bind event handlers, etc.
var vm = cv(View, data).attach(document.body);

Notes:

  • target must be the DOM element which corresponds to the top-level/root virtual node of the view you're attaching
  • Whitespace in the generated HTML is significant; indented, formatted or pretty-printed markup will not attach properly
  • The HTML parsing spec requires that an implicit <tbody> DOM node is created if <tr>s are nested directly within <table>. This causes problems when no corresponding <tbody> is defined in the vtree. Therefore, when attaching tables via SSR, it is necessary to explicitly define <tbody> vnodes via el("tbody",...) and avoid creating <tr> children of <table> nodes. See Issue #192

Optimizations

Before you continue...

  • Recognize that domvm with no optimizations is able to rebuild and diff a full vtree and reconcile a DOM of 3,000 nodes in < 1.5ms. See 0% dbmonster bench.
  • Make sure you've read and understood Sub-views vs Sub-templates.
  • Ensure you're not manually caching & reusing old vnodes or holding references to them in your app code. They're meant to be discarded by the GC; let them go.
  • Profile your code to be certain that domvm is the bottleneck and not something else in your app. e.g. Issue #173.
    • When using the DEVMODE build, are the logged DOM operations close to what you expect?
    • Are you rendering an enormous DOM that's already difficult for browsers to deal with? Run document.querySelectorAll("*").length in the devtools console. Live node counts over 10,000 should be evaluated for refactoring.
    • Are you calling vm.redraw() from unthrottled event listeners such as mousemove, scroll, resize, drag, touchmove?
    • Are you using requestAnimationFrame() where appropriate?
    • Are you using event delegation where appropriate to avoid binding thousands of event listeners?
    • Are you properly using CSS3 transforms, transitions and animation to do effects and animations rather than calling vm.redraw() at 60fps?
    • Do thousands of nodes or views have lifecycle hooks?
  • Finally, understand that optimizations can only reduce the work needed to regenerate the vtree, diff and reconcile the DOM; the performed DOM operations will always be identical and near-optimal. In the vast majority of cases, the lowest-hanging fruit will be in the above advice.

Still here? You must be a glutton for punishment, hell-bent on rendering enormous grids or tabular data ;) Very well, then...

Isolated Redraw

Let's start with the obvious. Do you need to redraw everything or just a sub-view? vm.redraw() lets you to redraw only specific views.

Flatten Nested Arrays

While domvm will flatten nested arrays in your templates, you may get a small boost by doing it yourself via Array.concat() before returning your templates from render().

Old VTree Reuse

If a view is static or is known to not have changed since the last redraw, render() can return the existing old vnode to short-circuit the vtree regeneration, diffing and dom reconciliation.

function View(vm) {
    return function(vm) {
        if (noChanges)
            return vm.node;
        else
            return el("div", "Hello World");
    };
}

The mechanism for determining if changes may exist is up to you, including caching old data within the closure and doing diffing on each redraw. Speaking of diffing...

View Change Assessment

Similar to React's shouldComponentUpdate(), vm.cfg({diff:...}) is able to short-circuit redraw calls. It provides a caching layer that does shallow comparison before every render() call and may return an array or object to shallow-compare for changes.

function View(vm) {
    vm.cfg({
        diff: function(vm, data) {
            return [data.foo.bar, data.baz];
        }
    });

    return function(vm, data) {
        return el("div", {class: data.baz}, "Hello World, " + data.foo.bar);
    };
}

diff may also return a plain value that's the result of your own DIY comparison, but is most useful for static views where no complex diff is required at all and a simple === will suffice.

With a plain-object view, it looks like this:

var StaticView = {
    diff: function(vm, data) {
        return 0;
    },
    render: function(vm, data) {},
};

Notes:

If you intend to do a simple diff of an object by its identity, then it's preferable to return it wrapped in an array to avoid domvm also diffing all of its enumerable keys when oldObj !== newObj. This is a micro-optimization and will not affect the resulting behavior. Also, see Issue #148.

VNode Patching

VNodes can be patched on an individual basis, and this can be done without having to patch the children, too. This makes mutating attributes, classes and styles much faster when the children have no changes.

var vDiv = el("div", {class: "foo", style: "color: red;"}, [
    el("div", "Mooo")
]);

vDiv.patch({class: "bar", style: "color: blue;"});

DOM patching can also be done via a full vnode rebuild:

function makeDiv(klass) {
    return el("div", {class: klass, style: "color: red;"}, [
        el("div", "Mooo")
    ]);
}

var vDiv = makeDiv("foo");

vDiv.patch(makeDiv("bar"));

vnode.patch(vnode|attrs, doRepaint) can be called with a doRepaint = true arg to force a DOM update. This is typically useful in cases when a CSS transition must start from a new state and should not be batched with any followup patch() calls. You can see this used in the lifecycle-hooks demo.

Fixed Structures

Let's say you have a bench like dbmonster in this repo. It's a huge grid that has a fixed structure. No elements are ever inserted, removed or reordered. In fact, the only mutations that ever happen are textContent of the cells and patching of attrs like class, and style.

There's a lot of work that domvm's DOM reconciler can avoid doing here, but you have to tell it that the structure of the DOM will not change. This is accomplished with a domvm.FIXED_BODY vnode flag on all nodes whose body will never change in shallow structure.

var Table = {
    render: function() {
        return el("table", {_flags: domvm.FIXED_BODY}, [
            el("tr", {_flags: domvm.FIXED_BODY}, [
                el("td", {_flags: domvm.FIXED_BODY}, "Hello"),
                el("td", {_flags: domvm.FIXED_BODY}, "World"),
            ])
        ]);
    }
};

This is rather tedious, so there's an easier way to get it done. The fourth argument to defineElement() is flags, so we create an additional element factory and use it normally:

function fel(tag, arg1, arg2) {
    return domvm.defineElement(tag, arg1, arg2, domvm.FIXED_BODY);
}

var Table = {
    render: function() {
        return fel("table", [
            fel("tr", [
                fel("td", "Hello"),
                fel("td", "World"),
            ])
        ]);
    }
};

Fully-Keyed Lists

In domvm, the term "list", implies that child elements are shallow-homogenous (the same views or elements with the same DOM tags). domvm does not require that child arrays are fully-keyed, but if they are, you can slightly simplify domvm's job of matching up the old vtree by only testing keys. This is done by setting the domvm.KEYED_LIST vnode flag on the parent.

Lazy Lists

Lazy lists allow for old vtree reuse in the absence of changes at the vnode level without having to refactor into more expensive views that return existing vnodes. This mostly saves on memory allocations. Lazy lists may be created for both, keyed and non-keyed lists. To these lists, you will need:

  • A list-item generating function, which you should have anyways as the callback passed to a Array.map iterator. Only defineElement() and defineView() nodes are currently supported.
  • For keyed lists, a key-generating function that allows for matching up proper items in the old vtree.
  • A diff function which allows a lazy list to determine if an item has changed and needs a new vnode generated or can have its vnode reused.
  • Create a domvm.list() iterator/generator using the above.
  • Provide {_key: key} for defineElement() vnodes or vw(ItemView, item, key) for defineView() vnodes.

While a bit involved, the resulting code is quite terse and not as daunting as it sounds: https://domvm.github.io/domvm/demos/playground/#lazy-list

Special Attrs

VNodes use attrs objects to also pass special properties: _key, _ref, _hooks, _data, _flags. If you only need to pass one of these special options to a vnode and have not actual attributes to set, you can avoid allocating attrs objects by assigning them directly to the created vnodes.

Instead of creating attrs objects just to set a key:

el("ul", [
    el("li", {_key: "foo"}, "hello"),
    el("li", {_key: "bar"}, "world"),
])

Create a helper to set the key directly:

function keyed(key, vnode) {
    vnode.key = key;
    return vnode;
}

el("ul", [
    keyed("foo", el("li", "hello")),
    keyed("bar", el("li", "world")),
])
Comments
  • upcoming v3 changes

    upcoming v3 changes

    • views will no longer be auto-keyed by the passed-in model. it's too surprising and i've had to explain it too many times. explicitly keying by the model when needed isnt all that difficult.
    • domvm.lazyList() creator & domvm.LAZY_LIST flag for creating deferred homogenous children that can reuse old vnodes without additional allocation.
    • render() will be able to return vm.node (the old vnode) to prevent/optimize redraw when no changes.
    • xlink:href support in svg
    • vm.diff() & vm.hook() will be removed in favor of what is already possible:
      • ~~direct assignment vm.hooks = {}~~
      • new: vm.config({hooks:..., diff...})
      • object return {hooks: ..., diff:..., render: ...}
      • externally passing in through opts: {hooks: ..., diff: ...}
    • dist builds may be split into a separate branch, since merging and rebasing are currently a nightmare and always result in conflicts.

    v3 should be ready within the next couple weeks.

    discuss 
    opened by leeoniya 66
  • Performance on large HTML graphs.

    Performance on large HTML graphs.

    Opening a new issue only to get this on the radar; I am not able to prioritize this for the next two weeks as the CEO is in town an I am demoing the proof-of-concept and will be in strategic meetings. However, I will definitely come back to this...

    On another (only tangentially related) thread I said:

    I do have to say, for my mildly complex view of a few thousand nodes there is noticeable lag that, so far, I can only attribute to view generation and downstream. But I haven't looked into it deeply since function is a much higher priority to me.

    It turns out that when I dumped and formatted the HTML my "few thousand" is closer to 8K. I have all the major details of my UI in place, now.

    I am seeing a rendering time of about 500ms to render. And about 700 ms in paint. This is on Firefox 47 and may be directly related to that. Chrome seems quite spritely by comparison, though still not quite as snappy as I'd hoped. The total is about a second of latency.

    I'll report back here as I make progress. The reformatted HTML is attached if you are curious. Please understand that this is still just a prototype.

    Screen-80x24.txt

    Also for the curious, the perf graph:

    renderperf

    opened by lawrence-dol 64
  • domvm 2.x-dev

    domvm 2.x-dev

    The 1.x view module's codebase is getting increasingly harder to reason about and properly optimize. Rather than trying to fight perf death by 1,000 papercuts, I've decided to test a clean-slate rewrite to see what can be learned.

    The current codebase does a lot of type-checking and value testing to process the JSONML templates (often in multiple places and several preprocessing passes). In addition there is the non-congruence in the template structure because everything must be in array-style declarative form. On top of that there is value coercion and other "helpful" features that are rarely used but always degrade perfomance even when they arent.

    What I've done with the rewrite is to start with a hyperscript API & explicit child arrays that delegates some of the type-checking to the user and generates vnodes directly, reducing many allocations and preproc code. In addition, I've added a fluent API that can be used to set special vnode properties directly rather than forcing object allocation (though both will still be supported with different speed implications):

    // 1.x
    [".test", {_key: "abc"}, "hello world"];
    
    // 2.x
    h(".test", {_key: "abc"}, "hello world");     // faster than 1.x
    h(".test").key("abc").body("hello world");    // fastest
    
    // 2.x child arrays must be explicit
    h(".test", [...]);
    h(".test", {_ref: "myNode"}, [...]);
    h(".test").ref("myNode").body([...]);
    

    In addition to a much leaner/cleaner codebase, the performance of 2.x is significantly better than 1.x. While it's still early and most features have not been ported. One of the things that 2.x excels at is "doing nothing". The current 1.x branch has 3.9ms overhead at 0% mutation while 2.x is 1.5ms. This is a massive improvement, besting the current Mithril rewrite by 1.1ms (2.6ms overhead). At 50% mutation, 2.x gains an extra 5fps, going from 47fps => 52fps. The code changes are trivial:

    Current JSONML implementation:

    let render = (vm, dbs) =>
        ["div",
            ["table.table.table-striped.latest-data",
                ["tbody",
                    dbs.map(db =>
                        ["tr",
                            ["td.dbname", db.dbname],
                            ["td.query-count",
                                ["span", { class: db.lastSample.countClassName }, db.lastSample.nbQueries]
                            ],
                            db.lastSample.topFiveQueries.map(query =>
                                ["td", { class: query.elapsedClassName },
                                    ["span", query.formatElapsed],
                                    [".popover.left",
                                        [".popover-content", query.query],
                                        [".arrow"],
                                    ]
                                ]
                            )
                        ]
                    )
                ]
            ]
        ]
    };
    

    2.x hyperscript:

    let h = domvm.view.createElement;
    
    let dbmon = (vm, dbs) =>
        h("div", [
            h("table.table.table-striped.latest-data", [
                h("tbody", dbs.map(db =>
                    h("tr", [
                        h("td.dbname", db.dbname),
                        h("td.query-count", [
                            h("span", { class: db.lastSample.countClassName }, db.lastSample.nbQueries)
                        ]),
                        db.lastSample.topFiveQueries.map(query =>
                            h("td", { class: query.elapsedClassName }, [
                                h("span", query.formatElapsed),
                                h(".popover.left", [
                                    h(".popover-content", query.query),
                                    h(".arrow"),
                                ])
                            ])
                        )
                    ])
                ))
            ])
        ])
    

    If anyone still wishes to use JSONML (and for migration purposes), something like a domvm.jsonml module will be made to preprocess old-style templates into a properly formed vtree. This way, with known opt-in overhead, you can still use JSONML definitions:

    let jml = domvm.jsonml;
    
    var tpl = jml(["div", {},
        ["br"],
        ["table",
            ["tr"]
        ],
    ]);
    

    I'm continuing to test which aspects of 1.x imposed penalties that were paid regardless of actual usage. One thing that may go away is objects as keys. this Map and Weakmap are slow, so keys will likely need to be either numbers or strings to be used in a js hash. This will allow much faster keyed matching and make destroyed view's dom reuse possible, among other things. Another thing that may change is ancestor redraw(level) and emit(ev, args...) may be moved out to modules as they only depend on each vnode having a parent reference, so can be cleanly implemented in optional modules.

    As soon as I reimplement the important aspects of the lib and rewrite the tests, I will push the 2.x branch to Github.

    discuss 
    opened by leeoniya 54
  • Memory Leak on Render

    Memory Leak on Render

    I've been chasing a severe memory leak since Saturday, that seems to boil down to references internal to DOMVM. The situation is a relatively simple, but large (4148 elements), list of pairs of string rendered in a DIV (translations, source text and target text).

    Every render grows memory significantly, until I crash the 32 bit browser at a little under 4 GBs.

    The usage pattern in the debugger from a heap differential looks like this, where it all seems to end with initElement, as if the elements are being cached and never discarded:

    image

    Any thoughts on where to go with this? Is there something my code might be doing to cause old vNode elements to be retained from render to render?

    Version 3.0.2.

    opened by lawrence-dol 53
  • view-scoped CSS via JSS

    view-scoped CSS via JSS

    Now that view closures can return {render: ...} objects, we should evaluate offering the option of returning {css: ...} as part of the mix, possibly accepting a JSS-style [1] stylesheet definition. Each vm is already tagged with a globally unique sequential .id, so the styles can be prefixed to isolate/prevent leakage. Another option is to prefix the defs with the function.name of the the view closure. This means all views created using the same closure would by styled uniformly. In both cases the view's root nodes would need the generated prefixes auto-appended to their className.

    JSS pairs well with vdom/hyperscript style libs since it's js-assembled CSS, much like like js-assembled DOM.

    This would make for a good isolated and high-ROI project if anyone wants to take charge, since it wouldn't require intricate familiarity with domvm's internals. 98% of it can live decoupled as an addon.

    cc @grumpi, @yosbelms, @lawrence-dol

    [1] https://github.com/cssinjs/jss

    enhancement help wanted 
    opened by leeoniya 44
  • VM/Node finders

    VM/Node finders

    Motivation

    Find a sibling, children, or a node with a complex related position. Find a node or a vm inside the tree.

    Proposal

    Two methods for vm and node. Those methods are down and up.

    down: finds a item which fulfill the specified condition in the tree starting from the children of the node in question. Example:

    var node = vm.down('someKey')
    
    

    Finds a node or vm with that key starting from the vm children

    up: the same with down but iterating over parents up in the tree.

    var parent = vm.up('someKey')
    

    Find a ancestor whith someKey key.

    Usage example:

    // root view with 2 keyed children
    function RootView(vm, data){
        return function(){
            return ['div'
                [ChildView1, data, 'ch1']
                [ChildView2, data, 'ch2']
            ]
        }
    }
    
    // children view 1
    function ChildView1(vm) {
    
        function click() {
            // redraw a keyed sibling
            // without redrawing the parent
            vm.up().down('ch2').redraw()
        }
    
        return function() {
            // execute update on click
            return ['button', {onclick: update}]
        }
    }
    
    // children view 2
    function ChildView2() {
        ...
    }
    

    ChildrenView1 redraws the sibling without triggering redraw in the parent.

    opened by yosbelms 43
  • Setting useRaf=false in config results in exception

    Setting useRaf=false in config results in exception

    Another problem which does not show in a simple fiddle.

    My application does not use routes. When I add domvm.view.config({ useRaf: false }); right before mounting the root view, I get an "impossible" exception on first redraw:

    19:42:23.807 TypeError: vm.render is null
    redraw() AppScript;01:105
    receiveData() AppScript;01:595
    init() AppScript;01:592
    MdlHostReplay() AppScript;01:605
    view() AppScript;01:544
    createView() AppScript;01:92
    init() AppScript;01:542
    AppMain() AppScript;01:549
    <anonymous> App:24
    1 AppScript;01:105:86
    

    Inspecting the vm object, it does indeed not contain a render function. All I need to do to fix things is remove that line. (I only added it because your fiddle had it and I was checking to see if it impacted #56.)

    Also, it doesn't appear to matter what you supply for config; even an empty object causes the exception.

    opened by lawrence-dol 42
  • Coming from Mithril

    Coming from Mithril

    Hey @mikegleasonjr,

    Before you hack up Mithril, take a look at domvm. I had similar issues with Mithril as the ones you've been discussing. Personally, I wanted to gut and/or externalize the "magic" and make it opt-in, have the framework be both concise and no-surprises as to what the internals were doing. I eliminated global auto-redraw and made it an explicit thing and made all sub-components independently redraw-able. I took some of the great ideas of Mithril and domchanger and wrote a new animal. There is less structural enforcement, so you can compose apps as best fits you needs. I am still writing some demo apps to showcase its usage in complex situations, such as http://threaditjs.com/. Currently, it's 2x faster than Mithril on dbmonster and has nearly identical syntax for templates, so porting a small app for investigation shouldn't require massive changes.

    Router is almost ready but still has a couple bugs w/ the History api to fix.

    Mithril's auto-redraw observers are implemented fully externally in /src/watch.js. For example, this is how opt-in global auto-redraw works in domvm with props:

    function randInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    
    // a view
    function PersonView(vm, person) {
        return function() {
            return [".person",
                ["strong", person.name],
                " ",
                ["em", {onclick: randomizeAge}, person.age],
            ]
        }
    
        function randomizeAge(e) {
            person.age(randInt(0,100));
        }
    }
    
    // create an observer
    var w = domvm.watch(function(ev) {
        maryVm.redraw();
    })
    
    // some model/data
    var mary = {
        name: "Mary",
        age: w.prop(25),      // use observer
    };
    
    var maryVm = domvm(PersonView, mary).mount(document.body);
    

    I'd be happy to answer any questions about how to get specific things done if you're coming from Mithril.

    I don't plan to support IE8, so if you need that, please ignore this message :)

    cheers!

    discuss 
    opened by leeoniya 38
  • Decoupled model and view where model has properties which trigger redraws on the view

    Decoupled model and view where model has properties which trigger redraws on the view

    I've been dancing around this issue since #60, but it's getting progressively more difficult. How would you recommend that a decoupled data-model which needs to trigger redraws on the vm that uses it be constructed?

    The essential problem being that the properties of the model can't be created until the vm reference is available, but the model wants to be created before the view and injected into it.

    var model=new DataModel();
    var view =domvm.view(new DataView(model)); // or (new DataView(),model)
    

    But DataModel needs the DataView view-model to do the moral equivalent of:

    var w=domvm.watch(function() { vm.redraw(); });
    ...
    exported.someProp=w.prop("default");
    

    So the model can't be injected into the view, and the view can't be injected into the model. I can't see a clean way to do this.

    opened by lawrence-dol 35
  • Solicit some feedback

    Solicit some feedback

    @lawrence-dol @barneycarroll @isiahmeadows @pygy @mindeavor @stevenvachon @kuraga @Zolmeister @ciscoheat @tivac

    Hi guys, thought you may find this interesting....

    I've been evaluating isomorphic, component-based, pure-js-template vdom frameworks for a while and something about each one didn't feel "right" to me. For Mithril, it was the somewhat odd MVC terminology/architecture [1], the always-global redraw and speed was not always great. Afterwards I evaluated domchanger, which was small and minimal, but it too was somewhat slow and the component architecture was close, but still didnt feel quite right [2]. Other issues still with virtual-dom. I looked at the blazing fast Inferno and Cito but didn't quite like the verbosity.

    [1] https://github.com/lhorie/mithril.js/issues/499 [2] https://github.com/creationix/domchanger/issues/5

    So I did what any self-respecting coder suffering from NIH syndrome would do: I wrote yet another vdom lib. It's an interesting mix of ideas from different frameworks and it's near cito in terms of perf on dbmonster and speedtest (see /test/bench). The docs are still a work in progress, but the main API can be seen in the README of this repo. I'm finishing up implementing some of the good ideas from Mithril for magic auto-redraw (m.prop, m.withAttr, m.request) on top of the core. The plan is to leave the Promise and ajax/fetch implementation up to the end user, but add support for handling promises if provided by the user where it makes sense. Routing is also open to evaluation.

    The focus has been speed, isomorphism, concise templates, isolated components, minimizing domain-to-view coupling or having to impose a specific structure onto domain models.

    DBmonster demo: http://leeoniya.github.io/domvm/test/bench/dbmonster/

    I would appreciate some feedback if any of you are interested and have time to test it out, otherwise please ignore/unsubscribe from this thread :) Feel free to ask any questions about how to accomplish specific tasks.

    Here's a quick demo of how to compose multiple components and perform independent sub-view redraw. Note that this is just one example, but you don't need create models via constructor functions, they can be plain objects or arrays as well - the coupling can be even looser and external to the models entirely. Nor are models limited to a single view, you can create as many views of a single model as you required and compose those as needed.

    Code below also on jsfiddle: https://jsfiddle.net/0Lt4kzLt/

    // domain model/data
    function List(items) {
        this.items = items;
    
        this.view = [ListView, this];   // minimal view coupling: [viewFn, model/ctx, _key]
    }
    
    // view
    function ListView(vm, list, _key) {
        // any view state can be stored in this closure, it executes once on init
    
        // export the vm back to the model so `vm.redraw()` etc can be invoked from
        // outside this closure. not required but you'll usually want to.
        list.vm = vm;
    
        return {
            render: function() {
                return ["ul", list.items.map(function(item) {
                    return item.view;
                })];
            }
        }
    }
    
    function Item(name, qty) {
        this.name = name;
        this.qty = qty;
    
        this.view = [ItemView, this];
    }
    
    function ItemView(vm, item, _key) {
        item.vm = vm;
        return {
            render: function() {
                return ["li", [
                    ["strong", item.name],
                    " ",
                    ["em", item.qty],
                ]];
            }
        }
    }
    
    var items = [
        new Item("a", 5),
        new Item("b", 10),
        new Item("c", 25),
    ];
    
    var myList = new List(items);
    
    // this root function inits and returns the vm of the top-level view but since
    // we also export it to <model>.vm, we don't need to use this returned one
    var vm = domvm(myList.view);
    
    // append it to a container
    myList.vm.mount(document.body);
    
    // now we can modify an item and redraw just that item
    setTimeout(function() {
        myList.items[0].qty += 10;
        myList.items[0].vm.redraw();
    }, 1000);
    
    // or remove one, add a couple, mod some and redraw everything
    setTimeout(function() {
        myList.items.shift();
    
        myList.items.push(
            new Item("foo", 3),
            new Item("bar", 66)
        );
    
        myList.items[0].name = "moo";
        myList.items[1].qty = 99;
    
        myList.vm.redraw();
    }, 3000);
    

    cheers! Leon

    opened by leeoniya 34
  • domvm 2.x-dev (part 3)

    domvm 2.x-dev (part 3)

    ...continuation of #101

    The view layer is now in final form.

    Hopefully this discussion will be the last needed prior to v2.0.0 release and focused mostly on ironing out the router.

    Some internal refinements have been postponed till after 2.0.0:

    • Exposed ref cleanup from ancestors when unmounting subviews. The fact that they can also be namespaced makes this somewhat tricky and possibly expensive.

    Features that will be considered for exploration post-2.0.0:

    • fragment views
    • dom recycling of unmounted views
    • internally using addEventListener instead of on* props. This would allow things like ontransitionend which cannot be bound using props. Tracking a registry of attached events and patching them in all forms (including delegation and parameterization) makes this tricky/possibly expensive.
    opened by leeoniya 33
  • Bad nodes in event handlers.

    Bad nodes in event handlers.

    Recently, as my app has grown more complicated, I have been seeing uncaught exceptions in event handing at:

    function handle(evt) {
    	let elm = evt.currentTarget,
    		dfn = elm._node.attrs["on" + evt.type];
                    //        ^^^^^ -- TypeError: can't access property "attrs", e._node is undefined
    
    	if (isArr(dfn)) {
    

    I am pretty sure that these are race conditions with events that get delivered just after the DOMVM structure is torn down. I can't find any means in user-land to either handle or suppress them, and they are difficult to replicate, happening "occasionally" in production (where they are harmless, but look really ugly).

    @leeoniya : Do you have any in-principle objection to adding a null check, either the modern elm?._node... or the older elm._node && elm._node...?

    This seems inescapable in my case, where I have a controlling app loading other DOMVM apps in iframes, and the iframe can be closed at any time by the user, resulting in the framed app being torn down at any arbitrary moment.

    opened by lawrence-dol 2
  • Incongruence of Normal and Parameterized Handlers

    Incongruence of Normal and Parameterized Handlers

    @leeoniya

    I am opening this as a new issue because I want others to be able to find it easily, and I don't want to confuse the issue with the recent enhancements to event handling (#235). So this should be food for thought (I would have opened a discussion item, if discussions were enabled). I should note up front that I don't really have a big dog in this fight, since I don't use this and with recent changes I can now live with simply always using parameterized handlers.

    There remain four ways in which DOMVM parameterized event handlers (henceforth p-handlers) are inconsistent with JavaScript event handlers (henceforth e-handlers):

    1. The this for e-handlers is the current target; for p-handlers it's the current vm.
    2. The first argument for e-handlers is the event; for p-handlers the event argument is after whatever arguments were attached in the handler declaration.
    3. The only argument for e-handlers is the event; for p-handlers DOMVM implicitly adds node, vm, and vm.data.
    4. The onevent handlers are not called for e-handlers, and they are for p-handlers.

    My issue with these differences is that none are technically necessary, and the differences create additional cognitive load and code fragility for me. Given that there's no longer a performance penalty for one or the other, I feel the differences are no longer technically justified. Note that I am not intending to speak to compatibility concerns -- that alone might render the entire discussion moot.

    When I am deciding between an e-handler and a p-handler the only factor in view is whether or not I need to bind context arguments. Often, as the design shifts, and especially when I am first developing a component, a specific handler will change from normal to parameterized, or vice versa. Doing so makes the existing handler code broken in subtle ways, and it's easy to forget one of them, which becomes a runtime error, often a fatal one, that occurs only when that event handler is actually triggered. Painful to test and easy to miss.

    The cognitive load increase is related, but applies to every handler I write -- is it an e-handler or p-handler; does it have arguments of evt,vm,nod,dta or just evt. Will the onevent handler be called or not (if I am using an auto-redraw system this could be critical). For programmers accustomed to this, I imagine having this be the current target in one context and the vm in another would be a constant source of frustration.

    As reflected in my recent pull request these issues could now be easily addressed, and though doing so would break compatibility a case could be mounted that these are bugs.

    The changes would be:

    1. Bind this to currentTarget.
    2. Always use the exec function.
    3. Bind added arguments after the event, so instead of (...,evt,node,vm,data) use evt,...,node,vm,data).

    The argument for #1 is purely consistency with JavaScript in general. The this value is always current-target, for every event handler I see in every JavaScript context.

    The argument for #2 is that the extra arguments do no harm and consistency within DOMVM code is better than inconsistency. This is the one that is the least important; there seems to be some defense for only adding the DOMVM arguments to p-handlers, so I lean toward doing this, but not strongly.

    The argument for #3 is that the event is the "fundamental" JavaScript argument, the explicitly declared arguments are context-specific, and node, vm, and data are implicit DOMVM "value-added" arguments. The event argument is always there, and it's always first, for every event handler I see in every JavaScript context -- the precedent for this is overwhelming. The explicit added arguments specified in the declaration seem to be naturally expected next, then finally the implicit DOMVM arguments.

    As I said, food for thought; and in my opinion worth a major version bump and a breaking change to remove this constant source of friction.

    enhancement breaking-change 
    opened by lawrence-dol 5
  • Next Steps

    Next Steps

    the current dom insertion/removal/reordering reconciler [1] relies on 1:1 vnode-to-element mapping. this design prevents proper implementation of fragment vnodes, which in turn forces all views to return a single defineElement() vnode.

    to resolve this impasse the reconciler must be:

    1. modified to use the old vtree rather than the dom. this is fairly straightforward.
    2. augmented for handling fragment vnodes. this is a bit more involved but workable.
    3. optimized to do parent-down rather than children-up reordering. this can significantly reduce the DOM load by not doing meaningless DOM ops. for instance, in React's fragment impl, a 12 fragment reversal (with in-fragment reversal) results in 34 appendChild calls [2]. the ideal count should be 23 appendChild (or insertBefore) calls. this appears to translate to ideal fragment revrsal (11) followed by in-fragment reordering (23). the issue in domvm's case is that it has will/didReinsert lifecycle hooks, so rather than calling 1 per move, it would call multiple (one for each useless move). i think this nicely illustrates the perf overhead that can grow exponentially in dealing with fragments.

    [1] https://github.com/leeoniya/domvm/blob/3.x-dev/src/view/syncChildren.js [2] https://codepen.io/anon/pen/zpdRKQ?editors=1000

    enhancement discuss 
    opened by leeoniya 5
  • TypeScript declarations

    TypeScript declarations

    i've started writing a domvm.d.ts file to make domvm pleasant to use with VSCode. so far i'm going through the following resources/examples written by people more familiar with TS:

    https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html https://basarat.gitbooks.io/typescript/docs/types/ambient/d.ts.html https://github.com/hyperapp/hyperapp/blob/master/hyperapp.d.ts

    it's likely the initial version will not be optimal and could be reduced with generics, etc. if anyone has TS experience, i'd appreciate any improvements or recommendations.

    cc @lawrence-dol @tropperstyle @iamjohnlong @Veid @logaan @sabine

    enhancement todo 
    opened by leeoniya 6
  • CSS transition helpers

    CSS transition helpers

    it would be nice to have some declarative transition support in domvm. i'm not sure what the api should look like, maybe {_trans: }, or if it can be an external plugin that will just set up the proper hooks as i do in the ModalStack [1] demo [2]...which is not general enough, but some bits can be adopted perhaps.

    api proposals and/or fiddles now being accepted!

    http://johan-gorter.github.io/maquette-demo-hero/ https://github.com/zaceno/hyperapp-transitions https://rawgit.com/snabbdom/snabbdom/master/examples/hero/index.html

    cc @iamjohnlong @sabine

    [1] https://github.com/leeoniya/domvm/blob/3.x-dev/demos/ModalStack/index.html [2] https://cdn.rawgit.com/leeoniya/domvm/3.x-dev/demos/ModalStack/index.html

    project 
    opened by leeoniya 1
Releases(3.4.14)
Absolutely minimal view layer for building web interfaces.

Superfine Superfine is a minimal view layer for building web interfaces. Think Hyperapp without the framework—no state machines, effects, or subscript

Jorge Bucaran 1.6k Dec 29, 2022
A dependency-free JavaScript ES6 slider and carousel. It’s lightweight, flexible and fast. Designed to slide. No less, no more

Glide.js is a dependency-free JavaScript ES6 slider and carousel. It’s lightweight, flexible and fast. Designed to slide. No less, no more What can co

null 6.7k Jan 3, 2023
Fast and lightweight dependency-free vanilla JavaScript polyfill for native lazy loading / the awesome loading='lazy'-attribute.

loading="lazy" attribute polyfill Fast and lightweight vanilla JavaScript polyfill for native lazy loading, meaning the behaviour to load elements rig

Maximilian Franzke 571 Dec 30, 2022
Thin wrapper around Rant-Lang for Obsidian.md

Obsidian Rant-Lang Thin wrapper around the Rant language Rust crate to be used in Obsidian. "Rant is a high-level procedural templating language with

Leander Neiss 10 Jul 12, 2022
A thin wrapper around arweave-js for versioned permaweb document management.

?? ar-wrapper A thin wrapper around arweave-js for versioned permaweb document management. Helps to abstract away complexity for document storage for

verses 8 May 12, 2022
Bitcoin thin client for iOS & Android. Built with React Native Google Colab

Run bluewallet-Google-Colab https://colab.research.google.com/drive/1OShIMVcFZ_khsUIBOIV1lzrqAGo1gfm_?usp=sharing Thin Bitcoin Wallet. Built with Reac

DE MINING 0 Feb 25, 2022
A thin, opinionated headless wiki with few features.

thinwiki A thin, opinionated headless wiki with few features. Git used as a backing store Markdown files with front matter used for pages index.md pag

Ava Chaney 2 Oct 13, 2022
A plugin for Strapi CMS that adds a preview button and live view button to the content manager edit view.

Strapi Preview Button A plugin for Strapi CMS that adds a preview button and live view button to the content manager edit view. Get Started Features I

Matt Milburn 53 Dec 30, 2022
StarkNet support extension for VSCode. Visualize StarkNet contracts: view storage variables, external and view functions, and events.

StarkNet Explorer extension This VSCode extension quickly shows relevant aspects of StarkNet contracts: Storage variables of the current contract, and

Crytic 6 Nov 4, 2022
Custom Vitest matchers to test the state of the DOM, forked from jest-dom.

vitest-dom Custom Vitest matchers to test the state of the DOM This library is a fork of @testing-library/jest-dom. It shares that library's implement

Chance Strickland 14 Dec 16, 2022
An extension of DOM-testing-library to provide hooks into the shadow dom

Why? Currently, DOM-testing-library does not support checking shadow roots for elements. This can be troublesome when you're looking for something wit

Konnor Rogers 28 Dec 13, 2022
a lightweight, dependency-free JavaScript plugin which makes a HTML table interactive

JSTable The JSTable is a lightweight, dependency-free JavaScript plugin which makes a HTML table interactive. The plugin is similar to the jQuery data

null 63 Oct 20, 2022
Small, typed, dependency free tool to round corners of 2d-polygon provided by an array of { x, y } points.

round-polygon Small, typed, dependency-free tool to round corners of 2d-polygon provided by an array of { x, y } points. The algorithm prevents roundi

Sergey Borovikov 10 Nov 26, 2022
A low-feature, dependency-free and performant test runner inspired by Rust and Deno

minitest A low-feature, dependency-free and performant test runner inspired by Rust and Deno Simplicity: Use the mt test runner with the test function

Sondre Aasemoen 4 Nov 12, 2022
The JSTable is a lightweight, dependency-free JavaScript plugin which makes a HTML table interactive

The JSTable is a lightweight, dependency-free JavaScript plugin which makes a HTML table interactive. The plugin is similar to the jQuery data

null 63 Oct 20, 2022
This is a dependency-free easy-to-use vanilla JavaScript addon allowing you to create HTML currency inputs with various different currencies and formattings.

intl-currency-input This is a dependency-free easy-to-use vanilla JavaScript addon allowing you to create HTML currency inputs with various different

null 6 Jan 4, 2023
A dependency-free JavaScript library for creating discreet pop-up notifications.

Polipop A dependency-free JavaScript library for creating discreet pop-up notifications. Demo See demo at minitek.github.io/polipop/. Documentation Se

Minitek 8 Aug 15, 2022
DateTimePickerComponent is a very lightweight and dependency-free web component written in pure JavaScript

DateTimePickerComponent Description DateTimePickerComponent is a very lightweight (just over 20KB) and dependency-free web component written in pure J

null 14 Dec 24, 2022
A dependency-free Vanilla JS Accordion Menu Nested

?? A dependency-free Vanilla JS Accordion Menu Nested No dependencies, no automation build tools. Copy/paste and ready to use. CSS and JS are inlined

Tomasz Bujnowicz 4 Dec 18, 2022