Coder Social home page Coder Social logo

domvm / domvm Goto Github PK

View Code? Open in Web Editor NEW
611.0 26.0 27.0 8.29 MB

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

Home Page: https://domvm.github.io/domvm/

License: MIT License

JavaScript 97.75% HTML 0.40% CSS 1.84%
virtual-dom vdom lightweight view-layer fast minimal

domvm's Introduction

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


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 paldepind/flyd#176 (comment)). 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")),
])

domvm's People

Contributors

jlgrall avatar lawrence-dol avatar leeoniya avatar xz64 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

domvm's Issues

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] MithrilJS/mithril.js#499
[2] creationix/domchanger#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

extending the vm in app-space

@lawrence-dol
@yosbelms

While I want to keep the lib core small, I don't want to enforce sometimes-lengthy-but-uniform ergonomics [1] [2] onto users. I'm thinking of adding a simple global hook that would extend every vm, kinda like Object.create or .prototype.myMethod.

Example:

domvm.view.extend(function(vm) {
    // node locator
    vm.up = function findNodeAbove(filter) {
        ....
    };

    // short version to get dom elems
    vm.el = function getDomRef(refName) {
        return vm.refs[refName].el;
    };

    // bala dom ref wrapper (https://github.com/finom/bala)
    vm.$ = function getDomRef(refName) {
        return $(vm.el(refName));
    };
});

The user will have to be careful not to clobber native methods and understand that any extend would need to be vetted against all domvm version upgrades since collisions may occur as the vm's native API could expand.

This could also serve as a vetting ground for useful future additions to the vm core.

[1] vm.refs.myRef.el (dom element)
[2] vm.redraw(someHugeNum) (redraw root)

Play nice with 3rd party libs

There are plans to add a couple features that would allow easy use of third-party libs.

Guarded nodes are nodes that are created and then protected from re-use by domvm, allowing you to use them as containers for other libs without worrying about them being mutated.

["div", {_guard: true}, someContent]

A way to sync a DOM node back into domvm's vtree. Not sure if this can be offered at node-level or only vm-level, but the latter is probably most likely. This allows domvm-managed nodes to be modified but then have their css or other properties re-synced back into the vtree.

vm.absorb();

For another issue, since some features cover multiple purposes: Lifecycle hooks either at node level and/or vm level, pub/sub event listerners/hooks. Implemented.

Routing and a missing link

I am having a little trouble trying to get my basic application airborne. I can't seem to make the connection between the initial route and initialization. My route is set up in the router:

    var exported            =this || {};

    function init() {                                                                                   // self-contained initialization avoids leaking temp objects into module closure
        router.config({
            useHist         : true,
            root            : "/"+window.location.pathname,

            init: function() {
                /**/console.log("[**] router config init",window.location.pathname);
                appMain.state.view=domvm.view(appMain.View,{ appMain: appMain, router: router });
                appMain.state.view.mount(document.body);
                appMain.state.view.hook({
                    willRedraw: function() {
                        /**/console.log("[**] Redraw App");
                        },
                    didRedraw: function() {
                        /**/console.log("[**] Redraw App done");
                        },
                    });

                router.refresh();
                },
            });
        Object.freeze(exported);
        }

    exported.root=Object.freeze({
        path: "/",

        onenter: function(segs) {
            /**/console.log("[**] Entered root path");
            document.title=setTitle();
            },
        })

    init();

(I use a pattern whereby I assign this to exported and then make separate assignments into exported.)

The console logs:

20:10:10.810 [**] router config init /temjs/App1 AppScript;01:164:235
20:10:10.812 Could not find route1 AppScript;01:37:473

Though what actually displays is "Could not find route", suggesting the trailing 1 in the copied text is a Firefox bug.

Comment the purpose of each function

It would be helpful if there was a comment before each function that gave a short description of what it does and what it's used for. I know a few are commented but it would make it much easier to read the source if you could add comments for every function.

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!

Calendar example has TypeError at dominstr.js 116

The calendar example () throws this exception for all mouse clicks:

14:23:05.566 TypeError: can't redefine non-configurable property "innerText"
DOMInstr/this.start() dominstr.js:116
opts.hooks.willRedraw() calendar.html:70
u.execAll/<() util.js:64
forEach() self-hosted:216
u.execAll() util.js:63
redraw() view.js:195
call() util.js:110
1 dominstr.js:116:1

FireFox: 44.0.2 on Windows 10.0.10240.

missing docs

Here is my (non-exhaustive) list of things to document. I will keep it updated as they are finished or as more are added.

