@cocreate/render 1.50.0 → 1.53.0

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/CONTRIBUTING.md CHANGED
@@ -13,7 +13,7 @@ Fork the [project on Github](https://github.com/CoCreate-app/CoCreate-render) an
13
13
  ```
14
14
  git render https://github.com/contributor/CoCreate-render.git
15
15
  cd CoCreate-render
16
- git remote add upstream https://github.com/CoCreate-app/CoCreate-render.git
16
+ git remote add upstream https://gitlab.com/cocreatejs/CoCreate-render.git
17
17
  ```
18
18
 
19
19
  ## Create a Topic Branch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/render",
3
- "version": "1.50.0",
3
+ "version": "1.53.0",
4
4
  "description": "A simple render component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "render",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/CoCreate-app/CoCreate-render.git"
25
+ "url": "git+https://gitlab.com/cocreatejs/CoCreate-render.git"
26
26
  },
27
27
  "author": "CoCreate LLC",
28
28
  "license": "AGPL-3.0",
@@ -36,14 +36,14 @@
36
36
  },
37
37
  "main": "./src/index.js",
38
38
  "dependencies": {
39
- "@cocreate/actions": "^1.23.0",
40
- "@cocreate/api": "^1.27.0",
41
- "@cocreate/element-prototype": "^1.34.0",
42
- "@cocreate/observer": "^1.21.0",
43
- "@cocreate/utils": "^1.50.3"
39
+ "@cocreate/actions": "^1.24.0",
40
+ "@cocreate/api": "^1.31.0",
41
+ "@cocreate/element-prototype": "^1.35.0",
42
+ "@cocreate/observer": "^1.23.0",
43
+ "@cocreate/utils": "^1.54.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@cocreate/webpack": "^1.7.0"
46
+ "@cocreate/webpack": "^1.9.0"
47
47
  },
