lightview 1.6.2-b → 1.6.5-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 CHANGED
@@ -1,4 +1,4 @@
1
- # lightview v1.4.10b (BETA)
1
+ # lightview v1.6.5b (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
+
@@ -23,7 +23,7 @@
23
23
  });
24
24
  let chart,
25
25
  datatable,
26
- options;
26
+ options = {};
27
27
  self.variables({type: "string", title: "string", style: "string"}, {observed});
28
28
  if(style) target.setAttribute("style",style);
29
29
  self.addRow = (row) => {
@@ -36,6 +36,9 @@
36
36
  chart.draw(datatable, options);
37
37
  }
38
38
  };
39
+ self.setOptions = (newOptions) => {
40
+ Object.assign(options,newOptions);
41
+ };
39
42
  self.init = () => {
40
43
  const node = self.firstChild,
41
44
  callback = (textNode, oldValue, newValue) => {
@@ -48,9 +51,7 @@
48
51
  target.innerText = e + newValue;
49
52
  }
50
53
  };
51
- options = {
52
- title: title
53
- };
54
+ if(!options.title && title) options.title = title;
54
55
  chart = new google.visualization[type](target);
55
56
  node.characterDataMutationCallback = callback;
56
57
  callback(node, node.textContent, node.textContent);
@@ -67,7 +68,7 @@
67
68
  resizeObserver.observe(target);
68
69
  self.removeAttribute("hidden");
69
70
  };