API

  • domvm.utils.repaint(vnode) + any other useful util methods
  • vm.patch()
    • vm.patch(oldNode, newTpl)
    • vm.patch(oldNode, {style:..., class:...})
  • vm.api (=== this in render() and init closure)
  • domvm.view.extend(function(vm) {...}) (view monkey-patching)
  • vm.unmount()
  • vm.mount(target, true) will treat target as root and clear it first
  • vm.update(newModel) for false-handle views (stateful slots)
  • namespaced refs: _ref: "a.b.c" => vm.refs.a.b.c
  • exposed refs (_ref: "^moo")
    • using them for a restricted event bus/selective sub-view redraw
  • node-level _diff: [cmpFn, data1, data2] as alternative to splitting off sub-views & caching old values
  • vm-level vm.diff(function(model) {return [model.data1, model.data2];});
  • returning false from render() as sub-view perf optim (inactive sub-views, 0-diff)
  • explicit tagging of child arrays kids._expl = true
  • willUpdate vm hook
  • passing hooks into sub-view from parent via opts
  • ["p.moo", {class: "active"} is additive and results in <p class="moo active">
  • watch, hooks and evctx params in view opts
  • full sigs of all callbacks/user-supplied funcs
  • examples of using _data
  • entire watch module
    • w.get/post/etc
      • signatures
      • success/error callbacks
    • w.prop
      • async props with initial values
      • asyc prop.update()
      • pass false to mutate without firing handler, or alternate handler
      • middleWare param for validation/value transforming
    • w.sync
    • w.fire
  • todo: add vm.route.extend?

General

  • explain what that vm is the lib's native api portion of the ViewModel
  • make it clear that all views require a single root node
  • the fact that name and _ref implicitly set _key if not present
  • no need for explicit props of some common input attrs, like ".checked" etc..
  • more detailed lifeycle hooks docs
  • more routing examples
  • document vtree/vm architecture #72 (comment)

Footguns

  • add section explaining handle/key and model persistence implications for state retention and DOM recycling #83
  • if model is destroyed and recreated on every redraw() (like via JSON.parse), opt view out of persistence to ensure dom is reused rather than re-created: [myView, tempData, false]
  • encourage use of sub-renderers to break up templates instead of sub-views when not necessary, since perf is better and nodes can carry persistent state via a combo of _ref and _data
  • explain that using _key is not a perf optimization and not required if model is persistent. and should be used only for json-style re-created data or to enforce destruction of state and prevent dom recycle.
  • show that while viewFn (init closure) and render() sigs are the same, the former is immutable while latter is mutable in cases of non-persistent models via vm.update(newModel) or [myView, newModel, false]
  • add section about ensuring that dynamic form elem props are always re-synced back to model
  • dont re-create event handlers inline, pull them out of render()
  • explain that dom event handlers have a sig of handler(arg1..., e, node, vm) if defined as onclick: [handler, arg1, arg2], else handler(e, node, vm).
  • debugging slow perf using DOMInstr
  • shorthand id/class attrs on elems must be in div#id.class1.class2... order
  • more visible docs about explicit child array restrictions & work-around
  • mount(body, true) from in-body script, ensure to wrap in DOMContentLoaded: #56 (comment)
  • avoid holding references to the vm.refs object to dom nodes #58

@lawrence-dol
@iamjohnlong

Event emitter stops on first handler.

The event emitter stops at the first vm to define a handler for that event.

function emit(event) {
    var args = Array.prototype.slice.call(arguments, 1);

    var targ = vm;

    while (targ) {
        if (targ.events[event]) {
            u.execAll(targ.events[event], args);
            break; // <==
        }
        targ = targ.parent;
    }
}

However, it's not uncommon for handlers to apply finer-grained filtering to events and choose to take no action for specific reasons.

A simple example is a key handler which only takes action for specific keys (say a filter-bar search box which is using key-down for debouncing) and allow the event to otherwise propagate (say to the filter-bar container which clears all filters on ESCAPE or immediately commits all filters on ENTER).

All GUI systems I've worked with allow the handler to indicate somehow that the event has been fully "handled" (i.e. to consume the event). With formal event objects this is typically by calling evt.consume(). With free-style event objects this is typically by returning true from the handler (less well defined and less clear without consulting docs).

Regardless, having first vm with a handler infer consumption is a serious limitation.

Am I to assume that if it chooses to forgo handling it is expected to re-emit that event? If so, what are the implications for event-loop race-conditions and other factors (like micro-tasks and execution order, etc). And the implications if multiple handlers at the same level forgo handling (which would re-emit the same event multiple times).

There are also semantics for multiple handlers at each level; I took the tack of executing all handlers, and then propagating the event up if none of them had consumed it.

[eval] possible event handler changes, delegation

currently event handlers are bound via

["p", {onclick: function(e) {...}}]
// or delegated
["p", {onclick: [".moo", function(e) {...}]}]

some limitations with this are

  1. cannot bind multiple handlers for same event type, though this seems like something to best avoided anyways so that people don't learn to rely on bind-order and e.stopImmediatePropagation() & friends
  2. delegated handlers are also limited to one. this is more problematic as you can choose to respond with different handlers in different cases.

one way to fix 2) is to simply leave delegation up to the user - it's easy to just run e.target.matches(sel) within the handler.

another option is to allow an object with selector keys

{
    "td": function(e) {...},
    ".moo": function(e) {...},
}

this seems like the best option to replace the array syntax for delegation. need to evaluate performance implications of diffing another struct, though we can maybe assume these never change contents and just ===

DOMVM Discuss

Would it make sense to create a domvm-discuss repo just for separating out discussion around DOMVM? Meaning opening issues there as topics for discussion, even closing them when the topic has reached a logical conclusion.

Solicit some feedback (round 2)

Hello good people!

With 1.0 slated for 4/1, it seems like a good point for a follow-up to #1.

Since #1, a lot of API polish, bug squashing and question answering has taken place. More features, tests and modules have been added, along with extra demos for different use cases. There are still docs to finish and tests to add (lifecycle hooks, router, observers, isomorphism), but the ergonomics of the lib are in a happy, wart-free place.

For those new to domvm and wondering about perf/size. It is modular and ~6k gzipped for all modules. It is currently the fastest threaditjs implementation demo (open console for timings), besting the next fastest (Mithril) by at least 100% and is one of the quickest on dbmonster bench demo, only marginally surpassed at high mutation rates by raw vdom diff/patch libs like kivi, citojs and Inferno.

demos can be found in /demos as well as /test/bench, the gh-pages branch is usually in sync with master, so can be viewed on github pages: http://leeoniya.github.io/domvm/...

@matthiasak

