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.
This commit is contained in:
Tony Garnock-Jones 2016-05-10 22:38:40 -04:00
parent efc444ac37
commit dede7f08a7
11 changed files with 67 additions and 78 deletions

View File

@ -10,7 +10,7 @@ $(document).ready(function() {
Syndicate.Actor.spawnActor(new Object(), function() { Syndicate.Actor.spawnActor(new Object(), function() {
this.counter = 0; this.counter = 0;
Syndicate.Actor.createFacet() 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() { .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++; this.counter++;
})).completeBuild(); })).completeBuild();

View File

@ -9,7 +9,7 @@ $(document).ready(function() {
actor { actor {
this.counter = 0; this.counter = 0;
react { react {
assert DOM('#button-label', '', Syndicate.seal(this.counter)); assert DOM('#button-label', '', '' + this.counter);
on message jQueryEvent('#counter', 'click', _) { on message jQueryEvent('#counter', 'click', _) {
this.counter++; this.counter++;
} }

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script>
<script src="../../third-party/jquery-2.2.0.min.js"></script> <script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script> <script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
@ -33,7 +34,14 @@
</section> </section>
<section id="active_users"> <section id="active_users">
<h1>Active Users</h1> <h1>Active Users</h1>
<ul id="nymlist"></ul> <ul id="nymlist">
<template id="nym_template">
<li>
<span class="nym">{{who}}</span>
<span class="nym_status">{{status}}</span>
</li>
</template>
</ul>
</section> </section>
</section> </section>

View File

@ -35,10 +35,8 @@ function spawnChatApp() {
assert toBroker(url, present(this.nym, this.status)); assert toBroker(url, present(this.nym, this.status));
during fromBroker(url, present($who, $status)) { during fromBroker(url, present($who, $status)) {
assert DOM('#nymlist', 'present-nym', Syndicate.seal( assert DOM('#nymlist', 'present-nym',
["li", Mustache.render($('#nym_template').html(), { who: who, status: status }));
["span", [["class", "nym"]], who],
["span", [["class", "nym_status"]], status]]));
} }
on message jQueryEvent('#send_chat', 'click', _) { on message jQueryEvent('#send_chat', 'click', _) {

View File

@ -1,3 +1,7 @@
template {
display: none;
}
h1 { h1 {
background: lightgrey; background: lightgrey;
} }

View File

@ -4,7 +4,6 @@ $(document).ready(function () {
var sub = Syndicate.sub; var sub = Syndicate.sub;
var assert = Syndicate.assert; var assert = Syndicate.assert;
var retract = Syndicate.retract; var retract = Syndicate.retract;
var seal = Syndicate.seal;
var __ = Syndicate.__; var __ = Syndicate.__;
var _$ = Syndicate._$; var _$ = Syndicate._$;
@ -18,8 +17,7 @@ $(document).ready(function () {
Dataspace.spawn({ Dataspace.spawn({
boot: function () { boot: function () {
return assert(DOM("#clicker-holder", "clicker", return assert(DOM("#clicker-holder", "clicker",
seal(["button", ["span", [["style", "font-style: italic"]], '<button><span style="font-style: italic">Click me!</span></button>'))
"Click me!"]])))
.andThen(sub(jQueryEvent("button.clicker", "click", __))); .andThen(sub(jQueryEvent("button.clicker", "click", __)));
}, },
handleEvent: function (e) { handleEvent: function (e) {
@ -38,9 +36,8 @@ $(document).ready(function () {
updateState: function () { updateState: function () {
Dataspace.stateChange(retract(DOM.pattern) Dataspace.stateChange(retract(DOM.pattern)
.andThen(assert(DOM("#counter-holder", "counter", .andThen(assert(DOM("#counter-holder", "counter",
seal(["div", '<div><p>The current count is: '+this.counter+
["p", "The current count is: ", '</p></div>'))));
this.counter]])))));
}, },
handleEvent: function (e) { handleEvent: function (e) {
if (e.type === "message" && e.message === "bump_count") { if (e.type === "message" && e.message === "bump_count") {

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script>
<script src="../../third-party/jquery-2.2.0.min.js"></script> <script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script> <script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script> <script src="../../dist/syndicate.js"></script>
@ -19,13 +20,22 @@
<section> <section>
<h3>TV</h3> <h3>TV</h3>
<div id="tv-container"> <div id="tv-container">
&nbsp;<div><ul id="tv" class="alerts"></ul></div> &nbsp;
<ul id="tv" class="alerts">
<template id="alert_template">
<li>{{text}}</li>
</template>
</ul>
</div> </div>
</section> </section>
<section> <section>
<h3>Stove switch</h3> <h3>Stove switch</h3>
<div id="stove-switch"></div> <div id="stove-switch">
<template id="stove_element_template">
<img src="{{imgurl}}">
</template>
</div>
<button id="stove-switch-on">Turn on switch</button> <button id="stove-switch-on">Turn on switch</button>
<button id="stove-switch-off">Turn off switch</button> <button id="stove-switch-off">Turn off switch</button>
</section> </section>
@ -37,7 +47,11 @@
<section> <section>
<h3>Power draw meter</h3> <h3>Power draw meter</h3>
<div id="power-draw-meter"></div> <div id="power-draw-meter">
<template id="power_draw_template">
<p>Power draw: <span class="power-meter-display">{{watts}} W</span></p>
</template>
</div>
</section> </section>
</section> </section>

View File

@ -15,7 +15,7 @@ function spawnTV() {
actor { actor {
react { react {
during tvAlert($text) { 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 switchState(this.powerOn);
assert DOM('#stove-switch', 'switch-state', assert DOM('#stove-switch', 'switch-state',
Syndicate.seal(["img", [["src", Mustache.render($('#stove_element_template').html(),
"img/stove-coil-element-" + { imgurl: ("img/stove-coil-element-" +
(this.powerOn ? "hot" : "cold") + ".jpg"]]])); (this.powerOn ? "hot" : "cold") + ".jpg") }));
on message jQueryEvent('#stove-switch-on', 'click', _) { this.powerOn = true; } on message jQueryEvent('#stove-switch-on', 'click', _) { this.powerOn = true; }
on message jQueryEvent('#stove-switch-off', 'click', _) { this.powerOn = false; } on message jQueryEvent('#stove-switch-off', 'click', _) { this.powerOn = false; }
@ -92,9 +92,7 @@ function spawnPowerDrawMonitor() {
assert powerDraw(this.watts); assert powerDraw(this.watts);
assert DOM('#power-draw-meter', 'power-draw', assert DOM('#power-draw-meter', 'power-draw',
Syndicate.seal(["p", "Power draw: ", Mustache.render($('#power_draw_template').html(), { watts: this.watts }));
["span", [["class", "power-meter-display"]],
this.watts + " W"]]));
on asserted switchState($on) { on asserted switchState($on) {
this.watts = on ? 1500 : 0; this.watts = on ? 1500 : 0;

View File

@ -1,3 +1,7 @@
template {
display: none;
}
#tv-container { #tv-container {
background: url('img/tvscreen.gif'); background: url('img/tvscreen.gif');
background-size: 100%; background-size: 100%;

View File

@ -19,15 +19,11 @@ $(document).ready(function () {
this.handX = 50 + 40 * Math.cos(this.angle); this.handX = 50 + 40 * Math.cos(this.angle);
this.handY = 50 + 40 * Math.sin(this.angle); this.handY = 50 + 40 * Math.sin(this.angle);
} }
assert DOM('#clock', 'clock', Syndicate.seal( assert DOM('#clock', 'clock',
["svg", [["xmlns", "http://www.w3.org/2000/svg"], '<svg width="300px" viewBox="0 0 100 100">'+
["width", "300px"], '<circle fill="#0B79CE" r=45 cx=50 cy=50/>'+
["viewBox", "0 0 100 100"]], '<line stroke="#023963" x1=50 y1=50 x2='+this.handX+' y2='+this.handY+' />'+
["circle", [["fill", "#0B79CE"], '</svg>')
["r", 45], ["cx", 50], ["cy", 50]]],
["line", [["stroke", "#023963"],
["x1", 50], ["y1", 50],
["x2", this.handX], ["y2", this.handY]]]]))
when (typeof this.angle === 'number'); when (typeof this.angle === 'number');
} }
} }

View File

@ -3,7 +3,6 @@ var Patch = require("./patch.js");
var DemandMatcher = require('./demand-matcher.js').DemandMatcher; var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
var Struct = require('./struct.js'); var Struct = require('./struct.js');
var Ack = require('./ack.js').Ack; var Ack = require('./ack.js').Ack;
var Seal = require('./seal.js').Seal;
var Dataspace_ = require("./dataspace.js"); var Dataspace_ = require("./dataspace.js");
var Dataspace = Dataspace_.Dataspace; 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 () { DOMFragment.prototype.buildNodes = function () {
var self = this; var self = this;
var nodes = []; var nodes = [];
$(self.selector).each(function (index, domNode) { $(self.selector).each(function (index, domNode) {
if (!(self.fragmentSpec instanceof Syndicate.Seal)) { if (typeof self.fragmentSpec !== 'string') {
throw new Error("DOM fragmentSpec not contained in a Syndicate.Seal: " + JSON.stringify(self.fragmentSpec)); throw new Error("DOM fragmentSpec not a string: " + JSON.stringify(self.fragmentSpec));
} }
var n = self.interpretSpec(self.fragmentSpec.sealContents, ''); var newNodes = $('<div>' + self.fragmentSpec + '</div>')[0].childNodes;
if ('classList' in n) { // This next loop looks SUPER SUSPICIOUS. What is happening is
n.classList.add(self.fragmentClass); // 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; return nodes;
}; };