domvm (DOM ViewModel)
A thin, fast, dependency-free vdom view layer (MIT Licensed)
Philosophy
UI-centric, exclusively declarative components suffer from locked-in syndrome, making them unusable outside of a specific framework. Frequently they must extend framework classes and adhere to compositional restrictions which typically mimic the underlying DOM tree and sacrifice powerful exposed APIs for the sake of designer-centric ease and beauty.
Instead, domvm offers straightforward, pure-js development without opinionated structural or single-paradigm buy-in. Uniformly compose imperative and declarative views, expose APIs, hold private state or don't, dependency-inject or closure, build monolithic or loosely coupled components.
Architect reusable apps without fighting a pre-defined structure, learning tomes-worth of idiomatic abstractions or leaning on non-reusable, esoteric template DSLs.
Features
- Thin API, no dependencies, build = concat & min
- Fast (2x Mithril, React, Riot; 1.3x Vue, Angular 2, Aurelia) - dbmonster, granular patch
- Small - ~7k all modules gzipped: 11k view core, 2k router, 2.3k observers, 0.8k isomorphism
- Concise js templates. No html-in-js, js-in-html or other esoteric syntax requiring tooling/compilation
- Sub-views - declarative or imperative, freely composable, stateful and independently refreshable
- Synthetic events - emit custom events with data to ancestor views
- Lifecycle hooks - view-level and granular node-level for e.g. async animations
- Decoupled client-side router (for SPAs) & mutation observers (for auto-redraw)
- Isomorphic - generate markup server-side and attach on client
- SVG & MathML support: demo, svg tiger
- IE9+ with tiny polyfills/shims sources, all compressed:
view
(rAF, element.matches), watch
(Promise, fetch)
Demos