Regarding https://medium.com/@matthiasak/mithril-isomorphic-universal-experiment-cb645d1a9238#.e37n0h3w4

Sub-view composition and redraw has been a solved problem in domvm since Day 1 and was one of the main reasons I moved away from Mithril. Node-level patching support is also possible and was added a while back demo

@sebadoom

Regarding https://auth0.com/blog/2016/01/07/more-benchmarks-virtual-dom-vs-angular-12-vs-mithril-js-vs-the-rest/

Would you be interested in an implementation of these benches in domvm?

Feedback and/or contribs are welcome, cheers!

@lawrence-dol
@yosbelms
@barneycarroll
@iamjohnlong
@koglerjs
@mikegleasonjr
@pdfernhout
@whitecolor

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.

PR Submission

@yosbelms

I appreciate your help, but if you'd like to make a change to the lib, please open an issue to discuss it first before writing code and submitting a PR with docs and everything. Especially in the beginning while you may not understand fully why things are the way they are. Some changes affect uniformity of the rest of the lib that you're not touching, others cannot be done in isolation without rethinking other parts of the whole. It will save you time in writing and me time in reviewing.

new: subview redraw optimization

Subviews can now return false instead of a template from render() to indicate "no changes", ร  la React's shouldComponentUpdate.

This results in a large reduction in heap allocations and significant perf increase if you can do your own cheap data caching & diff. A primary use case is any SPA where many large & expensive sections/tabs are "inactive" and can be ignored during redraws.

I was looking for ways to optimize the 1%-mutations dbmonster case and realized that the mutations happen at row level rather than cell level. This meant that i could simply cache and do oldRowData === newRowData and avoid a ton of tpl/diff/patch work by short-circuiting their redraws.

The numbers are (on my laptop):

  • 2.1x perf at 1% mutations (70fps vs 145fps)
  • 1.2x perf at 50% mutations (30fps vs 35fps)
  • 0.95x perf at 100% mutations (27fps vs 25.5fps)

You can see the optimized dbmonster at [1]. It only involved creating a tiny caching/diffing sub-view and cutting-pasting the row template into it.

The regression at 100% is because subviews carry a slight overhead which becomes measurable with high sub-view counts. This is known and needs more investigating as it's not obvious where the overhead accumulates.

Originally I planned to do this via a false return & redraw prevention from willRedraw hooks, but relying on hooks is more expensive and clunkier to set up.

[1] https://github.com/leeoniya/domvm/blob/1.x-dev/test/bench/dbmonster/app.js

@lawrence-dol @iamjohnlong

Node & NPM

Figure out needed polyfills? (fetch/xr, rAF, Promises, History)
Ensure everything works that's necessary under node, isomorphism.
Determine & minimize devDependencies
Qunit tests integration
Closure compiler or UglifyJS minification
Gulp? Browserify?
Publish beta

Prelim Docs Organization

brain dump, for now. pay no attention

## Quick Demo

## Philosophy

## Features

## Template Reference

## Views, Models & Composition (domvm.view.js)

## Emit & refs, keys

## Consuming and exposing APIs
    vm.imp
    vm.exp
    vm.redraw(newImp)

## Mutation Observers (domvm.watch.js)

## Routing (domvm.route.js)
    rt.goto
    rt.href
    rt.back/fwd
    rt.state

## Isomorphism (domvm.html.js)
    vm.html()
    vm.attach()

## Third-party DOM integration
    elem            raw elements?
    {_guard: true}  third-party nodes
    {_raw: true}    innerHTML
    vm.absorb()     // also used extreme optimization

## Optimization
    Event Delegation
    Template patching
        vm.patch()

## Useful Patterns

## Solutions to common problems / wiki
    css animation
    animate pre-destroy
    maintain focus
    animated re-order

## Utils (domvm.util.js)

## Suggested Polyfills / es5-shim
    bala.js

## Demos, benchmarks

in/out animations

Do you have any examples of in and out animations using hooks? Such as an image slider that cycles through an array? I'm playing with the hooks for in and out animations and I'm having trouble wrapping my head around it.

Here is what I have so far. http://jsfiddle.net/75m4k6b0/1/

Debugging: Concat unminimized

Would it be simple to include unminified copies of the two distribution JS files, by which I mean either just plain concatenated (ideally with comments stripped out), for debugging?

autofocus works on hard refresh (ctrl-f5) but not on soft f5

I have, for only the first field in my display:

[
    "input.field.text", {
        style                       : { height: fld.rHeight+"00%", width: "calc("+fld.rWidth+"00% + 12px)" },
        maxLength                   : fld.length,
        autocomplete                : "F"+fld.sCol+"-"+fld.eCol+"-"+fld.length,
        autofocus                   : true,
    },
]

DOMVM renders (noting autofocus="" on the first field instead of autofocus):

<input class="field text" autofocus="" autocomplete="F3-3-1" maxlength="1" style="height: 100%; width: calc(100% + 12px);">
...
<input class="field text" autocomplete="F7-79-73" maxlength="73" style="height: 100%; width: calc(7300% + 12px);">

It doesn't work in setting focus to the field.

The ambiguity of simple text first-child of element.

Sorry, but this inconsistency/restriction is really nagging at me. This is important because in the post-processing of a v-tree, consistency makes things remarkably easier to do and reason about.

Given this:

["textarea", {rows: 50},[           // Array may be a node or children list...
    "text",                         // this is ambiguous: element or text?
    ["br"],                         // this is unambiguously an element
]]

Why is this ambiguity not resolvable without concat by simply including a null or undefined first element?