70
- addEventListener("connected", ({target}) => {
71
+ self.addEventListener("connected", ({target}) => {
71
72
  google.charts.load("current", {"packages": ["corechart","gauge"]});
72
73
  google.charts.setOnLoadCallback(self.init);
73
74
  });
@@ -48,7 +48,7 @@
48
48
  resizeObserver.observe(target);
49
49
  self.removeAttribute("hidden");
50
50
  };
51
- addEventListener("connected", ({target}) => {
51
+ self.addEventListener("connected", ({target}) => {
52
52
  google.charts.load("current", {"packages": ["corechart","gauge"]});
53
53
  google.charts.setOnLoadCallback(self.init);
54
54
  });
@@ -50,15 +50,17 @@
50
50
  </l-chart>
51
51
  <script>
52
52
  const gauges = document.getElementById("myGauges");
53
- setInterval(function() {
54
- gauges.setValue(0, 1, 40 + Math.round(60 * Math.random()));
55
- }, 6000);
56
- setInterval(function() {
57
- gauges.setValue(1, 1, 40 + Math.round(60 * Math.random()));
58
- }, 5000);
59
- setInterval(function() {
60
- gauges.setValue(2, 1, 60 + Math.round(40 * Math.random()));
61
- }, 7500);
53
+ gauges.addEventListener("connected",() => {
54
+ setInterval(function() {
55
+ gauges.setValue(0, 1, 40 + Math.round(60 * Math.random()));
56
+ }, 6000);
57
+ setInterval(function() {
58
+ gauges.setValue(1, 1, 40 + Math.round(60 * Math.random()));
59
+ }, 5000);
60
+ setInterval(function() {
61
+ gauges.setValue(2, 1, 60 + Math.round(40 * Math.random()));
62
+ }, 7500);
63
+ })
62
64
  </script>
63
65
  </body>
64
66
  </html>
@@ -0,0 +1,59 @@
1
+ <html>
2
+ <head>
3
+ <title>Lightview:Examples:Medium:Remote</title>
4
+ <!--
5
+ for convenience, import the chart component from the Lightview component library
6
+ alias it to r-chart rather than use the default l-chart
7
+ we could use any gauge component that exposes methods to update its view
8
+ -->
9
+ <link href="../../components/chart.html" rel="module" crossorigin="use-credentials" as="r-chart">
10
+ <!--
11
+ load the lightview library, about 7K
12
+ use the body of this file to create a custom element to replace itself
13
+ -->
14
+ <script src="../../lightview.js?as=x-body"></script>
15
+ </head>
16
+ <body>
17
+ <!--
18
+ layout the dashboad using the chart component r-chart
19
+ -->
20
+ <div style="width:100%;text-align:center">
21
+ <!--
22
+ set the initial value 0 for all components in a relaxed JSON5 configuration data block
23
+ -->
24
+ <r-chart id="dashboard" style="display:inline-block" type="Gauge" title="Server Status">
25
+ [
26
+ ['Label', 'Value'], // gauge will always take two columns, Label and Value
27
+ ['Memory', 0],
28
+ ['CPU', 0],
29
+ ['Network', 0]
30
+ ]
31
+ </r-chart>
32
+ </div>
33
+ <script type="lightview/module">
34
+ // use local, normal variables for as much as possible
35
+ const {remote} = await import("../../types.js"), // load the functional type 'remote`
36
+ sensorIndex = { // map sensor names to indexes in the dashboard data
37
+ memory:0,
38
+ cpu:1,
39
+ network:2
40
+ },
41
+ dashboard = document.body.getElementById("dashboard"),
42
+ path = "https://lightview.dev/sensors/"; // replace base path for your own implementation
43
+ // create remote reactive variables for sensors with differing refresh rates (ttl in milliseconds)
44
+ self.variables({memory:"number"},{remote:remote({ttl:5000,path})});
45
+ self.variables({cpu:"number"},{remote:remote({ttl:2500,path})});
46
+ self.variables({network:"number"},{remote:remote({ttl:1500,path})});
47
+ dashboard.addEventListener("connected",() => {
48
+ dashboard.setOptions({ // when dashboard component has finished initializing, set more options
49
+ redFrom: 90, redTo: 100,
50
+ yellowFrom:75, yellowTo: 90,
51
+ minorTicks: 5});
52
+ addEventListener("change",({variableName,value}) => { // execute the magic with a localized eventListener
53
+ const index = sensorIndex[variableName];
54
+ dashboard.setValue(index, 1, value);
55
+ });
56
+ });
57
+ </script>
58
+ </body>
59
+ </html>
package/examples/xor.html CHANGED
@@ -28,7 +28,7 @@
28
28
  })
29
29
  </script>
30
30
  </template>
31
- <title>Form</title>
31
+ <title>Lightview:Examples:XOR</title>
32
32
  <script src="../lightview.js"></script>
33
33
  <script>
34
34
  Lightview.createComponent("x-audiostream", document.getElementById("audiostream"))
package/jest.config.json CHANGED
@@ -9,4 +9,4 @@
9
9
  "globalSetup": "jest-environment-puppeteer/setup",
10
10
  "globalTeardown": "jest-environment-puppeteer/teardown",
11
11
  "testEnvironment": "jest-environment-puppeteer"
12
- }
12
+ }
package/lightview.js CHANGED
@@ -241,10 +241,13 @@ 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 || type === "any" || (newtype === type && typetype==="string") || (typetype === "function" && (newValue && newtype === "object" && newValue instanceof type) || variable.validityState?.valid)) {
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
@@ -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
- const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
345
+
343
346
  const resolveNodeOrText = (node, component, safe) => {
344
347
  const type = typeof (node),
345
348
  template = type === "string" ? node.trim() : node.template;
@@ -444,13 +447,13 @@ const {observe} = (() => {
444
447
  eventlisteners = {};
445
448
  // needs to be local to the instance
446
449
  Object.defineProperty(this,"changeListener",{value:
447
- function({variableName, value}) {
448
- if (currentComponent.changeListener.targets.has(variableName)) {
449
- value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
450
- if (value == null) removeComponentAttribute(currentComponent, variableName);
451
- else setComponentAttribute(currentComponent, variableName, value);
452
- }
453
- }});
450
+ function({variableName, value}) {
451
+ if (currentComponent.changeListener.targets.has(variableName)) {
452
+ value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
453
+ if (value == null) removeComponentAttribute(currentComponent, variableName);
454
+ else setComponentAttribute(currentComponent, variableName, value);
455
+ }
456
+ }});
454
457
  this.vars = {
455
458
  ...reserved,
456
459
  addEventListener: {
@@ -505,23 +508,25 @@ const {observe} = (() => {
505
508
  instances.delete(this);
506
509
  }
507
510
 
508
- connectedCallback() {
511
+ async connectedCallback() {
509
512
  const ctx = this,
510
513
  shadow = ctx.shadowRoot;
511
514
  for (const attr of this.defaultAttributes) this.hasAttribute(attr.name) || this.setAttribute(attr.name, attr.value);
512
515
  const scripts = shadow.querySelectorAll("script"),
513
- promises = [],
514
- scriptpromises = [];
516
+ promises = [];
517
+ // scriptpromises = [];
515
518
  for (const script of [...scripts]) {
516
519
  if (script.attributes.src?.value?.includes("/lightview.js")) continue;
517
520
  const currentScript = document.createElement("script");
518
521
  if (script.className !== "lightview" && !((script.attributes.type?.value || "").includes("lightview/"))) {
519
522
  for (const attr of script.attributes) currentScript.setAttribute(attr.name,attr.value);
520
- scriptpromises.push(new Promise((resolve) => {
521
- currentScript.onload = () => resolve();
522
- }))
523
523
  shadow.appendChild(currentScript);
524
- currentScript.remove();
524
+ await new Promise((resolve) => {
525
+ currentScript.onload = () => {
526
+ currentScript.remove();
527
+ resolve();
528
+ }
529
+ })
525
530
  continue;
526
531
  };
527
532
  const scriptid = Math.random() + "";
@@ -531,166 +536,166 @@ const {observe} = (() => {
531
536
  currentScript.classList.remove("lightview");
532
537
  const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
533
538
  currentScript.innerHTML = `Object.getPrototypeOf(async function(){}).constructor('if(window["${scriptid}"]?.ctx) { const ctx = window["${scriptid}"].ctx; { with(ctx) { ${text}; } } }')().then(() => window["${scriptid}"]()); `;
534
- let resolver;
535
- window[scriptid] = () => {
536
- delete window[scriptid];
537
- currentScript.remove();
538
- resolver();
539
- }
540
- window[scriptid].ctx = ctx.varsProxy;
541
- promises.push(new Promise((resolve) => {
542
- resolver = resolve;
543
- // wait for all regular scripts to load first
544
- Promise.all(scriptpromises).then(() => shadow.appendChild(currentScript));
545
- }));
539
+ await new Promise((resolve) => {
540
+ window[scriptid] = () => {
541
+ delete window[scriptid];
542
+ currentScript.remove();
543
+ resolve();
544
+ }
545
+ window[scriptid].ctx = ctx.varsProxy;
546
+ shadow.appendChild(currentScript);
547
+ })
546
548
  }
547
- Promise.all(promises).then(() => {
548
- const nodes = getNodes(ctx);
549
- nodes.forEach((node) => {
550
- if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
551
- observe(() => resolveNodeOrText(node, this));
552
- } else if (node.nodeType === Node.ELEMENT_NODE) {
553
- // resolve the value before all else;
554
- const attr = node.attributes.value,
555
- template = attr?.template;
556
- if (attr && template) {
557
- let value = resolveNodeOrText(attr, this),
558
- eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
559
- const template = attr.template;
560
- if (template) {
561
- if (/\$\{[a-zA-z_]+\}/g.test(template)) {
562
- const name = template.substring(2, template.length - 1);
563
- if(!this.vars[name] || this.vars[name].reactive) {
564
- bindInput(node, name, this, value);
549
+ // Promise.all(promises).then(() => {
550
+ const nodes = getNodes(ctx);
551
+ nodes.forEach((node) => {
552
+ if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
553
+ observe(() => resolveNodeOrText(node, this));
554
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
555
+ // resolve the value before all else;
556
+ const attr = node.attributes.value,
557
+ template = attr?.template;
558
+ if (attr && template) {
559
+ let value = resolveNodeOrText(attr, this),
560
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
561
+ const template = attr.template;
562
+ if (template) {
563
+ if (/\$\{[a-zA-z_]+\}/g.test(template)) {
564
+ const name = template.substring(2, template.length - 1);
565
+ if(!this.vars[name] || this.vars[name].reactive) {
566
+ bindInput(node, name, this, value);
567
+ }
568
+ }
569
+ observe(() => {
570
+ const value = resolveNodeOrText(template, ctx);
571
+ if(value!==undefined) {
572
+ if (eltype === "checkbox") {
573
+ if (coerce(value, "boolean") === true) {
574
+ node.setAttribute("checked", "");
575
+ node.checked = true;
576
+ } else {
577
+ node.removeAttribute("checked");
578
+ node.checked = false;
565
579
  }
566
- }
567
- observe(() => {
568
- const value = resolveNodeOrText(template, ctx);
569
- if(value!==undefined) {
570
- if (eltype === "checkbox") {
571
- if (coerce(value, "boolean") === true) {
572
- node.setAttribute("checked", "");
573
- node.checked = true;
574
- } else {
575
- node.removeAttribute("checked");
576
- node.checked = false;
580
+ } else if (node.tagName === "SELECT") {
581
+ let values = [value];
582
+ if (node.hasAttribute("multiple")) values = coerce(value, Array);
583
+ [...node.querySelectorAll("option")].forEach(async (option) => {
584
+ if (option.hasAttribute("value")) {
585
+ if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
586
+ option.setAttribute("selected", "");
587
+ option.selected = true;
577
588
  }
578
- } else if (node.tagName === "SELECT") {
579
- let values = [value];
580
- if (node.hasAttribute("multiple")) values = coerce(value, Array);
581
- [...node.querySelectorAll("option")].forEach(async (option) => {
582
- if (option.hasAttribute("value")) {
583
- if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
584
- option.setAttribute("selected", "");
585
- option.selected = true;
586
- }
587
- } else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
588
- option.setAttribute("selected", "");
589
- option.selected = true;
590
- }
591
- })
592
- } else if (eltype!=="radio") {
593
- attr.value = value;
589
+ } else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
590
+ option.setAttribute("selected", "");
591
+ option.selected = true;
594
592
  }
595
- }
596
- });
593
+ })
594
+ } else if (eltype!=="radio") {
595
+ attr.value = value;
596
+ }
597
597
  }
598
+ });
599
+ }
600
+ }
601
+ [...node.attributes].forEach(async (attr) => {
602
+ if (attr.name === "value" && attr.template) return;
603
+ const {name, value} = attr,
604
+ vname = node.attributes.name?.value;
605
+ if (name === "type" && value=="radio" && vname) {
606
+ bindInput(node, vname, this);
607
+ observe(() => {
608
+ const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
609
+ if (node.attributes.value.value == varvalue) {
610
+ node.setAttribute("checked", "");
611
+ node.checked = true;
612
+ } else {
613
+ node.removeAttribute("checked");
614
+ node.checked = false;
615
+ }
616
+ });
598
617
  }
599
- [...node.attributes].forEach(async (attr) => {
600
- if (attr.name === "value" && attr.template) return;
601
- const {name, value} = attr,
602
- vname = node.attributes.name?.value;
603
- if (name === "type" && value=="radio" && vname) {
604
- bindInput(node, vname, this);
605
- observe(() => {
606
- const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
607
- if (node.attributes.value.value == varvalue) {
608
- node.setAttribute("checked", "");
609
- node.checked = true;
610
- } else {
611
- node.removeAttribute("checked");
612
- node.checked = false;
613
- }
614
- });
615
- }
616
618
 
617
- const [type, ...params] = name.split(":");
618
- if (type === "") { // name is :something
619
- observe(() => {
620
- const value = attr.value;
621
- if (params[0]) {
622
- if (value === "true") node.setAttribute(params[0], "")
623
- else node.removeAttribute(params[0]);
624
- } else {
625
- const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
626
- eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
627
- if (eltype === "checkbox" || node.tagName === "OPTION") {
628
- if (elvalue === "true") node.setAttribute("checked", "")
629
- else node.removeAttribute("checked");
630
- }
631
- }
632
- })
633
- } else if (type === "l-on") {
634
- let listener;
635
- observe(() => {
636
- const value = resolveNodeOrText(attr, this);
637
- if (listener) node.removeEventListener(params[0], listener);
638
- listener = this[value] || window[value] || Function(value);
639
- addListener(node, params[0], listener);
640
- })
641
- } else if (type === "l-if") {
642
- observe(() => {
643
- const value = resolveNodeOrText(attr, this);
644
- node.style.setProperty("display", value === "true" ? "revert" : "none");
645
- })
646
- } else if (type === "l-for") {
647
- node.template ||= node.innerHTML;
648
- observe(() => {
649
- const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
650
- value = resolveNodeOrText(attr, this),
651
- coerced = coerce(value, what === "each" ? Array : "object"),
652
- target = what === "each" ? coerced : Object[what](coerced),
653
- html = target.reduce( (html, item, i, target) => {
654
- return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
655
- ctx.varsProxy,
656
- {
657
- [vname]: item,
658
- [index]: i,
659
- [array]: target
660
- })
661
- }, ""),
662
- parsed = parser.parseFromString(html, "text/html");
663
- if (!window.lightviewDebug) {
664
- if (after) {
665
- node.style.setProperty("display", "none")
666
- } else {
667
- while (node.lastElementChild) node.lastElementChild.remove();
668
- }
619
+ const [type, ...params] = name.split(":");
620
+ if (type === "") { // name is :something
621
+ observe(() => {
622
+ const value = attr.value;
623
+ if (params[0]) {
624
+ if (value === "true") node.setAttribute(params[0], "")
625
+ else node.removeAttribute(params[0]);
626
+ } else {
627
+ const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
628
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
629
+ if (eltype === "checkbox" || node.tagName === "OPTION") {
630
+ if (elvalue === "true") node.setAttribute("checked", "")
631
+ else node.removeAttribute("checked");
669
632
  }
670
- while (parsed.body.firstChild) {
671
- if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
672
- else node.appendChild(parsed.body.firstChild);
633
+ }
634
+ })
635
+ } else if (type === "l-on") {
636
+ let listener;
637
+ observe(() => {
638
+ const value = resolveNodeOrText(attr, this);
639
+ if (listener) node.removeEventListener(params[0], listener);
640
+ listener = this[value] || window[value] || Function(value);
641
+ addListener(node, params[0], listener);
642
+ })
643
+ } else if (type === "l-if") {
644
+ observe(() => {
645
+ const value = resolveNodeOrText(attr, this);
646
+ node.style.setProperty("display", value === "true" ? "revert" : "none");
647
+ })
648
+ } else if (type === "l-for") {
649
+ node.template ||= node.innerHTML;
650
+ observe(() => {
651
+ const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
652
+ value = resolveNodeOrText(attr, this),
653
+ coerced = coerce(value, what === "each" ? Array : "object"),
654
+ target = what === "each" ? coerced : Object[what](coerced),
655
+ html = target.reduce( (html, item, i, target) => {
656
+ return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
657
+ ctx.varsProxy,
658
+ {
659
+ [vname]: item,
660
+ [index]: i,
661
+ [array]: target
662
+ })
663
+ }, ""),
664
+ parsed = parser.parseFromString(html, "text/html");
665
+ if (!window.lightviewDebug) {
666
+ if (after) {
667
+ node.style.setProperty("display", "none")
668
+ } else {
669
+ while (node.lastElementChild) node.lastElementChild.remove();
673
670
  }
674
- })
675
- } else if(attr.template) {
676
- observe(() => {
677
- resolveNodeOrText(attr, this);
678
- })
679
- }
680
- })
681
- }
682
- })
683
- shadow.normalize();
684
- observer ||= createObserver(ctx, framed);
685
- observer.observe(ctx, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
686
- ctx.vars.postEvent.value("connected");
671
+ }
672
+ while (parsed.body.firstChild) {
673
+ if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
674
+ else node.appendChild(parsed.body.firstChild);
675
+ }
676
+ })
677
+ } else if(attr.template) {
678
+ observe(() => {
679
+ resolveNodeOrText(attr, this);
680
+ })
681
+ }
682
+ })
683
+ }
687
684
  })
685
+ shadow.normalize();
686
+ observer ||= createObserver(ctx, framed);
687
+ observer.observe(ctx, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
688
+ //ctx.vars.postEvent.value("connected");
689
+ this.dispatchEvent(new Event("connected"));
690
+ // })
688
691
  }
689
692
  adoptedCallback(callback) {
690
- this.vars.postEvent.value("adopted");
693
+ this.dispatchEvent(new Event("adopted"));
694
+ //this.vars.postEvent.value("adopted");
691
695
  }
692
696
  disconnectedCallback() {
693
- this.vars.postEvent.value("disconnected");
697
+ this.dispatchEvent(new Event("disconnected"));
698
+ //this.vars.postEvent.value("disconnected");
694
699
  }
695
700
  get observedAttributes() {
696
701
  return CustomElement.observedAttributes;
@@ -782,7 +787,7 @@ const {observe} = (() => {
782
787
  variable.remote = remote;
783
788
  remote.handleRemote({variable, config:remote.config, reactive,component:this});
784
789
  }
785
- if(type.validate) type.validate(variable.value,variable);
790
+ if(type.validate && variable.value!==undefined) type.validate(variable.value,variable);
786
791
  });
787
792
  }
788
793
  return Object.entries(this.vars)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "1.6.2b",
3
+ "version": "1.6.5b",
4
4
  "description": "Small, simple, powerful web UI and micro front end creation ... Great ideas from Svelte, React, Vue and Riot combined.",
5
5
  "main": "lightview.js",
6
6
  "scripts": {
package/test/basic.html CHANGED
@@ -46,7 +46,7 @@
46
46
  counter = 0;
47
47
  myshare = 1;
48
48
 
49
- addEventListener("connected", ({target}) => {
49
+ self.addEventListener("connected", ({target}) => {
50
50
  iuntyped = "test";
51
51
  itext = "test";
52
52
  itel = "test";
@@ -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
  }
@@ -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 = !!(this.coerce ? tryParse(value) : value);
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 {
@@ -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
 
@@ -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.allwNaN = true;
224
+ number.allowNaN = true;
194
225
  number.step = 1;
195
226
 
196
227
  const validateObject = function(value,variable) {
@@ -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
 
@@ -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;
@@ -306,6 +339,7 @@ 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
 
@@ -404,9 +438,9 @@ const handleRemote = async ({variable, config, reactive, component},doput) => {
404
438
  : get(href,variable));
405
439
  if(variable.value===undefined) variable.value = value; // do not await here
406
440
  } else if (remote && type === "object") {
407
- let href;
408
441
  if(!config.path) config.path = `./${variable.name}`;
409
- if(config.path) href = new URL(config.path,window.location.href).href;
442
+ if(config.path.endsWith("/")) config.path = `${config.path}${variable.name}`;
443
+ const href = new URL(config.path,window.location.href).href;
410
444
  if(!config.get || !config.put) {
411
445
  if(!href) throw new Error(`A remote path is required if no put function is provided for remote data`)
412
446
  if(!config.get) config.get = get;
@@ -425,12 +459,12 @@ const handleRemote = async ({variable, config, reactive, component},doput) => {
425
459
  }
426
460
  if(value) {
427
461
  const json = await value;
428
- //value.then((json) => {
429
- if (json && typeof (json) === "object" && reactive) {
430
- variable.value = remoteProxy({json, variable,config, reactive, component});
431
- } else {
432
- component.setVariableValue(variable.name,json);
433
- }
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
+ }
434
468
  //})
435
469
  }
436
470
  }
@@ -442,4 +476,4 @@ const remote = (config) => {
442
476
  }
443
477
  }
444
478
 
445
- export {ValidityState,any,array,boolean,number,string,symbol,remote}
479
+ export {ValidityState,any,array,boolean,number,object,string,symbol,remote,reviver}