lightview 1.2.1-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.2.1.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;
@@ -57,21 +61,22 @@ const {observe} = (() => {
57
61
  }
58
62
  CURRENTOBSERVER = null;
59
63
  }
64
+
60
65
  observer.cancel = () => observer.cancelled = true;
61
66
  observer();
62
67
  return observer;
63
68
  }
64
69
  const coerce = (value, toType) => {
65
- if(value+""==="null" || value+""==="undefined") return value;
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 (newValue==null || 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
  }
@@ -277,7 +284,7 @@ const {observe} = (() => {
277
284
  if (node.template) {
278
285
  try {
279
286
  const value = Function("context", "with(context) { return `" + node.template + "` }")(component.varsProxy);
280
- node.nodeValue = value==="null" || value==="undefined" ? "" : value;
287
+ node.nodeValue = value === "null" || value === "undefined" ? "" : value;
281
288
  } catch (e) {
282
289
  if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
283
290
  }
@@ -298,57 +305,79 @@ const {observe} = (() => {
298
305
  if (["text", "tel", "email", "url", "search", "radio"].includes(inputType)) return "string";
299
306
  if (["number", "range"].includes(inputType)) return "number";
300
307
  if (["datetime"].includes(inputType)) return Date;
301
- if(["checkbox"].includes(inputType)) return "boolean";
308
+ if (["checkbox"].includes(inputType)) return "boolean";
302
309
  return "any";
303
310
  }
304
- const _importAnchors = (node,component) => {
311
+ const _importAnchors = (node, component) => {
305
312
  [...node.querySelectorAll('a[href][target^="#"]')].forEach((node) => {
306
313
  node.removeEventListener("click", anchorHandler);
307
- node.addEventListener("click", anchorHandler);
314
+ addListener(node,"click", anchorHandler);
308
315
  })
309
316
  }
310
317
  const _bindForms = (node, component) => {
311
- [...node.querySelectorAll("input")].forEach((input) => {
312
- bindInput(input,component);
313
- })
318
+ [...node.querySelectorAll("input")].forEach((input) => bindInput(input, component))
314
319
  }
315
- const bindInput = (input,component) => {
316
- const name = input.getAttribute("name"),
317
- 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;
318
324
  if (name) {
319
- if(!input.hasAttribute("l-bind")) input.setAttribute("l-bind",vname)
320
- 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),
321
328
  deflt = input.getAttribute("default"),
322
329
  value = input.getAttribute("value");
323
330
  let variable = component.vars[vname] || {type};
324
- if(type!==variable.type) {
325
- 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;
326
333
  else throw new TypeError(`Attempt to bind <input name="${name}" type="${type}"> to variable ${vname}:${variable.type}`)
327
334
  }
328
- component.variables({[vname]:type});
329
- variable = component.vars[vname]
330
- if (value || deflt) {
335
+ component.variables({[vname]: type});
336
+ variable = component.vars[vname];
337
+ //if (value || deflt) {
338
+ if (inputtype !== "radio") {
331
339
  if (value && !value.includes("${")) {
332
340
  variable.value = coerce(value, type);
333
- input.setAttribute("value", `\${${name}}`);
334
- }
335
- if (deflt && !deflt.includes("${")) {
341
+ //input.setAttribute("value", `\${${name}}`);
342
+ } else if (deflt && !deflt.includes("${")) {
336
343
  variable.value = coerce(deflt, type);
337
- input.setAttribute("default", `\${${name}}`);
344
+ //input.setAttribute("default", `\${${name}}`);
338
345
  }
339
346
  }
340
- input.addEventListener("change",(event) => {
347
+ //}
348
+ addListener(input,"change", (event) => {
341
349
  event.stopImmediatePropagation();
342
- 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);
343
362
  })
344
363
  }
345
364
  }
346
- 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}) => {
347
376
  const instances = new Set(),
348
- dom = domElementNode.tagName==="TEMPLATE"
377
+ dom = domElementNode.tagName === "TEMPLATE"
349
378
  ? domElementNode.content.cloneNode(true)
350
379
  : domElementNode.cloneNode(true);
351
- if(domElementNode.tagName==="TEMPLATE") domElementNode = domElementNode.cloneNode(true);
380
+ if (domElementNode.tagName === "TEMPLATE") domElementNode = domElementNode.cloneNode(true);
352
381
  return class CustomElement extends HTMLElement {
353
382
  static get instances() {
354
383
  return instances;
@@ -362,6 +391,7 @@ const {observe} = (() => {
362
391
  shadow = this.attachShadow({mode: "open"}),
363
392
  eventlisteners = {};
364
393
  this.vars = {
394
+ ...reserved,
365
395
  addEventListener: {
366
396
  value: (eventName, listener) => {
367
397
  const listeners = eventlisteners[eventName] ||= new Set();
@@ -382,17 +412,9 @@ const {observe} = (() => {
382
412
  type: "function",
383
413
  constant: true
384
414
  },
385
- self: {value: currentComponent, type: CustomElement, constant: true},
386
- boolean: {value: "boolean", constant: true},
387
- string: {value: "string", constant: true},
388
- number: {value: "number", constant: true},
389
- observed: {value: true, constant: true},
390
- reactive: {value: true, constant: true},
391
- shared: {value: true, constant: true},
392
- exported: {value: true, constant: true},
393
- imported: {value: true, constant: true}
415
+ self: {value: currentComponent, type: CustomElement, constant: true}
394
416
  };
395
- this.defaultAttributes = domElementNode.tagName==="TEMPLATE" ? domElementNode.attributes : dom.attributes;
417
+ this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
396
418
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
397
419
  ["getElementById", "querySelector", "querySelectorAll"]
398
420
  .forEach((fname) => {
@@ -402,15 +424,13 @@ const {observe} = (() => {
402
424
  value: (...args) => this.shadowRoot[fname](...args)
403
425
  })
404
426
  });
405
- [...dom.childNodes].forEach((child) => {
406
- shadow.appendChild(child.cloneNode(true));
407
- })
427
+ [...dom.childNodes].forEach((child) => shadow.appendChild(child.cloneNode(true)));
408
428
  if (bindForms) _bindForms(shadow, this);
409
- if(importAnchors) _importAnchors(shadow,this);
429
+ if (importAnchors) _importAnchors(shadow, this);
410
430
  }
411
431
 
412
432
  get siblings() {
413
- return [...CustomElement.instances].filter((sibling) => sibling!=this);
433
+ return [...CustomElement.instances].filter((sibling) => sibling != this);
414
434
  }
415
435
 
416
436
  adoptedCallback() {
@@ -451,9 +471,9 @@ const {observe} = (() => {
451
471
  ctx.appendChild(currentScript);
452
472
  }
453
473
  Promise.all(promises).then(() => {
454
- const inputs = [...ctx.shadowRoot.querySelectorAll("input[l-bind]")];
474
+ const inputs = [...ctx.shadowRoot.querySelectorAll("input[l-bind]"), ...ctx.shadowRoot.querySelectorAll("select[l-bind]")];
455
475
  inputs.forEach((input) => {
456
- bindInput(input,ctx);
476
+ bindInput(input, ctx);
457
477
  })
458
478
  const nodes = getNodes(ctx);
459
479
  nodes.forEach((node) => {
@@ -463,35 +483,46 @@ const {observe} = (() => {
463
483
  [...node.attributes].forEach((attr) => {
464
484
  const {name, value} = attr,
465
485
  [type, ...params] = name.split(":");
466
- if (["checked","selected"].includes(type)) {
467
- render(!!attr.template, () => {
468
- const value = resolveNode(attr, this);
469
- if (value === "true") node.setAttribute(name, "");
470
- else node.removeAttribute(name);
471
- })
472
- } else if(type==="") {
486
+ if (type === "" || type=="checked" || node.tagName === "SELECT") { // name is :something
473
487
  render(!!attr.template, () => {
474
- const value = resolveNode(attr, this);
475
- if(params[0]) {
476
- if(value==="true") node.setAttribute(params[0], "");
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], "")
477
494
  else node.removeAttribute(params[0]);
478
- } else {
479
- if(value!=="true") node.removeAttribute(name);
495
+ } else if (attrtype === "checkbox" || node.tagName === "OPTION") {
496
+ if (value === "true") {
497
+ node.setAttribute("checked", "");
498
+ } else {
499
+ node.removeAttribute("checked");
500
+ }
501
+ } else if (attrtype === "radio") {
502
+ if (elvalue === value) {
503
+ node.setAttribute("checked", "");
504
+ }
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
+ })
480
517
  }
481
518
  })
482
- } else if (["checked","selected"].includes(name)) {
483
- render(!!attr.template, () => {
484
- const value = resolveNode(attr, this);
485
- if (value === "true") node.setAttribute(name, "");
486
- else node.removeAttribute(name);
487
- })
488
519
  } else if (type === "l-on") {
489
520
  let listener;
490
521
  render(!!attr.template, () => {
491
522
  const value = resolveNode(attr, this);
492
523
  if (listener) node.removeEventListener(params[0], listener);
493
524
  listener = this[value] || window[value] || Function(value);
494
- node.addEventListener(params[0], listener);
525
+ addListener(node,params[0], listener);
495
526
  })
496
527
  } else if (type === "l-if") {
497
528
  render(!!attr.template, () => {
@@ -538,27 +569,39 @@ const {observe} = (() => {
538
569
  }
539
570
 
540
571
  adopted(value) {
541
- Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
572
+ this.adoptedCallback = value;
573
+ //Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
542
574
  }
543
575
 
544
576
  connected(value) {
545
- Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
577
+ this.connectedCallback = value;
578
+ //Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
546
579
  }
547
580
 
548
581
  attributeChanged(value) {
549
- Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
582
+ this.attributeChangedCallback = value;
583
+ //Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
550
584
  }
551
585
 
552
586
  disconnected(value) {
553
587
  Object.defineProperty(this, "disconnectedCallback", {
554
588
  configurable: true,
555
589
  writable: true,
556
- value:() => { value(); super.disconnectedCallback(value); }
590
+ value: () => {
591
+ value();
592
+ super.disconnectedCallback(value);
593
+ }
557
594
  });
558
595
  }
559
596
 
560
- setVariable(name, value, {shared,coerceTo = typeof (value)}={}) {
561
- 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) {
562
605
  instances.delete(this);
563
606
  return false;
564
607
  }
@@ -567,27 +610,35 @@ const {observe} = (() => {
567
610
  value = coerce(value, type);
568
611
  if (this.varsProxy[name] !== value) {
569
612
  const variable = this.vars[name];
570
- if(variable.shared) {
571
- 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
+ });
572
619
  variable.value = value;
573
- this.vars.postEvent.value("change",event);
574
- if(event.defaultPrevented) variable.value = value;
620
+ this.vars.postEvent.value("change", event);
621
+ if (event.defaultPrevented) variable.value = value;
575
622
  } else {
576
623
  this.varsProxy[name] = value;
577
624
  }
578
625
  }
579
626
  return true;
580
627
  }
581
- this.vars[name] = {type:coerceTo, value: coerce(value, coerceTo)};
628
+ this.vars[name] = {name, type: coerceTo, value: coerce(value, coerceTo)};
582
629
  return false;
583
630
  }
584
631
 
632
+ getValue(variableName) {
633
+ return this.vars[variableName]?.value;
634
+ }
635
+
585
636
  variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
586
637
  const addEventListener = this.varsProxy.addEventListener;
587
638
  if (variables !== undefined) {
588
639
  Object.entries(variables)
589
640
  .forEach(([key, type]) => {
590
- const variable = this.vars[key] ||= {type};
641
+ const variable = this.vars[key] ||= {name: key, type};
591
642
  if (observed || imported) {
592
643
  variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
593
644
  variable.observed = observed;
@@ -599,55 +650,65 @@ const {observe} = (() => {
599
650
  }
600
651
  if (shared) {
601
652
  variable.shared = true;
602
- addEventListener("change",({variableName,value}) => {
603
- if(this.vars[variableName]?.shared) {
653
+ addEventListener("change", ({variableName, value}) => {
654
+ if (this.vars[variableName]?.shared) {
604
655
  this.siblings.forEach((instance) => {
605
- instance.setVariable(variableName, value);
656
+ instance.setValue(variableName, value);
606
657
  })
607
658
  }
608
659
  })
609
660
  }
610
661
  if (exported) {
611
662
  variable.exported = true;
612
- // in case the export goes up to at iframe
613
- if(variable.value!=null) setComponentAttribute(this,key,variable.value);
614
- addEventListener("change",({variableName,value}) => {
615
- // Array.isArray will not work here, Proxies mess up JSON.stringify for Arrays
616
- //value = value && typeof (value) === "object" ? (value instanceof Array ? JSON.stringify([...value]) : JSON.stringify(value)) : value+"";
617
- value = typeof(value)==="string" || !value ? value : JSON.stringify(value);
618
- if(value==null) {
619
- removeComponentAttribute(this,variableName);
620
- } else {
621
- setComponentAttribute(this,variableName,value);
622
- }
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);
623
669
  })
624
670
  }
625
671
  });
626
- addEventListener("change",({variableName,value}) => {
627
- [...this.shadowRoot.querySelectorAll(`input[l-bind=${variableName}]`)].forEach((input) => {
628
- if(input.getAttribute("type")==="checkbox") { // at el option selected
629
- if(!value) input.removeAttribute("checked");
630
- input.checked = value;
631
- } else {
632
- // Array.isArray will not work here, Proxies mess up JSON.stringify for Arrays
633
- //value = value && typeof (value) === "object" ? (value instanceof Array ? JSON.stringify([...value]) : JSON.stringify(value)) : value+"";
634
- value = typeof(value)==="string" || value==null ? value : JSON.stringify(value);
635
- const oldvalue = input.getAttribute("value")||"";
636
- if(oldvalue!==value) {
637
- if(value==null) {
638
- input.removeAttribute("value");
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", "");
639
680
  } else {
640
- input.setAttribute("value",value);
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;
641
688
  }
642
- try {
643
- input.setSelectionRange(0, Math.max(oldvalue.length,value ? value.length : 0)); // shadowDom sometimes fails to rerender unless this is done;
644
- input.setRangeText(value||"",0, Math.max(oldvalue.length,value ? value.length : 0));
645
- } catch(e) {
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) {
646
707
 
708
+ }
647
709
  }
648
710
  }
649
- }
650
- })
711
+ })
651
712
  })
652
713
  }
653
714
  return Object.entries(this.vars)
@@ -676,17 +737,18 @@ const {observe} = (() => {
676
737
  }
677
738
  }
678
739
  }
679
- const createComponent = (name, node, {observer,bindForms,importAnchors}={}) => {
740
+ const createComponent = (name, node, {observer, bindForms, importAnchors} = {}) => {
680
741
  let ctor = customElements.get(name);
681
- if(ctor) {
742
+ if (ctor) {
682
743
  console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
683
744
  return ctor;
684
745
  }
685
- ctor = createClass(node, {observer,bindForms,importAnchors});
746
+ ctor = createClass(node, {observer, bindForms, importAnchors});
686
747
  customElements.define(name, ctor);
687
748
  return ctor;
688
749
  }
689
- 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})
690
752
  const importLink = async (link, observer) => {
691
753
  const url = (new URL(link.getAttribute("href"), window.location.href)),
692
754
  as = link.getAttribute("as") || getNameFromPath(url.pathname);
@@ -699,8 +761,8 @@ const {observe} = (() => {
699
761
  importAnchors = !!dom.head.querySelector('meta[name="l-importAnchors"]'),
700
762
  bindForms = !!dom.head.querySelector('meta[name="l-bindForms"]'),
701
763
  unhide = !!dom.head.querySelector('meta[name="l-unhide"]');
702
- if(unhide) dom.body.removeAttribute("hidden");
703
- createComponent(as, dom.body, {observer,importAnchors,bindForms});
764
+ if (unhide) dom.body.removeAttribute("hidden");
765
+ createComponent(as, dom.body, {observer, importAnchors, bindForms});
704
766
  }
705
767
  return {as};
706
768
  }
@@ -711,47 +773,51 @@ const {observe} = (() => {
711
773
  }
712
774
  }
713
775
 
714
- const bodyAsComponent = ({as="x-body",unhide,importAnchors,bindForms}={}) => {
776
+ const bodyAsComponent = ({as = "x-body", unhide, importAnchors, bindForms} = {}) => {
715
777
  const parent = document.body.parentElement;
716
- createComponent(as, document.body,{importAnchors,bindForms});
778
+ createComponent(as, document.body, {importAnchors, bindForms});
717
779
  const component = document.createElement(as);
718
780
  parent.replaceChild(component, document.body);
719
- Object.defineProperty(document,"body",{enumerable:true,configurable:true,get() { return component; }});
781
+ Object.defineProperty(document, "body", {
782
+ enumerable: true, configurable: true, get() {
783
+ return component;
784
+ }
785
+ });
720
786
  if (unhide) component.removeAttribute("hidden");
721
787
  }
722
788
  Lightview.bodyAsComponent = bodyAsComponent;
723
- const postMessage = (data,target=window.parent) => {
724
- if(postMessage.enabled) {
725
- if(target instanceof HTMLIFrameElement) {
726
- data = {...data,href:window.location.href};
727
- target.contentWindow.postMessage(JSON.stringify(data),"*");
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), "*");
728
794
  } else {
729
- data = {...data,iframeId:document.lightviewId,href:window.location.href};
730
- target.postMessage(JSON.stringify(data),"*");
795
+ data = {...data, iframeId: document.lightviewId, href: window.location.href};
796
+ target.postMessage(JSON.stringify(data), "*");
731
797
  }
732
798
  }
733
799
  }
734
- const setComponentAttribute = (node,name,value) => {
735
- if(node.getAttribute(name)!==value) node.setAttribute(name,value);
736
- postMessage({type:"setAttribute",argsList:[name,value]});
800
+ const setComponentAttribute = (node, name, value) => {
801
+ if (node.getAttribute(name) !== value) node.setAttribute(name, value);
802
+ postMessage({type: "setAttribute", argsList: [name, value]});
737
803
  }
738
- const removeComponentAttribute = (node,name,value) => {
804
+ const removeComponentAttribute = (node, name, value) => {
739
805
  node.removeAttribute(name);
740
- postMessage({type:"removeAttribute",argsList:[name]});
806
+ postMessage({type: "removeAttribute", argsList: [name]});
741
807
  }
742
- const getNodePath = (node,path=[]) => {
808
+ const getNodePath = (node, path = []) => {
743
809
  path.unshift(node);
744
- if(node.parentNode && node.parentNode!==node.parentNode) getNodePath(node.parentNode,path);
810
+ if (node.parentNode && node.parentNode !== node.parentNode) getNodePath(node.parentNode, path);
745
811
  return path;
746
812
  }
747
813
  const onresize = (node, callback) => {
748
- const resizeObserver = new ResizeObserver(() => callback() );
814
+ const resizeObserver = new ResizeObserver(() => callback());
749
815
  resizeObserver.observe(node);
750
816
  };
751
817
 
752
818
  const url = new URL(document.currentScript.getAttribute("src"), window.location.href);
753
819
  let domContentLoadedEvent;
754
- window.addEventListener("DOMContentLoaded",(event) => domContentLoadedEvent = event);
820
+ addListener(window,"DOMContentLoaded", (event) => domContentLoadedEvent = event);
755
821
  const loader = async (whenFramed) => {
756
822
  if (!!document.querySelector('meta[name="l-importLinks"]')) await importLinks();
757
823
  const importAnchors = !!document.querySelector('meta[name="l-importAnchors"]'),
@@ -759,101 +825,107 @@ const {observe} = (() => {
759
825
  unhide = !!document.querySelector('meta[name="l-unhide"]'),
760
826
  isolated = !!document.querySelector('meta[name="l-isolate"]'),
761
827
  enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
762
- if(whenFramed) {
763
- whenFramed({unhide,importAnchors,bindForms,isolated,enableFrames});
764
- if(!isolated) {
828
+ if (whenFramed) {
829
+ whenFramed({unhide, importAnchors, bindForms, isolated, enableFrames});
830
+ if (!isolated) {
765
831
  postMessage.enabled = true;
766
- window.addEventListener("message",({data}) => {
767
- const {type,argsList} = JSON.parse(data);
768
- if(type==="framed") {
832
+ addListener(window,"message", ({data}) => {
833
+ const {type, argsList} = JSON.parse(data);
834
+ if (type === "framed") {
769
835
  const resize = () => {
770
- const {width,height} = document.body.getBoundingClientRect();
771
- postMessage({type:"setAttribute",argsList:["width",width]})
772
- postMessage({type:"setAttribute",argsList:["height",height+20]});
836
+ const {width, height} = document.body.getBoundingClientRect();
837
+ postMessage({type: "setAttribute", argsList: ["width", width]})
838
+ postMessage({type: "setAttribute", argsList: ["height", height + 20]});
773
839
  }
774
840
  resize();
775
- onresize(document.body,() => {
841
+ onresize(document.body, () => {
776
842
  resize();
777
843
  });
778
844
  return
779
845
  }
780
- if(type==="setAttribute") {
781
- const [name,value] = [...argsList],
846
+ if (type === "setAttribute") {
847
+ const [name, value] = [...argsList],
782
848
  variable = document.body.vars[name];
783
- if(variable && variable.imported) {
784
- document.body.setVariable(name,value);
785
- }
849
+ if (variable && variable.imported) document.body.setValue(name, value);
786
850
  return;
787
851
  }
788
- if(type==="removeAttribute") {
852
+ if (type === "removeAttribute") {
789
853
  const [name] = argsList[0],
790
854
  variable = document.body.vars[name];
791
- if(variable && variable.imported) {
792
- document.body.setVariable(name,undefined);
793
- }
794
- return;
855
+ if (variable && variable.imported) document.body.setValue(name, undefined);
856
+
795
857
  }
796
858
  });
797
859
  const url = new URL(window.location.href);
798
860
  document.lightviewId = url.searchParams.get("id");
799
- postMessage({type:"DOMContentLoaded"})
861
+ postMessage({type: "DOMContentLoaded"})
800
862
  }
801
863
  } else if (url.searchParams.has("as")) {
802
- bodyAsComponent({as:url.searchParams.get("as"),unhide,importAnchors,bindForms});
864
+ bodyAsComponent({as: url.searchParams.get("as"), unhide, importAnchors, bindForms});
803
865
  }
804
- if(enableFrames) {
866
+ if (enableFrames) {
805
867
  postMessage.enabled = true;
806
- window.addEventListener("message",(message) => {
807
- const {type,iframeId,argsList,href} = JSON.parse(message.data),
868
+ addListener(window,"message", (message) => {
869
+ const {type, iframeId, argsList, href} = JSON.parse(message.data),
808
870
  iframe = document.getElementById(iframeId);
809
- if(iframe) {
810
- if(type==="DOMContentLoaded") {
811
- postMessage({type:"framed",href:window.location.href},iframe);
812
- Object.defineProperty(domContentLoadedEvent,"currentTarget",{enumerable:false,configurable:true,value:iframe});
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
+ });
813
879
  domContentLoadedEvent.href = href;
814
880
  domContentLoadedEvent.srcElement = iframe;
815
881
  domContentLoadedEvent.bubbles = false;
816
882
  domContentLoadedEvent.path = getNodePath(iframe);
817
- Object.defineProperty(domContentLoadedEvent,"timeStamp",{enumerable:false,configurable:true,value:performance.now()})
883
+ Object.defineProperty(domContentLoadedEvent, "timeStamp", {
884
+ enumerable: false,
885
+ configurable: true,
886
+ value: performance.now()
887
+ })
818
888
  iframe.dispatchEvent(domContentLoadedEvent);
819
889
  return;
820
890
  }
821
- if(type==="setAttribute") {
822
- const [name,value] = [...argsList];
823
- if(iframe.getAttribute(name)!==value+"") iframe.setAttribute(name,value);
891
+ if (type === "setAttribute") {
892
+ const [name, value] = [...argsList];
893
+ if (iframe.getAttribute(name) !== value + "") iframe.setAttribute(name, value);
824
894
  return;
825
895
  }
826
- if(type==="removeAttribute") {
896
+ if (type === "removeAttribute") {
827
897
  iframe.removeAttribute(...argsList);
828
898
  return;
829
899
  }
830
900
  }
831
- console.warn("iframe posted a message without providing an id",message);
901
+ console.warn("iframe posted a message without providing an id", message);
832
902
  });
833
903
  const mutationCallback = (mutationsList) => {
834
904
  const console = document.getElementById("console");
835
- for (const {target,attributeName,oldValue} of mutationsList) {
836
- if(!["height","width"].includes(attributeName)) {
905
+ for (const {target, attributeName, oldValue} of mutationsList) {
906
+ if (!["height", "width"].includes(attributeName)) {
837
907
  const value = target.getAttribute(attributeName);
838
908
  if (!value) postMessage({type: "removeAttribute", argsList: [attributeName]}, iframe)
839
- else if (value !== oldValue) postMessage({type: "setAttribute",argsList: [attributeName,value]}, iframe)
909
+ else if (value !== oldValue) postMessage({
910
+ type: "setAttribute",
911
+ argsList: [attributeName, value]
912
+ }, iframe)
840
913
  }
841
914
  }
842
915
  };
843
916
  const observer = new MutationObserver(mutationCallback),
844
917
  iframe = document.getElementById("myframe");
845
- observer.observe(iframe, { attributes:true, attributeOldValue: true });
918
+ observer.observe(iframe, {attributes: true, attributeOldValue: true});
846
919
  }
847
920
  }
848
- const whenFramed = (f,{isolated}={}) => {
849
- document.addEventListener("DOMContentLoaded",(event) => loader(f));
921
+ const whenFramed = (f, {isolated} = {}) => {
922
+ addListener(document,"DOMContentLoaded", (event) => loader(f));
850
923
  }
851
- Object.defineProperty(Lightview,"whenFramed",{configurable:true,writable:true,value:whenFramed});
852
- if(window.location===window.parent.location || !(window.parent instanceof Window) || window.parent!==window) { // CodePen mucks with window.parent
853
- document.addEventListener("DOMContentLoaded",() => loader())
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())
854
928
  }
855
929
 
856
930
  return {observe}
857
- })();
858
-
859
-
931
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "1.2.1b",
3
+ "version": "1.3.0b",
4
4
  "description": "Small, simple, powerful web UI creation ...",
5
5
  "main": "lightview.js",
6
6
  "scripts": {
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>