["textarea", {rows: 50},[           // Array may be a node or children list...
    null,                           // ... but a node name can't be null.
    "text",                         // So this is now unambiguously text, no?
    ["br"],                         // And this is still unambiguously an element
]]

Would that not be syntactically consistent and more coherent than concat?

The other possibility is to require that contents always be an array (though, no doubt this is the reason for your perception of bracket-soup). The bracket-soup argument doesn't seem to hold much merit to me because we already have bracket-soup and a fully regular and consistent maze of brackets will be consistently easier than one with some of the brackets optional and with surprising results (like unnecessary ambiguity):

["textarea", {rows: 50},["br"]]     // "br" can be assumed to be text
["textarea", {rows: 50},[["br"]]]   // "br" must be an element

["textarea", {rows: 50},            // Not valid; third element must be an array
    "text",                         // ... if it's included at all
    ["br"],                         
]

["textarea", {rows: 50},[           // Must be a children list...
    "text",                         // Therefore this is unambiguously text?
    ["br"],                         // Unambiguously an element with no content
]]

Thoughts? The null fits with current templates; the child-array-always is more rigorously consistent.

Lifecycle Hooks

@barneycarroll i noticed your mithril.exitable.js repo for animation hooks and also remember you asking a while back how I would handle animating on removal. df2e45c is the result.

lifecycle hooks:

  • nodes: {_hooks:} - will/didInsert, will/didReuse, will/didMove, will/didRemove
  • views: vm.hook() - will/didRedraw, will/didMount, will/didUnmount

will* node-level hooks allow a Promise return and can delay the event until the promise is resolved, allowing you to do what you need to CSS animate, etc.

will* view-level hooks are not yet promise handling, so cannot be used for delayed anims yet, but you can just rely on the view's root node's hooks to accomplish the same thing, just not externally like you can hook vms.

there is no redraw-locking during pending promise resolution right now, nor are multiple promises coalesed by .all() or similar. so there's a possibility of getting the vtree into a confused state if there are complex staggered/unaligned animations of sibling nodes. this has not been tested. how do you handle this in mithril.exitable.js besides global redraw lock (& queue?) ?

here's a demo [1] of all the hooks firing (color coded by type). didUnmount() is excluded since it still has some issues WRT the fact that it does recursive bottom-up child removal.

[1] https://leeoniya.github.io/domvm/demos/lifecycle-hooks.html

Splitting up the lib

domvm.view - provides core view/vm functionality, event emit system, jsonml template processor, lifecycle hooks, m.trust

External addons:
domvm.route - provides routing and state transitions
domvm.watch - provides mutation observers and fetch wrappers for "auto-redraw", similar to m.prop, m.withAttr, m.request.
domvm.html - provides domvm to html string converter amd attach() for isomorphism.
domvm.util - maybe split the core util functions out for use by external addons as well. isArr(), isFunc(), isObj(), isVal(), raft(), insertArray(), etc

domvm.html is currently part of domvm (domvm.view) but can be made completely external as it relies on 2 functions that only need access to the vm.node: hydrateWith [1] and collectHtml [2]. Though it may be useful to keep hydrateWith inside domvm.view for planned functionality of usinng an existing root like <body> cause vm.mount() always appends to container. collectHtml is the optional (and big) one.

[1] hydrateWith
https://github.com/leeoniya/domvm/blob/master/src/domvm.js#L326

[2] collectHtml
https://github.com/leeoniya/domvm/blob/master/src/domvm.js#L933

External polyfills are user's responsibility:

  • Node.matches/querySelectorAll (if you use event delegation features)
  • requestAnimationFrame (redraw debouncing)
  • promises
  • fetch
  • history
  • weakmaps (not yet, but possibly in future)

Base watcher fetch function

All the watcher AJAX wrappers defer to initFetch, which is a good internal name reflecting the asynchronous nature. Could we add one more generic exported function, fetch(mth, url, cb, opts), to allow any HTTP method to be used so that the others become mere common-case conveniences?

There is, in fact, an argument to be made that get, delete, post, put and patch should be in the application wrapper and that only fetch should be provided, but I think that they make a nice common-case-subset API.

fetch: function(mth, url, cb, opts) {
    return initFetch(mth, url, null, cb, opts);
},

get: function(url, cb, opts) {
    return initFetch("get", url, null, cb, opts);
},

...

For example, one of my existing APIs uses LINK and UNLINK.

Mental model of `vm`.

Forgive me if this is already documented, but would you be able to give a one or two paragraph description of what the vm thing is? I realize it stands for "view-model", but it's not the expected context object in which I store my view's model data and it's clearly not the data model, which is given as the second argument; rather it's a concept specific to DOMVM. I would almost venture to say it's the view's API to domvm, except that what I thought domvm.view was.

TodoMVC bustage

some recent change has made the TodoMVC demo go wonky.

must pray to the Git gods for answers \o/

View Closure

@yosbelms

I made a branch [1] to evaluate the benefit of eliminating the private view closure. It was surprisingly easy to convert thanks to the fact that templates nodes can be getter funcs. The tests only required a couple tweaks and for the benchmark tests it was just a matter of unwrapping the templates. You can see the diff yourself [2].

Ignore the infinite-scroll demo diff, it's all whitespace EOL. But actually this is the one I'd like to discuss. I did not convert this one because I think it exactly illustrates the benefits of having an architecture-provided predictable and logical init place & private closure.

