lightview 1.5.1-b → 1.6.4-b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -1
- package/examples/counter.html +3 -9
- package/examples/counter.test.mjs +47 -0
- package/examples/invalid-template-literals.html +45 -0
- package/examples/remote.html +3 -1
- package/examples/remote.json +1 -1
- package/examples/sensors/index.html +30 -0
- package/examples/sensors/sensor-server.js +30 -0
- package/jest.config.json +1 -1
- package/lightview.js +49 -187
- package/package.json +1 -1
- package/test/basic.html +1 -1
- package/test/extended.html +29 -0
- package/test/extended.test.mjs +270 -0
- package/types.js +179 -12
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# lightview v1.
|
|
1
|
+
# lightview v1.6.4b (BETA)
|
|
2
2
|
|
|
3
3
|
Small, simple, powerful web UI and micro front end creation ...
|
|
4
4
|
|
|
@@ -6,3 +6,37 @@ Great ideas from Svelte, React, Vue and Riot combined into one small tool: < 7K
|
|
|
6
6
|
|
|
7
7
|
See the docs and examples at [https://lightview.dev](https://lightview.dev).
|
|
8
8
|
|
|
9
|
+
Meanwhile, here is what you get:
|
|
10
|
+
|
|
11
|
+
1) Single file and <a href="#local-templates" target=_self>template</a> components.
|
|
12
|
+
|
|
13
|
+
1) [Sandboxed remote components](https://lightview.dev/#sandboxed-components) and micro front ends</a>.
|
|
14
|
+
|
|
15
|
+
1) [Unit testable](https://lightview.dev/#unit-testing) components and a [debug mode](https://lightview.dev/#debugging) for using standard JavaScript debuggers</a>.
|
|
16
|
+
|
|
17
|
+
1) No pre-deployment transpilation/compilation required.
|
|
18
|
+
|
|
19
|
+
1) Svelte like variable usage, i.e. write your state modifying code like normal code.
|
|
20
|
+
|
|
21
|
+
1) Extended variable type declarations including `min`, `max` and `step` on `number` or limits on `string` and `array` lengths.
|
|
22
|
+
|
|
23
|
+
1) [TypeScript like](https://lightview.dev/#variables) runtime type checking of variables in components.
|
|
24
|
+
|
|
25
|
+
1) Automatic server retrieval and update of variables declared as `remote`.
|
|
26
|
+
|
|
27
|
+
1) Automatic import, export, cross-component sync, or reactive response to attributes/props/variables. See [superVariable](https://lightview.dev/#super-variable).
|
|
28
|
+
|
|
29
|
+
1) [Automatic form variable creation and binding](https://lightview.dev/#auto-binding-forms).
|
|
30
|
+
|
|
31
|
+
1) [Attribute directives](https://lightview.dev/#attribute-directives) like `l-if`, and a single powerful `l-for` that handles array and object keys, values, and entries.
|
|
32
|
+
|
|
33
|
+
1) Reactive string template literals for content and attribute value replacement.
|
|
34
|
+
|
|
35
|
+
1) No virtual DOM. The Lightview dependency tracker laser targets just those nodes that need updates.
|
|
36
|
+
|
|
37
|
+
1) SPA, and MPA friendly ... somewhat SEO friendly and short steps away from fully SEO friendly.
|
|
38
|
+
|
|
39
|
+
1) A [component library](https://lightview.dev/components) including charts and gauges.
|
|
40
|
+
|
|
41
|
+
1) Lots of live [editable examples](https://lightview.dev/#examples).
|
|
42
|
+
|
package/examples/counter.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<head>
|
|
2
|
-
<title>Counter</title>
|
|
2
|
+
<title>Lightview:Examples:Counter</title>
|
|
3
3
|
<script src="../lightview.js?as=x-body"></script>
|
|
4
4
|
</head>
|
|
5
5
|
|
|
@@ -9,15 +9,9 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<script type="lightview/module">
|
|
12
|
-
|
|
13
|
-
self.variables({
|
|
14
|
-
count: "number"
|
|
15
|
-
}, {
|
|
16
|
-
reactive
|
|
17
|
-
});
|
|
18
|
-
count = 0;
|
|
12
|
+
self.variables({count: "number",}, {reactive,set:0});
|
|
19
13
|
self.bump = () => count++;
|
|
20
|
-
|
|
14
|
+
</script>
|
|
21
15
|
|
|
22
16
|
<style>
|
|
23
17
|
button {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import 'expect-puppeteer';
|
|
2
|
+
|
|
3
|
+
describe('Lightview:Example:Counter', () => {
|
|
4
|
+
beforeAll(async () => {
|
|
5
|
+
await page.goto('http://localhost:8080/examples/counter.html');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test('should be titled "Lightview:Examples:Counter"', async () => {
|
|
9
|
+
await expect(page.title()).resolves.toMatch("Lightview:Examples:Counter");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('count should be a variable', async () => {
|
|
13
|
+
const result = await page.evaluate(async () => {
|
|
14
|
+
return document.body.getVariable("count");
|
|
15
|
+
});
|
|
16
|
+
expect(result).toBeDefined();
|
|
17
|
+
const {name,type,value,reactive} = result;
|
|
18
|
+
expect(name).toBe("count");
|
|
19
|
+
expect(type).toBe("number");
|
|
20
|
+
expect(value).toBe(0);
|
|
21
|
+
expect(reactive).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('bump should be called', async () => {
|
|
25
|
+
const result = await page.evaluate(async () => {
|
|
26
|
+
document.body.bump();
|
|
27
|
+
return document.body.getVariableValue("count");
|
|
28
|
+
});
|
|
29
|
+
expect(result).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('click should bump', async () => {
|
|
33
|
+
const buttonHandle = await page.evaluateHandle('document.body.querySelector("button")');
|
|
34
|
+
await buttonHandle.click();
|
|
35
|
+
const result = await page.evaluate(async () => {
|
|
36
|
+
return document.body.getVariableValue("count");
|
|
37
|
+
});
|
|
38
|
+
expect(result).toBe(2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("should be custom element", async() => {
|
|
42
|
+
const result = await page.evaluate(async () => {
|
|
43
|
+
return document.body.constructor.name;
|
|
44
|
+
});
|
|
45
|
+
expect(result).toBe("CustomElement");
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Invalid Template Literals</title>
|
|
5
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<p>
|
|
9
|
+
<button l-on:click="bump">Click count:${count}</button>
|
|
10
|
+
</p>
|
|
11
|
+
<div style="margin:20px">
|
|
12
|
+
<p>
|
|
13
|
+
${"<h1>"+(test++)+"</h1>"}
|
|
14
|
+
</p>
|
|
15
|
+
<p>
|
|
16
|
+
${(while (test)<10 { test++}; test)}
|
|
17
|
+
</p>
|
|
18
|
+
<p>
|
|
19
|
+
${(() =>test)()}
|
|
20
|
+
</p>
|
|
21
|
+
<p>
|
|
22
|
+
${(() = >test)()}
|
|
23
|
+
</p>
|
|
24
|
+
<p>
|
|
25
|
+
${function(){return \${test}})()}
|
|
26
|
+
</p>
|
|
27
|
+
<p>
|
|
28
|
+
${window.alert("ok")}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<script type="lightview/module">
|
|
33
|
+
self.variables({count: "number",test:"number"}, {reactive,set:0});
|
|
34
|
+
self.bump = () => count++;
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<style>
|
|
38
|
+
button {
|
|
39
|
+
margin: 20px;
|
|
40
|
+
background: gray
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
43
|
+
</body>
|
|
44
|
+
|
|
45
|
+
</html>
|
package/examples/remote.html
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
<script type="lightview/module">
|
|
16
|
-
|
|
16
|
+
const {remote} = await import("../types.js");
|
|
17
|
+
|
|
18
|
+
self.variables({myRemote:"object"},{reactive,remote:remote("http://localhost:8000/remote.json")});
|
|
17
19
|
|
|
18
20
|
await myRemote; // must await remotes before the first time they are used, e.g. before HTML is rendered
|
|
19
21
|
|
package/examples/remote.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"joe","age":
|
|
1
|
+
{"name":"joe","age":30}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>Lightview:Sensor Demo</title>
|
|
4
|
+
<link href="../../components/gauge.html" rel="module">
|
|
5
|
+
<script src="../../lightview.js?as=x-body"></script>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<div style="width:100%;text-align:center">
|
|
9
|
+
<div style="display:inline-block">
|
|
10
|
+
<l-gauge id="sensor1" type="Gauge" label="Sensor One" value="50"></l-gauge>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div style="display:inline-block">
|
|
14
|
+
<l-gauge id="sensor2" type="Gauge" label="Sensor Two" value="50"></l-gauge>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<script type="lightview/module">
|
|
19
|
+
const {remote} = await import("../../types.js");
|
|
20
|
+
self.variables({sensor1:"number"},{remote:remote({ttl:5000,path:"https://lightview.dev/sensors/sensor1"})});
|
|
21
|
+
self.variables({sensor2:"number"},{remote:remote({ttl:7500,path:"https://lightview.dev/sensors/sensor2"})});
|
|
22
|
+
await sensor1;
|
|
23
|
+
await sensor2;
|
|
24
|
+
addEventListener("change",({variableName,value}) => {
|
|
25
|
+
const sensor = document.body.getElementById(variableName);
|
|
26
|
+
sensor.setValue(value);
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const http = require("http"),
|
|
2
|
+
fs = require("fs"),
|
|
3
|
+
host = 'localhost',
|
|
4
|
+
port = 8000,
|
|
5
|
+
requestListener = async function (req, res) {
|
|
6
|
+
const path = `.${req.url}`;
|
|
7
|
+
res.setHeader("Access-Control-Allow-Origin","*");
|
|
8
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,OPTIONS");
|
|
9
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
10
|
+
res.setHeader("Content-Type", "application/json");
|
|
11
|
+
if(req.method==="OPTIONS") {
|
|
12
|
+
res.end();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if(req.method==="GET") {
|
|
16
|
+
const value = `${40 + Math.round(60 * Math.random())}`;
|
|
17
|
+
console.log("GET",req.url,"<-",value);
|
|
18
|
+
res.setHeader("Content-Length", value.length);
|
|
19
|
+
res.write(value);
|
|
20
|
+
res.end();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(req.method);
|
|
24
|
+
},
|
|
25
|
+
server = http.createServer(requestListener);
|
|
26
|
+
server.listen(port, host, () => {
|
|
27
|
+
console.log(`Server is running on http://${host}:${port}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
package/jest.config.json
CHANGED
package/lightview.js
CHANGED
|
@@ -241,18 +241,21 @@ const {observe} = (() => {
|
|
|
241
241
|
const variable = target[property],
|
|
242
242
|
{type, value, shared, exported, constant, reactive, remote} = variable;
|
|
243
243
|
if (constant) throw new TypeError(`${property}:${type} is a constant`);
|
|
244
|
-
newValue = type.validate ? type.validate(newValue,target[property]) : coerce(newValue,type);
|
|
244
|
+
if(newValue!=null || type.required) newValue = type.validate ? type.validate(newValue,target[property]) : coerce(newValue,type);
|
|
245
245
|
const newtype = typeof (newValue),
|
|
246
246
|
typetype = typeof (type);
|
|
247
|
-
if (newValue == null
|
|
247
|
+
if ((newValue == null && !type.required) ||
|
|
248
|
+
type === "any" ||
|
|
249
|
+
(newtype === type && typetype==="string") ||
|
|
250
|
+
(typetype === "function" && !type.validate && (newValue && newtype === "object" && newValue instanceof type) || variable.validityState?.valid)) {
|
|
248
251
|
if (value !== newValue) {
|
|
249
252
|
event.oldValue = value;
|
|
250
253
|
target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
|
|
251
254
|
target.postEvent.value("change", event);
|
|
252
255
|
if (event.defaultPrevented) {
|
|
253
256
|
target[property].value = value;
|
|
254
|
-
} else if(remote) {
|
|
255
|
-
handleRemote({variable,remote,reactive},true);
|
|
257
|
+
} else if(remote && remote.put) {
|
|
258
|
+
remote.handleRemote({variable,config:remote.config,reactive},true);
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
261
|
return true;
|
|
@@ -260,7 +263,7 @@ const {observe} = (() => {
|
|
|
260
263
|
if (typetype === "function" && newValue && newtype === "object") {
|
|
261
264
|
throw new TypeError(`Can't assign instance of '${newValue.constructor.name}' to variable '${property}:${type.name.replace("bound ", "")}'`)
|
|
262
265
|
}
|
|
263
|
-
throw new TypeError(`Can't assign '${typeof (newValue)} ${newtype === "string" ? '"' + newValue + '"' : newValue}' to variable '${property}:${typetype === "function" ? type.name.replace("bound ", "") : type}'`)
|
|
266
|
+
throw new TypeError(`Can't assign '${typeof (newValue)} ${newtype === "string" ? '"' + newValue + '"' : newValue}' to variable '${property}:${typetype === "function" ? type.name.replace("bound ", "") : type} ${type.required ? "required" : ""}'`)
|
|
264
267
|
},
|
|
265
268
|
keys() {
|
|
266
269
|
return [...Object.keys(vars)];
|
|
@@ -339,7 +342,7 @@ const {observe} = (() => {
|
|
|
339
342
|
}
|
|
340
343
|
return nodes;
|
|
341
344
|
}
|
|
342
|
-
|
|
345
|
+
|
|
343
346
|
const resolveNodeOrText = (node, component, safe) => {
|
|
344
347
|
const type = typeof (node),
|
|
345
348
|
template = type === "string" ? node.trim() : node.template;
|
|
@@ -357,15 +360,6 @@ const {observe} = (() => {
|
|
|
357
360
|
}
|
|
358
361
|
return node?.nodeValue;
|
|
359
362
|
}
|
|
360
|
-
const render = (hasTemplate, render) => {
|
|
361
|
-
let observer;
|
|
362
|
-
if (hasTemplate) {
|
|
363
|
-
if (observer) observer.cancel();
|
|
364
|
-
observer = observe(render)
|
|
365
|
-
} else {
|
|
366
|
-
render();
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
363
|
const inputTypeToType = (inputType) => {
|
|
370
364
|
if (!inputType) return "any"
|
|
371
365
|
if (["text", "tel", "email", "url", "search", "radio", "color", "password"].includes(inputType)) return "string";
|
|
@@ -430,8 +424,7 @@ const {observe} = (() => {
|
|
|
430
424
|
reactive: {value: true, constant: true},
|
|
431
425
|
shared: {value: true, constant: true},
|
|
432
426
|
exported: {value: true, constant: true},
|
|
433
|
-
imported: {value: true, constant: true}
|
|
434
|
-
remote: {}
|
|
427
|
+
imported: {value: true, constant: true}
|
|
435
428
|
};
|
|
436
429
|
const createClass = (domElementNode, {observer, framed}) => {
|
|
437
430
|
const instances = new Set(),
|
|
@@ -475,7 +468,7 @@ const {observe} = (() => {
|
|
|
475
468
|
constant: true
|
|
476
469
|
},
|
|
477
470
|
postEvent: {
|
|
478
|
-
value: (eventName, event={}) => {
|
|
471
|
+
value: (eventName, event = {}) => {
|
|
479
472
|
//event = {...event}
|
|
480
473
|
event.type = eventName;
|
|
481
474
|
event.target = currentComponent;
|
|
@@ -558,13 +551,12 @@ const {observe} = (() => {
|
|
|
558
551
|
const nodes = getNodes(ctx);
|
|
559
552
|
nodes.forEach((node) => {
|
|
560
553
|
if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
|
|
561
|
-
|
|
554
|
+
observe(() => resolveNodeOrText(node, this));
|
|
562
555
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
563
556
|
// resolve the value before all else;
|
|
564
557
|
const attr = node.attributes.value,
|
|
565
558
|
template = attr?.template;
|
|
566
559
|
if (attr && template) {
|
|
567
|
-
//render(!!template, () => {
|
|
568
560
|
let value = resolveNodeOrText(attr, this),
|
|
569
561
|
eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
|
|
570
562
|
const template = attr.template;
|
|
@@ -612,22 +604,22 @@ const {observe} = (() => {
|
|
|
612
604
|
const {name, value} = attr,
|
|
613
605
|
vname = node.attributes.name?.value;
|
|
614
606
|
if (name === "type" && value=="radio" && vname) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
607
|
+
bindInput(node, vname, this);
|
|
608
|
+
observe(() => {
|
|
609
|
+
const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
|
|
610
|
+
if (node.attributes.value.value == varvalue) {
|
|
611
|
+
node.setAttribute("checked", "");
|
|
612
|
+
node.checked = true;
|
|
613
|
+
} else {
|
|
614
|
+
node.removeAttribute("checked");
|
|
615
|
+
node.checked = false;
|
|
616
|
+
}
|
|
617
|
+
});
|
|
626
618
|
}
|
|
627
619
|
|
|
628
620
|
const [type, ...params] = name.split(":");
|
|
629
621
|
if (type === "") { // name is :something
|
|
630
|
-
|
|
622
|
+
observe(() => {
|
|
631
623
|
const value = attr.value;
|
|
632
624
|
if (params[0]) {
|
|
633
625
|
if (value === "true") node.setAttribute(params[0], "")
|
|
@@ -643,20 +635,20 @@ const {observe} = (() => {
|
|
|
643
635
|
})
|
|
644
636
|
} else if (type === "l-on") {
|
|
645
637
|
let listener;
|
|
646
|
-
|
|
638
|
+
observe(() => {
|
|
647
639
|
const value = resolveNodeOrText(attr, this);
|
|
648
640
|
if (listener) node.removeEventListener(params[0], listener);
|
|
649
641
|
listener = this[value] || window[value] || Function(value);
|
|
650
642
|
addListener(node, params[0], listener);
|
|
651
643
|
})
|
|
652
644
|
} else if (type === "l-if") {
|
|
653
|
-
|
|
645
|
+
observe(() => {
|
|
654
646
|
const value = resolveNodeOrText(attr, this);
|
|
655
647
|
node.style.setProperty("display", value === "true" ? "revert" : "none");
|
|
656
648
|
})
|
|
657
649
|
} else if (type === "l-for") {
|
|
658
650
|
node.template ||= node.innerHTML;
|
|
659
|
-
|
|
651
|
+
observe(() => {
|
|
660
652
|
const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
|
|
661
653
|
value = resolveNodeOrText(attr, this),
|
|
662
654
|
coerced = coerce(value, what === "each" ? Array : "object"),
|
|
@@ -683,8 +675,8 @@ const {observe} = (() => {
|
|
|
683
675
|
else node.appendChild(parsed.body.firstChild);
|
|
684
676
|
}
|
|
685
677
|
})
|
|
686
|
-
} else {
|
|
687
|
-
|
|
678
|
+
} else if(attr.template) {
|
|
679
|
+
observe(() => {
|
|
688
680
|
resolveNodeOrText(attr, this);
|
|
689
681
|
})
|
|
690
682
|
}
|
|
@@ -699,11 +691,9 @@ const {observe} = (() => {
|
|
|
699
691
|
}
|
|
700
692
|
adoptedCallback(callback) {
|
|
701
693
|
this.vars.postEvent.value("adopted");
|
|
702
|
-
super.adoptedCallback();
|
|
703
694
|
}
|
|
704
695
|
disconnectedCallback() {
|
|
705
696
|
this.vars.postEvent.value("disconnected");
|
|
706
|
-
super.disconnectedCallback();
|
|
707
697
|
}
|
|
708
698
|
get observedAttributes() {
|
|
709
699
|
return CustomElement.observedAttributes;
|
|
@@ -713,9 +703,12 @@ const {observe} = (() => {
|
|
|
713
703
|
}
|
|
714
704
|
|
|
715
705
|
getVariableNames() {
|
|
716
|
-
return Object.keys(this.vars)
|
|
717
|
-
|
|
718
|
-
|
|
706
|
+
return Object.keys(this.vars)
|
|
707
|
+
.filter(name => !(name in reserved) && !["self", "addEventListener", "postEvent"].includes(name))
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
getVariable(name) {
|
|
711
|
+
return this.vars[name] ? {...this.vars[name]} : undefined;
|
|
719
712
|
}
|
|
720
713
|
|
|
721
714
|
setVariableValue(variableName, value, {coerceTo = typeof (value)} = {}) {
|
|
@@ -751,17 +744,25 @@ const {observe} = (() => {
|
|
|
751
744
|
return this.vars[variableName]?.value;
|
|
752
745
|
}
|
|
753
746
|
|
|
754
|
-
variables(variables, {observed, reactive, shared, exported, imported, remote} = {}) { // options = {observed,reactive,shared,exported,imported}
|
|
747
|
+
variables(variables, {observed, reactive, shared, exported, imported, remote, constant,set} = {}) { // options = {observed,reactive,shared,exported,imported}
|
|
755
748
|
const addEventListener = this.varsProxy.addEventListener;
|
|
756
749
|
if (variables !== undefined) {
|
|
757
750
|
Object.entries(variables)
|
|
758
751
|
.forEach(([key, type]) => {
|
|
759
752
|
const variable = this.vars[key] ||= {name: key, type};
|
|
753
|
+
if(set!==undefined && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't be set to ${set}`);
|
|
754
|
+
variable.value = set;
|
|
755
|
+
if(constant!==undefined) {
|
|
756
|
+
variable.constant = true;
|
|
757
|
+
variable.value = constant;
|
|
758
|
+
}
|
|
760
759
|
if (observed || imported) {
|
|
761
760
|
variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
|
|
762
|
-
variable.observed = observed;
|
|
763
761
|
variable.imported = imported;
|
|
764
|
-
if(variable.observed)
|
|
762
|
+
if(variable.observed) {
|
|
763
|
+
variable.observed = observed;
|
|
764
|
+
this.observedAttributes.add(key);
|
|
765
|
+
}
|
|
765
766
|
}
|
|
766
767
|
if (reactive) {
|
|
767
768
|
variable.reactive = true;
|
|
@@ -780,10 +781,11 @@ const {observe} = (() => {
|
|
|
780
781
|
this.changeListener.targets.add(key);
|
|
781
782
|
}
|
|
782
783
|
if (remote) {
|
|
784
|
+
if(typeof(remote)==="function") remote = remote(`./${key}`);
|
|
783
785
|
variable.remote = remote;
|
|
784
|
-
handleRemote({variable, remote, reactive});
|
|
786
|
+
remote.handleRemote({variable, config:remote.config, reactive,component:this});
|
|
785
787
|
}
|
|
786
|
-
if(type.validate) type.validate(
|
|
788
|
+
if(type.validate && variable.value!==undefined) type.validate(variable.value,variable);
|
|
787
789
|
});
|
|
788
790
|
}
|
|
789
791
|
return Object.entries(this.vars)
|
|
@@ -792,146 +794,6 @@ const {observe} = (() => {
|
|
|
792
794
|
return result;
|
|
793
795
|
}, {});
|
|
794
796
|
}
|
|
795
|
-
|
|
796
|
-
constants(variables) {
|
|
797
|
-
if (variables !== undefined) {
|
|
798
|
-
Object.entries(variables)
|
|
799
|
-
.forEach(([key, value]) => {
|
|
800
|
-
const type = typeof (value) === "function" ? value : typeof (value),
|
|
801
|
-
variable = this.vars[key];
|
|
802
|
-
if (variable !== undefined) throw new TypeError(`${variable.constant ? "const" : "let"} ${key}:${variable.type} already declared.`);
|
|
803
|
-
if (value === undefined) throw new TypeError(`const ${key}:undefined must be initialized.`);
|
|
804
|
-
this.vars[key] = {type, value, constant: true};
|
|
805
|
-
})
|
|
806
|
-
}
|
|
807
|
-
return Object.entries(this.vars)
|
|
808
|
-
.reduce((result, [key, variable]) => {
|
|
809
|
-
if (variable.constant) result[key] = {...variable};
|
|
810
|
-
return result;
|
|
811
|
-
}, {});
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const remoteProxy = ({json, variable,remote, reactive}) => {
|
|
817
|
-
const type = typeof (remote);
|
|
818
|
-
return new Proxy(json, {
|
|
819
|
-
get(target,property) {
|
|
820
|
-
if(property==="__remoteProxytarget__") return json;
|
|
821
|
-
return target[property];
|
|
822
|
-
},
|
|
823
|
-
async set(target, property, value) {
|
|
824
|
-
if(value && typeof(value)==="object" && value instanceof Promise) value = await value;
|
|
825
|
-
const oldValue = target[property];
|
|
826
|
-
if (oldValue !== value) {
|
|
827
|
-
let remotevalue;
|
|
828
|
-
if (type === "string") {
|
|
829
|
-
const href = new URL(remote,window.location.href).href;
|
|
830
|
-
remotevalue = patch({target,property,value,oldValue},href,variable);
|
|
831
|
-
} else if(remote && type==="object") {
|
|
832
|
-
let href;
|
|
833
|
-
if(remote.path) href = new URL(remote.path,window.location.href).href;
|
|
834
|
-
if(!remote.patch) {
|
|
835
|
-
if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
|
|
836
|
-
remote.patch = patch;
|
|
837
|
-
}
|
|
838
|
-
remotevalue = remote.patch({target,property,value,oldValue},href,variable);
|
|
839
|
-
}
|
|
840
|
-
if(remotevalue) {
|
|
841
|
-
await remotevalue.then((newjson) => {
|
|
842
|
-
if (newjson && typeof (newjson) === "object" && reactive) {
|
|
843
|
-
const target = variable.value?.__reactorProxyTarget__ ? json : variable.value;
|
|
844
|
-
Object.entries(newjson).forEach(([key,newValue]) => {
|
|
845
|
-
if(target[key]!==newValue) target[key] = newValue;
|
|
846
|
-
})
|
|
847
|
-
Object.keys(target).forEach((key) => {
|
|
848
|
-
if(!(key in newjson)) delete target[key];
|
|
849
|
-
});
|
|
850
|
-
if(variable.value?.__reactorProxyTarget__) {
|
|
851
|
-
const dependents = variable.value.__dependents__,
|
|
852
|
-
observers = dependents[property] || [];
|
|
853
|
-
[...observers].forEach((f) => {
|
|
854
|
-
if (f.cancelled) dependents[property].delete(f);
|
|
855
|
-
else f();
|
|
856
|
-
})
|
|
857
|
-
}
|
|
858
|
-
} else {
|
|
859
|
-
variable.value = json;
|
|
860
|
-
}
|
|
861
|
-
})
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
return true;
|
|
865
|
-
}
|
|
866
|
-
})
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const patch = ({target,property,value,oldValue},href,variable) => {
|
|
870
|
-
return fetch(href, {
|
|
871
|
-
method: "PATCH",
|
|
872
|
-
body: JSON.stringify({property,value,oldValue}),
|
|
873
|
-
headers: {
|
|
874
|
-
"Content-Type": "application/json"
|
|
875
|
-
}
|
|
876
|
-
}).then((response) => {
|
|
877
|
-
if (response.status < 400) return response.json();
|
|
878
|
-
})
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const get = (href,variable) => {
|
|
882
|
-
return fetch(href)
|
|
883
|
-
.then((response) => {
|
|
884
|
-
if (response.status < 400) return response.json();
|
|
885
|
-
})
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const put = (href,variable) => {
|
|
889
|
-
return fetch(href, {
|
|
890
|
-
method: "PUT",
|
|
891
|
-
body: JSON.stringify(variable.value),
|
|
892
|
-
headers: {
|
|
893
|
-
"Content-Type": "application/json"
|
|
894
|
-
}
|
|
895
|
-
}).then((response) => {
|
|
896
|
-
if (response.status === 200) return response.json();
|
|
897
|
-
})
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const handleRemote = async ({variable, remote, reactive},doput) => {
|
|
901
|
-
const type = typeof (remote);
|
|
902
|
-
let value;
|
|
903
|
-
if (type === "string") {
|
|
904
|
-
const href = new URL(remote,window.location.href).href;
|
|
905
|
-
value = (doput
|
|
906
|
-
? put(href,variable)
|
|
907
|
-
: get(href,variable));
|
|
908
|
-
if(variable.value===undefined) variable.value = value;
|
|
909
|
-
} else if (remote && type === "object") {
|
|
910
|
-
let href;
|
|
911
|
-
if(remote.path) href = new URL(remote.path,window.location.href).href;
|
|
912
|
-
if(!remote.get || !remote.put) {
|
|
913
|
-
if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
|
|
914
|
-
if(!remote.get) remote.get = get;
|
|
915
|
-
if(!remote.put) remote.put = put;
|
|
916
|
-
}
|
|
917
|
-
value = (doput
|
|
918
|
-
? remote.put(href,variable)
|
|
919
|
-
: remote.get(href,variable));
|
|
920
|
-
if(remote.ttl && !doput && !remote.intervalId) {
|
|
921
|
-
remote.intervalId = setInterval(async () => {
|
|
922
|
-
await handleRemote({variable, remote, reactive});
|
|
923
|
-
})
|
|
924
|
-
}
|
|
925
|
-
if(variable.value===undefined) variable.value = value;
|
|
926
|
-
}
|
|
927
|
-
if(value) {
|
|
928
|
-
variable.value = await value.then((json) => {
|
|
929
|
-
if (json && typeof (json) === "object" && reactive) {
|
|
930
|
-
return remoteProxy({json, variable,remote, reactive})
|
|
931
|
-
} else {
|
|
932
|
-
return json;
|
|
933
|
-
}
|
|
934
|
-
})
|
|
935
797
|
}
|
|
936
798
|
}
|
|
937
799
|
|
package/package.json
CHANGED
package/test/basic.html
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Extended</title>
|
|
6
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<script type="lightview/module">
|
|
10
|
+
const {array,boolean,number,object,string} = await import("../types.js");
|
|
11
|
+
|
|
12
|
+
self.variables({strictarray:array},{set:[]});
|
|
13
|
+
self.variables({strictboolean:boolean},{set:true});
|
|
14
|
+
self.variables({strictnumber:number},{set:0});
|
|
15
|
+
self.variables({strictobject:object},{set:{}});
|
|
16
|
+
self.variables({strictstring:string},{set:"test"});
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
self.variables({requiredarray:array({required:true})});
|
|
20
|
+
self.variables({requiredboolean:boolean({required:true})});
|
|
21
|
+
self.variables({requirednumber:number({required:true})});
|
|
22
|
+
self.variables({requiredobject:object({required:true})});
|
|
23
|
+
self.variables({requiredstring:string({required:true})});
|
|
24
|
+
|
|
25
|
+
//self.setVariableValue("requirednumber",null);
|
|
26
|
+
|
|
27
|
+
</script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import 'expect-puppeteer';
|
|
2
|
+
function reviver(property,value) {
|
|
3
|
+
if(value==="@-Infinity") return -Infinity;
|
|
4
|
+
if(value==="@Infinity") return Infinity;
|
|
5
|
+
if(value==="@NaN") return NaN;
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('Lightview - Variables', () => {
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await page.goto('http://localhost:8080/test/extended.html');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should be titled "Extended"', async () => {
|
|
15
|
+
await expect(page.title()).resolves.toMatch('Extended');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('strictarray - should be array', async () => {
|
|
19
|
+
const result = await page.evaluate(() => {
|
|
20
|
+
return document.body.getVariableValue("strictarray")
|
|
21
|
+
});
|
|
22
|
+
expect(Array.isArray(result)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('strictarray - should set', async () => {
|
|
26
|
+
const result = await page.evaluate(() => {
|
|
27
|
+
try {
|
|
28
|
+
document.body.setVariableValue("strictarray",[]);
|
|
29
|
+
} catch(e) {
|
|
30
|
+
return e;
|
|
31
|
+
}
|
|
32
|
+
return document.body.getVariableValue("strictarray")
|
|
33
|
+
});
|
|
34
|
+
expect(Array.isArray(result)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('strictarray - should allow null', async () => {
|
|
38
|
+
const result = await page.evaluate(() => {
|
|
39
|
+
try {
|
|
40
|
+
document.body.setVariableValue("strictarray",undefined);
|
|
41
|
+
document.body.setVariableValue("strictarray",null);
|
|
42
|
+
} catch(e) {
|
|
43
|
+
return e;
|
|
44
|
+
}
|
|
45
|
+
return document.body.getVariableValue("strictarray")
|
|
46
|
+
});
|
|
47
|
+
expect(result).toBe(null);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('strictarray - should throw', async () => {
|
|
51
|
+
const result = await page.evaluate(() => {
|
|
52
|
+
try {
|
|
53
|
+
document.body.setVariableValue("strictarray","false");
|
|
54
|
+
} catch(e) {
|
|
55
|
+
return e.message;
|
|
56
|
+
}
|
|
57
|
+
return document.body.getVariableValue("strictarray");
|
|
58
|
+
});
|
|
59
|
+
const {name,validityState} = JSON.parse(result,reviver);
|
|
60
|
+
expect(name).toBe("strictarray");
|
|
61
|
+
expect(validityState.typeMismatch).toBe(true);
|
|
62
|
+
expect(validityState.value).toBe("false");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('strictboolean - should be boolean', async () => {
|
|
66
|
+
const result = await page.evaluate(() => {
|
|
67
|
+
return document.body.getVariableValue("strictboolean")
|
|
68
|
+
});
|
|
69
|
+
expect(result).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('strictboolean - should set', async () => {
|
|
73
|
+
const result = await page.evaluate(() => {
|
|
74
|
+
try {
|
|
75
|
+
document.body.setVariableValue("strictboolean",false);
|
|
76
|
+
} catch(e) {
|
|
77
|
+
return e.message;
|
|
78
|
+
}
|
|
79
|
+
return document.body.getVariableValue("strictboolean")
|
|
80
|
+
});
|
|
81
|
+
expect(result).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('strictboolean - should allow null', async () => {
|
|
85
|
+
const result = await page.evaluate(() => {
|
|
86
|
+
try {
|
|
87
|
+
document.body.setVariableValue("strictboolean",undefined);
|
|
88
|
+
document.body.setVariableValue("strictboolean",null);
|
|
89
|
+
} catch(e) {
|
|
90
|
+
return e.message;
|
|
91
|
+
}
|
|
92
|
+
return document.body.getVariableValue("strictboolean")
|
|
93
|
+
});
|
|
94
|
+
expect(result).toBe(null);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('strictboolean - should throw', async () => {
|
|
98
|
+
const result = await page.evaluate(() => {
|
|
99
|
+
try {
|
|
100
|
+
document.body.setVariableValue("strictboolean","true");
|
|
101
|
+
} catch(e) {
|
|
102
|
+
return e.message;
|
|
103
|
+
}
|
|
104
|
+
return document.body.getVariableValue("strictboolean");
|
|
105
|
+
});
|
|
106
|
+
const {name,validityState} = JSON.parse(result,reviver);
|
|
107
|
+
expect(name).toBe("strictboolean");
|
|
108
|
+
expect(validityState.typeMismatch).toBe(true);
|
|
109
|
+
expect(validityState.value).toBe("true");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('strictnumber - should be number', async () => {
|
|
113
|
+
const result = await page.evaluate(() => {
|
|
114
|
+
return document.body.getVariableValue("strictnumber")
|
|
115
|
+
});
|
|
116
|
+
expect(result).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('strictnumber - should set', async () => {
|
|
120
|
+
const result = await page.evaluate(() => {
|
|
121
|
+
try {
|
|
122
|
+
document.body.setVariableValue("strictnumber",1);
|
|
123
|
+
} catch(e) {
|
|
124
|
+
return e.message;
|
|
125
|
+
}
|
|
126
|
+
return document.body.getVariableValue("strictnumber")
|
|
127
|
+
});
|
|
128
|
+
expect(result).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('strictnumber - should allow null', async () => {
|
|
132
|
+
const result = await page.evaluate(() => {
|
|
133
|
+
try {
|
|
134
|
+
document.body.setVariableValue("strictnumber",undefined);
|
|
135
|
+
document.body.setVariableValue("strictnumber",null);
|
|
136
|
+
} catch(e) {
|
|
137
|
+
return e.message;
|
|
138
|
+
}
|
|
139
|
+
return document.body.getVariableValue("strictnumber")
|
|
140
|
+
});
|
|
141
|
+
expect(result).toBe(null);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('strictnumber - should throw', async () => {
|
|
145
|
+
const result = await page.evaluate(() => {
|
|
146
|
+
try {
|
|
147
|
+
document.body.setVariableValue("strictnumber","0");
|
|
148
|
+
} catch(e) {
|
|
149
|
+
return e.message;
|
|
150
|
+
}
|
|
151
|
+
return document.body.getVariableValue("strictnumber");
|
|
152
|
+
});
|
|
153
|
+
const {name,validityState} = JSON.parse(result,reviver);
|
|
154
|
+
expect(name).toBe("strictnumber");
|
|
155
|
+
expect(validityState.typeMismatch).toBe(true);
|
|
156
|
+
expect(validityState.value).toBe("0");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('strictobject - should be object', async () => {
|
|
160
|
+
const result = await page.evaluate(() => {
|
|
161
|
+
return document.body.getVariableValue("strictobject")
|
|
162
|
+
});
|
|
163
|
+
expect(typeof(result)).toBe("object");
|
|
164
|
+
expect(Object.keys(result).length).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('strictobject - should set', async () => {
|
|
168
|
+
const result = await page.evaluate(() => {
|
|
169
|
+
try {
|
|
170
|
+
document.body.setVariableValue("strictobject", {test:"test"});
|
|
171
|
+
} catch(e) {
|
|
172
|
+
return e.message;
|
|
173
|
+
}
|
|
174
|
+
return document.body.getVariableValue("strictobject")
|
|
175
|
+
});
|
|
176
|
+
expect(typeof(result)).toBe("object");
|
|
177
|
+
expect(Object.keys(result).length).toBe(1);
|
|
178
|
+
expect(result.test).toBe("test");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('strictobject - should allow null', async () => {
|
|
182
|
+
const result = await page.evaluate(() => {
|
|
183
|
+
try {
|
|
184
|
+
document.body.setVariableValue("strictobject",undefined);
|
|
185
|
+
document.body.setVariableValue("strictobject",null);
|
|
186
|
+
} catch(e) {
|
|
187
|
+
return e.message;
|
|
188
|
+
}
|
|
189
|
+
return document.body.getVariableValue("strictobject")
|
|
190
|
+
});
|
|
191
|
+
expect(result).toBe(null);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('strictobject - should throw', async () => {
|
|
195
|
+
const result = await page.evaluate(() => {
|
|
196
|
+
try {
|
|
197
|
+
document.body.setVariableValue("strictobject","false");
|
|
198
|
+
} catch(e) {
|
|
199
|
+
return e.message;
|
|
200
|
+
}
|
|
201
|
+
return document.body.getVariableValue("strictobject");
|
|
202
|
+
});
|
|
203
|
+
const {name,validityState} = JSON.parse(result,reviver);
|
|
204
|
+
expect(name).toBe("strictobject");
|
|
205
|
+
expect(validityState.typeMismatch).toBe(true);
|
|
206
|
+
expect(validityState.value).toBe("false");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('strictstring - should be string', async () => {
|
|
210
|
+
const result = await page.evaluate(() => {
|
|
211
|
+
return document.body.getVariableValue("strictstring")
|
|
212
|
+
});
|
|
213
|
+
expect(result).toBe("test");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('strictstring - should set', async () => {
|
|
217
|
+
const result = await page.evaluate(() => {
|
|
218
|
+
try {
|
|
219
|
+
document.body.setVariableValue("strictstring","anothertest");
|
|
220
|
+
} catch(e) {
|
|
221
|
+
return e.message;
|
|
222
|
+
}
|
|
223
|
+
return document.body.getVariableValue("strictstring")
|
|
224
|
+
});
|
|
225
|
+
expect(result).toBe("anothertest");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('strictstring - should allow null', async () => {
|
|
229
|
+
const result = await page.evaluate(() => {
|
|
230
|
+
try {
|
|
231
|
+
document.body.setVariableValue("strictstring",undefined);
|
|
232
|
+
document.body.setVariableValue("strictstring",null);
|
|
233
|
+
} catch(e) {
|
|
234
|
+
return e.message;
|
|
235
|
+
}
|
|
236
|
+
return document.body.getVariableValue("strictstring")
|
|
237
|
+
});
|
|
238
|
+
expect(result).toBe(null);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('strictstring - should throw', async () => {
|
|
242
|
+
const result = await page.evaluate(() => {
|
|
243
|
+
try {
|
|
244
|
+
document.body.setVariableValue("strictstring",0);
|
|
245
|
+
} catch(e) {
|
|
246
|
+
return e.message;
|
|
247
|
+
}
|
|
248
|
+
return document.body.getVariableValue("strictstring");
|
|
249
|
+
});
|
|
250
|
+
const {name,validityState} = JSON.parse(result,reviver);
|
|
251
|
+
expect(name).toBe("strictstring");
|
|
252
|
+
expect(validityState.typeMismatch).toBe(true);
|
|
253
|
+
expect(validityState.value).toBe(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('requirednumber - should throw and not allow null', async () => {
|
|
257
|
+
const result = await page.evaluate(() => {
|
|
258
|
+
try {
|
|
259
|
+
document.body.setVariableValue("requirednumber",null);
|
|
260
|
+
} catch(e) {
|
|
261
|
+
return e.message;
|
|
262
|
+
}
|
|
263
|
+
return document.body.getVariableValue("requirednumber")
|
|
264
|
+
});
|
|
265
|
+
const {name,validityState} = JSON.parse(result,reviver);
|
|
266
|
+
expect(name).toBe("requirednumber");
|
|
267
|
+
expect(validityState.valueMissing).toBe(true);
|
|
268
|
+
//expect(validityState.value).toBe(null);
|
|
269
|
+
});
|
|
270
|
+
})
|
package/types.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
const toJSON = (value) => {
|
|
2
|
+
if([-Infinity,Infinity].includes(value)) return `@${value}`;
|
|
3
|
+
if(typeof(value)==="number" && isNaN(value)) return "@NaN";
|
|
4
|
+
if(value && typeof(value)==="object") {
|
|
5
|
+
return Object.entries(value)
|
|
6
|
+
.reduce((json,[key,value]) => {
|
|
7
|
+
if(value && typeof(value)==="object" && value.toJSON) value = value.toJSON();
|
|
8
|
+
json[key] = toJSON(value);
|
|
9
|
+
return json;
|
|
10
|
+
},Array.isArray(value) ? [] : {})
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
function reviver(property,value) {
|
|
15
|
+
if(value==="@-Infinity") return -Infinity;
|
|
16
|
+
if(value==="@Infinity") return Infinity;
|
|
17
|
+
if(value==="@NaN") return NaN;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
1
21
|
function ValidityState(options) {
|
|
2
22
|
if(!this || !(this instanceof ValidityState)) return new ValidityState(options);
|
|
3
23
|
Object.assign(this,{
|
|
@@ -15,9 +35,17 @@ function ValidityState(options) {
|
|
|
15
35
|
},options);
|
|
16
36
|
}
|
|
17
37
|
|
|
38
|
+
function DataType(options) {
|
|
39
|
+
if(!this || !(this instanceof DataType)) return new DataType(options);
|
|
40
|
+
Object.assign(this,options);
|
|
41
|
+
}
|
|
42
|
+
DataType.prototype.toJSON = function() {
|
|
43
|
+
return toJSON(this);
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
const tryParse = (value) => {
|
|
19
47
|
try {
|
|
20
|
-
return JSON.parse(value+"")
|
|
48
|
+
return JSON.parse(value+"",reviver)
|
|
21
49
|
} catch(e) {
|
|
22
50
|
|
|
23
51
|
}
|
|
@@ -25,7 +53,7 @@ const tryParse = (value) => {
|
|
|
25
53
|
|
|
26
54
|
const ifInvalid = (variable) => {
|
|
27
55
|
variable.validityState.type = typeof(variable.type)==="string" ? variable.type : variable.type.type;
|
|
28
|
-
throw new TypeError(JSON.stringify(variable));
|
|
56
|
+
throw new TypeError(JSON.stringify(DataType(variable)));
|
|
29
57
|
// or could return existing value variable.value
|
|
30
58
|
// or could return nothing
|
|
31
59
|
}
|
|
@@ -40,7 +68,7 @@ const validateAny = function(value,variable) {
|
|
|
40
68
|
variable.validityState = ValidityState({valid:true});
|
|
41
69
|
return value;
|
|
42
70
|
}
|
|
43
|
-
return this.whenInvalid(variable);
|
|
71
|
+
return this.whenInvalid(variable,value);
|
|
44
72
|
}
|
|
45
73
|
const any = ({required=false,whenInvalid = ifInvalid,...rest}) => { // ...rest allows use of property "default", which is otherwise reserved
|
|
46
74
|
if(typeof(required)!=="boolean") throw new TypeError(`required, ${JSON.stringify(required)}, must be a boolean`);
|
|
@@ -78,7 +106,7 @@ const validateArray = function(value,variable) {
|
|
|
78
106
|
return result;
|
|
79
107
|
}
|
|
80
108
|
}
|
|
81
|
-
return this.whenInvalid(variable);
|
|
109
|
+
return this.whenInvalid(variable,value);
|
|
82
110
|
}
|
|
83
111
|
const array = ({coerce=false, required = false,whenInvalid = ifInvalid,maxlength=Infinity,minlength=0,...rest}) => {
|
|
84
112
|
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
|
|
@@ -99,6 +127,7 @@ const array = ({coerce=false, required = false,whenInvalid = ifInvalid,maxlength
|
|
|
99
127
|
}
|
|
100
128
|
}
|
|
101
129
|
array.validate = validateArray;
|
|
130
|
+
array.whenInvalid = ifInvalid;
|
|
102
131
|
array.coerce = false;
|
|
103
132
|
array.required = false;
|
|
104
133
|
|
|
@@ -110,7 +139,7 @@ const validateBoolean = function(value,variable) {
|
|
|
110
139
|
if(this.required && value==null) {
|
|
111
140
|
variable.validityState = ValidityState({valueMissing: true});
|
|
112
141
|
} else {
|
|
113
|
-
const result =
|
|
142
|
+
const result = this.coerce ? tryParse(value) : value;
|
|
114
143
|
if(typeof(result)!=="boolean") {
|
|
115
144
|
variable.validityState = ValidityState({typeMismatch: true, value});
|
|
116
145
|
} else {
|
|
@@ -118,7 +147,7 @@ const validateBoolean = function(value,variable) {
|
|
|
118
147
|
return result;
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
|
-
return this.whenInvalid(variable);
|
|
150
|
+
return this.whenInvalid(variable,value);
|
|
122
151
|
}
|
|
123
152
|
const boolean = ({coerce=false,required=false, whenInvalid = ifInvalid,...rest}) =>{
|
|
124
153
|
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
|
|
@@ -135,6 +164,7 @@ const boolean = ({coerce=false,required=false, whenInvalid = ifInvalid,...rest})
|
|
|
135
164
|
}
|
|
136
165
|
}
|
|
137
166
|
boolean.validate = validateBoolean;
|
|
167
|
+
boolean.whenInvalid = ifInvalid;
|
|
138
168
|
boolean.coerce = false;
|
|
139
169
|
boolean.required = false;
|
|
140
170
|
|
|
@@ -161,7 +191,7 @@ const validateNumber = function(value,variable) {
|
|
|
161
191
|
return result;
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
|
-
return this.whenInvalid(variable);
|
|
194
|
+
return this.whenInvalid(variable,value);
|
|
165
195
|
}
|
|
166
196
|
const number = ({coerce=false,required = false,whenInvalid = ifInvalid,min=-Infinity,max=Infinity,step = 1,allowNaN = true,...rest}) => {
|
|
167
197
|
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
|
|
@@ -186,11 +216,12 @@ const number = ({coerce=false,required = false,whenInvalid = ifInvalid,min=-Infi
|
|
|
186
216
|
}
|
|
187
217
|
}
|
|
188
218
|
number.validate = validateNumber;
|
|
219
|
+
number.whenInvalid = ifInvalid;
|
|
189
220
|
number.min = -Infinity;
|
|
190
221
|
number.max = Infinity;
|
|
191
222
|
number.coerce = false;
|
|
192
223
|
number.required = false;
|
|
193
|
-
number.
|
|
224
|
+
number.allowNaN = true;
|
|
194
225
|
number.step = 1;
|
|
195
226
|
|
|
196
227
|
const validateObject = function(value,variable) {
|
|
@@ -208,7 +239,7 @@ const validateObject = function(value,variable) {
|
|
|
208
239
|
return result;
|
|
209
240
|
}
|
|
210
241
|
}
|
|
211
|
-
return this.whenInvalid(variable);
|
|
242
|
+
return this.whenInvalid(variable,value);
|
|
212
243
|
}
|
|
213
244
|
const object = ({coerce=false, required = false,whenInvalid = ifInvalid,...rest}) => {
|
|
214
245
|
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
|
|
@@ -225,6 +256,7 @@ const object = ({coerce=false, required = false,whenInvalid = ifInvalid,...rest}
|
|
|
225
256
|
}
|
|
226
257
|
}
|
|
227
258
|
object.validate = validateObject;
|
|
259
|
+
object.whenInvalid = ifInvalid;
|
|
228
260
|
object.coerce = false;
|
|
229
261
|
object.required = false;
|
|
230
262
|
|
|
@@ -247,7 +279,7 @@ const validateString = function(value,variable) {
|
|
|
247
279
|
return result;
|
|
248
280
|
}
|
|
249
281
|
}
|
|
250
|
-
return this.whenInvalid(variable);
|
|
282
|
+
return this.whenInvalid(variable,value);
|
|
251
283
|
}
|
|
252
284
|
const string = ({coerce=false, required = false,whenInvalid = ifInvalid, maxlength = Infinity, minlength = 0, pattern, ...rest}) => {
|
|
253
285
|
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
|
|
@@ -269,6 +301,7 @@ const string = ({coerce=false, required = false,whenInvalid = ifInvalid, maxleng
|
|
|
269
301
|
}
|
|
270
302
|
}
|
|
271
303
|
string.validate = validateString;
|
|
304
|
+
string.whenInvalid = ifInvalid;
|
|
272
305
|
string.coerce = false;
|
|
273
306
|
string.required = false;
|
|
274
307
|
string.maxlength = Infinity;
|
|
@@ -289,7 +322,7 @@ const validateSymbol = function(value,variable) {
|
|
|
289
322
|
return result;
|
|
290
323
|
}
|
|
291
324
|
}
|
|
292
|
-
return this.whenInvalid(variable);
|
|
325
|
+
return this.whenInvalid(variable,value);
|
|
293
326
|
}
|
|
294
327
|
const symbol = ({coerce=false,required=false, whenInvalid = ifInvalid,...rest}) =>{
|
|
295
328
|
if(typeof(coerce)!=="boolean") throw new TypeError(`coerce, ${JSON.stringify(coerce)}, must be a boolean`);
|
|
@@ -306,7 +339,141 @@ const symbol = ({coerce=false,required=false, whenInvalid = ifInvalid,...rest})
|
|
|
306
339
|
}
|
|
307
340
|
}
|
|
308
341
|
symbol.validate = validateSymbol;
|
|
342
|
+
symbol.whenInvalid = ifInvalid;
|
|
309
343
|
symbol.coerce = false;
|
|
310
344
|
symbol.required = false;
|
|
311
345
|
|
|
312
|
-
|
|
346
|
+
const remoteProxy = ({json, variable,config, reactive, component}) => {
|
|
347
|
+
const type = typeof (config);
|
|
348
|
+
return new Proxy(json, {
|
|
349
|
+
get(target,property) {
|
|
350
|
+
if(property==="__remoteProxytarget__") return json;
|
|
351
|
+
return target[property];
|
|
352
|
+
},
|
|
353
|
+
async set(target, property, value) {
|
|
354
|
+
if(value && typeof(value)==="object" && value instanceof Promise) value = await value;
|
|
355
|
+
const oldValue = target[property];
|
|
356
|
+
if (oldValue !== value) {
|
|
357
|
+
let remotevalue;
|
|
358
|
+
if (type === "string") {
|
|
359
|
+
const href = new URL(config,window.location.href).href;
|
|
360
|
+
remotevalue = patch({target,property,value,oldValue},href,variable);
|
|
361
|
+
} else if(config && type==="object") {
|
|
362
|
+
let href;
|
|
363
|
+
if(config.path) href = new URL(config.path,window.location.href).href;
|
|
364
|
+
if(!config.patch) {
|
|
365
|
+
if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
|
|
366
|
+
config.patch = patch;
|
|
367
|
+
}
|
|
368
|
+
remotevalue = config.patch({target,property,value,oldValue},href,variable);
|
|
369
|
+
}
|
|
370
|
+
if(remotevalue) {
|
|
371
|
+
await remotevalue.then((newjson) => {
|
|
372
|
+
if (newjson && typeof (newjson) === "object" && reactive) {
|
|
373
|
+
const target = variable.value?.__reactorProxyTarget__ ? json : variable.value;
|
|
374
|
+
Object.entries(newjson).forEach(([key,newValue]) => {
|
|
375
|
+
if(target[key]!==newValue) target[key] = newValue;
|
|
376
|
+
})
|
|
377
|
+
Object.keys(target).forEach((key) => {
|
|
378
|
+
if(!(key in newjson)) delete target[key];
|
|
379
|
+
});
|
|
380
|
+
if(variable.value?.__reactorProxyTarget__) {
|
|
381
|
+
const dependents = variable.value.__dependents__,
|
|
382
|
+
observers = dependents[property] || [];
|
|
383
|
+
[...observers].forEach((f) => {
|
|
384
|
+
if (f.cancelled) dependents[property].delete(f);
|
|
385
|
+
else f();
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
component.setVariableValue(variable.name,newjson)
|
|
390
|
+
//variable.value = json;
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const patch = ({target,property,value,oldValue},href,variable) => {
|
|
401
|
+
return fetch(href, {
|
|
402
|
+
method: "PATCH",
|
|
403
|
+
body: JSON.stringify({property,value,oldValue}),
|
|
404
|
+
headers: {
|
|
405
|
+
"Content-Type": "application/json"
|
|
406
|
+
}
|
|
407
|
+
}).then((response) => {
|
|
408
|
+
if (response.status < 400) return response.json();
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const get = (href,variable) => {
|
|
413
|
+
return fetch(href)
|
|
414
|
+
.then((response) => {
|
|
415
|
+
if (response.status < 400) return response.json();
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const put = (href,variable) => {
|
|
420
|
+
return fetch(href, {
|
|
421
|
+
method: "PUT",
|
|
422
|
+
body: JSON.stringify(variable.value),
|
|
423
|
+
headers: {
|
|
424
|
+
"Content-Type": "application/json"
|
|
425
|
+
}
|
|
426
|
+
}).then((response) => {
|
|
427
|
+
if (response.status === 200) return response.json();
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const handleRemote = async ({variable, config, reactive, component},doput) => {
|
|
432
|
+
const type = typeof (config);
|
|
433
|
+
let value;
|
|
434
|
+
if (type === "string") {
|
|
435
|
+
const href = new URL(config,window.location.href).href;
|
|
436
|
+
value = (doput
|
|
437
|
+
? put(href,variable)
|
|
438
|
+
: get(href,variable));
|
|
439
|
+
if(variable.value===undefined) variable.value = value; // do not await here
|
|
440
|
+
} else if (remote && type === "object") {
|
|
441
|
+
let href;
|
|
442
|
+
if(!config.path) config.path = `./${variable.name}`;
|
|
443
|
+
if(config.path) href = new URL(config.path,window.location.href).href;
|
|
444
|
+
if(!config.get || !config.put) {
|
|
445
|
+
if(!href) throw new Error(`A remote path is required if no put function is provided for remote data`)
|
|
446
|
+
if(!config.get) config.get = get;
|
|
447
|
+
if(!config.put && reactive) config.put = put;
|
|
448
|
+
}
|
|
449
|
+
value = (doput
|
|
450
|
+
? config.put(href,variable)
|
|
451
|
+
: config.get(href,variable));
|
|
452
|
+
if(config.ttl && !doput && !config.intervalId) {
|
|
453
|
+
config.intervalId = setInterval(async () => {
|
|
454
|
+
await handleRemote({variable, config, reactive, component});
|
|
455
|
+
//schedule();
|
|
456
|
+
},config.ttl);
|
|
457
|
+
}
|
|
458
|
+
if(variable.value===undefined) variable.value = value;
|
|
459
|
+
}
|
|
460
|
+
if(value) {
|
|
461
|
+
const json = await value;
|
|
462
|
+
//value.then((json) => {
|
|
463
|
+
if (json && typeof (json) === "object" && reactive) {
|
|
464
|
+
variable.value = remoteProxy({json, variable,config, reactive, component});
|
|
465
|
+
} else {
|
|
466
|
+
component.setVariableValue(variable.name,json);
|
|
467
|
+
}
|
|
468
|
+
//})
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const remote = (config) => {
|
|
473
|
+
return {
|
|
474
|
+
config,
|
|
475
|
+
handleRemote
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export {ValidityState,any,array,boolean,number,object,string,symbol,remote,reviver}
|