lightview 1.4.8-b → 1.4.10-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.8b (BETA)
1
+ # lightview v1.4.10b (BETA)
2
2
 
3
3
  Small, simple, powerful web UI and micro front end creation ...
4
4
 
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+
3
+ <head>
4
+ <title>Remote</title>
5
+ <link type="module" src="">
6
+ <meta name="l-enableFrames">
7
+ <script src="../lightview.js"></script>
8
+ </head>
9
+
10
+ <body>
11
+ <p>
12
+ The component below is loaded from an alternate domain and running in a child iframe.
13
+ The logging console is below the component in this frame.
14
+ </p>
15
+ <iframe id="myframe" src="https://lightview.dev/remoteform.html?id=myframe"></iframe>
16
+ <div id="console" style="max-height:250px;scroll:auto"></div>
17
+ <script>
18
+ const mutationCallback = (mutationsList) => {
19
+ const console = document.getElementById("console");
20
+ for (const {target,attributeName,oldValue} of mutationsList) {
21
+ const line = document.createElement("div"),
22
+ event = {attributeName,oldValue,value:target.getAttribute(attributeName)};
23
+ line.innerText = JSON.stringify(event);
24
+ console.appendChild(line);
25
+ }
26
+ };
27
+ const observer = new MutationObserver(mutationCallback),
28
+ iframe = document.getElementById("myframe");
29
+ observer.observe(iframe, { attributes:true, attributeOldValue: true });
30
+ iframe.addEventListener("DOMContentLoaded",(event) => {
31
+ console.log(event);
32
+ });
33
+ </script>
34
+ </body>
35
+
36
+ </html>
File without changes
@@ -0,0 +1,51 @@
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", "*");
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
+ console.log("GET",req.url);
17
+ res.write(fs.readFileSync(path));
18
+ res.end();
19
+ return;
20
+ }
21
+ const buffers = [];
22
+ for await(const chunk of req) {
23
+ buffers.push(chunk);
24
+ }
25
+ const data = JSON.parse(Buffer.concat(buffers).toString());
26
+ console.log(req.method,req.url,data);
27
+ if(req.method==="PUT") {
28
+ const string = JSON.stringify(data);
29
+ fs.writeFileSync(path,string);
30
+ res.write(string);
31
+ res.end();
32
+ return;
33
+ }
34
+ if(req.method==="PATCH") {
35
+ const {property,value,oldValue} = data,
36
+ json = JSON.parse(fs.readFileSync(path));
37
+ if(property!==undefined && json[property]===oldValue) { // probably need a deepEqual for a production use
38
+ json[property] = value;
39
+ fs.writeFileSync(path,JSON.stringify(json))
40
+ }
41
+ res.write(JSON.stringify(json));
42
+ res.end();
43
+ return;
44
+ }
45
+ },
46
+ server = http.createServer(requestListener);
47
+ server.listen(port, host, () => {
48
+ console.log(`Server is running on http://${host}:${port}`);
49
+ });
50
+
51
+
@@ -1,36 +1,30 @@
1
1
  <!DOCTYPE html>
2
-
2
+ <html lang="en">
3
3
  <head>
4
+ <meta charset="UTF-8">
4
5
  <title>Remote</title>
5
- <link type="module" src="">
6
- <meta name="l-enableFrames">
7
- <script src="../lightview.js"></script>
6
+ <script src="../lightview.js?as=x-body"></script>
8
7
  </head>
9
-
10
8
  <body>
11
- <p>
12
- The component below is loaded from an alternate domain and running in a child iframe.
13
- The logging console is below the component in this frame.
14
- </p>
15
- <iframe id="myframe" src="https://lightview.dev/remoteform.html?id=myframe"></iframe>
16
- <div id="console" style="max-height:250px;scroll:auto"></div>
17
- <script>
18
- const mutationCallback = (mutationsList) => {
19
- const console = document.getElementById("console");
20
- for (const {target,attributeName,oldValue} of mutationsList) {
21
- const line = document.createElement("div"),
22
- event = {attributeName,oldValue,value:target.getAttribute(attributeName)};
23
- line.innerText = JSON.stringify(event);
24
- console.appendChild(line);
25
- }
26
- };
27
- const observer = new MutationObserver(mutationCallback),
28
- iframe = document.getElementById("myframe");
29
- observer.observe(iframe, { attributes:true, attributeOldValue: true });
30
- iframe.addEventListener("DOMContentLoaded",(event) => {
31
- console.log(event);
32
- });
9
+
10
+ <input id="myRemote" type=text" value="${JSON.stringify(myRemote)}" size="${JSON.stringify(myRemote).length}"><br>
11
+ <button l-on:click="patch">Patch</button><br>
12
+ <button l-on:click="replace">Replace</button>
13
+
14
+
15
+ <script type="lightview/module">
16
+ self.variables({myRemote:object},{reactive,remote:"http://localhost:8000/remote.json"});
17
+
18
+ await myRemote; // must await remotes before the first time they are used, e.g. before HTML is rendered
19
+
20
+ self.patch = () => {
21
+ const json = JSON.parse(document.body.getElementById("myRemote").value);
22
+ Object.assign(myRemote,json);
23
+ };
24
+
25
+ self.replace = () => {
26
+ myRemote = JSON.parse(document.body.getElementById("myRemote").value);
27
+ };
33
28
  </script>
