lightview 1.2.0-b → 1.3.1-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.0.b (BETA)
1
+ # lightview v1.3.1b (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 (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,19 +412,11 @@ 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
- ["getElementById", "querySelector", "querySelectorAll"]
419
+ ["getElementById", "querySelector", "querySelectorAll"]
398
420
  .forEach((fname) => {
399
421
  Object.defineProperty(this, fname, {
400
422
  configurable: true,
@@ -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,114 +471,137 @@ const {observe} = (() => {
451
471
  ctx.appendChild(currentScript);
452
472
  }
453
473
  Promise.all(promises).then(() => {
454
- const inputs = [...ctx.shadowRoot.querySelectorAll("input[l-bind]")];
455
- inputs.forEach((input) => {
456
- bindInput(input,ctx);
457
- })
458
- const nodes = getNodes(ctx);
459
- nodes.forEach((node) => {
460
- if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
461
- render(!!node.template, () => resolveNode(node, this))
462
- } else if (node.nodeType === Node.ELEMENT_NODE) {
463
- [...node.attributes].forEach((attr) => {
464
- const {name, value} = attr,
465
- [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==="") {
473
- render(!!attr.template, () => {
474
- const value = resolveNode(attr, this);
475
- if(params[0]) {
476
- if(value==="true") node.setAttribute(params[0], "");
477
- 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", "");
478
498
  } else {
479
- if(value!=="true") node.removeAttribute(name);
499
+ node.removeAttribute("checked");
480
500
  }
481
- })
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
- } else if (type === "l-on") {
489
- let listener;
490
- render(!!attr.template, () => {
491
- const value = resolveNode(attr, this);
492
- if (listener) node.removeEventListener(params[0], listener);
493
- listener = this[value] || window[value] || Function(value);
494
- node.addEventListener(params[0], listener);
495
- })
496
- } else if (type === "l-if") {
497
- render(!!attr.template, () => {
498
- const value = resolveNode(attr, this);
499
- node.style.setProperty("display", value === "true" ? "revert" : "none");
500
- })
501
- } else if (type === "l-for") {
502
- node.template ||= node.innerHTML;
503
- render(!!attr.template, () => {
504
- const [what = "each", vname = "element", index = "index", array = "array", after = false] = params,
505
- value = resolveNode(attr, this),
506
- coerced = coerce(value, what === "each" ? Array : "object"),
507
- target = what === "each" ? coerced : Object[what](coerced),
508
- html = target.reduce((html, item, i, target) => {
509
- return html += Function("context", "with(context) { return `" + node.template + "` }")({
510
- [vname]: item,
511
- [index]: i,
512
- [array]: target
513
- })
514
- }, ""),
515
- parsed = parser.parseFromString(html, "text/html");
516
- if (!window.lightviewDebug) {
517
- if (after) {
518
- node.style.setProperty("display", "none")
519
- } else {
520
- while (node.lastElementChild) node.lastElementChild.remove();
521
- }
501
+ } else if (attrtype === "radio") {
502
+ if (elvalue === value) {
503
+ node.setAttribute("checked", "");
522
504
  }
523
- while (parsed.body.firstChild) {
524
- if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
525
- 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();
526
552
  }
527
- })
528
- } else if (attr.template) {
529
- render(!!attr.template, () => resolveNode(attr, this));
530
- }
531
- })
532
- }
533
- })
534
- shadow.normalize();
535
- observer.observe(ctx, {attributeOldValue: true});
536
- 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
+ }
537
564
  })
538
- }
565
+ shadow.normalize();
566
+ observer.observe(ctx, {attributeOldValue: true});
567
+ if (ctx.hasOwnProperty("connectedCallback")) ctx.connectedCallback();
568
+ })
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,29 +610,37 @@ 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
- variable.value = coerce(this.getAttribute(key), variable.type);
643
+ variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
593
644
  variable.observed = observed;
594
645
  variable.imported = imported;
595
646
  }
