One feature that sets Imperative.js apart from other frameworks like React and angular is that the structure of your application does not need to be tied to the DOM tree.

Imperative.js comes with a DOM-focused implementation, in the form of the hyperscript h function, and this is the easiest way to get started using Imperative.js. But h is actually implemented in terms of two lower-level functions - zip and mapOut - which can be used with any output type. At the top of the application one just has to use mapOut to create a virtual-dom representation.

For example, let’s build a tiny UI output type on top of a <canvas> element. The elements will be colored rectangles; we’ll allow them to be laid out using simple horizontal and vertical composition. They’ll handle click events within the rectangle boundary. First we’ll write the functions dealing with our output type itself.

// One colored rectangle, with click handler
function rect(color, w, h, click) {
  return {
    width: w,
    height: h,
    rects: [{
      color: color,
      left: 0,
      top: 0,
      width: w,
      height: h,
      click: click}]}}

// Place one drawing next to another
function beside(rect1, rect2) {
  return {
     width: rect1.width + rect2.width,
     height: Math.max(rect1.height, rect2.height),
     rects: rect1.rects.concat(
       rect2.rects.map(
         rect => Object.assign({}, rect, {left: rect.left + rect1.width})))
  }
}

// Place one drawing above another
function above(rect1, rect2) {
  return {
     width: Math.max(rect1.width, rect2.width),
     height: rect1.height + rect2.height,
     rects: rect1.rects.concat(
       rect2.rects.map(
         rect => Object.assign({}, rect, {top: rect.top + rect1.height})))
  }
}

// Send click event to the rect that overlaps (if any)
function handleClick(rects, ev) {
  for(const rect of rects) {
    if(rect.left <= ev.offsetX && rect.left + rect.width >= ev.offsetX
    && rect.top <= ev.offsetY && rect.top + rect.height >= ev.offsetY) {
      return rect.click(ev)
    }
  }
}

// Draw the rectangles to the canvas
function draw(rects, canvas) {
  const ctxt = canvas.getContext("2d")
  ctxt.clearRect(0, 0, rect.width, rect.height)
  for(const rect of rects) {
    ctxt.fillStyle = rect.color
    ctxt.fillRect(rect.left, rect.top, rect.width, rect.height)
  }
}

// Create a virtual-dom element from the drawing
function toVDom(rects) {
  return I.h('canvas',
    {
      props: {width: rects.width, height: rects.height},
      on: {click: ev => handleClick(rects.rects, ev)},
      hook: {
        insert: (vnode) => draw(rects.rects, vnode.elm),
        postpatch: (old, vnode) => draw(rects.rects, vnode.elm)
      }
    }, [])
}

Now that we have our output type, it’s very simple to write Imperative.js widgets with it. First, we wrap the atomic element rect. We use a click handler which calls resolve with the event, this will cause the widget to return with that value.

function *wrappedRect(color, w, h) {
  yield (resolve, reject) => rect(color, w, h, ev => resolve(ev))
}

Now we can use our wrapped rect widget with es6 generator syntax to build linear widgets with just one output at a time. The function below will move through a list of colors each time it is clicked.

function* colorSequence(colors, w, h) {
  let ii = 0
  while(true) {
    // clickEvent is ignored, since we always just move to the next color
    const clickEvent = yield* wrappedRect(colors[ii], w, h)
    ii = (ii + 1) % colors.length
  }
}

To compose widgets in parallel, we use I.zip and I.mapOut. zip takes a list of generators, each yielding (resolve, reject) => o, and returns a single generator which yields (resolve, reject) => [o]. The combined generator returns as soon as any of the list of child generators returns.

I.mapOut(f) takes a generator which yields (resolve, reject) => o, and returns a generator which yields (resolve, reject) => f(o).

Using the two together, we will take two widgets which yield rects, zip them together to make one widget which yields a list of two rects, and then mapOut to produce a widget which yields a single rect.

function wrappedBeside(l, r) {
  return I.mapOut(lr => beside(lr[0], lr[1]))(I.zip([l, r]))
}

function wrappedAbove(l, r) {
  return I.mapOut(lr => above(lr[0], lr[1]))(I.zip([l, r]))
}

Note that these functions are not using the ES6 generator syntax with function*. zip and mapOut operate on generators directly, so there is no need for the special syntax.

Finally, to run the application, we need to do one final mapOut to transform the output type to a virtual-dom.

example("Colorful rectangle widgets", I.mapOut(toVDom)(function*() {
  yield* wrappedAbove(
    colorSequence(['purple', 'white', 'orange'], 300, 100),
    wrappedBeside(
      colorSequence(['red', 'yellow', 'green'], 100, 100),
      colorSequence(['blue', 'green', 'black'], 200, 200)))
}()))