34
29
  </body>
35
-
36
30
  </html>
@@ -0,0 +1 @@
1
+ {"name":"joe","age":20}
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Template</title>
6
+ <template id="local-component">
7
+ <p>
8
+ <button l-on:click="click">Click Me</button>
9
+ </p>
10
+ <p>
11
+ ${message ? message : ""}
12
+ </p>
13
+ <script type="lightview/module">
14
+ self.variables({message: string}, {reactive});
15
+
16
+ self.click = (event) => {
17
+ message = "Hi there!";
18
+ };
19
+ </script>
20
+ </template>
21
+ <script src="../lightview.js"></script>
22
+ <script>
23
+ Lightview.createComponent("x-hello", document.getElementById("local-component"));
24
+ </script>
25
+ </head>
26
+
27
+ <body>
28
+ <div style="margin:20px">
29
+ <x-hello></x-hello>
30
+ </div>
31
+ </body>
32
+
33
+ </html>
@@ -0,0 +1,73 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Types</title>
6
+ <script src="../lightview.js?as=x-body"></script>
7
+ </head>
8
+
9
+ <body>
10
+ <div style="margin:20px">
11
+ <p>
12
+ <button l-on:click="run">Run</button>
13
+ <button l-on:click="clear">Clear</button>
14
+ </p>
15
+ <p id="console"></p>
16
+ </div>
17
+ <script type="lightview/module">
18
+ self.variables({
19
+ astring: string,
20
+ aDate: Date,
21
+ err: Error
22
+ });
23
+ self.run = () => {
24
+ try {
25
+ astring = "my string";
26
+ } catch (e) {
27
+ err = e;
28
+ }
29
+ try {
30
+ astring = 1;
31
+ } catch (e) {
32
+ err = e;
33
+ }
34
+ try {
35
+ aDate = new Date();
36
+ } catch (e) {
37
+ err = e;
38
+ }
39
+ try {
40
+ aDate = 1;
41
+ } catch (e) {
42
+ err = e;
43
+ }
44
+ try {
45
+ err = 1;
46
+ } catch (e) {
47
+ err = e;
48
+ }
49
+ };
50
+ // demo instrumentation
51
+ self.clear = () => {
52
+ const cnsl = self.getElementById("console");
53
+ while (cnsl.lastChild) cnsl.lastChild.remove();
54
+ };
55
+ addEventListener("change", ({
56
+ variableName,
57
+ value
58
+ }) => {
59
+ const cnsl = self.getElementById("console");
60
+ if (cnsl) {
61
+ const message = document.createElement("div");
62
+ if (variableName === "err") {
63
+ message.innerHTML = `<b>&gt;</b> ${value}<br>`;
64
+ } else {
65
+ message.innerHTML = `<b>&gt;</b> ${variableName} = ${value}<br>`;
66
+ }
67
+ cnsl.appendChild(message);
68
+ }
69
+ });
70
+ </script>
71
+ </body>
72
+
73
+ </html>
package/lightview.js CHANGED
@@ -31,19 +31,21 @@ const {observe} = (() => {
31
31
  const parser = new DOMParser();
32
32
 
33
33
  const templateSanitizer = (string) => {
34
- return string.replace(/function\s+/g,"")
35
- .replace(/function\(/g,"")
36
- .replace(/=\s*>/g,"")
37
- .replace(/(while|do|for|alert)\s*\(/g,"")
38
- .replace(/console\.[a-zA-Z$]+\s*\(/g,"");
34
+ return string.replace(/function\s+/g, "")
35
+ .replace(/function\(/g, "")
36
+ .replace(/=\s*>/g, "")
37
+ .replace(/(while|do|for|alert)\s*\(/g, "")
38
+ .replace(/console\.[a-zA-Z$]+\s*\(/g, "");
39
39
  }
40
40
  Lightview.sanitizeTemplate = templateSanitizer;
41
41
 
42
42
  const escaper = document.createElement('textarea');
43
+
43
44
  function escapeHTML(html) {
44
45
  escaper.textContent = html;
45
46
  return escaper.innerHTML;
46
47
  }
48
+
47
49
  Lightview.escapeHTML = escapeHTML;
48
50
 
49
51
  const addListener = (node, eventName, callback) => {
@@ -68,7 +70,8 @@ const {observe} = (() => {
68
70
  return "l-" + name;
69
71
  }
70
72
  const observe = (f, thisArg, argsList = []) => {
71
- function observer(...args) {
73
+ const observer = (...args) => {
74
+ if(observer.cancelled) return;
72
75
  CURRENTOBSERVER = observer;
73
76
  try {
74
77
  f.call(thisArg || this, ...argsList, ...args);
@@ -77,7 +80,6 @@ const {observe} = (() => {
77
80
  }
78
81
  CURRENTOBSERVER = null;
79
82
  }
80
-
81
83
  observer.cancel = () => observer.cancelled = true;
82
84
  observer();
83
85
  return observer;
@@ -89,7 +91,7 @@ const {observe} = (() => {
89
91
  if (toType === "number") return parseFloat(value + "");
90
92
  if (toType === "boolean") {
91
93
  if (["on", "checked", "selected"].includes(value)) return true;
92
- if(value==null || value==="") return false;
94
+ if (value == null || value === "") return false;
93
95
  try {
94
96
  const parsed = JSON.parse(value + "");
95
97
  if (typeof (parsed) === "boolean") return parsed;
@@ -112,10 +114,10 @@ const {observe} = (() => {
112
114
  if (instance instanceof Array) {
113
115
  let parsed = tryParse(value.startsWith("[") ? value : `[${value}]`);
114
116
  if (!Array.isArray(parsed)) {
115
- if(value.includes(",")) parsed = value.split(",");
117
+ if (value.includes(",")) parsed = value.split(",");
116
118
  else {
117
119
  parsed = tryParse(`["${value}"]`);
118
- if(!Array.isArray(parsed) || parsed[0]!==value && parsed.length!==1) parsed = null;
120
+ if (!Array.isArray(parsed) || parsed[0] !== value && parsed.length !== 1) parsed = null;
119
121
  }
120
122
  }
121
123
  if (!Array.isArray(parsed)) {
@@ -144,14 +146,16 @@ const {observe} = (() => {
144
146
  }
145
147
  throw new TypeError(`Unable to coerce ${value} to ${toType}`)
146
148
  }
147
- const Reactor = (value) => {
148
- if (value && typeof (value) === "object") {
149
- if (value.__isReactor__) return value;
149
+ const Reactor = (data) => {
150
+ if (data && typeof (data) === "object") {
151
+ if (data.__isReactor__) return data;
150
152
  const childReactors = [],
151
153
  dependents = {},
152
- proxy = new Proxy(value, {
154
+ proxy = new Proxy(data, {
153
155
  get(target, property) {
154
156
  if (property === "__isReactor__") return true;
157
+ if(property=== "__dependents__") return dependents;
158
+ if(property=== "__reactorProxyTarget__") return data;
155
159
  if (target instanceof Array) {
156
160
  if (property === "toJSON") return function toJSON() {
157
161
  return [...target];
@@ -166,9 +170,10 @@ const {observe} = (() => {
166
170
  const observers = dependents[property] ||= new Set();
167
171
  observers.add(CURRENTOBSERVER)
168
172
  }
173
+ if(value===undefined) return;
169
174
  if (childReactors.includes(value) || (value && type !== "object") || typeof (property) === "symbol") {
170
- // Dated must be bound to work with proxies
171
- if (type === "function" && [Date].includes(value)) value = value.bind(target)
175
+ // Dates and Promises must be bound to work with proxies
176
+ if (type === "function" && ([Date].includes(value) || property==="then")) value = value.bind(target)
172
177
  return value;
173
178
  }
174
179
  if (value && type === "object") {
@@ -178,8 +183,14 @@ const {observe} = (() => {
178
183
  target[property] = value;
179
184
  return value;
180
185
  },
181
- set(target, property, value) {
186
+ async set(target, property, value) {
187
+ if(target instanceof Promise) {
188
+ console.warn(`Setting ${property} = ${value} on a Promise in Reactor`);
189
+ }
182
190
  const type = typeof (value);
191
+ if(value && type==="object" && value instanceof Promise) {
192
+ value = await value;
193
+ }
183
194
  if (target[property] !== value) {
184
195
  if (value && type === "object") {
185
196
  value = Reactor(value);
@@ -197,7 +208,7 @@ const {observe} = (() => {
197
208
  });
198
209
  return proxy;
199
210
  }
200
- return value;
211
+ return data;
201
212
  }
202
213
 
203
214
  class VariableEvent {
@@ -214,6 +225,7 @@ const {observe} = (() => {
214
225
  return value;
215
226
  },
216
227
  set(target, property, newValue) {
228
+ //if(newValue && typeof(newValue)==="object" && newValue instanceof Promise) newValue = await newValue;
217
229
  const event = new VariableEvent({variableName: property, value: newValue});
218
230
  if (target[property] === undefined) {
219
231
  target[property] = {type: "any", value: newValue}; // should we allow this, do first to prevent loops
@@ -221,7 +233,7 @@ const {observe} = (() => {
221
233
  if (event.defaultPrevented) delete target[property].value;
222
234
  return true;
223
235
  }
224
- const {type, value, shared, exported, constant, reactive} = target[property];
236
+ const {type, value, shared, exported, constant, reactive, remote} = target[property];
225
237
  if (constant) throw new TypeError(`${property}:${type} is a constant`);
226
238
  const newtype = typeof (newValue),
227
239
  typetype = typeof (type);
@@ -230,7 +242,11 @@ const {observe} = (() => {
230
242
  event.oldValue = value;
231
243
  target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
232
244
  target.postEvent.value("change", event);
233
- if (event.defaultPrevented) target[property].value = value;
245
+ if (event.defaultPrevented) {
246
+ target[property].value = value;
247
+ } else if(remote) {
248
+ handleRemote({variable:target[property],remote,reactive},true);
249
+ }
234
250
  }
235
251
  return true;
236
252
  }
@@ -291,37 +307,40 @@ const {observe} = (() => {
291
307
  nodes.push(root, ...getNodes(root.shadowRoot))
292
308
  } else {
293
309
  for (const node of root.childNodes) {
294
- if(node.tagName==="SCRIPT") continue;
310
+ if (node.tagName === "SCRIPT") continue;
295
311
  if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
296
312
  node.template ||= node.nodeValue;
297
313
  nodes.push(node);
298
314
  } else if (node.nodeType === Node.ELEMENT_NODE) {
299
- let skip;
300
- if (node.getAttribute("type") === "radio") nodes.push(node);
301
- [...node.attributes].forEach((attr) => {
302
- if (attr.value.includes("${")) {
303
- attr.template ||= attr.value;
304
- nodes.push(node);
305
- } else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
306
- skip = attr.name.includes("l-for:");
307
- nodes.push(node)
308
- }
309
- })
315
+ let skip, pushed;
316
+ [...node.attributes].forEach((attr) => {
317
+ if (attr.value.includes("${")) {
318
+ attr.template ||= attr.value;
319
+ pushed = true;
320
+ nodes.push(node);
321
+ } else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
322
+ skip = attr.name.includes("l-for:");
323
+ pushed = true;
324
+ nodes.push(node)
325
+ }
326
+ })
327
+ if (!pushed && node.getAttribute("type") === "radio") nodes.push(node);
310
328
  if (!skip && !node.shadowRoot) nodes.push(...getNodes(node));
311
329
  }
312
330
  }
313
331
  }
314
332
  return nodes;
315
333
  }
316
- const resolveNodeOrText = (node, component,safe) => {
317
- const type = typeof(node),
334
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
335
+ const resolveNodeOrText = (node, component, safe) => {
336
+ const type = typeof (node),
318
337
  template = type === "string" ? node.trim() : node.template;
319
338
  if (template) {
320
339
  try {
321
340
  let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
322
- value = node.nodeType===Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
323
- if(type==="string") return value;
324
- node.nodeValue = value=="null" || value=="undefined" ? "" : value;
341
+ value = node.nodeType === Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
342
+ if (type === "string") return value;
343
+ node.nodeValue = value == "null" || value == "undefined" ? "" : value;
325
344
  } catch (e) {
326
345
  console.warn(e);
327
346
  if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
@@ -329,7 +348,7 @@ const {observe} = (() => {
329
348
  }
330
349
  return node?.nodeValue;
331
350
  }
332
- const render = (hasTemplate, render) => {
351
+ const render = (hasTemplate, render) => {
333
352
  let observer;
334
353
  if (hasTemplate) {
335
354
  if (observer) observer.cancel();
@@ -353,8 +372,8 @@ const {observe} = (() => {
353
372
  })
354
373
  }
355
374
  const bound = new WeakSet();
356
- const bindInput = (input, variableName, component,value) => {
357
- if(bound.has(input)) return;
375
+ const bindInput = (input, variableName, component, value) => {
376
+ if (bound.has(input)) return;
358
377
  bound.add(input);
359
378
  const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
360
379
  type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype),
@@ -366,13 +385,13 @@ const {observe} = (() => {
366
385
  else throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${variable.type}`)
367
386
  }
368
387
  component.variables({[variableName]: type});
369
- if(inputtype!=="radio") component.setValue(variableName,value);
388
+ if(inputtype!=="radio") component.setValue(variableName, value);
370
389
  let eventname = "change";
371
390
  if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
372
391
  eventname = "input";
373
392
  }
374
393
  const listener = (event) => {
375
- if(event) event.stopImmediatePropagation();
394
+ if (event) event.stopImmediatePropagation();
376
395
  let value = input.value;
377
396
  if (inputtype === "checkbox") {
378
397
  value = input.checked
@@ -380,7 +399,7 @@ const {observe} = (() => {
380
399
  if (input.hasAttribute("multiple")) {
381
400
  const varvalue = component.varsProxy[variableName];
382
401
  value = [...input.querySelectorAll("option")]
383
- .filter((option) => option.selected || resolveNodeOrText(option.attributes.value||option.innerText,component)===value)
402
+ .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value) //todo make sync comopat
384
403
  .map((option) => option.getAttribute("value") || option.innerText);
385
404
  }
386
405
  }
@@ -396,7 +415,7 @@ const {observe} = (() => {
396
415
  }
397
416
  }
398
417
  let reserved = {
399
- any: {value: "any",constant: true},
418
+ any: {value: "any", constant: true},
400
419
  boolean: {value: "boolean", constant: true},
401
420
  string: {value: "string", constant: true},
402
421
  number: {value: "number", constant: true},
@@ -405,7 +424,8 @@ const {observe} = (() => {
405
424
  reactive: {value: true, constant: true},
406
425
  shared: {value: true, constant: true},
407
426
  exported: {value: true, constant: true},
408
- imported: {value: true, constant: true}
427
+ imported: {value: true, constant: true},
428
+ remote: {}
409
429
  };
410
430
  const createClass = (domElementNode, {observer, framed}) => {
411
431
  const instances = new Set(),
@@ -427,6 +447,17 @@ const {observe} = (() => {
427
447
  eventlisteners = {};
428
448
  this.vars = {
429
449
  ...reserved,
450
+ changeListener: {
451
+ value: ({variableName, value}) => {
452
+ if (currentComponent.vars.changeListener.value.targets.has(variableName)) {
453
+ value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
454
+ if (value == null) removeComponentAttribute(this, variableName);
455
+ else setComponentAttribute(this, variableName, value);
456
+ }
457
+ },
458
+ type: "function",
459
+ constant: true
460
+ },
430
461
  addEventListener: {
431
462
  value: (eventName, listener) => {
432
463
  const listeners = eventlisteners[eventName] ||= new Set();
@@ -451,6 +482,8 @@ const {observe} = (() => {
451
482
  };
452
483
  this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
453
484
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
485
+ this.vars.changeListener.value.targets = new Set();
486
+ this.varsProxy.addEventListener("change", this.varsProxy.changeListener);
454
487
  if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported: true});
455
488
  ["getElementById", "querySelector", "querySelectorAll"]
456
489
  .forEach((fname) => {
@@ -492,7 +525,7 @@ const {observe} = (() => {
492
525
  }
493
526
  currentScript.classList.remove("lightview");
494
527
  const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
495
- currentScript.innerHTML = `Function('if(window["${scriptid}"]?.ctx) { with(window["${scriptid}"].ctx) { ${text}; } window["${scriptid}"](); }')(); `;
528
+ currentScript.innerHTML = `Object.getPrototypeOf(async function(){}).constructor('if(window["${scriptid}"]?.ctx) { with(window["${scriptid}"].ctx) { ${text}; } window["${scriptid}"](); }')(); `;
496
529
  let resolver;
497
530
  promises.push(new Promise((resolve) => resolver = resolve));
498
531
  window[scriptid] = () => {
@@ -507,19 +540,19 @@ const {observe} = (() => {
507
540
  const nodes = getNodes(ctx);
508
541
  nodes.forEach((node) => {
509
542
  if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
510
- render(!!node.template, () => resolveNodeOrText(node, this))
543
+ render(!!node.template, () => resolveNodeOrText(node, this))
511
544
  } else if (node.nodeType === Node.ELEMENT_NODE) {
512
545
  // resolve the value before all else;
513
546
  const attr = node.attributes.value;
514
547
  if (attr && attr.template) {
515
- render(!!attr.template, () => {
548
+ render(!!attr.template, () => {
516
549
  const value = resolveNodeOrText(attr, this),
517
550
  eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
518
- if(node.attributes.value) {
551
+ if (node.attributes.value) {
519
552
  const template = attr.template;
520
- if(/\$\{[a-zA-z_]+\}/g.test(template)) {
521
- const name = template.substring(2,template.length-1);
522
- bindInput(node,name,this,value);
553
+ if (/\$\{[a-zA-z_]+\}/g.test(template)) {
554
+ const name = template.substring(2, template.length - 1);
555
+ bindInput(node, name, this, value);
523
556
  }
524
557
  }
525
558
  if (eltype === "checkbox") {
@@ -536,13 +569,13 @@ const {observe} = (() => {
536
569
  if (node.tagName === "SELECT") {
537
570
  let values = [value];
538
571
  if (node.hasAttribute("multiple")) values = coerce(value, Array);
539
- [...node.querySelectorAll("option")].forEach((option) => {
572
+ [...node.querySelectorAll("option")].forEach(async (option) => {
540
573
  if (option.hasAttribute("value")) {
541
574
  if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
542
575
  option.setAttribute("selected", "");
543
576
  option.selected = true;
544
577
  }
545
- } else if (values.includes(resolveNodeOrText(option.innerText,ctx))) {
578
+ } else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
546
579
  option.setAttribute("selected", "");
547
580
  option.selected = true;
548
581
  }
@@ -550,18 +583,18 @@ const {observe} = (() => {
550
583
  }
551
584
  });
552
585
  }
553
- [...node.attributes].forEach((attr) => {
586
+ [...node.attributes].forEach(async (attr) => {
554
587
  if (attr.name === "value" && attr.template) return;
555
588
  const {name, value} = attr;
556
589
  if (name === "type") {
557
- if (value === "radio") {
590
+ if (value === "radio" && node.attributes.name) {
558
591
  const name = resolveNodeOrText(node.attributes.name, ctx);
559
592
  for (const vname of this.getVariableNames()) {
560
593
  if (vname === name) {
561
- render(true, () => {
594
+ render(true, () => {
562
595
  const name = resolveNodeOrText(node.attributes.name, ctx),
563
- varvalue = Function("context", "with(context) { return `${" + name + "}` }")(ctx.varsProxy);
564
- if (varvalue == resolveNodeOrText(node.attributes.value, ctx)) {
596
+ varvalue = Function("context", "with(context) { return `${" + name + "}` }")(ctx.varsProxy);
597
+ if (node.attributes.value && varvalue == resolveNodeOrText(node.attributes.value, ctx)) {
565
598
  node.setAttribute("checked", "");
566
599
  node.checked = true;
567
600
  } else {
@@ -578,41 +611,42 @@ const {observe} = (() => {
578
611
 
579
612
  const [type, ...params] = name.split(":");
580
613
  if (type === "") { // name is :something
581
- render(!!attr.template, () => {
582
- const value = attr.value,
583
- elvalue = resolveNodeOrText(node.attributes.value, ctx),
584
- eltype = resolveNodeOrText(node.attributes.type, ctx),
585
- elname = resolveNodeOrText(node.attributes.name, ctx);
614
+ render(!!attr.template, () => {
615
+ const value = attr.value;
586
616
  if (params[0]) {
587
617
  if (value === "true") node.setAttribute(params[0], "")
588
618
  else node.removeAttribute(params[0]);
589
- } else if (eltype === "checkbox" || node.tagName === "OPTION") {
590
- if (value === "true") node.setAttribute("checked", "")
591
- else node.removeAttribute("checked");
619
+ } else {
620
+ const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
621
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
622
+ if (eltype === "checkbox" || node.tagName === "OPTION") {
623
+ if (elvalue === "true") node.setAttribute("checked", "")
624
+ else node.removeAttribute("checked");
625
+ }
592
626
  }
593
627
  })
594
628
  } else if (type === "l-on") {
595
629
  let listener;
596
- render(!!attr.template, () => {
630
+ render(!!attr.template, () => {
597
631
  const value = resolveNodeOrText(attr, this);
598
632
  if (listener) node.removeEventListener(params[0], listener);
599
633
  listener = this[value] || window[value] || Function(value);
600
634
  addListener(node, params[0], listener);
601
635
  })
602
636
  } else if (type === "l-if") {
603
- render(!!attr.template, () => {
637
+ render(!!attr.template, () => {
604
638
  const value = resolveNodeOrText(attr, this);
605
639
  node.style.setProperty("display", value === "true" ? "revert" : "none");
606
640
  })
607
641
  } else if (type === "l-for") {
608
642
  node.template ||= node.innerHTML;
609
- render(!!attr.template, () => {
643
+ render(!!attr.template, () => {
610
644
  const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
611
645
  value = resolveNodeOrText(attr, this),
612
646
  coerced = coerce(value, what === "each" ? Array : "object"),
613
647
  target = what === "each" ? coerced : Object[what](coerced),
614
- html = target.reduce((html, item, i, target) => {
615
- return html += Function("vars","context", "with(vars) { with(context) { return `" + node.template + "` }}")(
648
+ html = target.reduce( (html, item, i, target) => {
649
+ return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
616
650
  ctx.varsProxy,
617
651
  {
618
652
  [vname]: item,
@@ -634,7 +668,7 @@ const {observe} = (() => {
634
668
  }
635
669
  })
636
670
  } else if (attr.template) {
637
- render(!!attr.template, () => resolveNodeOrText(attr, this));
671
+ render(!!attr.template, () => resolveNodeOrText(attr, this));
638
672
  }
639
673
  })
640
674
  }
@@ -710,7 +744,7 @@ const {observe} = (() => {
710
744
  return this.vars[variableName]?.value;
711
745
  }
712
746
 
713
- variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
747
+ variables(variables, {observed, reactive, shared, exported, imported, remote} = {}) { // options = {observed,reactive,shared,exported,imported}
714
748
  const addEventListener = this.varsProxy.addEventListener;
715
749
  if (variables !== undefined) {
716
750
  Object.entries(variables)
@@ -737,11 +771,11 @@ const {observe} = (() => {
737
771
  variable.exported = true;
738
772
  // in case the export goes up to an iframe
739
773
  if (variable.value != null) setComponentAttribute(this, key, variable.value);
740
- addEventListener("change", ({variableName, value}) => {
741
- value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
742
- if (value == null) removeComponentAttribute(this, variableName);
743
- else setComponentAttribute(this, variableName, value);
744
- })
774
+ this.vars.changeListener.value.targets.add(key);
775
+ }
776
+ if (remote) {
777
+ variable.remote = remote;
778
+ handleRemote({variable, remote, reactive});
745
779
  }
746
780
  });
747
781
  }
@@ -771,7 +805,136 @@ const {observe} = (() => {
771
805
  }
772
806
  }
773
807
  }
774
- const createComponent = (name, node, {framed,observer} = {}) => {
808
+
809
+ const remoteProxy = ({json, variable,remote, reactive}) => {
810
+ const type = typeof (remote);
811
+ return new Proxy(json, {
812
+ get(target,property) {
813
+ if(property==="__remoteProxytarget__") return json;
814
+ return target[property];
815
+ },
816
+ async set(target, property, value) {
817
+ if(value && typeof(value)==="object" && value instanceof Promise) value = await value;
818
+ const oldValue = target[property];
819
+ if (oldValue !== value) {
820
+ let remotevalue;
821
+ if (type === "string") {
822
+ const href = new URL(remote,window.location.href).href;
823
+ remotevalue = patch({target,property,value,oldValue},href);
824
+ } else if(remote && type==="object") {
825
+ let href;
826
+ if(remote.path) href = new URL(remote.path,window.location.href).href;
827
+ if(!remote.patch) {
828
+ if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
829
+ remote.patch = patch;
830
+ }
831
+ remotevalue = remote.patch({target,property,value,oldValue},href);
832
+ }
833
+ if(remotevalue) {
834
+ await remotevalue.then((newjson) => {
835
+ if (newjson && typeof (newjson) === "object" && reactive) {
836
+ const target = variable.value?.__reactorProxyTarget__ ? json : variable.value;
837
+ Object.entries(newjson).forEach(([key,newValue]) => {
838
+ if(target[key]!==newValue) {
839
+ target[key] = newValue;
840
+ }
841
+ })
842
+ Object.keys(target).forEach((key) => {
843
+ if(!(key in newjson)) {
844
+ delete target[key];
845
+ }
846
+ });
847
+ if(variable.value?.__reactorProxyTarget__) {
848
+ const dependents = variable.value.__dependents__,
849
+ observers = dependents[property] || [];
850
+ [...observers].forEach((f) => {
851
+ if (f.cancelled) dependents[property].delete(f);
852
+ else f();
853
+ })
854
+ }
855
+ } else {
856
+ variable.value = json;
857
+ }
858
+ })
859
+ }
860
+ }
861
+ return true;
862
+ }
863
+ })
864
+ }
865
+
866
+ const patch = ({target,property,value,oldValue},href) => {
867
+ return fetch(href, {
868
+ method: "PATCH",
869
+ body: JSON.stringify({property,value,oldValue}),
870
+ headers: {
871
+ "Content-Type": "application/json"
872
+ }
873
+ }).then((response) => {
874
+ if (response.status < 400) return response.json();
875
+ })
876
+ }
877
+
878
+ const get = ({variable,remote,reactive},path) => {
879
+ return fetch(path)
880
+ .then((response) => {
881
+ if (response.status < 400) return response.json();
882
+ })
883
+ }
884
+
885
+ const put = ({variable,remote,reactive},href) => {
886
+ return fetch(href, {
887
+ method: "PUT",
888
+ body: JSON.stringify(variable.value),
889
+ headers: {
890
+ "Content-Type": "application/json"
891
+ }
892
+ }).then((response) => {
893
+ if (response.status === 200) {
894
+ return response.json();
895
+ }
896
+ })
897
+ }
898
+
899
+ const handleRemote = async ({variable, remote, reactive},doput) => {
900
+ const type = typeof (remote);
901
+ let value;
902
+ if (type === "string") {
903
+ const href = new URL(remote,window.location.href).href;
904
+ value = (doput
905
+ ? put({variable, remote, reactive},href)
906
+ : get({variable, remote, reactive},href));
907
+ if(variable.value===undefined) variable.value = value;
908
+ } else if (remote && type === "object") {
909
+ let href;
910
+ if(remote.path) href = new URL(remote.path,window.location.href).href;
911
+ if(!remote.get || !remote.put) {
912
+ if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
913
+ if(!remote.get) remote.get = get;
914
+ if(!remote.put) remote.put = put;
915
+ }
916
+ value = (doput
917
+ ? remote.put({variable, remote, reactive},href)
918
+ : remote.get({variable, remote, reactive},href));
919
+ if(remote.ttl && !doput && !remote.intervalId) {
920
+ remote.intervalId = setInterval(async () => {
921
+ await handleRemote({variable, remote, reactive});
922
+ })
923
+ }
924
+ if(variable.value===undefined) variable.value = value;
925
+ }
926
+ if(value) {
927
+ variable.value = await value.then((json) => {
928
+ if (json && typeof (json) === "object" && reactive) {
929
+ return remoteProxy({json, variable,remote, reactive})
930
+ } else {
931
+ return json;
932
+ }
933
+ })
934
+ }
935
+ }
936
+
937
+ const createComponent = (name, node, {framed, observer} = {}) => {
775
938
  let ctor = customElements.get(name);
776
939
  if (ctor) {
777
940
  if (framed && !ctor.lightviewFramed) {
@@ -783,7 +946,7 @@ const {observe} = (() => {
783
946
  }
784
947
  ctor = createClass(node, {observer, framed});
785
948
  customElements.define(name, ctor);
786
- Lightview.customElements.set(name,ctor);
949
+ Lightview.customElements.set(name, ctor);
787
950
  return ctor;
788
951
  }
789
952
  Lightview.customElements = new Map();
@@ -800,12 +963,12 @@ const {observe} = (() => {
800
963
  dom = parser.parseFromString(html, "text/html"),
801
964
  unhide = !!dom.head.querySelector('meta[name="l-unhide"]'),
802
965
  links = dom.head.querySelectorAll('link[href$=".html"][rel=module]');
803
- for(const childlink of links) {
966
+ for (const childlink of links) {
804
967
  const href = childlink.getAttribute("href"),
805
- childurl = new URL(href,url.href);
806
- childlink.setAttribute("href",childurl.href);
807
- if(link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin",link.getAttribute("crossorigin"))
808
- await importLink(childlink,observer);
968
+ childurl = new URL(href, url.href);
969
+ childlink.setAttribute("href", childurl.href);
970
+ if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"))
971
+ await importLink(childlink, observer);
809
972
  }
810
973
  if (unhide) dom.body.removeAttribute("hidden");
811
974
  createComponent(as, dom.body, {observer});
@@ -815,7 +978,7 @@ const {observe} = (() => {
815
978
  const importLinks = async () => {
816
979
  const observer = createObserver(document.body);
817
980
  for (const link of [...document.querySelectorAll(`link[href$=".html"][rel=module]`)]) {
818
- await importLink(link,observer);
981
+ await importLink(link, observer);
819
982
  }
820
983
  }
821
984
 
@@ -985,7 +1148,7 @@ const {observe} = (() => {
985
1148
  addListener(document, "DOMContentLoaded", (event) => loader(callback));
986
1149
  }
987
1150
  Lightview.whenFramed = whenFramed;
988
- //Object.defineProperty(Lightview, "whenFramed", {configurable: true, writable: true, value: whenFramed});
1151
+
989
1152
  if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
990
1153
  // loads for unframed content
991
1154
  // CodePen mucks with window.parent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "1.4.8b",
3
+ "version": "1.4.10b",
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
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <title>Basic</title>
6
6
  <template id="x-test" name="joe" open="true" count=1 children='["mary"]' l-on:click="bump">
7
+
7
8
  <span id="name">${name}</span>
8
9
  <span id="open">${open}</span>
9
10
  <span id="count">${count}</span>
@@ -18,8 +19,9 @@
18
19
  <input id="itel" type="tel" value="${itel}">
19
20
  <input id="iemail" type="email" value="${iemail}">
20
21
  <input id="iurl" type="url" value="${iurl}">
21
- <input id="isearch" type="search" value="${isearch}">
22
22
  <input id="iradio" type="radio" value="${iradio}">
23
+ <input id="isearch" type="search" value="${isearch}">
24
+
23
25
  <input id="icolor" type="color" value="${icolor}">
24
26
  <input id="ipassword" type="password" value="${ipassword}">
25
27
 
@@ -31,7 +33,7 @@
31
33
  <input id="icheckbox" type="checkbox" value="${icheckbox}">
32
34
 
33
35
  <script type="lightview/module">
34
- debugger;
36
+ //debugger;
35
37
  self.variables({name:string,open:boolean,count:number,children:Array},{imported,reactive});
36
38
  self.variables({color:string,checked:boolean,age:number,hamburger:Array},{exported,reactive});
37
39
  self.variables({counter:number},{reactive});
@@ -194,7 +194,7 @@ describe('Lightview', () => {
194
194
  expect(value).toBe("test");
195
195
  expect(variable.name).toBe("i${type}");
196
196
  expect(variable.type).toBe("string");
197
- expect(variable.value).toBe(value);
197
+ if("${type}"!=="radio") expect(variable.value).toBe(value);
198
198
  }`)();
199
199
  test(`${type} input - i${type} should be "test"`,f);
200
200
  });