Skip to content

Instantly share code, notes, and snippets.

@milankinen
Created March 5, 2016 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milankinen/8d425a2b23fe426272ca to your computer and use it in GitHub Desktop.
Save milankinen/8d425a2b23fe426272ca to your computer and use it in GitHub Desktop.
import EventEmitter from "events"
import {Observable} from "rx"
import h from "virtual-dom/h"
import diff from "virtual-dom/diff"
import patch from "virtual-dom/patch"
import el from "virtual-dom/create-element"
import selmatch from "matches-selector"
import {values, indexBy, prop} from "ramda"
const delegate = (name, em) => ({
fn: e => em.emit("event", e),
add: l => em.addListener("event", l),
rm: l => em.removeListener("event", l),
isActive: () => em.listenerCount("event") > 0,
name,
em
})
const mergeDelegates = (oldD, newD) =>
indexBy(prop("name"), [...(new Set([...values(oldD), ...values(newD)]))])
class Node {
constructor(vtree) {
this.rendered = false
this.vtree = vtree
this.delegates = {}
this.domNode = null
this.addListener = this.addListener.bind(this)
this.removeListener = this.removeListener.bind(this)
}
_listen(domNode) {
Object.keys(this.delegates).forEach(name => {
domNode.addEventListener(name, this.delegates[name].fn)
})
}
init() {
this.domNode = el(this.vtree)
this._listen(this.domNode)
return this.domNode
}
update({delegates, vtree}, domNode) {
const patches = diff(vtree, this.vtree)
Object.assign(this, {domNode, delegates: mergeDelegates(delegates, this.delegates)})
this._listen(domNode)
return patch(domNode, patches)
}
destroy() {
this.domNode = null
}
addListener(name, l) {
const isNew = !this.delegates[name]
const d = isNew ? (this.delegates[name] = delegate(name, new EventEmitter())) : this.delegates[name]
d.add(l)
if (isNew && this.domNode) {
this.domNode.addEventListener(name, d.fn)
}
}
removeListener(name, l) {
const d = this.delegates[name]
if (d) {
d.rm(l)
if (!d.isActive()) {
delete this.delegates[name]
this.domNode && this.domNode.removeEventListener(name, d.fn)
}
}
}
}
Node.prototype.type = "Widget"
const isDOM = o =>
o && o.node === o.events && o.node instanceof Node
const liftDOM = vtree => {
vtree.children = vtree.children.map(c => c instanceof Node && !c.rendered ? c.vtree : isDOM(c) ? c.node : c)
return new Node(vtree)
}
const matches = sel => e =>
selmatch(e.target, sel)
const lazyEvents = (sel, type) => ({node}) =>
Observable.fromEvent(node, type).filter(matches(sel))
export function makeDOMDriver(rootSel) {
return dom$ => {
const root = document.querySelector(rootSel)
let vdom, node
dom$
.filter(isDOM)
.subscribe(({node: next}) => {
if (vdom) {
const patches = diff(vdom, next)
vdom = next
node = patch(node, patches)
} else {
vdom = next
node = el(vdom)
root.appendChild(node)
}
})
return {
h: (...args) =>
liftDOM(h(...args)),
render: node$ => node$
.tap(node => node.rendered = true)
.map(node => ({node, events: node, type: "Widget"}))
.shareReplay(1),
events: (dom$, sel, type) => dom$
.filter(isDOM)
.flatMapLatest(lazyEvents(sel, type))
.share()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment