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>
<title>Syndicate: Table Example</title>
<meta charset="utf-8">
<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="index.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>
</head>
<body>
<h1>Table Example</h1>
@ -27,8 +26,6 @@
Source code: <a href="src/index.ts">index.ts</a>
</p>
<div id="extra"></div>
<script>
Syndicate.bootModule(Main.__SYNDICATE__bootProc);
</script>
<script src="index.js"></script>
</body>
</html>

View File

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

View File

@ -1,56 +1,60 @@
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Embedded, Value } from '@syndicate-lang/core';
activate import { UIEvent, GlobalEvent, HtmlFragments, template, Anchor } from '@syndicate-lang/html';
import { Dataspace, Embedded, Value, Ref } from '@syndicate-lang/core';
import { boot as bootHtml, UIEvent, GlobalEvent, HtmlFragments, template, Anchor } from '@syndicate-lang/html';
assertion type Person(id, firstName, lastName, address, age);
message type SetSortColumn(number);
boot {
function newRow(id: number, firstName: string, lastName: string, address: string, age: number) {
spawn named ('model' + id) {
assert Person(id, firstName, lastName, address, age);
Dataspace.boot(ds => {
bootHtml(ds);
at ds {
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-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";
export * from "./protocol";
@ -9,15 +10,15 @@ export * from "./protocol";
import { HtmlFragments } from "./html";
export * from "./html";
boot {
spawnGlobalEventFactory(thisFacet);
spawnWindowEventFactory(thisFacet);
spawnUIFragmentFactory(thisFacet);
spawnUIAttributeFactory(thisFacet);
spawnUIPropertyFactory(thisFacet);
spawnUIChangeablePropertyFactory(thisFacet);
spawnLocationHashTracker(thisFacet);
spawnAttributeUpdater(thisFacet);
export function boot(ds: Ref) {
spawnGlobalEventFactory(ds);
spawnWindowEventFactory(ds);
spawnUIFragmentFactory(ds);
spawnUIAttributeFactory(ds);
spawnUIPropertyFactory(ds);
spawnUIChangeablePropertyFactory(ds);
spawnLocationHashTracker(ds);
spawnAttributeUpdater(ds);
}
//---------------------------------------------------------------------------
@ -32,16 +33,20 @@ export function newFragmentId() {
//---------------------------------------------------------------------------
export function spawnGlobalEventFactory<T>(thisFacet: Facet<T>) {
export function spawnGlobalEventFactory(ds: Ref) {
spawn named 'GlobalEventFactory' {
during Observe(P.GlobalEvent($selector: string, $eventType: string, _)) =>
spawn named ['GlobalEvent', selector, eventType] {
let sender = thisFacet.wrapExternal((thisFacet, e: Event) => {
send message P.GlobalEvent(selector, eventType, embed(e));
});
at ds {
during Observe({
"pattern": :pattern P.GlobalEvent(\Q.lit($selector: string),
\Q.lit($eventType: string),
\_)
}) => spawn named ['GlobalEvent', selector, eventType] {
const facet = Turn.activeFacet;
function handler(event: Event) {
sender(event);
facet.turn(() => {
send message P.GlobalEvent(selector, eventType, embed(create ({ data: event })));
});
return dealWithPreventDefault(eventType, event);
}
@ -50,7 +55,7 @@ export function spawnGlobalEventFactory<T>(thisFacet: Facet<T>) {
eventUpdater(cleanEventType(eventType), handler, install));
}
on start updateEventListeners(true);
updateEventListeners(true);
on stop updateEventListeners(false);
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
// 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' {
during Observe(P.WindowEvent($eventType: string, _)) =>
spawn named ['WindowEvent', eventType] {
let sender = thisFacet.wrapExternal((thisFacet, e: Event) => {
send message P.WindowEvent(eventType, embed(e));
});
at ds {
during Observe({
"pattern": :pattern P.WindowEvent(\Q.lit($eventType: string), \_)
}) => spawn named ['WindowEvent', eventType] {
const facet = Turn.activeFacet;
let handler = function (event: Event) {
sender(event);
facet.turn(() => {
send message P.WindowEvent(eventType, embed(create ({ data: 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);
}
}
}
}
@ -101,17 +110,17 @@ function isNodeOrderKey(x: any): x is NodeOrderKey {
type HandlerClosure = (event: Event) => void;
function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
function spawnUIFragmentFactory(ds: Ref) {
type RegistrationKey = [string, string]; // [selector, eventType]
spawn named 'UIFragmentFactory' {
during P.UIFragment($fragmentId0, _, _, _) =>
spawn named ['UIFragment', fragmentId0] {
at ds {
during P.UIFragment($fragmentId0, _, _, _) => spawn named ['UIFragment', fragmentId0] {
if (!isFragmentId(fragmentId0)) return;
const fragmentId = fragmentId0;
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 html: Array<ChildNode>;
@ -122,8 +131,13 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
on stop removeNodes();
during Observe(P.UIEvent(fragmentId, $selector: string, $eventType: string, _)) => {
on start updateEventListeners([ selector, eventType ], true);
during Observe({
"pattern": :pattern P.UIEvent(fragmentId,
\Q.lit($selector: string),
\Q.lit($eventType: string),
\_)
}) => {
updateEventListeners([ selector, eventType ], true);
on stop updateEventListeners([ selector, eventType ], false);
}
@ -133,7 +147,7 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
removeNodes();
selector = newSelector;
html = (newHtml as Embedded<Array<ChildNode>>).embeddedValue;
html = (newHtml as Embedded<Ref>).embeddedValue.target.data as ChildNode[];
orderBy = newOrderBy;
anchorNodes = (selector !== null) ? selectorMatch(document.body, selector) : [];
@ -153,7 +167,7 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
// (re)install event listeners
eventRegistrations.forEach((_handler, key) => updateEventListeners(key, true));
this.version++;
version.value++;
}
function removeNodes() {
@ -172,11 +186,11 @@ function spawnUIFragmentFactory<T>(thisFacet: Facet<T>) {
let handlerClosure: HandlerClosure;
if (!eventRegistrations.has(key)) {
let sender = thisFacet.wrapExternal((thisFacet, e: Event) => {
send message P.UIEvent(fragmentId, selector, eventType, embed(e));
});
const facet = Turn.activeFacet;
function handler(event: Event) {
sender(event);
facet.turn(() => {
send message P.UIEvent(fragmentId, selector, eventType, embed(create ({ data: event })));
});
return dealWithPreventDefault(eventType, event);
}
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' {
during P.UIAttribute($selector: string, $attribute: string, $value) =>
spawn named ['UIAttribute', selector, attribute, value] {
_attributeLike(thisFacet, selector, attribute, value, 'attribute');
}
at ds {
during P.UIAttribute($selector: string, $attribute: string, $value) =>
spawn named ['UIAttribute', selector, attribute, value] {
_attributeLike(selector, attribute, value, 'attribute');
}
}
}
}
function spawnUIPropertyFactory<T>(thisFacet: Facet<T>) {
function spawnUIPropertyFactory(ds: Ref) {
spawn named 'UIPropertyFactory' {
during P.UIProperty($selector: string, $property: string, $value) =>
spawn named ['UIProperty', selector, property, value] {
_attributeLike(thisFacet, selector, property, value, 'property');
}
at ds {
during P.UIProperty($selector: string, $property: string, $value) =>
spawn named ['UIProperty', selector, property, value] {
_attributeLike(selector, property, value, 'property');
}
}
}
}
function _attributeLike<T>(thisFacet: Facet<T>,
selector: string,
key: string,
value: Value,
kind: 'attribute' | 'property')
function _attributeLike(selector: string,
key: string,
value: AnyValue,
kind: 'attribute' | 'property')
{
let savedValues: Array<{node: Element, value: any}> = [];
@ -387,7 +405,7 @@ function _attributeLike<T>(thisFacet: Facet<T>,
});
savedValues = [];
}
};
}
function splitClassValue(v: string | null): Array<string> {
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' {
during Observe(P.UIChangeableProperty($selector: string, $property: string, _)) =>
spawn named ['UIChangeableProperty', selector, property] {
on start selectorMatch(document.body, selector).forEach(node => {
at ds {
during Observe({
"pattern": :pattern P.UIChangeableProperty(\Q.lit($selector: string),
\Q.lit($property: string),
\_)
}) => spawn named ['UIChangeableProperty', selector, property] {
selectorMatch(document.body, selector).forEach(node => {
react {
field value: any = (node as any)[property];
assert P.UIChangeableProperty(selector, property, this.value);
const handlerClosure = thisFacet.wrapExternal((_thisFacet, _e: Event) => {
this.value = (node as any)[property];
field propValue: any = (node as any)[property];
assert P.UIChangeableProperty(selector, property, propValue.value);
const facet = Turn.activeFacet;
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);
}
});
}
}
}
}
@ -463,56 +487,56 @@ export class Anchor {
}
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' {
field hashValue: string = '/';
assert P.LocationHash(this.hashValue);
at ds {
field hashValue: string = '/';
assert P.LocationHash(hashValue.value);
const loadHash = () => {
var h = window.location.hash;
if (h.length && h[0] === '#') {
h = h.slice(1);
}
this.hashValue = h || '/';
};
const loadHash = () => {
var h = window.location.hash;
if (h.length && h[0] === '#') {
h = h.slice(1);
}
hashValue.value = h || '/';
};
const facet = Turn.activeFacet;
const handlerClosure = () => facet.turn(loadHash);
let handlerClosure = thisFacet.wrapExternal(loadHash);
on start {
loadHash();
window.addEventListener('hashchange', handlerClosure);
}
on stop {
window.removeEventListener('hashchange', handlerClosure);
}
on stop window.removeEventListener('hashchange', handlerClosure);
on message P.SetLocationHash($newHash: string) => {
window.location.hash = newHash;
on message P.SetLocationHash($newHash: string) => {
window.location.hash = newHash;
}
}
}
}
//---------------------------------------------------------------------------
function spawnAttributeUpdater<T>(thisFacet: Facet<T>) {
function spawnAttributeUpdater(ds: Ref) {
spawn named 'AttributeUpdater' {
on message P.SetAttribute($s: string, $k: string, $v: string) =>
update(s, n => n.setAttribute(k, v));
on message P.RemoveAttribute($s: string, $k: string) =>
update(s, n => n.removeAttribute(k));
on message P.SetProperty($s: string, $k: string, $v) =>
update(s, n => { (n as any)[k] = v });
on message P.RemoveProperty($s: string, $k: string) =>
update(s, n => { delete (n as any)[k]; });
at ds {
on message P.SetAttribute($s: string, $k: string, $v: string) =>
update(s, n => n.setAttribute(k, v));
on message P.RemoveAttribute($s: string, $k: string) =>
update(s, n => n.removeAttribute(k));
on message P.SetProperty($s: string, $k: string, $v) =>
update(s, n => { (n as any)[k] = v });
on message P.RemoveProperty($s: string, $k: string) =>
update(s, n => { delete (n as any)[k]; });
function update(selector: string, nodeUpdater: (n: Element) => void) {
selectorMatch(document.body, selector).forEach(nodeUpdater);
function update(selector: string, nodeUpdater: (n: Element) => void) {
selectorMatch(document.body, selector).forEach(nodeUpdater);
}
}
}
}