@@ -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
- 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,126 +773,159 @@ 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
- if (!!document.querySelector('meta[name="l-importLinks"]')) await importLinks();
757
- const importAnchors = !!document.querySelector('meta[name="l-importAnchors"]'),
758
- bindForms = !!document.querySelector('meta[name="l-bindForms"]'),
759
- unhide = !!document.querySelector('meta[name="l-unhide"]'),
760
- isolated = !!document.querySelector('meta[name="l-isolate"]'),
761
- enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
762
- if(whenFramed) {
763
- whenFramed({unhide,importAnchors,bindForms,isolated,enableFrames});
764
- if(!isolated) {
765
- postMessage.enabled = true;
766
- window.addEventListener("message",({data}) => {
767
- data = JSON.parse(data);
768
- if(data.type==="framed") {
769
- const resize = () => {
770
- const {width,height} = document.body.getBoundingClientRect();
771
- postMessage({type:"setAttribute",argsList:["width",width]})
772
- postMessage({type:"setAttribute",argsList:["height",height+20]});
773
- }
774
- resize();
775
- onresize(document.body,() => {
776
- resize();
777
- })
778
- }
779
- const event = new CustomEvent(data.type,{detail:data});
780
-
781
- });
782
- /* window.addEventListener("resize",() => {
783
- const {width,height} = document.body.getBoundingClientRect();
784
- postMessage({type:"setAttribute",argsList:["width",width]})
785
- postMessage({type:"setAttribute",argsList:["height",height]})
786
- })*/
787
- const url = new URL(window.location.href);
788
- document.lightviewId = url.searchParams.get("id");
789
- postMessage({type:"DOMContentLoaded"})
790
- }
791
- } else if (url.searchParams.has("as")) {
792
- bodyAsComponent({as:url.searchParams.get("as"),unhide,importAnchors,bindForms});
793
- }
794
- if(enableFrames) {
822
+ if (!!document.querySelector('meta[name="l-importLinks"]')) await importLinks();
823
+ const importAnchors = !!document.querySelector('meta[name="l-importAnchors"]'),
824
+ bindForms = !!document.querySelector('meta[name="l-bindForms"]'),
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) {
795
831
  postMessage.enabled = true;
796
- window.addEventListener("message",(message) => {
797
- const {type,iframeId,argsList,href} = JSON.parse(message.data),
798
- iframe = document.getElementById(iframeId);
799
- if(iframe) {
800
- if(type==="DOMContentLoaded") {
801
- postMessage({type:"framed",href:window.location.href},iframe);
802
- Object.defineProperty(domContentLoadedEvent,"currentTarget",{enumerable:false,configurable:true,value:iframe});
803
- domContentLoadedEvent.href = href;
804
- domContentLoadedEvent.srcElement = iframe;
805
- domContentLoadedEvent.bubbles = false;
806
- domContentLoadedEvent.path = getNodePath(iframe);
807
- Object.defineProperty(domContentLoadedEvent,"timeStamp",{enumerable:false,configurable:true,value:performance.now()})
808
- iframe.dispatchEvent(domContentLoadedEvent);
809
- return;
810
- }
811
- if(type==="setAttribute") {
812
- const [name,value] = [...argsList];
813
- if(iframe.getAttribute(name)!==value+"") iframe.setAttribute(name,value);
814
- return;
815
- }
816
- if(type==="removeAttribute") {
817
- iframe.removeAttribute(...argsList);
818
- return;
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]});
819
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);
856
+
820
857
  }
821
- console.warn("iframe posted a message without providing an id",message);
822
858
  });
859
+ const url = new URL(window.location.href);
860
+ document.lightviewId = url.searchParams.get("id");
861
+ postMessage({type: "DOMContentLoaded"})
823
862
  }
863
+ } else if (url.searchParams.has("as")) {
864
+ bodyAsComponent({as: url.searchParams.get("as"), unhide, importAnchors, bindForms});
824
865
  }
825
- const whenFramed = (f,{isolated}={}) => {
826
- document.addEventListener("DOMContentLoaded",(event) => loader(f));
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));
827
923
  }
828
- Object.defineProperty(Lightview,"whenFramed",{configurable:true,writable:true,value:whenFramed});
829
- if(window.location===window.parent.location || !(window.parent instanceof Window)) { // CodePen mucks with window.parent
830
- 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())
831
928
  }
832
929
 
833
930
  return {observe}
834
- })();
835
-
836
-
931
+ })();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "1.2.0b",
4
- "description": "Small, simple, powerful web UI creation ...",
3
+ "version": "1.3.1b",
4
+ "description": "Small, simple, powerful web UI creation ... imagine a blend of Svelte, React, Vue, Riot and more.",
5
5
  "main": "lightview.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -15,7 +15,10 @@
15
15
  "react",
16
16
  "angular",
17
17
  "riot",
18
- "vue"
18
+ "vue",
19
+ "moon",
20
+ "hyperapp",
21
+ "hyperhtml"
19
22
  ],
20
23
  "author": "Simon Y. Blackwell",
21
24
  "license": "MIT",
package/remoteform.html CHANGED
@@ -19,7 +19,7 @@
19
19
  </form>
20
20
  <div id="console"></div>
21
21
  <script type="lightview/module">
22
- self.variables({name:string,age:number},{exported});
22
+ self.variables({name:string,age:number},{exported,imported});
23
23
  self.run = () => {
24
24
  name = "Bill";
25
25
  age = 30;
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>