A data-binding function for the DOM.

npm i simulacra --save #v2.1.1 Fork on GitHub


Simulacra.js returns a DOM Node that updates when its state object changes. Its entire API surface area is a single function (with some optional helpers and symbols), and it does not introduce any new syntax or a template language.

It is a fairly low cost abstraction, though it may not be quite as fast as hand-optimized code. What this library emphasizes is making the naïve approach of mutating objects to update state as performant and opaque as possible.

The approximate size of this library is ~5 KB (minified and gzipped).


With Simulacra.js, any changes on bound objects reflect immediately in the DOM, no manual intervention required. Here is an example of Simulacra.js in action:

Live demo, try it out:

  • = "Caramel Latte"
  • state.details.size.push("Trenta")
  • state.details.size = ["S", "M", "L"]

Simulacra.js uses plain HTML for templating, and it does not introduce its own template language. This makes it straightforward to start with a static HTML page and add interactive parts. Here's a sample template:

<template id="product">
  <h1 class="name"></h1>
  <div class="details">
    <div><span class="size"></span></div>
    <h4 class="vendor"></h4>

Using the <template> tag is optional, any DOM element will suffice. The shape of the state is important since it has a straightforward mapping to the DOM, and arrays are iterated over to output multiple DOM elements. Here's some sample state:

var state = {
  name: 'Pumpkin Spice Latte',
  details: {
    size: [ 'Tall', 'Grande', 'Venti' ],
    vendor: 'Starbucks'

Simulacra.js exports only a single function, which binds an object to the DOM. The first argument must be a singular object, and the second argument is a data structure that defines the bindings. The definition must be a CSS selector string, DOM Node, change function or definition object (parent binding only), or an array with at most three elements:

var bindObject = require('simulacra') // or `window.simulacra`
var template = document.getElementById('product')

var node = bindObject(state, [ template, {
  name: '.name',
  details: [ '.details', {
    size: '.size',
    vendor: '.vendor'
  } ]
} ])


The DOM will update if any of the bound keys are assigned a different value, or if any Array.prototype methods on the value are invoked. Arrays and single values may be used interchangeably, the only difference is that Simulacra.js will iterate over array values.

Change Function#

By default, the value will be assigned to the element's textContent property (or value or checked for inputs). A user-defined change function may be passed for arbitrary element manipulation, and its return value determines the new textContent, value, or checked attribute if it is not applied on a definition object. The change function may be passed as the second or third position, it has the signature (element, value, previousValue, path):

To manipulate an element in a custom way, one may define a change function like so:

[ selector, function (element, value, previousValue) {
  // Attach listeners before inserting a DOM Node.
  if (previousValue === null)
    element.addEventListener('click', function () {

  return 'Hi ' + value + '!'
} ]

A change function can be determined to be an insert, mutate, or remove operation based on whether the value or previous value is null:

There are some special cases for the change function:

Helper Functions#

Here is an example of using the built-in helper functions to control animations and events:

Simulacra.js includes some built-in helper functions for common use cases, such as event listening and animations. They are optional, and included for convenience. To use them, one can define a change function like so:

var bindObject = require('simulacra')
var bindEvents = bindObject.bindEvents
var animate = bindObject.animate
var retainElement = bindObject.retainElement

// Accepts a hash keyed by event names, using this has the advantage of
// automatically removing event listeners, even if the element is still
// in the DOM. The optional second argument is `useCapture`.
var bindFn = bindEvents({
  // The first argument is the DOM event, second is the path.
  click: function (event, path) {'alternate')

// Accepts class names on insert, mutate, and remove, and a time in ms for
// how long to retain an element after removal.
var animateFn = animate('fade-in', 'bounce', 'fade-out', 1500)

function change (node, value) {
  animateFn.apply(null, arguments)
  bindFn.apply(null, arguments)
  return value || retainElement

Server-Side Rendering#

Simulacra.js includes an optimized string rendering function. It implements a subset of Simulacra.js and the DOM, but it should work for most common use cases.

const render = require('simulacra/render')

const state = { message: 'Hello world!' }
const binding = { message: 'h1' }
const template = '<h1></h1>'

// The first call to `render` will process the template.
render(state, binding, template)

// Subsequent calls do not need the template anymore.
console.log(render(state, binding))

This will print the string <h1>Hello world!</h1> to stdout.

The DOM API in Node.js can also work, it should be called within the context of the window global, however this may be optional in some implementations. In the following example, Domino is used as the DOM implementation.

const domino = require('domino')
const bindObject = require('simulacra')

const window = domino.createWindow('<h1></h1>')
const $ = bindObject.bind(window)
const state = { message: 'Hello world!' }
const binding = [ 'body', { message: 'h1' } ]

console.log($(state, binding).innerHTML)

This will also print the string <h1>Hello world!</h1> to stdout.

Rehydrating from Server Rendered Page#

Simulacra.js also allows server-rendered DOM to be re-used or rehydrated. The main function accepts an optional third argument for this purpose:

const bindObject = require('simulacra')

const state = { /* the state must be populated beforehand */ }
const binding = [ ... ]

// Rehydrate from existing DOM Node.
const node = document.querySelector(...)

bindObject(state, binding, node)

Instead of returning a new Node, it will return the Node that was passed in, so it's not necessary to manually append the return value to the DOM. All change functions will be run so that event binding can happen, but return values will be ignored. If the Node could not be rehydrated properly, it will throw an error.

Dependent Values#

Nodes are updated if and only if their values change, that is each value has a 1:1 correspondence to the DOM. To implement changes due to external values, it can be handled externally. For example, given this setup:

var state = {
  products: [
    { name: 'Foo', basePrice: 1.21 },
    { name: 'Bar', basePrice: 2.35 }
var binding = [ '.product', {
  name: '.name',
  displayPrice: '.price'
}, initializeProduct ]

The displayPrice value can be computed dynamically based on a conversionRate:

var conversionRate

Object.defineProperty(state, 'conversionRate', {
  get: function () { return conversionRate },
  set: function (value) {
    var i, j, product

    if (state.products)
      for (i = 0, j = state.products.length; i < j; i++) {
        product = state.products[i]
        product.displayPrice = calculatePrice(product.basePrice, value)

    return conversionRate = value

This will work whenever the conversionRate is set, but will not change when the basePrice is set. That needs to be handled separately:

function initializeProduct (node, value) {
  var basePrice, initialValue

  if (value) {
    initialValue = value.basePrice

    Object.defineProperty(value, 'basePrice', {
      get: function () { return basePrice },
      set: function (price) {
        value.displayPrice =
          calculatePrice(value.basePrice, state.conversionRate)
        return basePrice = price

    value.basePrice = initialValue

function calculatePrice (basePrice, conversionRate) {
  return basePrice * conversionRate

The above is just an example, it is opaque how this computation is done. More abstractions on top allow state to have a more flexible structure that may not correspond to the DOM.


There are a few benchmarks implemented with Simulacra.js:


The namesake of this library comes from Jean Baudrillard's Simulacra and Simulation. The mental model it provides is that the user interface is a first order simulacrum, or a faithful representation of state.

Simulacra.js does data binding differently:

What Simulacra.js does is capture the intent of state changes, so it is important to use the correct semantics. Using state.details = { ... } is different from Object.assign(state.details, { ... }), the former will assume that the entire object changed and remove and append a new element, while the latter will re-use the same element and check the differences in the key values. For arrays, it is almost always more efficient to use the proper array mutator methods (push, splice, pop, etc). This is also important for implementing animations, since it determines whether elements are created, updated, or removed.

How it Works#

On initialization, Simulacra.js replaces bound elements from the template with empty text nodes (markers) for memoizing their positions. Based on a value in the bound state object, it clones template elements and applies the change function on the cloned elements, and appends them near the marker or adjacent nodes.

When a bound key is assigned, it gets internally casted into an array if it is not an array already, and the values of the array are compared with previous values. Based on whether a value at an index has changed, Simulacra.js will remove, insert, or mutate a DOM element corresponding to the value. Array mutator methods are overridden with optimized implementations, which are faster and simpler than diffing changes between DOM trees.


Under the Hood#

This library requires these JavaScript features:

It also makes use of these DOM API features:

No shims are included. The bare minimum should be IE9, which has object property support.

Similar Projects#


This software is licensed under the MIT license.