The way it is set up now, you have the init closure, which has access to everything and it returns a renderer which will get called on every redraw to re-generate the template. It is extremely clear and uniform. While you save one layer in simple cases where there's not much init in the new branch, you now have to create your own init "factories" for other cases. I prefer to have a predictable place for init with everything clearly dependency-injected rather than returning a render function closed over who-knows-what. It seems like a bad pattern if there's any hope of having multiple people writing plugins whose structure is entirely free-form. React's and Mithril's ecosystems are healthy for a reason, and it's not because they offer absolute flexibility - quite the opposite, in fact.

If you re-structure the current infinite-scroll demo [3] for the closure-free view, don't you just end up re-creating what domvm already provides but less powerful because you dont have access to the vm until things are rendered?. Technically you could init the vm imperatively with your render func and use it externally for .hook(), .on(), .redraw(), etc..

Try reworking it yourself, I'd like to see where you end up!

[1] https://github.com/leeoniya/domvm/tree/view-as-renderer
[2] a6e7992
[3] http://leeoniya.github.io/domvm/demos/infinite-scroll.html

Confusing Inconsistency in Templates... maybe?

I wanted had an existing template element like this:

["td","X"]
      ^^^ child of td

and wanted to add children to it. I expected to write code that resulted in this:

["td",["X",[input,"Some text here"]]]
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ contents of td 

or

["td",[["X"],[input,"Some text here"]]]
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ contents of td 

which both failed (with a rather obtuse and unhelpful error, as are all the template errors, especially because the stacktrace stops within DOMVM; though not that I necessarily want to bloat the code with error handling):

11:44:09.780 TypeError: sibAtIdx._node is undefined
hydrateNode() AppScript;01:159
hydrateNode() AppScript;01:162
redraw() AppScript;01:107
call() AppScript;01:76
1 AppScript;01:159:79

The guide says I can do this:

["textarea", {rows: 50}].concat([                           // use concat() to avoid explicit
    "text",                                                 // child array restrictions
    ["br"],
    "", null, undefined, [],                                // these will be removed
    NaN, true, false, {}, Infinity                          // these will be coerced to strings
])

Which, if I understand correctly results in:

["td","X",[input,"Some text here"]]
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ contents of td

which strikes me as weird and inconsistent.

Now, this is not allowed, presumably because "X" is ambiguous with "tag":

["td",[["X"],[input,"Some text here"]]]
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ contents of td

but from the viewpoint of not having analyzed to the level here, it was surprising, "Why can I have simple values in children expressed as additional elements of a node, but not within an explicit array of the node's children?"

Is there something better that can be done that more closely mirrors the container contents permitted in HTML? Perhaps allowing:

["td",[["","X"],[input,"Some text here"]]]
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ contents of td 

Which means a blank tag should not be dropped.

Or, if we want consistency, should we always just express all nodes children like this:

return ["td",
    "X",
    [input,"Some text here"]
    [div,
        "Some other text here",
        [... other elements...]...
    ...
    ]
]
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ contents of td

(On a side note, having null, undefined,[] dropped is really useful.)

Coerce non-renderable values?

What to do if a node or value is any of the following (or a getter/promise that returns them)?

[], null, false, true, undefined, NaN, Infinity, {}, {a:6},

Right now there's just breakage, usually when these values hit the DOM operations that throw errors when the parameters they expect don't match.

While these can be avoided, they sometimes show up at runtime as a result of doing inline computation within the template or creating children by mapping an empty result array, etc. Ideally, these cases should be handled outside domvm but it makes the templates much dirtier/boilerplatey. If domvm handled these situations, there would be a lot of overhead in checking all these cases as it would need to be done on every .redraw() for:

  • Each node itself
  • Each prop/attr/event value
  • Each css prop value

Second, it's not clear what should be done. Coerce them all to empty strings? Splice them all out? Insert placeholder comment nodes and .replaceChild() if they morph to something renderable?

A related issue is isomorphism and what to do about empty text nodes WRT the strategy above? Squash empty text nodes since they cannot be accounted for in innerHTML (as is done now)? Insert comment nodes as placeholders?

As for coercion, probably the best option is to simply assist in debugging these issues at dev time? The DOM instrumentation helper in test/lib/dominstr.js can be used to wrap try/catch around these DOM ops and barf out more useful info like the params. Or maybe provide a debug build that has try/catch integrated and can barf out the actual template node info, etc?

impCtx has gone away

PSA: impCtx and its step-cousin vm.imp are dead! Long live a mixed model and explicit handle!

The new view signature is:

domvm.view(viewFn, model, handle, opts);

handle is the replacement for key and is the sole definer of how views behave between redraws. It dictates whether a view will be reused or will re-init and destroy state and whether the DOM will be recycled or removed and re-created.

Specifying an array, object, function, string or number will use that value to compare the viewFn+handle pairs between old and new vtrees to determine the persistence and reuse of vms and the DOM across redraws.

Specifying undefined or null is equivalent to setting handle to the model itself.

Specifying false opts the view out of persistence assumptions and will cause the vm and DOM to be reused even when the model is completely replaced between redraws. This is useful for rendering re-fetched re-decoded JSON data structures.

I will write more about this in the docs and provide samples later.

For the time being it is still called key. I'm hestient to rename it to handle since the post-1.0 plan is for full congruence between vm and the vm's root node, so I don't want to change _key to _handle in the node props.

@lawrence-dol

Dependency-inject external ctx & expose a view API/state.

The view constructor signature looks like:

function MyView(vm, model, _key) {...}

When used declaratively inside another template, it looks like:

[MyView, myModel, "63A5"]

If the model is stateful/persistent, then a key is not needed since the internal node diff can be done via oldViewFn === newViewFn && oldModel === newModel after each render()/template re-gen.

