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() {
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();

View File

@ -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++;
}

View File

@ -5,6 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script>
@ -33,7 +34,14 @@
</section>
<section id="active_users">
<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>

View File

@ -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', _) {

View File

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

View File

@ -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!"]])))
'<button><span style="font-style: italic">Click me!</span></button>'))
.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]])))));
'<div><p>The current count is: '+this.counter+
'</p></div>'))));
},
handleEvent: function (e) {
if (e.type === "message" && e.message === "bump_count") {

View File

@ -5,6 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script>
@ -19,13 +20,22 @@
<section>
<h3>TV</h3>
<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>
</section>
<section>
<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-off">Turn off switch</button>
</section>
@ -37,7 +47,11 @@
<section>
<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>

View File

@ -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;

View File

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

View File

@ -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',
'<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+' />'+
'</svg>')
when (typeof this.angle === 'number');
}
}

View File

@ -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 = $('<div>' + self.fragmentSpec + '</div>')[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;
};