48
48
  "allowScripts": {
49
49
  "@cocreate/actions@1.21.4": true,
package/src/index.js CHANGED
@@ -34,9 +34,51 @@ import {
34
34
  import "@cocreate/element-prototype";
35
35
  import "./index.css";
36
36
 
37
- const sources = new Map();
38
- const renderedNodes = new Map();
39
- const elementSelector = "[render-query]";
37
+ const sources = new Map();
38
+ const renderedNodes = new Map();
39
+ const elementSelector = "[render-query]";
40
+
41
+ function getRenderContext(renderedNode, key) {
42
+ const keys = renderedNode?.keys;
43
+ if (!keys || Array.isArray(keys) || typeof keys !== "object") return;
44
+
45
+ const contextKey = key || Object.keys(keys).at(-1);
46
+ if (!contextKey || !keys[contextKey]) return;
47
+
48
+ return {
49
+ key: contextKey,
50
+ keyPath: keys[contextKey].path,
51
+ renderData: keys[contextKey].data,
52
+ renderAs: keys[contextKey].renderAs
53
+ };
54
+ }
55
+
56
+ function setRenderContext(renderedNode, context = {}) {
57
+ if (!renderedNode.keys || Array.isArray(renderedNode.keys))
58
+ renderedNode.keys = {};
59
+
60
+ const contextKey =
61
+ context.key || Object.keys(renderedNode.keys).at(-1) || "$default";
62
+ const renderContext = renderedNode.keys[contextKey] || {};
63
+ if (context.keyPath !== undefined)
64
+ renderContext.path = context.keyPath;
65
+ if (context.renderData !== undefined)
66
+ renderContext.data = context.renderData;
67
+ if (context.renderAs !== undefined)
68
+ renderContext.renderAs = context.renderAs;
69
+
70
+ // Reinsert the context so the last key always represents the active
71
+ // render scope without requiring top-level key metadata.
72
+ delete renderedNode.keys[contextKey];
73
+ renderedNode.keys[contextKey] = renderContext;
74
+
75
+ return {
76
+ key: contextKey,
77
+ keyPath: renderContext.path,
78
+ renderData: renderContext.data,
79
+ renderAs: renderContext.renderAs
80
+ };
81
+ }
40
82
 
41
83
  /**
42
84
  * Initializes elements based on specified rendering attributes. If a specific element is provided, it initializes that element.
@@ -82,7 +124,7 @@ function init(element) {
82
124
  } catch (error) {
83
125
  console.error("Failed to parse render-data:", error);
84
126
  }
85
- }
127
+ }
86
128
  }
87
129
  }
88
130
 
@@ -163,13 +205,19 @@ async function render({
163
205
  }
164
206
  }
165
207
 
166
- if (data.$filter) {
167
- index = index || data.$filter.startingIndex || data.$filter.index;
168
- update = update || data.$filter.update;
169
- remove = remove || data.$filter.remove;
208
+ if (data.filter) {
209
+ update ??= data.filter.update;
210
+ remove ??= data.filter.remove;
211
+
212
+ const isMutation = remove || update || data.filter.create;
213
+ if (index == null) {
214
+ index = isMutation
215
+ ? data.filter.index
216
+ : data.filter.startingIndex ?? data.filter.index;
217
+ }
170
218
  }
171
219
 
172
- let isMutation = remove || update || (data.$filter && data.$filter.create);
220
+ let isMutation = remove || update || (data.filter && data.filter.create);
173
221
 
174
222
  let sourceData = sources.get(source);
175
223
  if (!sourceData) {
@@ -232,12 +280,15 @@ async function render({
232
280
 
233
281
  let clone;
234
282
  if (remove) {
235
- for (let j = 0; j < data[key].length; j++) {
283
+ const removedItems = data[key];
284
+ if (!Array.isArray(removedItems) || !clones) continue;
285
+
286
+ for (let j = 0; j < removedItems.length; j++) {
236
287
  let cloneKey;
237
288
  if (key === "object") {
238
- cloneKey = data[key][j]._id;
289
+ cloneKey = removedItems[j]._id;
239
290
  } else {
240
- cloneKey = data[key][j].name;
291
+ cloneKey = removedItems[j].name;
241
292
  }
242
293
 
243
294
  clone = clones.get(cloneKey);
@@ -251,7 +302,7 @@ async function render({
251
302
  (key && (Array.isArray(data[key]) || renderType)) ||
252
303
  Array.isArray(data)
253
304
  ) {
254
- if (update) {
305
+ if (update && renderType !== "object") {
255
306
  for (let j = 0; j < data[key].length; j++) {
256
307
  if (key === "object") {
257
308
  clone = clones.get(data[key][j]._id);
@@ -259,7 +310,8 @@ async function render({
259
310
  clone = clones.get(data[key][j].name);
260
311
  }
261
312
 
262
- if (!currentIndex) currentIndex = data.$filter.currentIndex;
313
+ if (currentIndex == null)
314
+ currentIndex = data.filter.currentIndex;
263
315
 
264
316
  if (!clone) continue;
265
317
 
@@ -270,7 +322,7 @@ async function render({
270
322
  } else {
271
323
  // TODO: if $auto here every subsequent clone will have same value, not replacing here will apply unique value to each clone
272
324
  if (key === "$auto")
273
- key = key.replace(/\$auto/g, uuid(6));
325
+ key = key.replace(/\$auto/g, uuid(6));
274
326
 
275
327
  if (key) element[i].setAttribute("render", key);
276
328
 
@@ -289,15 +341,25 @@ async function render({
289
341
  }
290
342
 
291
343
  async function renderTemplate(template, data, key, index, keyPath) {
292
- if (!(template instanceof HTMLElement)) {
293
- console.log("template not an element but should be")
344
+ if (template && template.nodeType !== 1) {
345
+ template = template.ownerElement || template.parentElement;
294
346
  }
295
- let templateData = renderedNodes.get(template);
296
- if (!templateData) {
297
- templateData = {element: template, clones: new Map() };
298
- renderedNodes.set(template, templateData);
299
- }
300
-
347
+ if (!template) return;
348
+ if (template instanceof HTMLTemplateElement && !template.content.firstElementChild) return;
349
+ let templateData = renderedNodes.get(template);
350
+ if (!templateData) {
351
+ templateData = {
352
+ element: template,
353
+ clones: new Map(),
354
+ isTemplate: true
355
+ };
356
+ renderedNodes.set(template, templateData);
357
+ }
358
+
359
+ templateData.element = template;
360
+ if (!templateData.clones) templateData.clones = new Map();
361
+ templateData.isTemplate = true;
362
+
301
363
  if (!templateData.parent && !templateData.source) {
302
364
  let parentRender = template.parentElement.closest("[render]")
303
365
  if (parentRender || template.source)
@@ -306,65 +368,60 @@ async function renderTemplate(template, data, key, index, keyPath) {
306
368
  console.log("template parent missing is there no parent or is this an error? and source is", templateData.source)
307
369
  }
308
370
 
309
- if(!templateData.source && templateData.parent && templateData.parent.source){
371
+ if (!templateData.source && templateData.parent && templateData.parent.source) {
310
372
  templateData.source = templateData.parent.source
311
373
  }
312
374
 
313
- if (!templateData.keyPath) {
314
- if (keyPath) {
315
- templateData.keyPath = keyPath;
316
- } else if (templateData.parent && templateData.parent.keyPath) {
317
- templateData.keyPath = templateData.parent.keyPath;
318
- } else if (key) {
319
- templateData.keyPath = key;
320
- }
321
- }
322
-
323
- let renderData;
375
+ let renderData;
324
376
  if (key) {
325
377
  renderData = getRenderValue(templateData.element, data, key);
326
- }
327
-
328
- if (!renderData) {
329
- renderData = data;
330
378
  }
331
379
 
332
- templateData.renderData = renderData;
333
- let el = templateData.element;
380
+ if (renderData === undefined || renderData === null) {
381
+ renderData = key ? [] : data;
382
+ }
383
+
384
+ let el = templateData.element;
334
385
  if (el.nodeType !== 1) {
335
- el = templateData.parent.element;
386
+ el = templateData.parent.element;
336
387
  }
337
388
 
338
- let renderAs = templateData.renderAs
339
- if (!templateData.renderAs) {
340
- renderAs =
341
- el.getAttribute("render-as") ||
342
- el.getAttribute("render") ||
343
- key;
344
- templateData.renderAs = renderAs;
345
- }
389
+ let renderAs =
390
+ el.getAttribute("render-as") ||
391
+ el.getAttribute("render") ||
392
+ getRenderContext(templateData, key)?.renderAs ||
393
+ key;
394
+ const parentContext = getRenderContext(templateData.parent);
395
+ const resolvedKeyPath =
396
+ keyPath || key || parentContext?.keyPath;
397
+ const templateContext = setRenderContext(templateData, {
398
+ key,
399
+ keyPath: resolvedKeyPath,
400
+ renderData,
401
+ renderAs
402
+ });
346
403
 
347
404
  let renderType;
348
405
  if (templateData.source && templateData.source.element)
349
406
  renderType = templateData.source.element.getAttribute("render-type");
350
407
 
351
- let renderReverse;
352
- if (templateData.source && templateData.source.element) {
353
- renderReverse =
354
- templateData.source.element.getAttribute("render-reverse");
355
- if (renderReverse === null)
356
- renderReverse = el.getAttribute("render-reverse");
357
- }
408
+ const renderReverse =
409
+ templateData.source?.element?.getAttribute("render-reverse") ??
410
+ el.getAttribute("render-reverse");
358
411
 
359
412
  let exclude = el.getAttribute("render-exclude") || "";
360
413
  if (exclude) {
361
414
  exclude = exclude.replace(/ /g, "").split(",");
362
415
  }
416
+ const include = el.getAttribute("render-include");
417
+ const includePattern = include ? new RegExp(include) : null;
363
418
  let reference = el.getAttribute("render-reference");
364
419
 
365
- let isInsert = data.$filter && (data.$filter.create || data.$filter.update);
420
+ const isLoadMore = data.filter?.loadMore && !data.filter?.overwrite;
421
+ const appendIndex = templateData.clones.size;
422
+ let isInsert = data.filter && (data.filter.create || data.filter.update);
366
423
  if (renderType !== "object") {
367
- if ((!isInsert && !index) || data.$filter.overwrite) {
424
+ if ((!isInsert && !isLoadMore && !index) || data.filter?.overwrite) {
368
425
  if (!templateData.clones) {
369
426
  return
370
427
  if (templateData.template) {
@@ -379,7 +436,7 @@ async function renderTemplate(template, data, key, index, keyPath) {
379
436
  element.remove();
380
437
  templateData.clones.delete(key);
381
438
  }
382
- }
439
+ }
383
440
  }
384
441
 
385
442
  if ((key && !Array.isArray(renderData)) || renderType === "object") {
@@ -387,19 +444,35 @@ async function renderTemplate(template, data, key, index, keyPath) {
387
444
  renderData = renderData[0];
388
445
 
389
446
  const keys = Object.keys(renderData);
447
+ if (renderType === "object" && data.method?.endsWith(".read")) {
448
+ const present = new Set(keys);
449
+ for (const [id, element] of templateData.clones) {
450
+ if (present.has(element.getAttribute("renderedKey"))) continue;
451
+ renderedNodes.delete(element);
452
+ element.remove();
453
+ templateData.clones.delete(id);
454
+ }
455
+ }
456
+ const fieldClones = new Map(
457
+ Array.from(templateData.clones.values(), (element) => [
458
+ element.getAttribute("renderedKey"),
459
+ element
460
+ ])
461
+ );
390
462
  for (let i = 0; i < keys.length; i++) {
391
- let clone = templateData.clones.get(keys[i]);
463
+ let clone = fieldClones.get(keys[i]);
392
464
  clone = renderedNodes.get(clone);
393
465
 
394
466
  if (
395
467
  exclude.includes(keys[i]) ||
468
+ (includePattern && !includePattern.test(keys[i])) ||
396
469
  (reference === "false" &&
397
470
  ["$storage", "$database", "$array"].includes(key))
398
471
  ) {
399
472
  if (clone) {
400
473
  renderedNodes.delete(clone.element);
401
474
  clone.element.remove();
402
- templateData.clones.delete(keys[i]);
475
+ templateData.clones.delete(clone.element.getAttribute("eid"));
403
476
  }
404
477
  continue;
405
478
  }
@@ -410,7 +483,11 @@ async function renderTemplate(template, data, key, index, keyPath) {
410
483
  if (Array.isArray(value)) type = "array";
411
484
  else if (typeof value == "object") type = "object";
412
485
 
413
- let Data = { [renderAs]: { key: keys[i], value, type } };
486
+ // Nested object templates still need fields from the source record.
487
+ let Data = {
488
+ ...data,
489
+ [renderAs]: { key: keys[i], value, type }
490
+ };
414
491
 
415
492
  if (!clone) {
416
493
  clone = cloneTemplate(templateData);
@@ -424,13 +501,19 @@ async function renderTemplate(template, data, key, index, keyPath) {
424
501
  let renderedKey = key.split(".");
425
502
  renderedKey = renderedKey[renderedKey.length - 1];
426
503
 
427
- clone.key = keys[i];
428
- clone.keyPath = templateData.keyPath + "." + renderedKey;
429
- clone.parentKey = renderedKey;
430
- clone.renderKey = key;
504
+ const cloneKeyPath = `${templateContext.keyPath}.${renderedKey}`;
505
+ clone.parentKey = renderedKey;
506
+ clone.renderKey = key;
507
+ setRenderContext(clone, {
508
+ key: keys[i],
509
+ keyPath: cloneKeyPath,
510
+ renderData: Data,
511
+ renderAs
512
+ });
431
513
  clone.element.setAttribute("renderedKey", keys[i]);
432
514
  await renderValues(clone.element, Data, keys[i], renderAs);
433
515
  insertElement(templateData, clone.element, index);
516
+ fieldClones.set(keys[i], clone.element);
434
517
  }
435
518
  } else {
436
519
  if (!key && !Array.isArray(renderData)) {
@@ -441,7 +524,12 @@ async function renderTemplate(template, data, key, index, keyPath) {
441
524
 
442
525
  if (!renderData) {
443
526
  let clone = cloneTemplate(templateData);
444
- clone.keyPath = templateData.keyPath;
527
+ setRenderContext(clone, {
528
+ key,
529
+ keyPath: templateContext.keyPath,
530
+ renderData: data,
531
+ renderAs
532
+ });
445
533
  await renderValues(clone.element, data, key, renderAs);
446
534
  insertElement(templateData, clone.element, index);
447
535
  } else {
@@ -453,7 +541,7 @@ async function renderTemplate(template, data, key, index, keyPath) {
453
541
  let test = clone.element.getAttribute("render-as");
454
542
  if (test) renderAs = test;
455
543
 
456
- clone.keyPath = templateData.keyPath || "" + `[${i}]`;
544
+ const cloneKeyPath = `${templateContext.keyPath || ""}[${i}]`;
457
545
 
458
546
  let object;
459
547
  if (
@@ -480,11 +568,22 @@ async function renderTemplate(template, data, key, index, keyPath) {
480
568
  object = dotNotationToObject(object);
481
569
  } else object = object;
482
570
 
483
- let index;
484
- if (key !== "data") index = i;
571
+ let insertIndex;
572
+ if (key !== "data") {
573
+ insertIndex = isLoadMore
574
+ ? appendIndex + i
575
+ : data.filter?.create && index != null
576
+ ? index + i
577
+ : i;
578
+ }
485
579
 
486
- clone.renderData = object
487
- clone.index = index
580
+ setRenderContext(clone, {
581
+ key,
582
+ keyPath: cloneKeyPath,
583
+ renderData: object,
584
+ renderAs
585
+ });
586
+ clone.index = insertIndex
488
587
  await renderValues(
489
588
  clone.element,
490
589
  object,
@@ -492,12 +591,12 @@ async function renderTemplate(template, data, key, index, keyPath) {
492
591
  renderAs,
493
592
  null,
494
593
  null,
495
- index
594
+ insertIndex
496
595
  );
497
596
  insertElement(
498
597
  templateData,
499
598
  clone.element,
500
- index,
599
+ insertIndex,
501
600
  "",
502
601
  data,
503
602
  renderReverse
@@ -507,17 +606,17 @@ async function renderTemplate(template, data, key, index, keyPath) {
507
606
  }
508
607
  }
509
608
 
510
- function cloneTemplate(template) {
609
+ function cloneTemplate(template) {
511
610
  let clone = template.element.cloneNode(true);
512
611
  let cloneTagName = clone.tagName;
513
612
 
514
613
  if (clone.tagName == "TEMPLATE") {
515
- clone = template.element.content.firstElementChild;
614
+ clone = clone.content.firstElementChild;
516
615
  for (let attribute of template.element.attributes) {
517
616
  clone.setAttribute(attribute.name, attribute.value || "");
518
617
  }
519
618
  } else {
520
- clone.classList.remove("template");
619
+ clone.classList?.remove("template");
521
620
  clone.removeAttribute("template");
522
621
  }
523
622
 
@@ -525,7 +624,7 @@ function cloneTemplate(template) {
525
624
 
526
625
  let renderAs = clone.getAttribute("render-as");
527
626
  if (renderAs === "$auto") {
528
- renderAs = uuid(6);
627
+ renderAs = uuid(6);
529
628
  clone = clone.outerHTML.replace(/\$auto/g, renderAs);
530
629
  }
531
630
 
@@ -542,10 +641,27 @@ function cloneTemplate(template) {
542
641
 
543
642
  let renderedNode = { template, element: clone };
544
643
  renderedNodes.set(clone, renderedNode);
545
- return renderedNode;
546
- }
547
-
548
- function insertElement(
644
+ return renderedNode;
645
+ }
646
+
647
+ function getCloneIdentity(data, renderAs) {
648
+ const value = renderAs ? getValueFromObject(data, renderAs) : data;
649
+ if (!value || typeof value !== "object") return;
650
+
651
+ for (const key of ["_id", "id", "object", "name", "key"]) {
652
+ const identity = value[key];
653
+ if (
654
+ (typeof identity === "string" || typeof identity === "number") &&
655
+ identity !== "" &&
656
+ identity !== "{}" &&
657
+ identity !== "[]"
658
+ ) {
659
+ return String(identity);
660
+ }
661
+ }
662
+ }
663
+
664
+ function insertElement(
549
665
  template,
550
666
  element,
551
667
  index,
@@ -555,7 +671,7 @@ function insertElement(
555
671
  ) {
556
672
  let eid = element.getAttribute("eid");
557
673
  if (!eid) {
558
- eid = uuid(6);
674
+ eid = uuid(6);
559
675
  element.setAttribute("eid", eid);
560
676
  }
561
677
 
@@ -574,11 +690,11 @@ function insertElement(
574
690
  item = clones.splice(currentIndex, 1)[0];
575
691
  }
576
692
 
577
- if (data && data.$filter && data.$filter.startingIndex)
578
- index += data.$filter.startingIndex;
693
+ if (data && data.filter && data.filter.startingIndex && !data.filter.loadMore)
694
+ index += data.filter.startingIndex;
579
695
  clones.splice(index, 0, item); // Insert item into clones at the specified index
580
696
 
581
- if (renderReverse !== null && renderReverse !== "false" ) {
697
+ if (renderReverse != null && renderReverse !== false && renderReverse !== "false") {
582
698
  // Chat-specific logic: reverse rendering but still inserting based on sorting
583
699
  if (clones[index + 1] && clones[index + 1][1] !== element) {
584
700
  clones[index + 1][1].insertAdjacentElement("afterend", element);
@@ -612,50 +728,47 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
612
728
  if (!data) return;
613
729
 
614
730
  let renderedNode = renderedNodes.get(node);
615
- if (!renderedNode) renderedNode = { key, renderAs };
616
-
617
- if (!renderedNode.key) renderedNode.key = key;
618
- else if (!key) key = renderedNode.key;
619
-
620
- if (!renderedNode.renderAs) renderedNode.renderAs = renderAs;
621
- else if (!renderAs) renderAs = renderedNode.renderAs;
622
-
623
- if (keyPath) renderedNode.keyPath = keyPath;
624
- else if (renderedNode.keyPath) keyPath = renderedNode.keyPath;
731
+ if (!renderedNode) {
732
+ renderedNode = { element: node };
733
+ renderedNodes.set(node, renderedNode);
734
+ }
735
+
736
+ const previousContext = getRenderContext(renderedNode, key);
737
+ if (!key) key = previousContext?.key;
738
+ if (!renderAs) renderAs = previousContext?.renderAs;
739
+ if (!keyPath) keyPath = previousContext?.keyPath;
740
+ setRenderContext(renderedNode, {
741
+ key,
742
+ keyPath,
743
+ renderData: data,
744
+ renderAs
745
+ });
625
746
 
626
747
  if (parent) renderedNode.parent = parent;
627
748
 
628
- if (node.nodeType == 1) {
629
- if (node.hasAttribute("render-clone")) {
630
- parent = renderedNode;
631
-
632
- for (let eid of ["_id", "name", "key"]) {
633
- if (renderAs) eid = data[renderAs][eid];
634
- else eid = data[eid];
635
-
636
- if (!eid) continue;
637
-
638
- let oldEid = renderedNode.eid;
639
- let temp = renderedNode.template;
640
- if (!temp) {
641
- console.log("temp could not be found");
642
- } else if (!temp.clones) {
643
- if (temp.template) temp = temp.template;
644
- else console.log(temp);
645
-
646
- if (oldEid && oldEid !== eid) temp.clones.delete(oldEid);
647
-
749
+ if (node.nodeType == 1) {
750
+ if (node.hasAttribute("render-clone")) {
751
+ parent = renderedNode;
752
+
753
+ const eid = getCloneIdentity(data, renderAs);
754
+ if (eid) {
755
+ let oldEid = renderedNode.eid;
756
+ let temp = renderedNode.template;
757
+ while (temp && !temp.clones)
758
+ temp = temp.template || temp.parent;
759
+
760
+ if (temp?.clones && oldEid && oldEid !== eid) {
761
+ temp.clones.delete(oldEid);
648
762
  temp.clones.set(eid, node);
649
763
  }
650
764
 
651
765
  renderedNode.eid = eid;
766
+
767
+ node.setAttribute("eid", eid);
768
+ }
769
+ }
652
770
 
653
- node.setAttribute("eid", eid);
654
- break;
655
- }
656
- }
657
-
658
- for (let attr of node.attributes) {
771
+ for (let attr of Array.from(node.attributes)) {
659
772
  let name = attr.name;
660
773
  let value = attr.value;
661
774
 
@@ -667,6 +780,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
667
780
  renderAs,
668
781
  parent: renderedNode
669
782
  };
783
+ renderedNodes.set(attr, renderedAttribute);
670
784
  }
671
785
 
672
786
  let namePlaceholder = renderedAttribute.placeholder.name || name;
@@ -688,15 +802,13 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
688
802
  renderedAttribute,
689
803
  index
690
804
  );
691
- if (namePlaceholder.includes("{{") && name) {
692
- const attributes = name.match(/([^\s]+="[^"]*"|[^\s]+)/g) || [];
693
- attributes.forEach((attr) => {
694
- let [attributeName, attributeValue] = attr.split("=");
695
- attributeValue = attributeValue
696
- ? attributeValue.replace(/"/g, "")
697
- : "";
698
- node.setAttribute(attributeName, attributeValue);
699
- });
805
+ if (namePlaceholder.includes("{{") && name && !name.includes("{{")) {
806
+ if (name !== attr.name) {
807
+ node.removeAttribute(attr.name);
808
+ renderedNodes.delete(attr);
809
+ }
810
+ node.setAttribute(name, value ?? "");
811
+ renderedNodes.set(node.getAttributeNode(name), renderedAttribute);
700
812
  } else if (name === undefined || name === null || name === "") {
701
813
  renderedNodes.delete(attr);
702
814
  node.removeAttribute(attr.name);
@@ -732,11 +844,22 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
732
844
  return;
733
845
  }
734
846
  }
847
+
848
+ // A nested render-query owns its template and descendants. Parent
849
+ // rendering may resolve this controller's attributes, but its own
850
+ // render cycle must populate its target.
851
+ if (
852
+ node.hasAttribute("render-query") &&
853
+ !node.hasAttribute("render-clone")
854
+ ) {
855
+ return;
856
+ }
857
+
735
858
  if (node.hasAttribute("render") && !node.hasAttribute("render-clone")) {
736
859
  let renderKey = node.getAttribute("render") || key
737
- renderTemplate(node, data, renderKey);
738
- } else if (node.childNodes.length > 0) {
739
- for (let childNode of node.childNodes) {
860
+ await renderTemplate(node, data, renderKey);
861
+ } else if (node.childNodes.length > 0) {
862
+ for (let childNode of Array.from(node.childNodes)) {
740
863
  await renderValues(
741
864
  childNode,
742
865
  data,
@@ -792,10 +915,12 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
792
915
  };
793
916
  }
794
917
  renderedParent.text = text;
795
- for (let newChild of newNode.childNodes) {
796
- renderedNode.element = newChild;
797
- renderedNodes.set(newChild, renderedNode);
798
- }
918
+ for (let newChild of newNode.childNodes) {
919
+ renderedNodes.set(newChild, {
920
+ ...renderedNode,
921
+ element: newChild
922
+ });
923
+ }
799
924
  renderedNodes.delete(node);
800
925
  node.replaceWith(...newNode.childNodes);
801
926
  }
@@ -856,7 +981,7 @@ async function renderValue(
856
981
  match = output.match(regex);
857
982
  if (match) {
858
983
  let value;
859
- let placeholderContent = match[1].trim();
984
+ let placeholderContent = match[1].trim();
860
985
 
861
986
  try {
862
987
  if (placeholderContent === "index" && index >= 0) {
@@ -894,7 +1019,7 @@ async function renderValue(
894
1019
  });
895
1020
  }
896
1021
  }
897
-
1022
+
898
1023
  for (let r of replacements) {
899
1024
  preProcessed = preProcessed.replace(r.original, r.replacement);
900
1025
  }
@@ -912,7 +1037,8 @@ async function renderValue(
912
1037
  node,
913
1038
  data,
914
1039
  preProcessed,
915
- renderAs
1040
+ renderAs,
1041
+ index
916
1042
  );
917
1043
  }
918
1044
  } catch (error) {
@@ -948,7 +1074,7 @@ async function renderValue(
948
1074
  return output;
949
1075
  }
950
1076
 
951
- function getRenderValue(node, data, key, renderAs) {
1077
+ function getRenderValue(node, data, key, renderAs, renderIndex) {
952
1078
  let operator;
953
1079
  if (key.startsWith("$")) {
954
1080
  let keys = key.split(".");
@@ -958,20 +1084,37 @@ function getRenderValue(node, data, key, renderAs) {
958
1084
  }
959
1085
  }
960
1086
 
961
- let value = getValueFromObject(data, key);
962
-
963
- if (!value && value !== "" && node) {
964
- let parentTemplate = node;
965
- do {
966
- let parentNode = renderedNodes.get(parentTemplate);
967
- if (parentNode) {
968
- if (!value && (parentNode.parent || parentNode.template)) {
969
- let Data,
970
- eid,
971
- parent = parentNode.parent || parentNode.template;
972
- do {
973
- if (key.includes("keyPath")) {
974
- value = parentNode.keyPath || parent.keyPath;
1087
+ // Field reads can arrive wrapped under a literal dotted or bracketed key.
1088
+ const hasLiteralKey = Object.prototype.hasOwnProperty.call(data, key);
1089
+ const rootKey = key.split(/[.\[]/, 1)[0];
1090
+ const ownsRootScope =
1091
+ hasLiteralKey || Object.prototype.hasOwnProperty.call(data, rootKey);
1092
+ let value = hasLiteralKey
1093
+ ? data[key]
1094
+ : getValueFromObject(data, key);
1095
+
1096
+ // Once the current scope owns a root (for example `object`), a missing
1097
+ // descendant must remain missing instead of leaking in from another
1098
+ // clone's ancestor/source data.
1099
+ if ((value === undefined || value === null) && node && !ownsRootScope) {
1100
+ let parentTemplate = node;
1101
+ do {
1102
+ let parentNode = renderedNodes.get(parentTemplate);
1103
+ if (parentNode) {
1104
+ if (
1105
+ (value === undefined || value === null) &&
1106
+ (parentNode.parent || parentNode.template)
1107
+ ) {
1108
+ let Data,
1109
+ eid,
1110
+ parent = parentNode.parent || parentNode.template;
1111
+ do {
1112
+ const parentNodeContext = getRenderContext(parentNode);
1113
+ const parentContext = getRenderContext(parent);
1114
+ if (key.includes("keyPath")) {
1115
+ value =
1116
+ parentNodeContext?.keyPath ||
1117
+ parentContext?.keyPath;
975
1118
  } else if (key.includes("parentKey")) {
976
1119
  value =
977
1120
  parentNode.parentKey ||
@@ -982,7 +1125,7 @@ function getRenderValue(node, data, key, renderAs) {
982
1125
  } else if (key === "object" || key === "_id")
983
1126
  value = ObjectId().toString();
984
1127
  else if (key === "uuid") {
985
- value = uuid(6);
1128
+ value = uuid(6);
986
1129
  } else if (parent.source) {
987
1130
  Data = parent.source.data;
988
1131
  } else if (parent.parent) {
@@ -994,28 +1137,36 @@ function getRenderValue(node, data, key, renderAs) {
994
1137
  }
995
1138
 
996
1139
  if (!Data && parent && parent.eid) eid = parent.eid;
997
- } while (!value && parent && !Data);
998
-
999
- if (!value && Data) {
1000
- value = getValueFromObject(Data, key);
1001
- }
1002
- if (!value && key.includes(".")) {
1140
+ } while (
1141
+ (value === undefined || value === null) &&
1142
+ parent &&
1143
+ !Data
1144
+ );
1145
+
1146
+ if ((value === undefined || value === null) && Data) {
1147
+ value = getValueFromObject(Data, key);
1148
+ }
1149
+ if (
1150
+ (value === undefined || value === null) &&
1151
+ key.includes(".")
1152
+ ) {
1003
1153
  let renderedData = getValueFromObject(
1004
1154
  Data,
1005
1155
  key.split(".")[0]
1006
1156
  );
1007
- if (renderedData) {
1008
- let index;
1157
+ if (renderedData !== undefined && renderedData !== null) {
1158
+ let index = renderIndex;
1009
1159
  if (
1010
- !parent.clones.size ||
1011
- parent.clones.size === 1
1160
+ index === undefined &&
1161
+ (!parent.clones.size ||
1162
+ parent.clones.size === 1)
1012
1163
  ) {
1013
1164
  index = 0;
1014
- } else if (eid) {
1165
+ } else if (index === undefined && eid) {
1015
1166
  index = Array.from(
1016
1167
  parent.clones.keys()
1017
1168
  ).indexOf(eid);
1018
- } else console.log("eid not found");
1169
+ }
1019
1170
 
1020
1171
  if (index >= 0) {
1021
1172
  Data = {
@@ -1028,13 +1179,16 @@ function getRenderValue(node, data, key, renderAs) {
1028
1179
  }
1029
1180
  }
1030
1181
 
1031
- if (!value && parentTemplate.parentElement) {
1182
+ if (
1183
+ (value === undefined || value === null) &&
1184
+ parentTemplate.parentElement
1185
+ ) {
1032
1186
  parentTemplate =
1033
1187
  parentTemplate.parentElement.closest("[render]");
1034
1188
  } else {
1035
1189
  parentTemplate = undefined;
1036
1190
  }
1037
- } while (!value && parentTemplate);
1191
+ } while ((value === undefined || value === null) && parentTemplate);
1038
1192
  }
1039
1193
 
1040
1194
  if (operator) {
@@ -1150,12 +1304,13 @@ Observer.init({
1150
1304
  let parentElement = mutation.target.parentElement.closest("[render]");
1151
1305
  if (!parentElement) return;
1152
1306
 
1153
- let renderedNode = renderedNodes.get(parentElement);
1154
- let data;
1155
- if (renderedNode) {
1156
- if (renderedNode.renderData) {
1157
- data = renderedNode.renderData;
1158
- }
1307
+ let renderedNode = renderedNodes.get(parentElement);
1308
+ let data;
1309
+ if (renderedNode) {
1310
+ const renderContext = getRenderContext(renderedNode);
1311
+ if (renderContext?.renderData) {
1312
+ data = renderContext.renderData;
1313
+ }
1159
1314
  } else {
1160
1315
  let sourceElement = mutation.target.parentElement.closest("[render-query]")
1161
1316
  let sourceData = sources.get(sourceElement);
@@ -1184,4 +1339,4 @@ Observer.init({
1184
1339
 
1185
1340
  init();
1186
1341
 
1187
- export { render, renderValue, sources, renderedNodes };
1342
+ export { render, renderValue, sources, renderedNodes };
@@ -0,0 +1,90 @@
1
+ const assert = require('node:assert/strict');
2
+ const { readFileSync } = require('node:fs');
3
+ const { join } = require('node:path');
4
+ const test = require('node:test');
5
+ const vm = require('node:vm');
6
+
7
+ const source = readFileSync(join(__dirname, '../src/index.js'), 'utf8');
8
+ const renderTemplate = source.slice(source.indexOf('async function renderTemplate('), source.indexOf('function cloneTemplate('));
9
+ const insertElement = source.slice(source.indexOf('function insertElement('), source.indexOf('async function renderValues('));
10
+ const regions = ['us-east-1', 'us-east-2', 'us-west-1', 'me-south-1'];
11
+
12
+ async function renderOrder({ templateReverse = null, sourceReverse, withSource = false } = {}) {
13
+ const siblings = [];
14
+ let nextId = 0;
15
+
16
+ class Element {
17
+ constructor(reverse = null) {
18
+ this.nodeType = 1;
19
+ this.attributes = new Map();
20
+ if (reverse !== null) this.attributes.set('render-reverse', reverse);
21
+ }
22
+ getAttribute(name) { return this.attributes.get(name) ?? null; }
23
+ setAttribute(name, value) { this.attributes.set(name, value); }
24
+ insertAdjacentElement(position, element) {
25
+ assert.ok(['beforebegin', 'afterend'].includes(position));
26
+ const previous = siblings.indexOf(element);
27
+ if (previous >= 0) siblings.splice(previous, 1);
28
+ siblings.splice(siblings.indexOf(this) + (position === 'afterend' ? 1 : 0), 0, element);
29
+ }
30
+ }
31
+
32
+ const template = new Element(templateReverse);
33
+ template.setAttribute('render-as', 'region');
34
+ siblings.push(template);
35
+ const templateData = { element: template, clones: new Map(), parent: {} };
36
+ if (withSource) templateData.source = { element: new Element(sourceReverse) };
37
+
38
+ const context = vm.createContext({
39
+ template,
40
+ data: { regions: regions.map(region => ({ region })) },
41
+ renderedNodes: new Map([[template, templateData]]),
42
+ HTMLTemplateElement: class {},
43
+ uuid: () => String(++nextId),
44
+ getRenderValue: (element, data, key) => data[key],
45
+ getRenderContext: () => undefined,
46
+ setRenderContext: (node, context) => context,
47
+ cloneTemplate: () => ({ element: new Element() }),
48
+ // Value interpolation is independent of list insertion and reverse settings.
49
+ renderValues: async (element, data) => { element.region = data.region.region; }
50
+ });
51
+ vm.runInContext(`${renderTemplate}\n${insertElement}\nglobalThis.done = renderTemplate(template, data, 'regions');`, context);
52
+ await context.done;
53
+ return siblings.filter(element => element !== template).map(element => element.region);
54
+ }
55
+
56
+ test('nested region arrays preserve input order without a source or reverse attribute', async () => {
57
+ assert.deepEqual(await renderOrder(), regions);
58
+ });
59
+
60
+ test('a source with no reverse attribute preserves input order', async () => {
61
+ assert.deepEqual(await renderOrder({ withSource: true }), regions);
62
+ });
63
+
64
+ test('false reverse settings preserve input order', async t => {
65
+ for (const value of [false, 'false']) {
66
+ await t.test(String(value) + ' (' + typeof value + ')', async () => {
67
+ assert.deepEqual(await renderOrder({ templateReverse: value }), regions);
68
+ assert.deepEqual(await renderOrder({ withSource: true, sourceReverse: value }), regions);
69
+ });
70
+ }
71
+ });
72
+
73
+ test('explicit reverse settings still reverse the list', async t => {
74
+ for (const value of ['', true, 'true']) {
75
+ await t.test(JSON.stringify(value), async () => {
76
+ const expected = [...regions].reverse();
77
+ assert.deepEqual(await renderOrder({ templateReverse: value }), expected);
78
+ assert.deepEqual(await renderOrder({ withSource: true, sourceReverse: value }), expected);
79
+ });
80
+ }
81
+ });
82
+
83
+ test('template reverse settings apply when the source attribute is absent', async () => {
84
+ assert.deepEqual(await renderOrder({ withSource: true, templateReverse: '' }), [...regions].reverse());
85
+ });
86
+
87
+ test('an explicit source setting takes precedence over the template', async () => {
88
+ assert.deepEqual(await renderOrder({ withSource: true, sourceReverse: 'false', templateReverse: '' }), regions);
89
+ assert.deepEqual(await renderOrder({ withSource: true, sourceReverse: '', templateReverse: 'false' }), [...regions].reverse());
90
+ });