diff --git a/js/examples/dom/index.html b/js/examples/dom/index.html
new file mode 100644
index 0000000..3b90a38
--- /dev/null
+++ b/js/examples/dom/index.html
@@ -0,0 +1,16 @@
+
+
+
+ Syndicate: DOM Example
+
+
+
+
+
+
+ DOM example
+
+
+
+
+
diff --git a/js/examples/dom/index.js b/js/examples/dom/index.js
new file mode 100644
index 0000000..9369c0b
--- /dev/null
+++ b/js/examples/dom/index.js
@@ -0,0 +1,54 @@
+var G;
+$(document).ready(function () {
+ var Network = Syndicate.Network;
+ var sub = Syndicate.sub;
+ var assert = Syndicate.assert;
+ var retract = Syndicate.retract;
+ var __ = Syndicate.__;
+ var _$ = Syndicate._$;
+
+ G = new Syndicate.Ground(function () {
+ console.log('starting ground boot');
+
+ Syndicate.DOM.spawnDOMDriver();
+
+ Network.spawn({
+ boot: function () {
+ return assert(["DOM", "#clicker-holder", "clicker",
+ ["button", ["span", [["style", "font-style: italic"]], "Click me!"]]])
+ .andThen(sub(["jQuery", "button.clicker", "click", __]));
+ },
+ handleEvent: function (e) {
+ if (e.type === "message" && e.message[0] === "jQuery") {
+ Network.send("bump_count");
+ }
+ }
+ });
+
+ Network.spawn({
+ counter: 0,
+ boot: function () {
+ this.updateState();
+ return sub("bump_count");
+ },
+ updateState: function () {
+ Network.stateChange(retract(["DOM", __, __, __])
+ .andThen(assert(["DOM", "#counter-holder", "counter",
+ ["div",
+ ["p", "The current count is: ", this.counter]]])));
+ },
+ handleEvent: function (e) {
+ if (e.type === "message" && e.message === "bump_count") {
+ this.counter++;
+ this.updateState();
+ }
+ }
+ });
+ });
+
+ G.network.onStateChange = function (mux, patch) {
+ $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+ };
+
+ G.startStepping();
+});
diff --git a/js/src/dom-driver.js b/js/src/dom-driver.js
new file mode 100644
index 0000000..4139ede
--- /dev/null
+++ b/js/src/dom-driver.js
@@ -0,0 +1,120 @@
+// DOM fragment display driver
+var Syndicate = require("./syndicate.js");
+var Patch = require("./patch.js");
+var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
+var Network = Syndicate.Network;
+var __ = Syndicate.__;
+var _$ = Syndicate._$;
+
+function spawnDOMDriver(domWrapFunction, jQueryWrapFunction) {
+ domWrapFunction = domWrapFunction || defaultWrapFunction;
+ var spec = domWrapFunction(_$('selector'), _$('fragmentClass'), _$('fragmentSpec'));
+ Network.spawn(
+ new DemandMatcher(spec,
+ Patch.advertise(spec),
+ {
+ onDemandIncrease: function (c) {
+ Network.spawn(new DOMFragment(c.selector,
+ c.fragmentClass,
+ c.fragmentSpec,
+ domWrapFunction,
+ jQueryWrapFunction));
+ }
+ }));
+}
+
+function defaultWrapFunction(selector, fragmentClass, fragmentSpec) {
+ return ["DOM", selector, fragmentClass, fragmentSpec];
+}
+
+function DOMFragment(selector, fragmentClass, fragmentSpec, domWrapFunction, jQueryWrapFunction) {
+ this.selector = selector;
+ this.fragmentClass = fragmentClass;
+ this.fragmentSpec = fragmentSpec;
+ this.domWrapFunction = domWrapFunction;
+ this.jQueryWrapFunction = jQueryWrapFunction;
+ this.nodes = this.buildNodes();
+}
+
+DOMFragment.prototype.boot = function () {
+ var self = this;
+ var specification = self.domWrapFunction(self.selector, self.fragmentClass, self.fragmentSpec);
+
+ Network.spawn(new Network(function () {
+ Syndicate.JQuery.spawnJQueryDriver(self.selector+" > ."+self.fragmentClass,
+ 1,
+ self.jQueryWrapFunction);
+ Network.spawn({
+ boot: function () {
+ return Patch.sub(Patch.advertise(specification), 1);
+ },
+ handleEvent: function (e) {
+ if (e.type === "stateChange" && e.patch.hasRemoved()) {
+ Network.exitNetwork();
+ }
+ }
+ });
+ }));
+
+ return Patch.sub(specification).andThen(Patch.pub(specification));
+};
+
+DOMFragment.prototype.handleEvent = function (e) {
+ if (e.type === "stateChange" && e.patch.hasRemoved()) {
+ for (var i = 0; i < this.nodes.length; i++) {
+ var n = this.nodes[i];
+ n.parentNode.removeChild(n);
+ }
+ Network.exit();
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+function isAttributes(x) {
+ return Array.isArray(x) && ((x.length === 0) || Array.isArray(x[0]));
+}
+
+DOMFragment.prototype.interpretSpec = function (spec) {
+ // Fragment specs are roughly JSON-equivalents of SXML.
+ // spec ::== ["tag", [["attr", "value"], ...], spec, spec, ...]
+ // | ["tag", spec, spec, ...]
+ // | "cdata"
+ if (typeof(spec) === "string" || typeof(spec) === "number") {
+ return document.createTextNode(spec);
+ } else if ($.isArray(spec)) {
+ var tagName = spec[0];
+ var hasAttrs = isAttributes(spec[1]);
+ var attrs = hasAttrs ? spec[1] : {};
+ var kidIndex = hasAttrs ? 2 : 1;
+
+ // Wow! Such XSS! Many hacks! So vulnerability! Amaze!
+ var n = document.createElement(tagName);
+ for (var i = 0; i < attrs.length; i++) {
+ n.setAttribute(attrs[i][0], attrs[i][1]);
+ }
+ for (var i = kidIndex; i < spec.length; i++) {
+ n.appendChild(this.interpretSpec(spec[i]));
+ }
+ return n;
+ } else {
+ throw new Error("Ill-formed DOM specification");
+ }
+};
+
+DOMFragment.prototype.buildNodes = function () {
+ var self = this;
+ var nodes = [];
+ $(self.selector).each(function (index, domNode) {
+ var n = self.interpretSpec(self.fragmentSpec.toJS());
+ n.classList.add(self.fragmentClass);
+ domNode.appendChild(n);
+ nodes.push(n);
+ });
+ return nodes;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.spawnDOMDriver = spawnDOMDriver;
+module.exports.defaultWrapFunction = defaultWrapFunction;
diff --git a/js/src/main.js b/js/src/main.js
index fd18f2b..1c2984c 100644
--- a/js/src/main.js
+++ b/js/src/main.js
@@ -20,7 +20,7 @@ copyKeys(['__', '_$', '$Capture', '$Special',
module.exports.DemandMatcher = require('./demand-matcher.js').DemandMatcher;
module.exports.Seal = require('./seal.js').Seal;
-// module.exports.DOM = require("./dom-driver.js");
+module.exports.DOM = require("./dom-driver.js");
module.exports.JQuery = require("./jquery-driver.js");
// module.exports.RoutingTableWidget = require("./routing-table-widget.js");
// module.exports.WebSocket = require("./websocket-driver.js");