From dede7f08a7b04e24bd87fb8e1f14590478f5a3ef Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Tue, 10 May 2016 22:38:40 -0400 Subject: [PATCH] Use strings-of-HTML and mustache.js for DOM fragments. This avoids churn in the dataspace for no-op DOM updates, but at the cost of losing the identity of multiple pieces of asserted DOM when they end up being textually identical. The fix is, generally, to make sure your DOM fragments are different in some (perhaps invisible when rendered) way. Next commit updates the IoT demo to avoid duplicate fragments. --- js/examples/button/index.expanded.js | 2 +- js/examples/button/index.js | 2 +- js/examples/chat/index.html | 10 ++++- js/examples/chat/index.js | 6 +-- js/examples/chat/style.css | 4 ++ js/examples/dom/index.js | 9 ++-- js/examples/iot/index.html | 20 +++++++-- js/examples/iot/index.js | 12 +++--- js/examples/iot/style.css | 4 ++ js/examples/svg/index.js | 14 +++---- js/src/dom-driver.js | 62 +++++++--------------------- 11 files changed, 67 insertions(+), 78 deletions(-) diff --git a/js/examples/button/index.expanded.js b/js/examples/button/index.expanded.js index 3dfe4ee..efc7055 100644 --- a/js/examples/button/index.expanded.js +++ b/js/examples/button/index.expanded.js @@ -10,7 +10,7 @@ $(document).ready(function() { Syndicate.Actor.spawnActor(new Object(), function() { this.counter = 0; Syndicate.Actor.createFacet() -.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(DOM('#button-label','',Syndicate.seal(this.counter)), 0); })) +.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(DOM('#button-label','',''+this.counter), 0); })) .onEvent(false, "message", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(jQueryEvent('#counter','click',_), 0); }), (function() { var _ = Syndicate.__; return { assertion: jQueryEvent('#counter','click',_), metalevel: 0 }; }), (function() { this.counter++; })).completeBuild(); diff --git a/js/examples/button/index.js b/js/examples/button/index.js index f9ad9bd..c329a07 100644 --- a/js/examples/button/index.js +++ b/js/examples/button/index.js @@ -9,7 +9,7 @@ $(document).ready(function() { actor { this.counter = 0; react { - assert DOM('#button-label', '', Syndicate.seal(this.counter)); + assert DOM('#button-label', '', '' + this.counter); on message jQueryEvent('#counter', 'click', _) { this.counter++; } diff --git a/js/examples/chat/index.html b/js/examples/chat/index.html index 611a991..0920ab6 100644 --- a/js/examples/chat/index.html +++ b/js/examples/chat/index.html @@ -5,6 +5,7 @@ + @@ -33,7 +34,14 @@

Active Users

- +
diff --git a/js/examples/chat/index.js b/js/examples/chat/index.js index 617a269..c254dd7 100644 --- a/js/examples/chat/index.js +++ b/js/examples/chat/index.js @@ -35,10 +35,8 @@ function spawnChatApp() { assert toBroker(url, present(this.nym, this.status)); during fromBroker(url, present($who, $status)) { - assert DOM('#nymlist', 'present-nym', Syndicate.seal( - ["li", - ["span", [["class", "nym"]], who], - ["span", [["class", "nym_status"]], status]])); + assert DOM('#nymlist', 'present-nym', + Mustache.render($('#nym_template').html(), { who: who, status: status })); } on message jQueryEvent('#send_chat', 'click', _) { diff --git a/js/examples/chat/style.css b/js/examples/chat/style.css index a0fe84b..17dcfdd 100644 --- a/js/examples/chat/style.css +++ b/js/examples/chat/style.css @@ -1,3 +1,7 @@ +template { + display: none; +} + h1 { background: lightgrey; } diff --git a/js/examples/dom/index.js b/js/examples/dom/index.js index 0af4a3e..ed7dc84 100644 --- a/js/examples/dom/index.js +++ b/js/examples/dom/index.js @@ -4,7 +4,6 @@ $(document).ready(function () { var sub = Syndicate.sub; var assert = Syndicate.assert; var retract = Syndicate.retract; - var seal = Syndicate.seal; var __ = Syndicate.__; var _$ = Syndicate._$; @@ -18,8 +17,7 @@ $(document).ready(function () { Dataspace.spawn({ boot: function () { return assert(DOM("#clicker-holder", "clicker", - seal(["button", ["span", [["style", "font-style: italic"]], - "Click me!"]]))) + '')) .andThen(sub(jQueryEvent("button.clicker", "click", __))); }, handleEvent: function (e) { @@ -38,9 +36,8 @@ $(document).ready(function () { updateState: function () { Dataspace.stateChange(retract(DOM.pattern) .andThen(assert(DOM("#counter-holder", "counter", - seal(["div", - ["p", "The current count is: ", - this.counter]]))))); + '

The current count is: '+this.counter+ + '

')))); }, handleEvent: function (e) { if (e.type === "message" && e.message === "bump_count") { diff --git a/js/examples/iot/index.html b/js/examples/iot/index.html index 3a862a8..d03b18b 100644 --- a/js/examples/iot/index.html +++ b/js/examples/iot/index.html @@ -5,6 +5,7 @@ + @@ -19,13 +20,22 @@

TV

-  
    +   +
      + +

    Stove switch

    -
    +
    + +
    @@ -37,7 +47,11 @@

    Power draw meter

    -
    +
    + +
    diff --git a/js/examples/iot/index.js b/js/examples/iot/index.js index 3293a56..8ff23b3 100644 --- a/js/examples/iot/index.js +++ b/js/examples/iot/index.js @@ -15,7 +15,7 @@ function spawnTV() { actor { react { during tvAlert($text) { - assert DOM('#tv', 'alert', Syndicate.seal(["li", text])); + assert DOM('#tv', 'alert', Mustache.render($('#alert_template').html(), { text: text })); } } } @@ -68,9 +68,9 @@ function spawnStoveSwitch() { assert switchState(this.powerOn); assert DOM('#stove-switch', 'switch-state', - Syndicate.seal(["img", [["src", - "img/stove-coil-element-" + - (this.powerOn ? "hot" : "cold") + ".jpg"]]])); + Mustache.render($('#stove_element_template').html(), + { imgurl: ("img/stove-coil-element-" + + (this.powerOn ? "hot" : "cold") + ".jpg") })); on message jQueryEvent('#stove-switch-on', 'click', _) { this.powerOn = true; } on message jQueryEvent('#stove-switch-off', 'click', _) { this.powerOn = false; } @@ -92,9 +92,7 @@ function spawnPowerDrawMonitor() { assert powerDraw(this.watts); assert DOM('#power-draw-meter', 'power-draw', - Syndicate.seal(["p", "Power draw: ", - ["span", [["class", "power-meter-display"]], - this.watts + " W"]])); + Mustache.render($('#power_draw_template').html(), { watts: this.watts })); on asserted switchState($on) { this.watts = on ? 1500 : 0; diff --git a/js/examples/iot/style.css b/js/examples/iot/style.css index 2fd6050..0f34cb3 100644 --- a/js/examples/iot/style.css +++ b/js/examples/iot/style.css @@ -1,3 +1,7 @@ +template { + display: none; +} + #tv-container { background: url('img/tvscreen.gif'); background-size: 100%; diff --git a/js/examples/svg/index.js b/js/examples/svg/index.js index 485db4d..279d06b 100644 --- a/js/examples/svg/index.js +++ b/js/examples/svg/index.js @@ -19,15 +19,11 @@ $(document).ready(function () { this.handX = 50 + 40 * Math.cos(this.angle); this.handY = 50 + 40 * Math.sin(this.angle); } - assert DOM('#clock', 'clock', Syndicate.seal( - ["svg", [["xmlns", "http://www.w3.org/2000/svg"], - ["width", "300px"], - ["viewBox", "0 0 100 100"]], - ["circle", [["fill", "#0B79CE"], - ["r", 45], ["cx", 50], ["cy", 50]]], - ["line", [["stroke", "#023963"], - ["x1", 50], ["y1", 50], - ["x2", this.handX], ["y2", this.handY]]]])) + assert DOM('#clock', 'clock', + ''+ + ''+ + ''+ + '') when (typeof this.angle === 'number'); } } diff --git a/js/src/dom-driver.js b/js/src/dom-driver.js index ddffcbf..57c540c 100644 --- a/js/src/dom-driver.js +++ b/js/src/dom-driver.js @@ -3,7 +3,6 @@ var Patch = require("./patch.js"); var DemandMatcher = require('./demand-matcher.js').DemandMatcher; var Struct = require('./struct.js'); var Ack = require('./ack.js').Ack; -var Seal = require('./seal.js').Seal; var Dataspace_ = require("./dataspace.js"); var Dataspace = Dataspace_.Dataspace; @@ -89,57 +88,28 @@ DOMFragment.prototype.handleEvent = function (e) { /////////////////////////////////////////////////////////////////////////// -function isAttributes(x) { - return Array.isArray(x) && ((x.length === 0) || Array.isArray(x[0])); -} - -DOMFragment.prototype.interpretSpec = function (spec, xmlns) { - // 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; - - var xmlnsAttr = attrs.find(function (e) { return e[0] === 'xmlns' }); - if (xmlnsAttr) { - xmlns = xmlnsAttr[1]; - } - - // TODO: Wow! Such XSS! Many hacks! So vulnerability! Amaze! - var n = xmlns - ? document.createElementNS(xmlns, tagName) - : document.createElement(tagName); - for (var i = 0; i < attrs.length; i++) { - if (attrs[i][0] !== 'xmlns') n.setAttribute(attrs[i][0], attrs[i][1]); - } - for (var i = kidIndex; i < spec.length; i++) { - n.appendChild(this.interpretSpec(spec[i], xmlns)); - } - 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) { - if (!(self.fragmentSpec instanceof Syndicate.Seal)) { - throw new Error("DOM fragmentSpec not contained in a Syndicate.Seal: " + JSON.stringify(self.fragmentSpec)); + if (typeof self.fragmentSpec !== 'string') { + throw new Error("DOM fragmentSpec not a string: " + JSON.stringify(self.fragmentSpec)); } - var n = self.interpretSpec(self.fragmentSpec.sealContents, ''); - if ('classList' in n) { - n.classList.add(self.fragmentClass); + var newNodes = $('
    ' + self.fragmentSpec + '
    ')[0].childNodes; + // This next loop looks SUPER SUSPICIOUS. What is happening is + // that each time we call domNode.appendChild(n), where n is an + // element of the NodeList newNodes, the DOM is **removing** n + // from the NodeList in order to place it in its new parent. So, + // each call to appendChild shrinks the NodeList by one node until + // it is finally empty, and its length property yields zero. + while (newNodes.length) { + var n = newNodes[0]; + if ('classList' in n) { + n.classList.add(self.fragmentClass); + } + domNode.appendChild(n); + nodes.push(n); } - domNode.appendChild(n); - nodes.push(n); }); return nodes; };