https://leeoniya.github.io/domvm/demos/
Documentation
- Installation
- Modules, Building
- Template Reference
- Create, Modify, Redraw
- Subviews, Components, Patterns
- Trigger Ancestor redraw()
- Lifecycle Hooks, Async Animation
- Synthetic Events, emit(), on:{}
- DOM Refs, Raw Element Access
- Isomorphism, html(), attach()
- Route Module
- ...WIP, help wanted! https://github.com/leeoniya/domvm/issues/36
Installation
Browser
<script src="domvm.min.js"></script>
Node
var domvm = require("domvm");
Modules, Building
Each module is a single js file in /src
. The first 3 are the "core", the rest are optional and can be replaced by your own implementations. For development, just include each via <script>
tags.
domvm
: namespace & wrapperdomvm.utils
: generic funcs required by other modulesdomvm.view
: the core vdom & template libdomvm.html
: vtree => HTML generator, if you need isomorphism/SSRdomvm.watch
: auto-redraw helpers (mutation observers, ajax wrappers)domvm.route
: router & href generator for single page apps (SPAs)
Building is simple: concat the needed modules and minify with tools of your choice. Closure Compiler is recommended for both:
java -jar compiler.jar
--language_in ECMASCRIPT5
--js src/domvm.js
--js src/utils.js
--js src/view.js
--js src/html.js
--js src/watch.js
--js src/route.js
--js_output_file dist/domvm.min.js
Template Reference
domvm templates are a superset of JSONML
If you prefer hyperscript, just use this wrapper:
function h() {
return Array.prototype.slice.call(arguments);
}
["p", "Hello"]
["p#foo.bar.baz", "Hello"]
["input", {type: "checkbox", checked: true}]
["input", {type: "checkbox", ".checked": true}]
["button", {onclick: function(e) {...}}, "Hello"]
["button", {onclick: [myFn, arg1, arg2]}, "Hello"]
["ul", {onclick: {".item": function(e) {...}}}, ...]
["p", {style: "font-size: 10pt;"}, "Hello"]
["p", {style: {fontSize: "10pt"}}, "Hello"]
["div", {style: {width: 35}}, "Hello"]
["h1", {class: "header"},
["em", "Important!"],
"foo",
myElement,
function() { return ["div", "clown"]; },
]
["h1", [
["em", "Important!"],
["sub", "tiny"],
[
["strong", "stuff"],
["em", "more stuff"],
],
]]
["p", function() {
return [
["span", "foo"],
["em", "bar"],
];
}]
["textarea", {rows: 50}].concat([
"text",
["br"],
"", null, undefined, [],
NaN, true, false, {}, Infinity
])
["#ui",
[NavBarView, navbar],
[PanelView, panel, "panelA"],
preInitVm,
]
[".myHtml", {_raw: true}, "<p>A am text!</p>"]
["p", {_key: "myParag"}, "Some text"]
["p", {_ref: "myParag"}, "Some text"]
["p", {_data: {foo: 123}}, "Some text"]
Create, Modify, Redraw
function PeopleView(vm, people) {
return function() {
return ["ul.people-list", people.map(function(person) {
return ["li", person.name + " (aged " + person.age + ")"];
})];
};
}
var myPeeps = [
{name: "Peter", age: 31},
{name: "Morgan", age: 27},
{name: "Mark", age: 70},
];
var vm = domvm.view(PeopleView, myPeeps);
vm.mount(document.body);
myPeeps.shift();
myPeeps.push(
{name: "Allison", age: 15},
{name: "Sergey", age: 39}
);
vm.redraw();
Subviews, Components, Patterns
In very large apps, you may need to optimize performance by restricting what you redraw. Let's restructure the example into nested sub-views.
Pattern A: decoupled-model-view
Here, the views are separated from the pure-data models. During initial redraw, the views expose themselves back into the models to provide external redraw control.
function PeopleView(vm, people) {
return function() {
return ["ul.people-list", people.map(function(person) {
return [PersonView, person];
})];
};
}
function PersonView(vm, person) {
person.vm = vm;
return function() {
return ["li", person.name + " (aged " + person.age + ")"];
};
}
var myPeeps = [
{name: "Peter", age: 31},
{name: "Morgan", age: 27},
{name: "Mark", age: 70},
];
var peepVm = domvm.view(PeopleView, myPeeps).mount(document.body);
Now we can redraw each model's view independently.
var allison = {name: "Allison", age: 15};
var sergy = {name: "Sergey", age: 39};
myPeeps.push(allison, sergy);
peepVm.redraw();
allison.age = 100;
allison.vm.redraw();
Pattern B: view-linking-model
You can opt for slight coupling by having the models pre-define a model-view pairing, moving that responsibility out of any parent templates. Below, we employ OO models, but you could use Object.create
or other more pure methods to achieve the same goals.
function People(list) {
this.list = list;
this.view = [PeopleView, this];
}
function Person(name, age) {
this.name = name;
this.age = age;
this.view = [PersonView, this];
}
function PeopleView(vm, people) {
people.vm = vm;
return function() {
return ["ul.people-list", people.list.map(function(person) {
return person.view;
})];
};
}
function PersonView(vm, person) {
person.vm = vm;
return function() {
return ["li", person.name + " (aged " + person.age + ")"];
};
}
var myPeeps = [
new Person("Peter", 31),
new Person("Morgan", 27),
new Person("Mark", 70),
];
var people = new People(myPeeps);
var vm = domvm.view(people.view);
Pattern C: view-enclosing-model
Continuing our steady march towards progressively more monolithic components, you can enclose the views in the models to make each component more self-contained. You can also imperatively pre-init the views (vms), for example:
function People(list) {
this.list = list;
this.vm = domvm.view(PeopleView, this);
function PeopleView(vm, people) {
return function() {
return ["ul.people-list", people.list.map(function(person) {
return person.vm;
})];
};
}
}
function Person(name, age) {
this.name = name;
this.age = age;
this.vm = domvm.view(PersonView, this);
function PersonView(vm, person) {
return function() {
return ["li", person.name + " (aged " + person.age + ")"];
};
}
}
Pattern D: model-enclosing-view
This is similar to how React components work and inverts the model-view structure to be more UI-centric, with every component being both the model and a single view without explicit model constructors or OO.
function PeopleView(vm, people) {
return function() {
return ["ul.people-list", people.map(function(person) {
return [PersonView, person];
})];
};
}
function PersonView(vm, person) {
return function() {
return ["li", person.name + " (aged " + person.age + ")"];
};
}
var people = domvm.view(PeopleView, myPeeps);
Pattern E: make up your own!
The above examples demonstrate the flexibility afforded by uniformly-composable imperative and declarative paradigms. For instance, models can expose multiple views which can then be consumed by disjoint parts of some larger template, such as a single NavMenu
component with shared state and sitemap tree but exposing split TopNav
, SideNav
and FooterNav
views. Alternatively or additionally, more views of your model can be constructed after the fact if you choose to expose enough state/api.
Trigger Ancestor redraw()
You can invoke .redraw()
of any ancestor view (e.g. parent, root) by passing a numeric level
.
vm.redraw(0);
vm.redraw(1);
vm.redraw(1000);
Lifecycle Hooks, Async Animation
Demo: lifecycle-hooks different hooks animate in/out with different colors.
Node-level
Usage: ["div", {_hooks: {...}}, "Hello"]
- will/didInsert (initial insert)
- will/didRecycle (reuse & patch)
- will/didReinsert (detach & move)
- will/didRemove
Node-level will*
hooks allow a Promise/thennable return and can delay the event until the promise is resolved, allowing you to CSS animate, etc.
View-level
Usage: vm.hook("didRedraw", function() {...})
or vm.hook({didRedraw: function() {...}})
- will/didRedraw
- will/didMount
- will/didUnmount
View-level will*
hooks are not yet promise handling, so cannot be used for delay, but you can just rely on the view's root node's hooks to accomplish similar goals.
Synthetic Events, emit(), on()
Custom events can be emitted up the view hierarchy (with data) and handled by ancestors. When a matching handler is found, the callbacks are executed and the bubbling halts.
function ParentView(vm) {
vm.on({
myEvent: function(arg1, arg2) {
console.log("caught myEvent", arguments);
}
});
return function() {
return ["div", [ChildView]];
};
}
function ChildView(vm) {
var handleClick = function(e) {
vm.emit("myEvent", "arg1", "arg2");
};
return function() {
return ["em", {onclick: handleClick}, "some text"];
};
}
Node Refs, DOM Element Access
Virtual nodes created by templates can be accessd via vm.refs.*
. Since DOM nodes can be recycled, always access the refs object via the vm since it will get re-generated on each redaw.
function SomeView(vm) {
function handleMyBtnClick() {
vm.refs.strongFoo.el;
}
return function() {
return ["div",
["strong", {_ref: "strongFoo"}, "Strong foo text"],
["br"],
["a.myBtn", {href: "#", onclick: handleMyBtnClick}, "some link"],
];
};
}
Isomorphism, html(), attach()
function SomeView(vm) {
return function() {
return ["div#foo", "foobar"];
};
}
var vm = domvm.view(SomeView, someModel);
var html = domvm.html(vm.node);
var vm = domvm.view(SomeView, someModel);
vm.attach(document.getElementById("foo"));
Route Module
The route
module is a small, unassuming router. It takes your route definitions and invokes handlers on hashchange
or popstate
events. It can parse and regex-validate params, generate hrefs & click handlers for use in templates and provides a goto
API.
High-level example:
function MyRouter(router, deps) {
return {
home: {
path: "/",
onenter: function() {
},
onexit: function() {
}
},
blogPost: {
path: "/blog/posts/:slug",
vars: {slug: /[a-z0-9\-]+/},
onenter: function(segs) {
},
}
};
}
var deps = {app: myApp};
var router = domvm.route(MyRouter, deps);
router.refresh();
router.goto("/blog/posts/some-viral-heading-2016");
router.goto("blogPost", {slug: "some-viral-heading-2016"});
["a", {href: router.href("blogPost", {slug: "some-viral-heading-2016"})}, "Some Viral Heading 2016!!!"];
var curRoute = router.location();
Some things to keep in mind. The router
argument passed to the closure is the same one returned by externally. However, until the closure returns the routes, it is not fully initialized and cannot be used from inside for routing yet. If you perfer to keep everything in the closure, you can set up some config:
function MyRouter(router, deps) {
router.config({
useHist: false,
root: "/myApp",
init: function() {
},
willEnter: function(to, from) {},
willExit: function(from, to) {},
});
return {
};
}
For a working example of this, check out /demos/threaditjs
: https://github.com/leeoniya/domvm/blob/1.x-dev/demos/threaditjs/app.js#L3-L49
Demos
See /demos and /test/bench