But if the model is not persistent and is replaced/re-inited on each redraw then either a consistent key is required or else the view will be destroyed and recreated each time.

The model can be just data (object/array) or can be a stateful instance created via some constructor. In either case, the view can directly interact with the model's API and mutate it as needed from any event handlers in the template or other private view functions.

You may also want to import (dependency-inject) other domain helper functions, storage providers, additional config or external state into the view that are decoupled from the model (especially when the model is just data). These are not involved in the diffing process, so must be provided separately. To get this accomplished declaratively, domvm reserves a special-case wrapper that splits the two:

{model: ..., ctx: ...}

If this is passed to the view as the model, it accommodates dependency-injecting things from an external context alongside the diffed actual model. Since the wrapper itself is not diffed, it can be declarative and be recreated within the template/render() on each redraw().

If the view is created imperatively via vm = domvm(MyView, myModel), it's possible to expose a view API or internal state setting vm.ctx.*, eg vm.ctx.highlightNewOrders = function(){} from inside the view closure. The "ctx" is merely a formality for consistency and namespacing, since you can always directly set vm.* but this risks name collisions and breakage with internal and native methods like vm.redraw(), vm.mount(), vm.emit(), vm.node, etc.

Some thoughts:

This arrangement accommodates the needs of both the user and the internal needs of domvm. As a result it's a compromise in ergonomics and practicality. Ideally it would be great for vm state and API to be exposed directly on vm.*, but that means either name collisions or having to move all native [but useful] methods behind something clunky like vm._.redraw(). It's hard to judge which option will be more useful to the 51%, but i'm betting most people will keep view state and functions private (unfortunately) and not use imperative vms much except to optimize granular redraw(), so leaving the native methods more ergonomic seems best.

missing tests todo

The more tests I write, the more I realize how many more tests need to be written. I'm not event talking about covering some edge-case behaviors, but features. At 301 assertions so far and 1,600 LOC I'm probably only at 70% coverage of just View module, granted that's where 75% of the code lives.

It's possible to write tests till I'm blue in the face, so the question is, what should be considered sufficient to tag 1.0? Is it enough to say simply that the API is stable and major features have tests and working generally as documented in numerous demos? Should there be a disclaimer that things which have no explicit tests are not subject to semver and may break as tests/cases are added and behavior is refined and formalized?

Even without testing all browsers or polyfills, there's literally TONS of tests still to write including but not limited to:

View
- [x] dep injection w/diff key types DOM reuse
- [x] imperative vms
- [x] unjailed refs
- lifecycle hooks
- isomorphism
- perf regressions, GC/jank
- Node
Router
- hash routing
- href generation
- param validation
- callback order and args
Observers
- prop + async
- sync (withAttr)
- ajax (get/post/put..)

move domvm.config to domvm.view.config

now that the lib is modular, things like domvm.config({useRaf: false}) are a bit of a misnomer especially since rAF-debouncing is used in multiple modules but mostly hinders debugging/timing in the view module for debounced/coalesced redraw.

on the other hand having a config for each module is rather tedious but perhaps preferable to long-ass-but-clear key names in a global config e.g. routerUseRaf

needs some pondering...

Search a template to locate specific table cells and augment them

Having just built a table template (e.g. below), is there a way I can safely and uniformly location particular cells and either augment or overlay their contents? I need to create input fields & text areas which are sized to overlay a rectangular area of cells in this table. I don't want to be depending on DOMVM internals which might change.

Ideally I'd do this by positioning the elements relative to top left of cell x,y and the bottom left of cell' x',y'. But I can't conceive of any way to position siblings relative to other siblings or their contents.

If I was dealing with the rendered DOM I'd tag the cells on interest and overlay the input fields. But, crucially, this must all happen in a single frame render since there must not be a flicker of display then display with inputs.

I am also trying to avoid creating several thousand VMs for each of the cells and avoid complicating the creation of base grid structure with the (quite numerous things that can augment it, include providing customers the ability to provide their own augmentation around this grid structure.

So, ideally I want an end result that can say "position this widget at row 3, col 7, width 10 cols height 2 cols, offset +/- n pixels.

function view(vm,mdl) {
    return function render() {
        var txt=mdl.text();

        if(txt.length==0) { return []; }

        var dsptxt=[ "table.display-panel", buildRows(vm,txt) ];

        // invoke customer supplied code here and get a list of augmenting objects
        // augment dsptxt cells here

        return dsptxt;
    };
}

function buildRows(vm,txt) {
    var arr=[];
    arr.push([ "tr.hidden",buildCols(vm,"th",txt[0])]);
    for(var xa=0,ln=txt.length; xa<ln; xa++) {
        arr.push([ "tr", buildCols(vm,"td",txt[xa])]);
    }
    return arr;
}

function buildCols(vm,typ,row) {
    var arr=[];
    for(var xa=0,ln=row.length; xa<ln; xa++) {
        arr.push([ typ, row[xa] ]);
    }
    return arr;
}

View state composition?

Hey, @dustingetz

Continuing the discussion from HN: https://news.ycombinator.com/item?id=10942343

I dont think this is the type of composition I am talking about. This composes pure views, like react, but it doesn't compose their state, same as react.

View state is, in fact, retained in both declarative and imperative style composition, unless I'm not understanding.

Would you mind clarifying and/or providing a pattern you're looking for?

thanks!

threaditjs implementation

@lawrence-dol
@yosbelms
@barneycarroll
@koglerjs

Here is my intial implementation of http://threaditjs.com/

http://leeoniya.github.io/domvm/demos/threaditjs

It is not yet in final form for official submission but gets everything done (and more) with 2x perf and smaller lib size than the current leader (Mithril).

All the niceties below are handled, however a route prefix is prepended by the code to handle running from a directory url rather than from a dedicated domain as the officially published implementations do. As an additional consequence of this, if a sub-page fails to load and renders an error, it cannot be refreshed (via browser F5) since the server is unable/not configured to serve up index.html regardless of requested url...these are not bugs and work fine otherwise.

  • Handling document.title
  • Handling 404s from the API
  • Handling an error state
  • Losing the ugly hash in the URL
  • Trimming the title
  • Pluralizing the comment count text (0 comments, 1 comment, n comments)

Perf is excellent in all cases thanks to explicit sub-view redraw and fast render in general. In addition, every user action/nav has immediate "Loading..." feedback of some sort, whereas current implementations will freeze/wait for the [slow] server to respond in cases besides the main view load. Like other implementations, when a comment reply is added, I just append what comes back to the cached parent's children array and redraw. This is not ideal and I'd rather re-fetch and redraw the entire parent to reflect any new sibling/descendant comments but that requires another painful server trip to re-fetch the parent since "It's not a very well programmed API, apparently." ;)

I've been using threaditjs to figure out exactly where the pain points are WRT how much module decoupling/0-magic is worth the cost of wiring everything together. Currently the implementation LOC size is on-par with the others (if not on the shorter end) but is rather ad-hoc in places. For example, some places use the Mithril-inspired mutation observer wrappers for auto-redraw while others call redraw() of sub-views explicitly. This implementation also strives to keep the views separate from the model/API Pattern A (w/OO) in [1], which leads to some technically unavoidable roughness and more code than might be used for a more monolithic Pattern C or UI-centric/React-like Pattern D in [1].

I'm thinking of reworking the base implementation to be with explicit redraw only (without any mutation observers or auto-redraw) for clarity and making another version where it's all based on observers w/auto-redraw. I don't know how practical the latter would be since in huge threads it makes no sense to pay the overhead to watch a couple properties x1000s of recursive sub-views to save a couple explicit redraw() lines. Right now it's a pre-meditated mix for this reason.

Overall it's not too incomprehensible IMO but could possibly be somewhat better. I'd like to do a couple alternate versions using the more concise/monolithic patterns as well. I chose the more difficult route since a decoupled pattern with an exposed model API is the one which demands most forethought and up-front engineering from the lib, else you compel pure hackery from app-space users.

Keep in mind that various domvm modules rely on modern APIs: ES6 Promise, history, requestAnimationFrame, fetch, querySelectorAll/Node.matches and you'll need to use polyfills to get it working in browsers with lagging or non-conforming support. No features are used which cannot be polyfilled down to IE9+.

@koglerjs, thanks a ton for threaditjs and I would be very interested in your feedback or suggestions if you have time. I 100% agree with your MVR terminology [2], which is why MVC never made sense to me for front-end as Mithril pushes it and domvm was written specifically to liberate M and V from any particular structural impositions but retain flexible composition and M-V binding where needed.

[1] https://github.com/leeoniya/domvm#subviews-components-patterns
[2] https://koglerjs.com/verbiage/threadit

Road to 1.0

Road to 1.0

My hope is to release a stable version by April 1st. This means no further breakage of:

  • The published API (Docs)
  • Defined behavior (Tests)

There's a lot of work just in the above items for functionality that already exists in the lib. Since I'm currently the only dev, claims of IE9+ compat will have to wait till post-1.0 as I simply have no time to fix everything that might be broken in older browsers and vet all needed polyfills. If anyone cares to help with this, great. IE9+ is done!

Generally, I'm satisfied with the view and html modules' semantics and performance at this point. However, there's still some significant effort to be put into the route and watch modules. I'd prefer to stabilize each module indepedently, but this prevents offering a stable concat+min dist build that includes everything. I'm not sure what to do here, but either remove/re-branch some features that are experimental or simply leave them out of the docs; the latter would be simpler, but would bloat the lib size to a small degree to include codepaths that shouldn't run.

Big-Ticket Items (besides more tests and docs)

  • Hash-based routing interop, query params (#11 (comment)) Done.
  • Prop/attr magic (#18). Maybe needed for 1.0 since it affects ergonomics/expectations. Not sure if this magic should be opt-out or opt-in? If implemented, will need clear and simply-explained behavior plus tests to ensure it doesn't do wonky things. Done.

Small-But-Necessary Items

  • Event handlers can be arrays w/args: {onclick: [cb, arg1...]} Done.
  • Ponder arguments & order provided to various callbacks and pass-through semantics.
    • view: Event handlers (delegated handlers should also get attachment node) Done: function(arg1..,e, node) where arg1.. only appears when using [cb, arg1..] style handlers.
    • view: Emit arg pass-through format vm.emit("myEvent", arg1..) or vm.emit("myEvent", args) Done: vm.emit("myEvent", arg1..)
    • view: Lifecycle hooks handlers and what's valuable in each case? didMount/willRecycle/etc, oldNode, newNode
    • route: handlers (e, arg1..) how should this interop with named querystring params when implemented? Done.
    • route: rt.goto("routeName", args), same concerns as above, named vs ordinal? both can exist in URI. Done.
    • route: rt.href("routeName", arg1...) Done.
    • watch: allow setting a context for handlers (vm, vnode?)
    • probably more i've forgotten here

Maybe Items (probably post-1.0)

  • Third-party lib interop (#7)
    • {_guard: true} for nodes
    • vm.absorb(vnode, el, props)
    • Accept existing DOM elements in tpl defs (assume guarded?)
  • Optimization channel: allow vtree diff & re-build suppression for known-static trees at both self and children levels. Would just graft the old vtree branch to new.

@lawrence-dol
@yosbelms

vdom sub-tree-diffing

@vicnicius

Wanted to see what you think about domvm, since you're evaluating incremental-dom :)

It was written to solve a lot of issues I had with Mithril's mandatory global-redraw and MVC style.

cheers!

vm.refs[] => vm.refs() to allow objects as _ref?

i've decided to change vm.refs from an array to a getter function. app code updates should be trivial (unless you were iterating vm.refs): vm.refs[key] will become vm.refs(key).

this is necessary for future-proofing the lib and allowing the internal refs impl to be non-uniform. the change will pave the way for _ref as well as _key on nodes to be objects OR values, transparently backed by Map, Weakmap or Hash.

invoking vm.refs with multiple args should bring back an array of vnodes while invoking it with 0 args should bring back an object containing both the Map and the Hash where object and value refs are held respectively.

expect the change to drop in the next few days.

Routing

Router will wrap HTML5 history API (pushState, replaceState, maybe hashchange). Its responsibilites:

  • Listen for url changes
  • Translate url to route and invoke onenter/onexit handlers, passing through any parsed/validated params and/or popped HTML5 history state.
  • The user-defined route handlers then process any needed state transitions such as
    • [onexit] Suspending/saving/clearing prior state (in cache, localStorage, DOM, server, etc..)
    • [onenter] Restoring/re-fetching state
    • Maybe validating input/preventing further (or reverting) navigation?
    • All this logic can be done in the handlers or deferred to the model and/or view's APIs
  • Views then redraw either implicitly or explicitly to reflect new state/model
  • Router must also must provide a reverse routing API to manually invoke/set routes by name w/params and generate urls so views and templates can be wired via dom handlers and href attrs

Keeping with domvm's view creation convention (which will be changing from domvm() to domvm.view() alongside domvm.watch() and domvm.route()), the proposed API is:

function AppRouter(rt, app) {
    return {
        root: "/threaditjs/examples/domvm/",
        routes: {
            threadList: {
                url: "",
                onenter: function() {
                    app.getThreads();
                }
                onexit: function(e) {
                    app.threads = [];
                }
            },
            thread: {
                url: "thread/:id",
                params: {id: /[a-z0-9]{5}/i},           // TODO: fn validation
                onenter: function(id) {
                    app.getComments(id);
                },
                onexit: function(e) {
                    app.comments = [];
                }
            },
            _404: {

            }
        }
    }
}

function AppView(vm, app, key, impCtx) {
    return function() {
        var href = impCtx.appRt.href("thread", ["some_thread_id"]);
        return ["a", {href: href}, "View Comments"];
    };
}

// state/model
var app = new App();

var appRt = domvm.route(AppRouter, app);
var impCtx = {appRt: appRt};
var appVm = domvm.view(AppView, app, null, impCtx);

appRt.goto("threadList");

As with views, app is just a model to be manipulated/rendered by the router and the views. The router is passed into the view in an imported context alongside the main model (app).

Most of this is functionally near-ready for alpha.

@lawrence-dol thoughts?

Mount appends view to element content

Not sure if this would be considered a bug or not, but the mount function appends the view content to that of the element on which it's mounted. That seems a little counter-intuitive and it also prevents a very simple script-block warning implementation.

<body>
    <div>This application requires JavaScript to work.</div>
    <script src="AppScript;01"></script>
</body>

Result:

This application requires JavaScript to work.

This is my view

Instead of:

This is my view

Idiomatic way to set model value to match value of input?

Is there a recommended or idiomatic way to make a value in the model always reflect the value of an input field?

I know I can do something like:

...
onkeypress: function(evt) { model.value=evt.target.value; }
oninput:    function(evt) { model.value=evt.target.value; }
onchange:   function(evt) { model.value=evt.target.value; }
...

But is there a better way? For example, the scoreboard demo makes use of vm.refs, but that is not desirable in my case because the machine generated fields are not named.

When I create the corresponding input from the field model, I want to immediately link the model's value to the input field in such a way that any change to the input field immediately updates the model without doing a redraw. It is not necessary to link in the other direction, though if I did I assume changing the model and redrawing would do the job.

In Mithril I would have used m.withAttr, but before I recreate that I want to be sure that DOMVM hasn't solved the problem better.

_refs should be vnodes, not DOM els?

then vm.refs.myRef.el would be the dom node.

currently dom nodes already get tagged with ._node to allow dom => vtree access. while this is useful for hacking, it seems a roundabout way to get back in from hooks.

thinking about this in the context of attaching data to nodes via a new _data prop rather than relying on el.dataset.* or data-* attrs which incur expensive setAttribute costs.

while this adds an extra layer to refs and dips users into the vtree, it allows per-node data storage without dom reliance.

weekli demo (partial)

Hey guys,

Added a partial 100-LOC implementation [1] (source [2]) of weekli [3]. It only covers the interesting parts with events and delegation. I didn't do any mediaquery listeners to flip it to a mobile layout or do anything like divs w/ display: table toggling. It does have an exposed api though, which it uses to pick some random hours on init.

Pretty happy with how the delegation semantics worked out. I was worried not being able to bind multiple mousedown handlers would be problematic, but turned out to be easily worked around as well as more optimal for perf.

[1] http://leeoniya.github.io/domvm/demos/weekli.html
[2] https://github.com/leeoniya/domvm/blob/master/demos/weekli.html
[3] http://collnwalkr.github.io/weekli/

@lawrence-dol @iamjohnlong @collnwalkr @barneycarroll @yosbelms

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.