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:
parent
efc444ac37
commit
dede7f08a7
|
@ -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();
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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', _) {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
background: lightgrey;
|
||||
}
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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">
|
||||
<div><ul id="tv" class="alerts"></ul></div>
|
||||
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tv-container {
|
||||
background: url('img/tvscreen.gif');
|
||||
background-size: 100%;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, '');
|
||||
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);
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue