Port html package (and one example)

This commit is contained in:
Tony Garnock-Jones 2021-12-09 22:15:47 +01:00
parent 7a7ad76036
commit 74377b87f6
4 changed files with 173 additions and 146 deletions

View File

@ -3,9 +3,8 @@
<head> <head>
<title>Syndicate: Table Example</title> <title>Syndicate: Table Example</title>
<meta charset="utf-8"> <meta charset="utf-8">
<script src="../../../../node_modules/@syndicate-lang/core/dist/syndicate.js"></script> <script src="node_modules/@syndicate-lang/core/dist/syndicate.js"></script>
<script src="../../../../node_modules/@syndicate-lang/html/dist/syndicate-html.js"></script> <script src="node_modules/@syndicate-lang/html/dist/syndicate-html.js"></script>
<script src="index.js"></script>
</head> </head>
<body> <body>
<h1>Table Example</h1> <h1>Table Example</h1>
@ -27,8 +26,6 @@
Source code: <a href="src/index.ts">index.ts</a> Source code: <a href="src/index.ts">index.ts</a>
</p> </p>
<div id="extra"></div> <div id="extra"></div>
<script> <script src="index.js"></script>
Syndicate.bootModule(Main.__SYNDICATE__bootProc);
</script>
</body> </body>
</html> </html>

View File

@ -6,7 +6,9 @@
"scripts": { "scripts": {
"prepare": "yarn compile && yarn rollup", "prepare": "yarn compile && yarn rollup",
"compile": "syndicate-tsc", "compile": "syndicate-tsc",
"compile:watch": "syndicate-tsc -w --verbose --intermediate-directory src.ts",
"rollup": "rollup -c", "rollup": "rollup -c",
"rollup:watch": "rollup -c -w",
"clean": "rm -rf lib/ index.js index.js.map" "clean": "rm -rf lib/ index.js index.js.map"
}, },
"author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>", "author": "Tony Garnock-Jones <tonyg@leastfixedpoint.com>",

View File

@ -1,56 +1,60 @@
/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com> /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Embedded, Value } from '@syndicate-lang/core'; import { Dataspace, Embedded, Value, Ref } from '@syndicate-lang/core';
activate import { UIEvent, GlobalEvent, HtmlFragments, template, Anchor } from '@syndicate-lang/html'; import { boot as bootHtml, UIEvent, GlobalEvent, HtmlFragments, template, Anchor } from '@syndicate-lang/html';
assertion type Person(id, firstName, lastName, address, age); assertion type Person(id, firstName, lastName, address, age);
message type SetSortColumn(number); message type SetSortColumn(number);
boot { Dataspace.boot(ds => {
function newRow(id: number, firstName: string, lastName: string, address: string, age: number) { bootHtml(ds);
spawn named ('model' + id) { at ds {
assert Person(id, firstName, lastName, address, age); function newRow(id: number, firstName: string, lastName: string, address: string, age: number) {
spawn named ('model' + id) {
assert Person(id, firstName, lastName, address, age);
}
}
newRow(1, 'Keith', 'Example', '94 Main St.', 44);
newRow(2, 'Karen', 'Fakeperson', '5504 Long Dr.', 34);
newRow(3, 'Angus', 'McFictional', '2B Pioneer Heights', 39);
newRow(4, 'Sue', 'Donnem', '1 Infinite Loop', 104);
newRow(5, 'Boaty', 'McBoatface', 'Arctic Ocean', 1);
spawn named 'view' {
let ui = new Anchor();
field orderColumn: number = 2;
function cell(text: Value): HtmlFragments {
return template`<td>${text.toString()}</td>`;
}
on message SetSortColumn($c: number) => orderColumn.value = c;
during Person($id: number, $firstName: string, $lastName: string, $address: string, $age: number) => {
assert ui.context(id).html(
'table#the-table tbody',
template`<tr>${[id, firstName, lastName, address, age].map(cell)}</tr>`,
[id, firstName, lastName, address, age][orderColumn.value]);
}
}
spawn named 'controller' {
on message GlobalEvent('table#the-table th', 'click', $e) => {
const event = (e as Embedded<Ref>).embeddedValue.target.data as Event;
send message SetSortColumn(JSON.parse((event.target as HTMLElement).dataset.column!));
}
}
spawn named 'alerter' {
let ui = new Anchor();
assert ui.html('#extra', template`<button>Click me</button>`);
on message UIEvent(ui.fragmentId, '.', 'click', $_e) => {
alert("Hello!");
}
} }
} }
});
newRow(1, 'Keith', 'Example', '94 Main St.', 44);
newRow(2, 'Karen', 'Fakeperson', '5504 Long Dr.', 34);
newRow(3, 'Angus', 'McFictional', '2B Pioneer Heights', 39);
newRow(4, 'Sue', 'Donnem', '1 Infinite Loop', 104);
newRow(5, 'Boaty', 'McBoatface', 'Arctic Ocean', 1);
spawn named 'view' {
let ui = new Anchor();
field orderColumn: number = 2;
function cell(text: Value): HtmlFragments {
return template`<td>${text.toString()}</td>`;
}
on message SetSortColumn($c: number) => this.orderColumn = c;
during Person($id: number, $firstName: string, $lastName: string, $address: string, $age: number) => {
assert ui.context(id)
.html('table#the-table tbody',
template`<tr>${[id, firstName, lastName, address, age].map(cell)}</tr>`,
[id, firstName, lastName, address, age][this.orderColumn]);
}
}
spawn named 'controller' {
on message GlobalEvent('table#the-table th', 'click', $e) => {
send message SetSortColumn(
JSON.parse(((e as Embedded<Event>).embeddedValue.target as HTMLElement).dataset.column!));
}
}
spawn named 'alerter' {
let ui = new Anchor();
assert ui.html('#extra', template`<button>Click me</button>`);
on message UIEvent(ui.fragmentId, '.', 'click', $_e) => {
alert("Hello!");
}
}
}

View File

@ -1,7 +1,8 @@
/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com> /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { randomId, Facet, Observe, FlexMap, Value, embed, Embedded } from "@syndicate-lang/core"; import { randomId, Observe, FlexMap, embed, Embedded, Ref, Turn, AnyValue } from "@syndicate-lang/core";
import { QuasiValue as Q } from "@syndicate-lang/core";
import * as P from "./protocol"; import * as P from "./protocol";
export * from "./protocol"; export * from "./protocol";
@ -9,15 +10,15 @@ export * from "./protocol";
import { HtmlFragments } from "./html"; import { HtmlFragments } from "./html";
export * from "./html"; export * from "./html";
boot { export function boot(ds: Ref) {
spawnGlobalEventFactory(thisFacet); spawnGlobalEventFactory(ds);
spawnWindowEventFactory(thisFacet); spawnWindowEventFactory(ds);
spawnUIFragmentFactory(thisFacet); spawnUIFragmentFactory(ds);
spawnUIAttributeFactory(thisFacet); spawnUIAttributeFactory(ds);
spawnUIPropertyFactory(thisFacet); spawnUIPropertyFactory(ds);
spawnUIChangeablePropertyFactory(thisFacet); spawnUIChangeablePropertyFactory(ds);
spawnLocationHashTracker(thisFacet); spawnLocationHashTracker(ds);
spawnAttributeUpdater(thisFacet); spawnAttributeUpdater(ds);
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
@ -32,16 +33,20 @@ export function newFragmentId() {
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
export function spawnGlobalEventFactory<T>(thisFacet: Facet<T>) { export function spawnGlobalEventFactory(ds: Ref) {
spawn named 'GlobalEventFactory' { spawn named 'GlobalEventFactory' {
during Observe(P.GlobalEvent($selector: string, $eventType: string, _)) => at ds {
spawn named ['GlobalEvent', selector, eventType] { during Observe({
let sender = thisFacet.wrapExternal((thisFacet, e: Event) => { "pattern": :pattern P.GlobalEvent(\Q.lit($selector: string),
send message P.GlobalEvent(selector, eventType, embed(e)); \Q.lit($eventType: string),
}); \_)
}) => spawn named ['GlobalEvent', selector, eventType] {
const facet = Turn.activeFacet;
function handler(event: Event) { function handler(event: Event) {
sender(event); facet.turn(() => {
send message P.GlobalEvent(selector, eventType, embed(create ({ data: event })));
});
return dealWithPreventDefault(eventType, event); return dealWithPreventDefault(eventType, event);
} }
@ -50,7 +55,7 @@ export function spawnGlobalEventFactory<T>(thisFacet: Facet<T>) {
eventUpdater(cleanEventType(eventType), handler, install)); eventUpdater(cleanEventType(eventType), handler, install));
} }
on start updateEventListeners(true); updateEventListeners(true);
on stop updateEventListeners(false); on stop updateEventListeners(false);
on asserted P.UIFragmentVersion($_i, $_v) => updateEventListeners(true); on asserted P.UIFragmentVersion($_i, $_v) => updateEventListeners(true);
@ -58,19 +63,22 @@ export function spawnGlobalEventFactory<T>(thisFacet: Facet<T>) {
// lets us ignore UIFragmentVersion records coming and going; on // lets us ignore UIFragmentVersion records coming and going; on
// the other hand, we do potentially a lot of redundant work. // the other hand, we do potentially a lot of redundant work.
} }
}
} }
} }
export function spawnWindowEventFactory<T>(thisFacet: Facet<T>) { export function spawnWindowEventFactory(ds: Ref) {
spawn named 'WindowEventFactory' { spawn named 'WindowEventFactory' {
during Observe(P.WindowEvent($eventType: string, _)) => at ds {
spawn named ['WindowEvent', eventType] { during Observe({
let sender = thisFacet.wrapExternal((thisFacet, e: Event) => { "pattern": :pattern P.WindowEvent(\Q.lit($eventType: string), \_)
send message P.WindowEvent(eventType, embed(e)); }) => spawn named ['WindowEvent', eventType] {
}); const facet = Turn.activeFacet;
let handler = function (event: Event) { let handler = function (event: Event) {
sender(event); facet.turn(() => {
send message P.WindowEvent(eventType, embed(create ({ data: event })));
});
return dealWithPreventDefault(eventType, event); return dealWithPreventDefault(eventType, event);
} }
@ -82,9 +90,10 @@ export function spawnWindowEventFactory<T>(thisFacet: Facet<T>) {
} }
} }
on start updateEventListeners(true); updateEventListeners(true);
on stop updateEventListeners(false); on stop updateEventListeners(false);
} }
}
} }
} }
@ -101,17 +110,17 @@ function isNodeOrderKey(x: any): x is NodeOrderKey {
type HandlerClosure = (event: Event) => void; type HandlerClosure = (event: Event) => void;
function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) { function spawnUIFragmentFactory(ds: Ref) {
type RegistrationKey = [string, string]; // [selector, eventType] type RegistrationKey = [string, string]; // [selector, eventType]
spawn named 'UIFragmentFactory' { spawn named 'UIFragmentFactory' {
during P.UIFragment($fragmentId0, _, _, _) => at ds {
spawn named ['UIFragment', fragmentId0] { during P.UIFragment($fragmentId0, _, _, _) => spawn named ['UIFragment', fragmentId0] {
if (!isFragmentId(fragmentId0)) return; if (!isFragmentId(fragmentId0)) return;
const fragmentId = fragmentId0; const fragmentId = fragmentId0;
field version: number = 0; field version: number = 0;
assert P.UIFragmentVersion(fragmentId, this.version) when (this.version > 0); assert P.UIFragmentVersion(fragmentId, version.value) when (version.value > 0);
let selector: string; let selector: string;
let html: Array<ChildNode>; let html: Array<ChildNode>;
@ -122,8 +131,13 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
on stop removeNodes(); on stop removeNodes();
during Observe(P.UIEvent(fragmentId, $selector: string, $eventType: string, _)) => { during Observe({
on start updateEventListeners([ selector, eventType ], true); "pattern": :pattern P.UIEvent(fragmentId,
\Q.lit($selector: string),
\Q.lit($eventType: string),
\_)
}) => {
updateEventListeners([ selector, eventType ], true);
on stop updateEventListeners([ selector, eventType ], false); on stop updateEventListeners([ selector, eventType ], false);
} }
@ -133,7 +147,7 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
removeNodes(); removeNodes();
selector = newSelector; selector = newSelector;
html = (newHtml as Embedded<Array<ChildNode>>).embeddedValue; html = (newHtml as Embedded<Ref>).embeddedValue.target.data as ChildNode[];
orderBy = newOrderBy; orderBy = newOrderBy;
anchorNodes = (selector !== null) ? selectorMatch(document.body, selector) : []; anchorNodes = (selector !== null) ? selectorMatch(document.body, selector) : [];
@ -153,7 +167,7 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
// (re)install event listeners // (re)install event listeners
eventRegistrations.forEach((_handler, key) => updateEventListeners(key, true)); eventRegistrations.forEach((_handler, key) => updateEventListeners(key, true));
this.version++; version.value++;
} }
function removeNodes() { function removeNodes() {
@ -172,11 +186,11 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
let handlerClosure: HandlerClosure; let handlerClosure: HandlerClosure;
if (!eventRegistrations.has(key)) { if (!eventRegistrations.has(key)) {
let sender = thisFacet.wrapExternal((thisFacet, e: Event) => { const facet = Turn.activeFacet;
send message P.UIEvent(fragmentId, selector, eventType, embed(e));
});
function handler(event: Event) { function handler(event: Event) {
sender(event); facet.turn(() => {
send message P.UIEvent(fragmentId, selector, eventType, embed(create ({ data: event })));
});
return dealWithPreventDefault(eventType, event); return dealWithPreventDefault(eventType, event);
} }
eventRegistrations.set(key, handler); eventRegistrations.set(key, handler);
@ -203,6 +217,7 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
} }
} }
} }
}
} }
} }
@ -303,29 +318,32 @@ function configureNode(n: ChildNode) {
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
function spawnUIAttributeFactory<T>(thisFacet: Facet<T>) { function spawnUIAttributeFactory(ds: Ref) {
spawn named 'UIAttributeFactory' { spawn named 'UIAttributeFactory' {
during P.UIAttribute($selector: string, $attribute: string, $value) => at ds {
spawn named ['UIAttribute', selector, attribute, value] { during P.UIAttribute($selector: string, $attribute: string, $value) =>
_attributeLike(thisFacet, selector, attribute, value, 'attribute'); spawn named ['UIAttribute', selector, attribute, value] {
} _attributeLike(selector, attribute, value, 'attribute');
}
}
} }
} }
function spawnUIPropertyFactory<T>(thisFacet: Facet<T>) { function spawnUIPropertyFactory(ds: Ref) {
spawn named 'UIPropertyFactory' { spawn named 'UIPropertyFactory' {
during P.UIProperty($selector: string, $property: string, $value) => at ds {
spawn named ['UIProperty', selector, property, value] { during P.UIProperty($selector: string, $property: string, $value) =>
_attributeLike(thisFacet, selector, property, value, 'property'); spawn named ['UIProperty', selector, property, value] {
} _attributeLike(selector, property, value, 'property');
}
}
} }
} }
function _attributeLike<T>(thisFacet: Facet<T>, function _attributeLike(selector: string,
selector: string, key: string,
key: string, value: AnyValue,
value: Value, kind: 'attribute' | 'property')
kind: 'attribute' | 'property')
{ {
let savedValues: Array<{node: Element, value: any}> = []; let savedValues: Array<{node: Element, value: any}> = [];
@ -387,7 +405,7 @@ function _attributeLike<T>(thisFacet: Facet<T>,
}); });
savedValues = []; savedValues = [];
} }
}; }
function splitClassValue(v: string | null): Array<string> { function splitClassValue(v: string | null): Array<string> {
v = (v ?? '').trim(); v = (v ?? '').trim();
@ -396,22 +414,28 @@ function splitClassValue(v: string | null): Array<string> {
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
function spawnUIChangeablePropertyFactory<T>(thisFacet: Facet<T>) { function spawnUIChangeablePropertyFactory(ds: Ref) {
spawn named 'UIChangeablePropertyFactory' { spawn named 'UIChangeablePropertyFactory' {
during Observe(P.UIChangeableProperty($selector: string, $property: string, _)) => at ds {
spawn named ['UIChangeableProperty', selector, property] { during Observe({
on start selectorMatch(document.body, selector).forEach(node => { "pattern": :pattern P.UIChangeableProperty(\Q.lit($selector: string),
\Q.lit($property: string),
\_)
}) => spawn named ['UIChangeableProperty', selector, property] {
selectorMatch(document.body, selector).forEach(node => {
react { react {
field value: any = (node as any)[property]; field propValue: any = (node as any)[property];
assert P.UIChangeableProperty(selector, property, this.value); assert P.UIChangeableProperty(selector, property, propValue.value);
const handlerClosure = thisFacet.wrapExternal((_thisFacet, _e: Event) => { const facet = Turn.activeFacet;
this.value = (node as any)[property]; const handlerClosure = (_e: Event) => facet.turn(() => {
propValue.value = (node as any)[property];
}); });
on start eventUpdater('change', handlerClosure, true)(node); eventUpdater('change', handlerClosure, true)(node);
on stop eventUpdater('change', handlerClosure, false)(node); on stop eventUpdater('change', handlerClosure, false)(node);
} }
}); });
} }
}
} }
} }
@ -463,56 +487,56 @@ export class Anchor {
} }
html(selector: string, html: HtmlFragments, orderBy: NodeOrderKey = ''): ReturnType<typeof P.UIFragment> { html(selector: string, html: HtmlFragments, orderBy: NodeOrderKey = ''): ReturnType<typeof P.UIFragment> {
return P.UIFragment(this.fragmentId, selector, embed(html.nodes()), orderBy); return P.UIFragment(this.fragmentId, selector, embed(create ({ data: html.nodes() })), orderBy);
} }
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
function spawnLocationHashTracker<T>(thisFacet: Facet<T>) { function spawnLocationHashTracker(ds: Ref) {
spawn named 'LocationHashTracker' { spawn named 'LocationHashTracker' {
field hashValue: string = '/'; at ds {
assert P.LocationHash(this.hashValue); field hashValue: string = '/';
assert P.LocationHash(hashValue.value);
const loadHash = () => { const loadHash = () => {
var h = window.location.hash; var h = window.location.hash;
if (h.length && h[0] === '#') { if (h.length && h[0] === '#') {
h = h.slice(1); h = h.slice(1);
} }
this.hashValue = h || '/'; hashValue.value = h || '/';
}; };
const facet = Turn.activeFacet;
const handlerClosure = () => facet.turn(loadHash);
let handlerClosure = thisFacet.wrapExternal(loadHash);
on start {
loadHash(); loadHash();
window.addEventListener('hashchange', handlerClosure); window.addEventListener('hashchange', handlerClosure);
} on stop window.removeEventListener('hashchange', handlerClosure);
on stop {
window.removeEventListener('hashchange', handlerClosure);
}
on message P.SetLocationHash($newHash: string) => { on message P.SetLocationHash($newHash: string) => {
window.location.hash = newHash; window.location.hash = newHash;
}
} }
} }
} }
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
function spawnAttributeUpdater<T>(thisFacet: Facet<T>) { function spawnAttributeUpdater(ds: Ref) {
spawn named 'AttributeUpdater' { spawn named 'AttributeUpdater' {
on message P.SetAttribute($s: string, $k: string, $v: string) => at ds {
update(s, n => n.setAttribute(k, v)); on message P.SetAttribute($s: string, $k: string, $v: string) =>
on message P.RemoveAttribute($s: string, $k: string) => update(s, n => n.setAttribute(k, v));
update(s, n => n.removeAttribute(k)); on message P.RemoveAttribute($s: string, $k: string) =>
on message P.SetProperty($s: string, $k: string, $v) => update(s, n => n.removeAttribute(k));
update(s, n => { (n as any)[k] = v }); on message P.SetProperty($s: string, $k: string, $v) =>
on message P.RemoveProperty($s: string, $k: string) => update(s, n => { (n as any)[k] = v });
update(s, n => { delete (n as any)[k]; }); on message P.RemoveProperty($s: string, $k: string) =>
update(s, n => { delete (n as any)[k]; });
function update(selector: string, nodeUpdater: (n: Element) => void) { function update(selector: string, nodeUpdater: (n: Element) => void) {
selectorMatch(document.body, selector).forEach(nodeUpdater); selectorMatch(document.body, selector).forEach(nodeUpdater);
}
} }
} }
} }