From 3489b5fab7489fcf604ab018a153e90866540f26 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Sat, 6 Feb 2016 07:42:31 -0500 Subject: [PATCH] DOM driver. --- js/examples/dom/index.html | 16 +++++ js/examples/dom/index.js | 54 +++++++++++++++++ js/src/dom-driver.js | 120 +++++++++++++++++++++++++++++++++++++ js/src/main.js | 2 +- 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 js/examples/dom/index.html create mode 100644 js/examples/dom/index.js create mode 100644 js/src/dom-driver.js 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");