lightview 1.0.0-b → 1.3.0-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,6 +1,8 @@
1
- # lightview v1.0.0.b (BETA)
1
+ # lightview v1.3.0b (BETA)
2
2
 
3
3
  Small, simple, powerful web UI creation ...
4
4
 
5
+ Great ideas from Svelte, React, Vue and Riot combined into one small tool: < 6K (minified/gzipped).
6
+
5
7
  See the docs and examples at [https://lightview.dev](https://lightview.dev).
6
8
 
package/counter.html ADDED
@@ -0,0 +1,30 @@
1
+ <head>
2
+ <title>Counter</title>
3
+ <script src="../lightview.js?as=x-body"></script>
4
+ </head>
5
+
6
+ <body>
7
+ <p>
8
+ <button l-on:click="bump">Click count:${count}</button>
9
+ </p>
10
+
11
+ <script type="lightview/module">
12
+ self.variables({
13
+ count: number
14
+ }, {
15
+ reactive
16
+ });
17
+ debugger;
18
+ count = 0;
19
+ self.bump = () => count++;
20
+ </script>
21
+
22
+ <style>
23
+ button {
24
+ margin: 20px;
25
+ background: gray
26
+ }
27
+ </style>
28
+ </body>
29
+
30
+ </html>
@@ -0,0 +1,80 @@
1
+ <!DOCTYPE html>
2
+
3
+ <head>
4
+ <title>Directives</title>
5
+ <script src="./lightview.js?as=x-body"></script>
6
+ </head>
7
+
8
+ <body>
9
+
10
+
11
+ <p>
12
+ Show: <input type="checkbox" :="${on}" l-bind="on">
13
+ <div l-if="${on}">
14
+ Show is true
15
+ </div>
16
+ </p>
17
+ <p>
18
+
19
+ <input id="red" type="radio" name="myradio" value="red" :="${color}" l-bind="color"> Red
20
+ <input id="yellow" type="radio" name="myradio" value="yellow" :="${color}" l-bind="color"> Yellow
21
+ <input id="green" type="radio" name="myradio" value="green" :="${color}" l-bind="color"> Green
22
+ </p>
23
+
24
+ <p>
25
+ <select l-bind="color" value="${color}">
26
+ <option value="red">Red</option>
27
+ <option value="yellow">Yellow</option>
28
+ <option value="green">Green</option>
29
+ </select>
30
+ </p>
31
+
32
+
33
+ <p>
34
+ How would you like that burger?<br>
35
+ <select l-bind="options" value="${options}" multiple>
36
+ <option>lettuce</option>
37
+ <option>tomato</option>
38
+ <option>cheese</option>
39
+ </select>
40
+ </p>
41
+
42
+
43
+
44
+ <ul l-for:each="${children}">
45
+ <li>${index}:${element}</li>
46
+ </ul>
47
+ <ul l-for:values:value:index='{"1":"v1","2":"v2","3":"v3"}'>
48
+ <li>${value}:${index}</li>
49
+ </ul>
50
+ <ul l-for:keys:key='{"name":"joe","age":27}'>
51
+ <li>${key}</li>
52
+ </ul>
53
+ <ul l-for:entries:entry="${children}">
54
+ <li>${entry[0]}:${entry[1]}</li>
55
+ </ul>
56
+
57
+ Variable Values
58
+ <p id="variables"></p>
59
+
60
+ <script type="lightview/module">
61
+ self.variables({on:boolean,off:boolean,color:string,children:Array,options:Array},{reactive});
62
+
63
+ on = true;
64
+ color = "yellow";
65
+ children = ["John","Mary","Jane"];
66
+ options = ["tomato"];
67
+
68
+ addEventListener("change",()=> {
69
+ const el = self.getElementById("variables");
70
+ while(el.lastElementChild) el.lastElementChild.remove();
71
+ self.getVariableNames().forEach((name) => {
72
+ const line = document.createElement("div");
73
+ line.innerText = `${name} = ${JSON.stringify(self.getValue(name))}`;
74
+ el.appendChild(line);
75
+ });
76
+ });
77
+ </script>
78
+ </body>
79
+
80
+ </html>
package/lightview.js CHANGED
@@ -29,6 +29,10 @@ const Lightview = {};
29
29
  const {observe} = (() => {
30
30
  let CURRENTOBSERVER;
31
31
  const parser = new DOMParser();
32
+
33
+ const addListener = (node,eventName,callback) => {
34
+ node.addEventListener(eventName,callback); // just used to make code footprint smaller
35
+ }
32
36
  const anchorHandler = async (event) => {
33
37
  event.preventDefault();
34
38
  const target = event.target;
@@ -63,15 +67,16 @@ const {observe} = (() => {
63
67
  return observer;
64
68
  }
65
69
  const coerce = (value, toType) => {
70
+ if (value + "" === "null" || value + "" === "undefined") return value;
66
71
  const type = typeof (value);
67
72
  if (type === toType) return value;
68
73
  if (toType === "number") return parseFloat(value + "");
69
74
  if (toType === "boolean") {
70
- if(["on","checked","selected"].includes(value)) return true;
75
+ if (["on", "checked", "selected"].includes(value)) return true;
71
76
  try {
72
77
  const parsed = JSON.parse(value + "");
73
78
  if (typeof (parsed) === "boolean") return parsed;
74
- return [1,"on","checked","selected"].includes(parsed);
79
+ return [1, "on", "checked", "selected"].includes(parsed);
75
80
  } catch (e) {
76
81
  throw new TypeError(`Unable to convert ${value} into 'boolean'`);
77
82
  }
@@ -79,10 +84,10 @@ const {observe} = (() => {
79
84
  if (toType === "string") return value + "";
80
85
  const isfunction = typeof (toType) === "function";
81
86
  if ((toType === "object" || isfunction)) {
82
- if(type==="object") {
83
- if(value instanceof toType) return value;
87
+ if (type === "object") {
88
+ if (value instanceof toType) return value;
84
89
  }
85
- if(type === "string") {
90
+ if (type === "string") {
86
91
  value = value.trim();
87
92
  try {
88
93
  if (isfunction) {
@@ -121,9 +126,13 @@ const {observe} = (() => {
121
126
  proxy = new Proxy(value, {
122
127
  get(target, property) {
123
128
  if (property === "__isReactor__") return true;
124
- if (property === "toJSON" && target instanceof Array) {
125
- const toJSON = function() { return target.toJSON(); }
126
- return toJSON;
129
+ if (target instanceof Array) {
130
+ if (property === "toJSON") return function toJSON() {
131
+ return [...target];
132
+ }
133
+ if (property === "toString") return function toString() {
134
+ return JSON.stringify(target);
135
+ }
127
136
  }
128
137
  let value = target[property];
129
138
  const type = typeof (value);
@@ -183,19 +192,19 @@ const {observe} = (() => {
183
192
  if (target[property] === undefined) {
184
193
  target[property] = {type: "any", value: newValue}; // should we allow this, do first to prevent loops
185
194
  target.postEvent.value("change", event);
186
- if(event.defaultPrevented) delete target[property].value;
195
+ if (event.defaultPrevented) delete target[property].value;
187
196
  return true;
188
197
  }
189
- const {type, value, shared, exported, constant,reactive} = target[property];
198
+ const {type, value, shared, exported, constant, reactive} = target[property];
190
199
  if (constant) throw new TypeError(`${property}:${type} is a constant`);
191
200
  const newtype = typeof (newValue),
192
201
  typetype = typeof (type);
193
- if (type === "any" || newtype === type || (typetype === "function" && newValue && newtype === "object" && newValue instanceof type)) {
202
+ if (newValue == null || type === "any" || newtype === type || (typetype === "function" && newValue && newtype === "object" && newValue instanceof type)) {
194
203
  if (value !== newValue) {
195
204
  event.oldValue = value;
196
205
  target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
197
206
  target.postEvent.value("change", event);
198
- if(event.defaultPrevented) target[property].value = value;
207
+ if (event.defaultPrevented) target[property].value = value;
199
208
  }
200
209
  return true;
201
210
  }
@@ -218,7 +227,7 @@ const {observe} = (() => {
218
227
  if (target.observedAttributes && target.observedAttributes.includes(name)) {
219
228
  const value = target.getAttribute(name);
220
229
  if (value !== mutation.oldValue) {
221
- target.setVariable(name, value);
230
+ target.setValue(name, value);
222
231
  if (target.attributeChangedCallback) target.attributeChangedCallback(name, value, mutation.oldValue);
223
232
  }
224
233
  }
@@ -265,9 +274,7 @@ const {observe} = (() => {
265
274
  if (!nodes.includes(node)) nodes.push(node);
266
275
  }
267
276
  })
268
- if (!skip) {
269
- if (!node.shadowRoot) nodes.push(...getNodes(node));
270
- }
277
+ if (!skip && !node.shadowRoot) nodes.push(...getNodes(node));
271
278
  }
272
279
  }
273
280
  }
@@ -276,9 +283,10 @@ const {observe} = (() => {
276
283
  const resolveNode = (node, component) => {
277
284
  if (node.template) {
278
285
  try {
279
- node.nodeValue = Function("context", "with(context) { return `" + node.template + "` }")(component.varsProxy);
286
+ const value = Function("context", "with(context) { return `" + node.template + "` }")(component.varsProxy);
287
+ node.nodeValue = value === "null" || value === "undefined" ? "" : value;
280
288
  } catch (e) {
281
- if (!e.message.includes("defined")) throw e;
289
+ if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
282
290
  }
283
291
  }
284
292
  return node.nodeValue;
@@ -297,57 +305,79 @@ const {observe} = (() => {
297
305
  if (["text", "tel", "email", "url", "search", "radio"].includes(inputType)) return "string";
298
306
  if (["number", "range"].includes(inputType)) return "number";
299
307
  if (["datetime"].includes(inputType)) return Date;
300
- if(["checkbox"].includes(inputType)) return "boolean";
308
+ if (["checkbox"].includes(inputType)) return "boolean";
301
309
  return "any";
302
310
  }
303
- const _importAnchors = (node,component) => {
311
+ const _importAnchors = (node, component) => {
304
312
  [...node.querySelectorAll('a[href][target^="#"]')].forEach((node) => {
305
313
  node.removeEventListener("click", anchorHandler);
306
- node.addEventListener("click", anchorHandler);
314
+ addListener(node,"click", anchorHandler);
307
315
  })
308
316
  }
309
317
  const _bindForms = (node, component) => {
310
- [...node.querySelectorAll("input")].forEach((input) => {
311
- bindInput(input,component);
312
- })
318
+ [...node.querySelectorAll("input")].forEach((input) => bindInput(input, component))
313
319
  }
314
- const bindInput = (input,component) => {
315
- const name = input.getAttribute("name"),
316
- vname = input.getAttribute("l-bind")||name;
320
+ const bindInput = (input, component) => {
321
+ let name = input.getAttribute("name"),
322
+ vname = input.getAttribute("l-bind") || name;
323
+ name ||= vname;
317
324
  if (name) {
318
- if(!input.hasAttribute("l-bind")) input.setAttribute("l-bind",vname)
319
- const type = inputTypeToType(input.getAttribute("type")),
325
+ if (!input.hasAttribute("l-bind")) input.setAttribute("l-bind", vname)
326
+ const inputtype = input.tagName === "SELECT" ? "text" : input.getAttribute("type"),
327
+ type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype),
320
328
  deflt = input.getAttribute("default"),
321
329
  value = input.getAttribute("value");
322
330
  let variable = component.vars[vname] || {type};
323
- if(type!==variable.type) {
324
- if(variable.type==="any" || variable.type==="unknown") variable.type = type;
331
+ if (type !== variable.type) {
332
+ if (variable.type === "any" || variable.type === "unknown") variable.type = type;
325
333
  else throw new TypeError(`Attempt to bind <input name="${name}" type="${type}"> to variable ${vname}:${variable.type}`)
326
334
  }
327
- component.variables({[vname]:type});
328
- variable = component.vars[vname]
329
- if (value || deflt) {
335
+ component.variables({[vname]: type});
336
+ variable = component.vars[vname];
337
+ //if (value || deflt) {
338
+ if (inputtype !== "radio") {
330
339
  if (value && !value.includes("${")) {
331
340
  variable.value = coerce(value, type);
332
- input.setAttribute("value", `\${${name}}`);
333
- }
334
- if (deflt && !deflt.includes("${")) {
341
+ //input.setAttribute("value", `\${${name}}`);
342
+ } else if (deflt && !deflt.includes("${")) {
335
343
  variable.value = coerce(deflt, type);
336
- input.setAttribute("default", `\${${name}}`);
344
+ //input.setAttribute("default", `\${${name}}`);
337
345
  }
338
346
  }
339
- input.addEventListener("change",(event) => {
347
+ //}
348
+ addListener(input,"change", (event) => {
340
349
  event.stopImmediatePropagation();
341
- component.varsProxy[vname] = coerce(event.target.value,type);
350
+ const target = event.target;
351
+ let value = target.value;
352
+ if (inputtype === "checkbox") {
353
+ value = input.checked
354
+ } else if (target.tagName === "SELECT") {
355
+ if (target.hasAttribute("multiple")) {
356
+ value = [...target.querySelectorAll("option")]
357
+ .filter((option) => option.selected || option.getAttribute("value") == value || option.innerText == value)
358
+ .map((option) => option.getAttribute("value") || option.innerText);
359
+ }
360
+ }
361
+ component.varsProxy[vname] = coerce(value, type);
342
362
  })
343
363
  }
344
364
  }
345
- const createClass = (domElementNode, {observer,bindForms,importAnchors}) => {
365
+ let reserved = {
366
+ boolean: {value: "boolean", constant: true},
367
+ string: {value: "string", constant: true},
368
+ number: {value: "number", constant: true},
369
+ observed: {value: true, constant: true},
370
+ reactive: {value: true, constant: true},
371
+ shared: {value: true, constant: true},
372
+ exported: {value: true, constant: true},
373
+ imported: {value: true, constant: true}
374
+ };
375
+ const createClass = (domElementNode, {observer, bindForms, importAnchors}) => {
346
376
  const instances = new Set(),
347
- dom = domElementNode.tagName==="TEMPLATE"
377
+ dom = domElementNode.tagName === "TEMPLATE"
348
378
  ? domElementNode.content.cloneNode(true)
349
379
  : domElementNode.cloneNode(true);
350
- if(domElementNode.tagName==="TEMPLATE") domElementNode = domElementNode.cloneNode(true);
380
+ if (domElementNode.tagName === "TEMPLATE") domElementNode = domElementNode.cloneNode(true);
351
381
  return class CustomElement extends HTMLElement {
352
382
  static get instances() {
353
383
  return instances;
@@ -361,6 +391,7 @@ const {observe} = (() => {
361
391
  shadow = this.attachShadow({mode: "open"}),
362
392
  eventlisteners = {};
363
393
  this.vars = {
394
+ ...reserved,
364
395
  addEventListener: {
365
396
  value: (eventName, listener) => {
366
397
  const listeners = eventlisteners[eventName] ||= new Set();
@@ -381,19 +412,11 @@ const {observe} = (() => {
381
412
  type: "function",
382
413
  constant: true
383
414
  },
384
- self: {value: currentComponent, type: CustomElement, constant: true},
385
- boolean: {value: "boolean", constant: true},
386
- string: {value: "string", constant: true},
387
- number: {value: "number", constant: true},
388
- observed: {value: true, constant: true},
389
- reactive: {value: true, constant: true},
390
- shared: {value: true, constant: true},
391
- exported: {value: true, constant: true},
392
- imported: {value: true, constant: true}
415
+ self: {value: currentComponent, type: CustomElement, constant: true}
393
416
  };
394
- this.defaultAttributes = domElementNode.tagName==="TEMPLATE" ? domElementNode.attributes : dom.attributes;
417
+ this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
395
418
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
396
- ["getElementById", "querySelector", "querySelectorAll"]
419
+ ["getElementById", "querySelector", "querySelectorAll"]
397
420
  .forEach((fname) => {
398
421
  Object.defineProperty(this, fname, {
399
422
  configurable: true,
@@ -401,15 +424,13 @@ const {observe} = (() => {
401
424
  value: (...args) => this.shadowRoot[fname](...args)
402
425
  })
403
426
  });
404
- [...dom.childNodes].forEach((child) => {
405
- shadow.appendChild(child.cloneNode(true));
406
- })
427
+ [...dom.childNodes].forEach((child) => shadow.appendChild(child.cloneNode(true)));
407
428
  if (bindForms) _bindForms(shadow, this);
408
- if(importAnchors) _importAnchors(shadow,this);
429
+ if (importAnchors) _importAnchors(shadow, this);
409
430
  }
410
431
 
411
432
  get siblings() {
412
- return [...CustomElement.instances].filter((sibling) => sibling!=this);
433
+ return [...CustomElement.instances].filter((sibling) => sibling != this);
413
434
  }
414
435
 
415
436
  adoptedCallback() {
@@ -450,114 +471,137 @@ const {observe} = (() => {
450
471
  ctx.appendChild(currentScript);
451
472
  }
452
473
  Promise.all(promises).then(() => {
453
- const inputs = [...ctx.shadowRoot.querySelectorAll("input[l-bind]")];
454
- inputs.forEach((input) => {
455
- bindInput(input,ctx);
456
- })
457
- const nodes = getNodes(ctx);
458
- nodes.forEach((node) => {
459
- if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
460
- render(!!node.template, () => resolveNode(node, this))
461
- } else if (node.nodeType === Node.ELEMENT_NODE) {
462
- [...node.attributes].forEach((attr) => {
463
- const {name, value} = attr,
464
- [type, ...params] = name.split(":");
465
- if (["checked","selected"].includes(type)) {
466
- render(!!attr.template, () => {
467
- const value = resolveNode(attr, this);
468
- if (value === "true") node.setAttribute(name, "");
469
- else node.removeAttribute(name);
470
- })
471
- } else if(type==="") {
472
- render(!!attr.template, () => {
473
- const value = resolveNode(attr, this);
474
- if(params[0]) {
475
- if(value==="true") node.setAttribute(params[0], "");
476
- else node.removeAttribute(params[0]);
474
+ const inputs = [...ctx.shadowRoot.querySelectorAll("input[l-bind]"), ...ctx.shadowRoot.querySelectorAll("select[l-bind]")];
475
+ inputs.forEach((input) => {
476
+ bindInput(input, ctx);
477
+ })
478
+ const nodes = getNodes(ctx);
479
+ nodes.forEach((node) => {
480
+ if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
481
+ render(!!node.template, () => resolveNode(node, this))
482
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
483
+ [...node.attributes].forEach((attr) => {
484
+ const {name, value} = attr,
485
+ [type, ...params] = name.split(":");
486
+ if (type === "" || type=="checked" || node.tagName === "SELECT") { // name is :something
487
+ render(!!attr.template, () => {
488
+ const attrtype = node.getAttribute("type"),
489
+ value = resolveNode(attr, this),
490
+ elvalue = node.getAttribute("value"),
491
+ elname = node.getAttribute("name");
492
+ if (params[0]) {
493
+ if (value === "true") node.setAttribute(params[0], "")
494
+ else node.removeAttribute(params[0]);
495
+ } else if (attrtype === "checkbox" || node.tagName === "OPTION") {
496
+ if (value === "true") {
497
+ node.setAttribute("checked", "");
477
498
  } else {
478
- if(value!=="true") node.removeAttribute(name);
499
+ node.removeAttribute("checked");
479
500
  }
480
- })
481
- } else if (["checked","selected"].includes(name)) {
482
- render(!!attr.template, () => {
483
- const value = resolveNode(attr, this);
484
- if (value === "true") node.setAttribute(name, "");
485
- else node.removeAttribute(name);
486
- })
487
- } else if (type === "l-on") {
488
- let listener;
489
- render(!!attr.template, () => {
490
- const value = resolveNode(attr, this);
491
- if (listener) node.removeEventListener(params[0], listener);
492
- listener = this[value] || window[value] || Function(value);
493
- node.addEventListener(params[0], listener);
494
- })
495
- } else if (type === "l-if") {
496
- render(!!attr.template, () => {
497
- const value = resolveNode(attr, this);
498
- node.style.setProperty("display", value === "true" ? "revert" : "none");
499
- })
500
- } else if (type === "l-for") {
501
- node.template ||= node.innerHTML;
502
- render(!!attr.template, () => {
503
- const [what = "each", vname = "element", index = "index", array = "array", after = false] = params,
504
- value = resolveNode(attr, this),
505
- coerced = coerce(value, what === "each" ? Array : "object"),
506
- target = what === "each" ? coerced : Object[what](coerced),
507
- html = target.reduce((html, item, i, target) => {
508
- return html += Function("context", "with(context) { return `" + node.template + "` }")({
509
- [vname]: item,
510
- [index]: i,
511
- [array]: target
512
- })
513
- }, ""),
514
- parsed = parser.parseFromString(html, "text/html");
515
- if (!window.lightviewDebug) {
516
- if (after) {
517
- node.style.setProperty("display", "none")
518
- } else {
519
- while (node.lastElementChild) node.lastElementChild.remove();
520
- }
501
+ } else if (attrtype === "radio") {
502
+ if (elvalue === value) {
503
+ node.setAttribute("checked", "");
521
504
  }
522
- while (parsed.body.firstChild) {
523
- if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
524
- else node.appendChild(parsed.body.firstChild);
505
+ } else if (name === "value" && node.tagName === "SELECT") {
506
+ node.setAttribute("value", value);
507
+ const values = value[0] === "[" ? JSON.parse(value) : value.split(","); // handle multiselect
508
+ [...node.querySelectorAll("option")].forEach((option) => {
509
+ if (option.hasAttribute("value")) {
510
+ if (values.includes(option.getAttribute("value"))) {
511
+ option.setAttribute("selected", true);
512
+ }
513
+ } else if (values.includes(option.innerText)) {
514
+ option.setAttribute("selected", true);
515
+ }
516
+ })
517
+ }
518
+ })
519
+ } else if (type === "l-on") {
520
+ let listener;
521
+ render(!!attr.template, () => {
522
+ const value = resolveNode(attr, this);
523
+ if (listener) node.removeEventListener(params[0], listener);
524
+ listener = this[value] || window[value] || Function(value);
525
+ addListener(node,params[0], listener);
526
+ })
527
+ } else if (type === "l-if") {
528
+ render(!!attr.template, () => {
529
+ const value = resolveNode(attr, this);
530
+ node.style.setProperty("display", value === "true" ? "revert" : "none");
531
+ })
532
+ } else if (type === "l-for") {
533
+ node.template ||= node.innerHTML;
534
+ render(!!attr.template, () => {
535
+ const [what = "each", vname = "element", index = "index", array = "array", after = false] = params,
536
+ value = resolveNode(attr, this),
537
+ coerced = coerce(value, what === "each" ? Array : "object"),
538
+ target = what === "each" ? coerced : Object[what](coerced),
539
+ html = target.reduce((html, item, i, target) => {
540
+ return html += Function("context", "with(context) { return `" + node.template + "` }")({
541
+ [vname]: item,
542
+ [index]: i,
543
+ [array]: target
544
+ })
545
+ }, ""),
546
+ parsed = parser.parseFromString(html, "text/html");
547
+ if (!window.lightviewDebug) {
548
+ if (after) {
549
+ node.style.setProperty("display", "none")
550
+ } else {
551
+ while (node.lastElementChild) node.lastElementChild.remove();
525
552
  }
526
- })
527
- } else if (attr.template) {
528
- render(!!attr.template, () => resolveNode(attr, this));
529
- }
530
- })
531
- }
532
- })
533
- shadow.normalize();
534
- observer.observe(ctx, {attributeOldValue: true});
535
- if (ctx.hasOwnProperty("connectedCallback")) ctx.connectedCallback();
553
+ }
554
+ while (parsed.body.firstChild) {
555
+ if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
556
+ else node.appendChild(parsed.body.firstChild);
557
+ }
558
+ })
559
+ } else if (attr.template) {
560
+ render(!!attr.template, () => resolveNode(attr, this));
561
+ }
562
+ })
563
+ }
536
564
  })
537
- }
565
+ shadow.normalize();
566
+ observer.observe(ctx, {attributeOldValue: true});
567
+ if (ctx.hasOwnProperty("connectedCallback")) ctx.connectedCallback();
568
+ })
569
+ }
538
570
 
539
571
  adopted(value) {
540
- Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
572
+ this.adoptedCallback = value;
573
+ //Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
541
574
  }
542
575
 
543
576
  connected(value) {
544
- Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
577
+ this.connectedCallback = value;
578
+ //Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
545
579
  }
546
580
 
547
581
  attributeChanged(value) {
548
- Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
582
+ this.attributeChangedCallback = value;
583
+ //Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
549
584
  }
550
585
 
551
586
  disconnected(value) {
552
587
  Object.defineProperty(this, "disconnectedCallback", {
553
588
  configurable: true,
554
589
  writable: true,
555
- value:() => { value(); super.disconnectedCallback(value); }
590
+ value: () => {
591
+ value();
592
+ super.disconnectedCallback(value);
593
+ }
556
594
  });
557
595
  }
558
596
 
559
- setVariable(name, value, {shared,coerceTo = typeof (value)}={}) {
560
- if(!this.isConnected) {
597
+ getVariableNames() {
598
+ return Object.keys(this.vars).filter((name) => {
599
+ return !(name in reserved) && !["self","addEventListener","postEvent"].includes(name)
600
+ })
601
+ }
602
+
603
+ setValue(name, value, {shared, coerceTo = typeof (value)} = {}) {
604
+ if (!this.isConnected) {
561
605
  instances.delete(this);
562
606
  return false;
563
607
  }
@@ -566,29 +610,37 @@ const {observe} = (() => {
566
610
  value = coerce(value, type);
567
611
  if (this.varsProxy[name] !== value) {
568
612
  const variable = this.vars[name];
569
- if(variable.shared) {
570
- const event = new VariableEvent({variableName: name, value: value,oldValue:variable.value});
613
+ if (variable.shared) {
614
+ const event = new VariableEvent({
615
+ variableName: name,
616
+ value: value,
617
+ oldValue: variable.value
618
+ });
571
619
  variable.value = value;
572
- this.vars.postEvent.value("change",event);
573
- if(event.defaultPrevented) variable.value = value;
620
+ this.vars.postEvent.value("change", event);
621
+ if (event.defaultPrevented) variable.value = value;
574
622
  } else {
575
623
  this.varsProxy[name] = value;
576
624
  }
577
625
  }
578
626
  return true;
579
627
  }
580
- this.vars[name] = {type:coerceTo, value: coerce(value, coerceTo)};
628
+ this.vars[name] = {name, type: coerceTo, value: coerce(value, coerceTo)};
581
629
  return false;
582
630
  }
583
631
 
632
+ getValue(variableName) {
633
+ return this.vars[variableName]?.value;
634
+ }
635
+
584
636
  variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
585
637
  const addEventListener = this.varsProxy.addEventListener;
586
638
  if (variables !== undefined) {
587
639
  Object.entries(variables)
588
640
  .forEach(([key, type]) => {
589
- const variable = this.vars[key] ||= {type};
641
+ const variable = this.vars[key] ||= {name: key, type};
590
642
  if (observed || imported) {
591
- variable.value = coerce(this.getAttribute(key), variable.type);
643
+ variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
592
644
  variable.observed = observed;
593
645
  variable.imported = imported;
594
646
  }
@@ -598,45 +650,65 @@ const {observe} = (() => {
598
650
  }
599
651
  if (shared) {
600
652
  variable.shared = true;
601
- addEventListener("change",({variableName,value}) => {
602
- if(this.vars[variableName]?.shared) {
653
+ addEventListener("change", ({variableName, value}) => {
654
+ if (this.vars[variableName]?.shared) {
603
655
  this.siblings.forEach((instance) => {
604
- instance.setVariable(variableName, value);
656
+ instance.setValue(variableName, value);
605
657
  })
606
658
  }
607
659
  })
608
660
  }
609
661
  if (exported) {
610
662
  variable.exported = true;
611
- addEventListener("change",({variableName,value}) => {
612
- // Array.isArray will not work here, Proxies mess up JSON.stringify for Arrays
613
- //value = value && typeof (value) === "object" ? (value instanceof Array ? JSON.stringify([...value]) : JSON.stringify(value)) : value+"";
614
- value = typeof(value)==="string" ? value : JSON.stringify(value);
615
- this.setAttribute(variableName,value);
663
+ // in case the export goes up to an iframe
664
+ if (variable.value != null) setComponentAttribute(this, key, variable.value);
665
+ addEventListener("change", ({variableName, value}) => {
666
+ value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
667
+ if (value == null) removeComponentAttribute(this, variableName);
668
+ else setComponentAttribute(this, variableName, value);
616
669
  })
617
670
  }
618
671
  });
619
- addEventListener("change",({variableName,value}) => {
620
- [...this.shadowRoot.querySelectorAll(`input[l-bind=${variableName}]`)].forEach((input) => {
621
- if(input.getAttribute("type")==="checkbox") { // at el option selected
622
- if(!value) input.removeAttribute("checked");
623
- input.checked = value;
624
- } else {
625
- // Array.isArray will not work here, Proxies mess up JSON.stringify for Arrays
626
- //value = value && typeof (value) === "object" ? (value instanceof Array ? JSON.stringify([...value]) : JSON.stringify(value)) : value+"";
627
- value = typeof(value)==="string" ? value : JSON.stringify(value);
628
- const oldvalue = input.getAttribute("value")||"";
629
- if(oldvalue!==value) {
630
- input.setAttribute("value",value);
631
- try {
632
- input.setSelectionRange(0, Math.max(oldvalue.length,value.length)); // shadowDom sometimes fails to rerender unless this is done;
633
- input.setRangeText(value,0, Math.max(oldvalue.length,value.length));
634
- } catch(e) {
672
+ addEventListener("change", ({variableName, value}) => {
673
+ [...this.shadowRoot.querySelectorAll(`input[l-bind=${variableName}]`),
674
+ ...this.shadowRoot.querySelectorAll(`select[l-bind=${variableName}]`)]
675
+ .forEach((input) => {
676
+ const eltype = input.getAttribute("type");
677
+ if (eltype === "checkbox") { // at el option selected
678
+ if(!!value) {
679
+ input.setAttribute("checked", "");
680
+ } else {
681
+ input.removeAttribute("checked");
682
+ }
683
+ input.checked = !!value;
684
+ } else if (eltype === "radio") {
685
+ if (input.getAttribute("value") === value) {
686
+ input.setAttribute("checked", "");
687
+ input.checked = true;
688
+ }
689
+ } else if (input.tagName === "SELECT") {
690
+ const values = value && typeof (value) === "object" && value instanceof Array ? value : [value];
691
+ [...input.querySelectorAll("option")].forEach((option) => {
692
+ if (values.includes(option.getAttribute("value") || option.innerText)) {
693
+ option.setAttribute("selected", "");
694
+ option.selected = true;
695
+ }
696
+ })
697
+ } else if (!eltype || eltype === "text") {
698
+ value = typeof (value) === "string" || value == null ? value : JSON.stringify(value);
699
+ const oldvalue = input.getAttribute("value") || "";
700
+ if (oldvalue !== value) {
701
+ if (value == null) input.removeAttribute("value");
702
+ else input.setAttribute("value", value);
703
+ try {
704
+ input.setSelectionRange(0, Math.max(oldvalue.length, value ? value.length : 0)); // shadowDom sometimes fails to rerender unless this is done;
705
+ input.setRangeText(value || "", 0, Math.max(oldvalue.length, value ? value.length : 0));
706
+ } catch (e) {
635
707
 
708
+ }
636
709
  }
637
710
  }
638
- }
639
- })
711
+ })
640
712
  })
641
713
  }
642
714
  return Object.entries(this.vars)
@@ -665,10 +737,18 @@ const {observe} = (() => {
665
737
  }
666
738
  }
667
739
  }
668
- const createComponent = (name, node, {observer,bindForms,importAnchors}={}) => {
669
- if (!customElements.get(name)) customElements.define(name, createClass(node, {observer,bindForms,importAnchors}));
740
+ const createComponent = (name, node, {observer, bindForms, importAnchors} = {}) => {
741
+ let ctor = customElements.get(name);
742
+ if (ctor) {
743
+ console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
744
+ return ctor;
745
+ }
746
+ ctor = createClass(node, {observer, bindForms, importAnchors});
747
+ customElements.define(name, ctor);
748
+ return ctor;
670
749
  }
671
- Object.defineProperty(Lightview,"createComponent",{writable:true,configurable:true,value:createComponent})
750
+ Lightview.createComponent = createComponent;
751
+ //Object.defineProperty(Lightview, "createComponent", {writable: true, configurable: true, value: createComponent})
672
752
  const importLink = async (link, observer) => {
673
753
  const url = (new URL(link.getAttribute("href"), window.location.href)),
674
754
  as = link.getAttribute("as") || getNameFromPath(url.pathname);
@@ -681,8 +761,8 @@ const {observe} = (() => {
681
761
  importAnchors = !!dom.head.querySelector('meta[name="l-importAnchors"]'),
682
762
  bindForms = !!dom.head.querySelector('meta[name="l-bindForms"]'),
683
763
  unhide = !!dom.head.querySelector('meta[name="l-unhide"]');
684
- if(unhide) dom.body.removeAttribute("hidden");
685
- createComponent(as, dom.body, {observer,importAnchors,bindForms});
764
+ if (unhide) dom.body.removeAttribute("hidden");
765
+ createComponent(as, dom.body, {observer, importAnchors, bindForms});
686
766
  }
687
767
  return {as};
688
768
  }
@@ -693,24 +773,159 @@ const {observe} = (() => {
693
773
  }
694
774
  }
695
775
 
696
- const bodyAsComponent = (as, {unhide,importAnchors,bindForms}={}) => {
776
+ const bodyAsComponent = ({as = "x-body", unhide, importAnchors, bindForms} = {}) => {
697
777
  const parent = document.body.parentElement;
698
- createComponent(as, document.body,{importAnchors,bindForms});
778
+ createComponent(as, document.body, {importAnchors, bindForms});
699
779
  const component = document.createElement(as);
700
780
  parent.replaceChild(component, document.body);
781
+ Object.defineProperty(document, "body", {
782
+ enumerable: true, configurable: true, get() {
783
+ return component;
784
+ }
785
+ });
701
786
  if (unhide) component.removeAttribute("hidden");
702
787
  }
788
+ Lightview.bodyAsComponent = bodyAsComponent;
789
+ const postMessage = (data, target = window.parent) => {
790
+ if (postMessage.enabled) {
791
+ if (target instanceof HTMLIFrameElement) {
792
+ data = {...data, href: window.location.href};
793
+ target.contentWindow.postMessage(JSON.stringify(data), "*");
794
+ } else {
795
+ data = {...data, iframeId: document.lightviewId, href: window.location.href};
796
+ target.postMessage(JSON.stringify(data), "*");
797
+ }
798
+ }
799
+ }
800
+ const setComponentAttribute = (node, name, value) => {
801
+ if (node.getAttribute(name) !== value) node.setAttribute(name, value);
802
+ postMessage({type: "setAttribute", argsList: [name, value]});
803
+ }
804
+ const removeComponentAttribute = (node, name, value) => {
805
+ node.removeAttribute(name);
806
+ postMessage({type: "removeAttribute", argsList: [name]});
807
+ }
808
+ const getNodePath = (node, path = []) => {
809
+ path.unshift(node);
810
+ if (node.parentNode && node.parentNode !== node.parentNode) getNodePath(node.parentNode, path);
811
+ return path;
812
+ }
813
+ const onresize = (node, callback) => {
814
+ const resizeObserver = new ResizeObserver(() => callback());
815
+ resizeObserver.observe(node);
816
+ };
703
817
 
704
818
  const url = new URL(document.currentScript.getAttribute("src"), window.location.href);
705
- document.addEventListener("DOMContentLoaded", async () => {
706
- if (url.searchParams.has("importLinks")) await importLinks();
819
+ let domContentLoadedEvent;
820
+ addListener(window,"DOMContentLoaded", (event) => domContentLoadedEvent = event);
821
+ const loader = async (whenFramed) => {
822
+ if (!!document.querySelector('meta[name="l-importLinks"]')) await importLinks();
707
823
  const importAnchors = !!document.querySelector('meta[name="l-importAnchors"]'),
708
824
  bindForms = !!document.querySelector('meta[name="l-bindForms"]'),
709
- unhide = !!document.querySelector('meta[name="l-unhide"]');
710
- if (url.searchParams.has("as")) bodyAsComponent(url.searchParams.get("as"), {unhide,importAnchors,bindForms});
711
- });
712
-
713
- return {observe}
714
- })();
825
+ unhide = !!document.querySelector('meta[name="l-unhide"]'),
826
+ isolated = !!document.querySelector('meta[name="l-isolate"]'),
827
+ enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
828
+ if (whenFramed) {
829
+ whenFramed({unhide, importAnchors, bindForms, isolated, enableFrames});
830
+ if (!isolated) {
831
+ postMessage.enabled = true;
832
+ addListener(window,"message", ({data}) => {
833
+ const {type, argsList} = JSON.parse(data);
834
+ if (type === "framed") {
835
+ const resize = () => {
836
+ const {width, height} = document.body.getBoundingClientRect();
837
+ postMessage({type: "setAttribute", argsList: ["width", width]})
838
+ postMessage({type: "setAttribute", argsList: ["height", height + 20]});
839
+ }
840
+ resize();
841
+ onresize(document.body, () => {
842
+ resize();
843
+ });
844
+ return
845
+ }
846
+ if (type === "setAttribute") {
847
+ const [name, value] = [...argsList],
848
+ variable = document.body.vars[name];
849
+ if (variable && variable.imported) document.body.setValue(name, value);
850
+ return;
851
+ }
852
+ if (type === "removeAttribute") {
853
+ const [name] = argsList[0],
854
+ variable = document.body.vars[name];
855
+ if (variable && variable.imported) document.body.setValue(name, undefined);
715
856
 
857
+ }
858
+ });
859
+ const url = new URL(window.location.href);
860
+ document.lightviewId = url.searchParams.get("id");
861
+ postMessage({type: "DOMContentLoaded"})
862
+ }
863
+ } else if (url.searchParams.has("as")) {
864
+ bodyAsComponent({as: url.searchParams.get("as"), unhide, importAnchors, bindForms});
865
+ }
866
+ if (enableFrames) {
867
+ postMessage.enabled = true;
868
+ addListener(window,"message", (message) => {
869
+ const {type, iframeId, argsList, href} = JSON.parse(message.data),
870
+ iframe = document.getElementById(iframeId);
871
+ if (iframe) {
872
+ if (type === "DOMContentLoaded") {
873
+ postMessage({type: "framed", href: window.location.href}, iframe);
874
+ Object.defineProperty(domContentLoadedEvent, "currentTarget", {
875
+ enumerable: false,
876
+ configurable: true,
877
+ value: iframe
878
+ });
879
+ domContentLoadedEvent.href = href;
880
+ domContentLoadedEvent.srcElement = iframe;
881
+ domContentLoadedEvent.bubbles = false;
882
+ domContentLoadedEvent.path = getNodePath(iframe);
883
+ Object.defineProperty(domContentLoadedEvent, "timeStamp", {
884
+ enumerable: false,
885
+ configurable: true,
886
+ value: performance.now()
887
+ })
888
+ iframe.dispatchEvent(domContentLoadedEvent);
889
+ return;
890
+ }
891
+ if (type === "setAttribute") {
892
+ const [name, value] = [...argsList];
893
+ if (iframe.getAttribute(name) !== value + "") iframe.setAttribute(name, value);
894
+ return;
895
+ }
896
+ if (type === "removeAttribute") {
897
+ iframe.removeAttribute(...argsList);
898
+ return;
899
+ }
900
+ }
901
+ console.warn("iframe posted a message without providing an id", message);
902
+ });
903
+ const mutationCallback = (mutationsList) => {
904
+ const console = document.getElementById("console");
905
+ for (const {target, attributeName, oldValue} of mutationsList) {
906
+ if (!["height", "width"].includes(attributeName)) {
907
+ const value = target.getAttribute(attributeName);
908
+ if (!value) postMessage({type: "removeAttribute", argsList: [attributeName]}, iframe)
909
+ else if (value !== oldValue) postMessage({
910
+ type: "setAttribute",
911
+ argsList: [attributeName, value]
912
+ }, iframe)
913
+ }
914
+ }
915
+ };
916
+ const observer = new MutationObserver(mutationCallback),
917
+ iframe = document.getElementById("myframe");
918
+ observer.observe(iframe, {attributes: true, attributeOldValue: true});
919
+ }
920
+ }
921
+ const whenFramed = (f, {isolated} = {}) => {
922
+ addListener(document,"DOMContentLoaded", (event) => loader(f));
923
+ }
924
+ Lightview.whenFramed = whenFramed;
925
+ //Object.defineProperty(Lightview, "whenFramed", {configurable: true, writable: true, value: whenFramed});
926
+ if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) { // CodePen mucks with window.parent
927
+ addListener(document,"DOMContentLoaded", () => loader())
928
+ }
716
929
 
930
+ return {observe}
931
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "1.0.0b",
3
+ "version": "1.3.0b",
4
4
  "description": "Small, simple, powerful web UI creation ...",
5
5
  "main": "lightview.js",
6
6
  "scripts": {
package/remote.html ADDED
@@ -0,0 +1,35 @@
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 an iframe.
13
+ </p>
14
+ <iframe id="myframe" src="https://lightview.dev/remoteform.html?id=myframe"></iframe>
15
+ <div id="console" style="max-height:250px;scroll:auto"></div>
16
+ <script>
17
+ const mutationCallback = (mutationsList) => {
18
+ const console = document.getElementById("console");
19
+ for (const {target,attributeName,oldValue} of mutationsList) {
20
+ const line = document.createElement("div"),
21
+ event = {attributeName,oldValue,value:target.getAttribute(attributeName)};
22
+ line.innerText = JSON.stringify(event);
23
+ console.appendChild(line);
24
+ }
25
+ };
26
+ const observer = new MutationObserver(mutationCallback),
27
+ iframe = document.getElementById("myframe");
28
+ observer.observe(iframe, { attributes:true, attributeOldValue: true });
29
+ iframe.addEventListener("DOMContentLoaded",(event) => {
30
+ console.log(event);
31
+ });
32
+ </script>
33
+ </body>
34
+
35
+ </html>
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+
3
+ <head>
4
+ <title>Form</title>
5
+ <meta name="l-bindForms">
6
+ <script src="./lightview.js?as=x-body"></script>
7
+ <script>Lightview.whenFramed(({as,unhide,importAnchors,bindForms,isolated,enableFrames}) => {
8
+ Lightview.bodyAsComponent({as,unhide,importAnchors,bindForms,isolated,enableFrames});
9
+ })</script>
10
+ </head>
11
+
12
+ <body style="height:fit-content;width:fit-content;display:flex;flex-direction:column;max-height:100%;overflow:auto;">
13
+ <p>
14
+ <button l-on:click="run">Run</button> <button l-on:click="reset">Reset</button> <button l-on:click="setNull">Set Null</button>
15
+ </p>
16
+ <form>
17
+ <input name="name" type="text" value="Joe"><br>
18
+ <input name="age" type="number" value="20"><br>
19
+ </form>
20
+ <div id="console"></div>
21
+ <script type="lightview/module">
22
+ self.variables({name:string,age:number},{exported,imported});
23
+ self.run = () => {
24
+ name = "Bill";
25
+ age = 30;
26
+ action("run");
27
+ };
28
+ self.reset = () => {
29
+ name = "Joe";
30
+ age = 20;
31
+ const console = self.getElementById("console");
32
+ while(console.lastElementChild) console.lastElementChild.remove();
33
+ };
34
+ self.setNull = () => {
35
+ name = null;
36
+ age = undefined;
37
+ action("setNull");
38
+ };
39
+ const action = (name) => {
40
+ const div = document.createElement("div");
41
+ div.innerText = name;
42
+ self.getElementById("console").appendChild(div);
43
+ }
44
+ </script>
45
+ </body>
46
+
47
+ </html>
package/xor.html ADDED
@@ -0,0 +1,62 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <template id="audiostream">
6
+ <p>${name}</p>
7
+ <p>
8
+ Play: <input name="play" type="checkbox" l-bind="run" checked="${run}">
9
+ </p>
10
+ <script type="lightview/module">
11
+ self.variables({
12
+ run: boolean
13
+ });
14
+ self.variables({
15
+ name: string
16
+ }, {
17
+ imported
18
+ });
19
+ addEventListener("change", ({
20
+ variableName,
21
+ value
22
+ }) => {
23
+ if (variableName === "run" && value === true) {
24
+ self.siblings.forEach((sibling) => {
25
+ sibling.setValue(variableName, false);
26
+ })
27
+ }
28
+ })
29
+ </script>
30
+ </template>
31
+ <title>Form</title>
32
+ <script src="./lightview.js"></script>
33
+ <script>
34
+ Lightview.createComponent("x-audiostream", document.getElementById("audiostream"), {
35
+ bindForms: true
36
+ })
37
+ </script>
38
+ </head>
39
+
40
+ <body>
41
+ <div style="margin:20px">
42
+ <table>
43
+ <th>
44
+ <td colspan="3">Audio Streams</td>
45
+ </th>
46
+ <tr>
47
+ <td style="width:33%;text-align:center">
48
+ <x-audiostream name="Classical"></x-audiostream>
49
+ </td>
50
+ <td style="width:33%;text-align:center">
51
+ <x-audiostream name="Country"></x-audiostream>
52
+ </td>
53
+ <td style="width:33%;text-align:center">
54
+ <x-audiostream name="Classic Rock"></x-audiostream>
55
+ </td>
56
+ </tr>
57
+ </table>
58
+ </div>
59
+
60
+ </body>
61
+
62
+ </html>