@cocreate/render 1.51.0 → 2.0.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.
@@ -18,7 +18,7 @@
18
18
  // Commercial Licensing Information:
19
19
  // For commercial use of this software without the copyleft provisions of the AGPLv3,
20
20
  // you must obtain a commercial license from CoCreate LLC.
21
- // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
21
+ // For details, visit <https://CoCreateJS.com/licenses/> or contact us at sales@CoCreateJS.com.
22
22
 
23
23
  /*globals CustomEvent*/
24
24
  import Actions from "@cocreate/actions";
@@ -28,15 +28,67 @@ import {
28
28
  getValueFromObject,
29
29
  dotNotationToObject,
30
30
  ObjectId,
31
- astAsync,
31
+ renderValue as renderValueUtility,
32
32
  uuid
33
33
  } from "@cocreate/utils";
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
+ /**
38
+ *
39
+ * @public
40
+ * @environment client
41
+ */
42
+ const sources = new Map();
43
+ /**
44
+ *
45
+ * @public
46
+ * @environment client
47
+ */
48
+ const renderedNodes = new Map();
49
+ const elementSelector = "[render-query]";
50
+
51
+ function getRenderContext(renderedNode, key) {
52
+ const keys = renderedNode?.keys;
53
+ if (!keys || Array.isArray(keys) || typeof keys !== "object") return;
54
+
55
+ const contextKey = key || Object.keys(keys).at(-1);
56
+ if (!contextKey || !keys[contextKey]) return;
57
+
58
+ return {
59
+ key: contextKey,
60
+ keyPath: keys[contextKey].path,
61
+ renderData: keys[contextKey].data,
62
+ renderAs: keys[contextKey].renderAs
63
+ };
64
+ }
65
+
66
+ function setRenderContext(renderedNode, context = {}) {
67
+ if (!renderedNode.keys || Array.isArray(renderedNode.keys))
68
+ renderedNode.keys = {};
69
+
70
+ const contextKey =
71
+ context.key || Object.keys(renderedNode.keys).at(-1) || "$default";
72
+ const renderContext = renderedNode.keys[contextKey] || {};
73
+ if (context.keyPath !== undefined)
74
+ renderContext.path = context.keyPath;
75
+ if (context.renderData !== undefined)
76
+ renderContext.data = context.renderData;
77
+ if (context.renderAs !== undefined)
78
+ renderContext.renderAs = context.renderAs;
79
+
80
+ // Reinsert the context so the last key always represents the active
81
+ // render scope without requiring top-level key metadata.
82
+ delete renderedNode.keys[contextKey];
83
+ renderedNode.keys[contextKey] = renderContext;
84
+
85
+ return {
86
+ key: contextKey,
87
+ keyPath: renderContext.path,
88
+ renderData: renderContext.data,
89
+ renderAs: renderContext.renderAs
90
+ };
91
+ }
40
92
 
41
93
  /**
42
94
  * Initializes elements based on specified rendering attributes. If a specific element is provided, it initializes that element.
@@ -49,6 +101,8 @@ const elementSelector = "[render-query]";
49
101
  * - If an HTMLCollection or an array of elements is provided, each element in the collection/array is initialized.
50
102
  * - If a single element is provided, only that element is initialized.
51
103
  * - If null or omitted, the function queries and initializes all elements matching the 'elementSelector'.
104
+ * @private
105
+ * @environment client
52
106
  */
53
107
  function init(element) {
54
108
  if (
@@ -82,7 +136,7 @@ function init(element) {
82
136
  } catch (error) {
83
137
  console.error("Failed to parse render-data:", error);
84
138
  }
85
- }
139
+ }
86
140
  }
87
141
  }
88
142
 
@@ -100,6 +154,8 @@ function init(element) {
100
154
  * @param {number} [options.currentIndex] - The current index in a list or array rendering process.
101
155
  * @param {boolean} [options.update] - Flag to indicate if the rendering should update existing content.
102
156
  * @param {boolean} [options.remove] - Flag to indicate if the rendering should remove elements instead of updating or appending.
157
+ * @public
158
+ * @environment client
103
159
  */
104
160
  async function render({
105
161
  source,
@@ -163,13 +219,19 @@ async function render({
163
219
  }
164
220
  }
165
221
 
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;
222
+ if (data.filter) {
223
+ update ??= data.filter.update;
224
+ remove ??= data.filter.remove;
225
+
226
+ const isMutation = remove || update || data.filter.create;
227
+ if (index == null) {
228
+ index = isMutation
229
+ ? data.filter.index
230
+ : data.filter.startingIndex ?? data.filter.index;
231
+ }
170
232
  }
171
233
 
172
- let isMutation = remove || update || (data.$filter && data.$filter.create);
234
+ let isMutation = remove || update || (data.filter && data.filter.create);
173
235
 
174
236
  let sourceData = sources.get(source);
175
237
  if (!sourceData) {
@@ -190,6 +252,8 @@ async function render({
190
252
  element = [element];
191
253
  }
192
254
 
255
+ const renderedNodesToClear = new Set();
256
+
193
257
  for (let i = 0; i < element.length; i++) {
194
258
  let renderedNode = renderedNodes.get(element[i]);
195
259
  if (renderedNode) {
@@ -211,6 +275,7 @@ async function render({
211
275
  };
212
276
  renderedNodes.set(element[i], renderedNode);
213
277
  }
278
+ renderedNodesToClear.add(renderedNode);
214
279
 
215
280
  let clones = renderedNode.clones; // || renderedNode.template.clones..
216
281
  if (!clones && renderedNode.template) {
@@ -232,12 +297,15 @@ async function render({
232
297
 
233
298
  let clone;
234
299
  if (remove) {
235
- for (let j = 0; j < data[key].length; j++) {
300
+ const removedItems = data[key];
301
+ if (!Array.isArray(removedItems) || !clones) continue;
302
+
303
+ for (let j = 0; j < removedItems.length; j++) {
236
304
  let cloneKey;
237
305
  if (key === "object") {
238
- cloneKey = data[key][j]._id;
306
+ cloneKey = removedItems[j]._id;
239
307
  } else {
240
- cloneKey = data[key][j].name;
308
+ cloneKey = removedItems[j].name;
241
309
  }
242
310
 
243
311
  clone = clones.get(cloneKey);
@@ -251,7 +319,7 @@ async function render({
251
319
  (key && (Array.isArray(data[key]) || renderType)) ||
252
320
  Array.isArray(data)
253
321
  ) {
254
- if (update) {
322
+ if (update && renderType !== "object") {
255
323
  for (let j = 0; j < data[key].length; j++) {
256
324
  if (key === "object") {
257
325
  clone = clones.get(data[key][j]._id);
@@ -259,7 +327,8 @@ async function render({
259
327
  clone = clones.get(data[key][j].name);
260
328
  }
261
329
 
262
- if (!currentIndex) currentIndex = data.$filter.currentIndex;
330
+ if (currentIndex == null)
331
+ currentIndex = data.filter.currentIndex;
263
332
 
264
333
  if (!clone) continue;
265
334
 
@@ -270,7 +339,7 @@ async function render({
270
339
  } else {
271
340
  // TODO: if $auto here every subsequent clone will have same value, not replacing here will apply unique value to each clone
272
341
  if (key === "$auto")
273
- key = key.replace(/\$auto/g, uuid(6));
342
+ key = key.replace(/\$auto/g, uuid(6));
274
343
 
275
344
  if (key) element[i].setAttribute("render", key);
276
345
 
@@ -281,6 +350,10 @@ async function render({
281
350
  await renderValues(element[i], data, key, renderAs);
282
351
  }
283
352
  }
353
+ for (const renderedNode of renderedNodesToClear) {
354
+ clearRendering(renderedNode);
355
+ }
356
+
284
357
  // Dispatch input and change events after rendering is complete
285
358
  if (source instanceof HTMLElement) {
286
359
  source.dispatchEvent(new Event("input"));
@@ -289,15 +362,25 @@ async function render({
289
362
  }
290
363
 
291
364
  async function renderTemplate(template, data, key, index, keyPath) {
292
- if (!(template instanceof HTMLElement)) {
293
- console.log("template not an element but should be")
365
+ if (template && template.nodeType !== 1) {
366
+ template = template.ownerElement || template.parentElement;
294
367
  }
295
- let templateData = renderedNodes.get(template);
296
- if (!templateData) {
297
- templateData = {element: template, clones: new Map() };
298
- renderedNodes.set(template, templateData);
299
- }
300
-
368
+ if (!template) return;
369
+ if (template instanceof HTMLTemplateElement && !template.content.firstElementChild) return;
370
+ let templateData = renderedNodes.get(template);
371
+ if (!templateData) {
372
+ templateData = {
373
+ element: template,
374
+ clones: new Map(),
375
+ isTemplate: true
376
+ };
377
+ renderedNodes.set(template, templateData);
378
+ }
379
+
380
+ templateData.element = template;
381
+ if (!templateData.clones) templateData.clones = new Map();
382
+ templateData.isTemplate = true;
383
+
301
384
  if (!templateData.parent && !templateData.source) {
302
385
  let parentRender = template.parentElement.closest("[render]")
303
386
  if (parentRender || template.source)
@@ -306,65 +389,60 @@ async function renderTemplate(template, data, key, index, keyPath) {
306
389
  console.log("template parent missing is there no parent or is this an error? and source is", templateData.source)
307
390
  }
308
391
 
309
- if(!templateData.source && templateData.parent && templateData.parent.source){
392
+ if (!templateData.source && templateData.parent && templateData.parent.source) {
310
393
  templateData.source = templateData.parent.source
311
394
  }
312
395
 
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;
396
+ let renderData;
324
397
  if (key) {
325
398
  renderData = getRenderValue(templateData.element, data, key);
326
- }
327
-
328
- if (!renderData) {
329
- renderData = data;
330
399
  }
331
400
 
332
- templateData.renderData = renderData;
333
- let el = templateData.element;
401
+ if (renderData === undefined || renderData === null) {
402
+ renderData = key ? [] : data;
403
+ }
404
+
405
+ let el = templateData.element;
334
406
  if (el.nodeType !== 1) {
335
- el = templateData.parent.element;
407
+ el = templateData.parent.element;
336
408
  }
337
409
 
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
- }
410
+ let renderAs =
411
+ el.getAttribute("render-as") ||
412
+ el.getAttribute("render") ||
413
+ getRenderContext(templateData, key)?.renderAs ||
414
+ key;
415
+ const parentContext = getRenderContext(templateData.parent);
416
+ const resolvedKeyPath =
417
+ keyPath || key || parentContext?.keyPath;
418
+ const templateContext = setRenderContext(templateData, {
419
+ key,
420
+ keyPath: resolvedKeyPath,
421
+ renderData,
422
+ renderAs
423
+ });
346
424
 
347
425
  let renderType;
348
426
  if (templateData.source && templateData.source.element)
349
427
  renderType = templateData.source.element.getAttribute("render-type");
350
428
 
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
- }
429
+ const renderReverse =
430
+ templateData.source?.element?.getAttribute("render-reverse") ??
431
+ el.getAttribute("render-reverse");
358
432
 
359
433
  let exclude = el.getAttribute("render-exclude") || "";
360
434
  if (exclude) {
361
435
  exclude = exclude.replace(/ /g, "").split(",");
362
436
  }
437
+ const include = el.getAttribute("render-include");
438
+ const includePattern = include ? new RegExp(include) : null;
363
439
  let reference = el.getAttribute("render-reference");
364
440
 
365
- let isInsert = data.$filter && (data.$filter.create || data.$filter.update);
441
+ const isLoadMore = data.filter?.loadMore && !data.filter?.overwrite;
442
+ const appendIndex = templateData.clones.size;
443
+ let isInsert = data.filter && (data.filter.create || data.filter.update);
366
444
  if (renderType !== "object") {
367
- if ((!isInsert && !index) || data.$filter.overwrite) {
445
+ if ((!isInsert && !isLoadMore && !index) || data.filter?.overwrite) {
368
446
  if (!templateData.clones) {
369
447
  return
370
448
  if (templateData.template) {
@@ -379,7 +457,7 @@ async function renderTemplate(template, data, key, index, keyPath) {
379
457
  element.remove();
380
458
  templateData.clones.delete(key);
381
459
  }
382
- }
460
+ }
383
461
  }
384
462
 
385
463
  if ((key && !Array.isArray(renderData)) || renderType === "object") {
@@ -387,19 +465,35 @@ async function renderTemplate(template, data, key, index, keyPath) {
387
465
  renderData = renderData[0];
388
466
 
389
467
  const keys = Object.keys(renderData);
468
+ if (renderType === "object" && data.method?.endsWith(".read")) {
469
+ const present = new Set(keys);
470
+ for (const [id, element] of templateData.clones) {
471
+ if (present.has(element.getAttribute("renderedKey"))) continue;
472
+ renderedNodes.delete(element);
473
+ element.remove();
474
+ templateData.clones.delete(id);
475
+ }
476
+ }
477
+ const fieldClones = new Map(
478
+ Array.from(templateData.clones.values(), (element) => [
479
+ element.getAttribute("renderedKey"),
480
+ element
481
+ ])
482
+ );
390
483
  for (let i = 0; i < keys.length; i++) {
391
- let clone = templateData.clones.get(keys[i]);
484
+ let clone = fieldClones.get(keys[i]);
392
485
  clone = renderedNodes.get(clone);
393
486
 
394
487
  if (
395
488
  exclude.includes(keys[i]) ||
489
+ (includePattern && !includePattern.test(keys[i])) ||
396
490
  (reference === "false" &&
397
491
  ["$storage", "$database", "$array"].includes(key))
398
492
  ) {
399
493
  if (clone) {
400
494
  renderedNodes.delete(clone.element);
401
495
  clone.element.remove();
402
- templateData.clones.delete(keys[i]);
496
+ templateData.clones.delete(clone.element.getAttribute("eid"));
403
497
  }
404
498
  continue;
405
499
  }
@@ -410,7 +504,11 @@ async function renderTemplate(template, data, key, index, keyPath) {
410
504
  if (Array.isArray(value)) type = "array";
411
505
  else if (typeof value == "object") type = "object";
412
506
 
413
- let Data = { [renderAs]: { key: keys[i], value, type } };
507
+ // Nested object templates still need fields from the source record.
508
+ let Data = {
509
+ ...data,
510
+ [renderAs]: { key: keys[i], value, type }
511
+ };
414
512
 
415
513
  if (!clone) {
416
514
  clone = cloneTemplate(templateData);
@@ -424,13 +522,19 @@ async function renderTemplate(template, data, key, index, keyPath) {
424
522
  let renderedKey = key.split(".");
425
523
  renderedKey = renderedKey[renderedKey.length - 1];
426
524
 
427
- clone.key = keys[i];
428
- clone.keyPath = templateData.keyPath + "." + renderedKey;
429
- clone.parentKey = renderedKey;
430
- clone.renderKey = key;
525
+ const cloneKeyPath = `${templateContext.keyPath}.${renderedKey}`;
526
+ clone.parentKey = renderedKey;
527
+ clone.renderKey = key;
528
+ setRenderContext(clone, {
529
+ key: keys[i],
530
+ keyPath: cloneKeyPath,
531
+ renderData: Data,
532
+ renderAs
533
+ });
431
534
  clone.element.setAttribute("renderedKey", keys[i]);
432
535
  await renderValues(clone.element, Data, keys[i], renderAs);
433
536
  insertElement(templateData, clone.element, index);
537
+ fieldClones.set(keys[i], clone.element);
434
538
  }
435
539
  } else {
436
540
  if (!key && !Array.isArray(renderData)) {
@@ -441,7 +545,12 @@ async function renderTemplate(template, data, key, index, keyPath) {
441
545
 
442
546
  if (!renderData) {
443
547
  let clone = cloneTemplate(templateData);
444
- clone.keyPath = templateData.keyPath;
548
+ setRenderContext(clone, {
549
+ key,
550
+ keyPath: templateContext.keyPath,
551
+ renderData: data,
552
+ renderAs
553
+ });
445
554
  await renderValues(clone.element, data, key, renderAs);
446
555
  insertElement(templateData, clone.element, index);
447
556
  } else {
@@ -453,7 +562,7 @@ async function renderTemplate(template, data, key, index, keyPath) {
453
562
  let test = clone.element.getAttribute("render-as");
454
563
  if (test) renderAs = test;
455
564
 
456
- clone.keyPath = templateData.keyPath || "" + `[${i}]`;
565
+ const cloneKeyPath = `${templateContext.keyPath || ""}[${i}]`;
457
566
 
458
567
  let object;
459
568
  if (
@@ -480,11 +589,22 @@ async function renderTemplate(template, data, key, index, keyPath) {
480
589
  object = dotNotationToObject(object);
481
590
  } else object = object;
482
591
 
483
- let index;
484
- if (key !== "data") index = i;
592
+ let insertIndex;
593
+ if (key !== "data") {
594
+ insertIndex = isLoadMore
595
+ ? appendIndex + i
596
+ : data.filter?.create && index != null
597
+ ? index + i
598
+ : i;
599
+ }
485
600
 
486
- clone.renderData = object
487
- clone.index = index
601
+ setRenderContext(clone, {
602
+ key,
603
+ keyPath: cloneKeyPath,
604
+ renderData: object,
605
+ renderAs
606
+ });
607
+ clone.index = insertIndex
488
608
  await renderValues(
489
609
  clone.element,
490
610
  object,
@@ -492,12 +612,12 @@ async function renderTemplate(template, data, key, index, keyPath) {
492
612
  renderAs,
493
613
  null,
494
614
  null,
495
- index
615
+ insertIndex
496
616
  );
497
617
  insertElement(
498
618
  templateData,
499
619
  clone.element,
500
- index,
620
+ insertIndex,
501
621
  "",
502
622
  data,
503
623
  renderReverse
@@ -507,17 +627,17 @@ async function renderTemplate(template, data, key, index, keyPath) {
507
627
  }
508
628
  }
509
629
 
510
- function cloneTemplate(template) {
630
+ function cloneTemplate(template) {
511
631
  let clone = template.element.cloneNode(true);
512
632
  let cloneTagName = clone.tagName;
513
633
 
514
634
  if (clone.tagName == "TEMPLATE") {
515
- clone = template.element.content.firstElementChild;
635
+ clone = clone.content.firstElementChild;
516
636
  for (let attribute of template.element.attributes) {
517
637
  clone.setAttribute(attribute.name, attribute.value || "");
518
638
  }
519
639
  } else {
520
- clone.classList.remove("template");
640
+ clone.classList?.remove("template");
521
641
  clone.removeAttribute("template");
522
642
  }
523
643
 
@@ -525,7 +645,7 @@ function cloneTemplate(template) {
525
645
 
526
646
  let renderAs = clone.getAttribute("render-as");
527
647
  if (renderAs === "$auto") {
528
- renderAs = uuid(6);
648
+ renderAs = uuid(6);
529
649
  clone = clone.outerHTML.replace(/\$auto/g, renderAs);
530
650
  }
531
651
 
@@ -542,10 +662,27 @@ function cloneTemplate(template) {
542
662
 
543
663
  let renderedNode = { template, element: clone };
544
664
  renderedNodes.set(clone, renderedNode);
545
- return renderedNode;
546
- }
547
-
548
- function insertElement(
665
+ return renderedNode;
666
+ }
667
+
668
+ function getCloneIdentity(data, renderAs) {
669
+ const value = renderAs ? getValueFromObject(data, renderAs) : data;
670
+ if (!value || typeof value !== "object") return;
671
+
672
+ for (const key of ["_id", "id", "object", "name", "key"]) {
673
+ const identity = value[key];
674
+ if (
675
+ (typeof identity === "string" || typeof identity === "number") &&
676
+ identity !== "" &&
677
+ identity !== "{}" &&
678
+ identity !== "[]"
679
+ ) {
680
+ return String(identity);
681
+ }
682
+ }
683
+ }
684
+
685
+ function insertElement(
549
686
  template,
550
687
  element,
551
688
  index,
@@ -555,7 +692,7 @@ function insertElement(
555
692
  ) {
556
693
  let eid = element.getAttribute("eid");
557
694
  if (!eid) {
558
- eid = uuid(6);
695
+ eid = uuid(6);
559
696
  element.setAttribute("eid", eid);
560
697
  }
561
698
 
@@ -563,9 +700,11 @@ function insertElement(
563
700
  if (!template.clones) template = template.template;
564
701
 
565
702
  let item;
703
+ let isNewClone = false;
566
704
  if (!currentIndex && currentIndex !== 0) {
567
705
  if (template.clones.has(eid)) return;
568
706
  item = [eid, element];
707
+ isNewClone = true;
569
708
  }
570
709
 
571
710
  const clones = Array.from(template.clones);
@@ -574,11 +713,13 @@ function insertElement(
574
713
  item = clones.splice(currentIndex, 1)[0];
575
714
  }
576
715
 
577
- if (data && data.$filter && data.$filter.startingIndex)
578
- index += data.$filter.startingIndex;
716
+ if (data && data.filter && data.filter.startingIndex && !data.filter.loadMore)
717
+ index += data.filter.startingIndex;
579
718
  clones.splice(index, 0, item); // Insert item into clones at the specified index
580
719
 
581
- if (renderReverse !== null && renderReverse !== "false" ) {
720
+ if (isNewClone) trackRendering(template, element);
721
+
722
+ if (renderReverse != null && renderReverse !== false && renderReverse !== "false") {
582
723
  // Chat-specific logic: reverse rendering but still inserting based on sorting
583
724
  if (clones[index + 1] && clones[index + 1][1] !== element) {
584
725
  clones[index + 1][1].insertAdjacentElement("afterend", element);
@@ -603,59 +744,77 @@ function insertElement(
603
744
 
604
745
  template.clones = new Map(clones);
605
746
  } else {
747
+ const isNewClone = !template.clones.has(eid);
748
+ if (isNewClone) trackRendering(template, element);
606
749
  template.clones.set(eid, element);
607
750
  template.element.insertAdjacentElement("beforebegin", element);
608
751
  }
609
752
  }
610
753
 
611
- async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
612
- if (!data) return;
613
-
614
- 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;
754
+ function trackRendering(renderedNode, element) {
755
+ element.classList.add("render-pending");
756
+ if (!renderedNode.rendering) renderedNode.rendering = [];
757
+ renderedNode.rendering.push(element);
758
+ }
625
759
 
626
- if (parent) renderedNode.parent = parent;
760
+ function clearRendering(renderedNode) {
761
+ if (!renderedNode?.rendering) return;
627
762
 
628
- if (node.nodeType == 1) {
629
- if (node.hasAttribute("render-clone")) {
630
- parent = renderedNode;
763
+ const elements = renderedNode.rendering;
764
+ delete renderedNode.rendering;
631
765
 
632
- for (let eid of ["_id", "name", "key"]) {
633
- if (renderAs) eid = data[renderAs][eid];
634
- else eid = data[eid];
766
+ setTimeout(() => {
767
+ for (const element of elements) {
768
+ element.classList.remove("render-pending");
769
+ }
770
+ }, 300);
771
+ }
635
772
 
636
- if (!eid) continue;
773
+ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
774
+ if (!data) return;
637
775
 
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);
776
+ let renderedNode = renderedNodes.get(node);
777
+ if (!renderedNode) {
778
+ renderedNode = { element: node };
779
+ renderedNodes.set(node, renderedNode);
780
+ }
781
+
782
+ const previousContext = getRenderContext(renderedNode, key);
783
+ if (!key) key = previousContext?.key;
784
+ if (!renderAs) renderAs = previousContext?.renderAs;
785
+ if (!keyPath) keyPath = previousContext?.keyPath;
786
+ setRenderContext(renderedNode, {
787
+ key,
788
+ keyPath,
789
+ renderData: data,
790
+ renderAs
791
+ });
645
792
 
646
- if (oldEid && oldEid !== eid) temp.clones.delete(oldEid);
793
+ if (parent) renderedNode.parent = parent;
647
794
 
795
+ if (node.nodeType == 1) {
796
+ if (node.hasAttribute("render-clone")) {
797
+ parent = renderedNode;
798
+
799
+ const eid = getCloneIdentity(data, renderAs);
800
+ if (eid) {
801
+ let oldEid = renderedNode.eid;
802
+ let temp = renderedNode.template;
803
+ while (temp && !temp.clones)
804
+ temp = temp.template || temp.parent;
805
+
806
+ if (temp?.clones && oldEid && oldEid !== eid) {
807
+ temp.clones.delete(oldEid);
648
808
  temp.clones.set(eid, node);
649
809
  }
650
810
 
651
811
  renderedNode.eid = eid;
812
+
813
+ node.setAttribute("eid", eid);
814
+ }
815
+ }
652
816
 
653
- node.setAttribute("eid", eid);
654
- break;
655
- }
656
- }
657
-
658
- for (let attr of node.attributes) {
817
+ for (let attr of Array.from(node.attributes)) {
659
818
  let name = attr.name;
660
819
  let value = attr.value;
661
820
 
@@ -667,6 +826,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
667
826
  renderAs,
668
827
  parent: renderedNode
669
828
  };
829
+ renderedNodes.set(attr, renderedAttribute);
670
830
  }
671
831
 
672
832
  let namePlaceholder = renderedAttribute.placeholder.name || name;
@@ -688,15 +848,13 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
688
848
  renderedAttribute,
689
849
  index
690
850
  );
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
- });
851
+ if (namePlaceholder.includes("{{") && name && !name.includes("{{")) {
852
+ if (name !== attr.name) {
853
+ node.removeAttribute(attr.name);
854
+ renderedNodes.delete(attr);
855
+ }
856
+ node.setAttribute(name, value ?? "");
857
+ renderedNodes.set(node.getAttributeNode(name), renderedAttribute);
700
858
  } else if (name === undefined || name === null || name === "") {
701
859
  renderedNodes.delete(attr);
702
860
  node.removeAttribute(attr.name);
@@ -732,11 +890,23 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
732
890
  return;
733
891
  }
734
892
  }
893
+
894
+ // A nested render-query owns its template and descendants. Parent
895
+ // rendering may resolve this controller's attributes, but its own
896
+ // render cycle must populate its target.
897
+ if (
898
+ node.hasAttribute("render-query") &&
899
+ !node.hasAttribute("render-clone")
900
+ ) {
901
+ return;
902
+ }
903
+
735
904
  if (node.hasAttribute("render") && !node.hasAttribute("render-clone")) {
736
905
  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) {
906
+ await renderTemplate(node, data, renderKey);
907
+ } else if (node.childNodes.length > 0) {
908
+ for (let childNode of Array.from(node.childNodes)) {
909
+ if (childNode.parentNode !== node) continue;
740
910
  await renderValues(
741
911
  childNode,
742
912
  data,
@@ -748,7 +918,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
748
918
  );
749
919
  }
750
920
  }
751
- } else if (node.nodeType == 3) {
921
+ } else if (node.nodeType == 3 || (node.nodeType == 8 && renderedNode.htmlEnd)) {
752
922
  let valueType = node.parentElement.getAttribute("value-type");
753
923
 
754
924
  let textContent, text;
@@ -772,33 +942,34 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
772
942
  if (text != renderedNode.text) {
773
943
  renderedNode.text = text;
774
944
 
775
- if (
776
- valueType == "text" ||
777
- valueType == "string" ||
778
- valueType != "html"
779
- ) {
780
- node.textContent = text;
781
- } else {
782
- const newNode = document.createElement("div");
783
- newNode.innerHTML = text;
784
- let parentElement = node.parentElement;
785
-
786
- let renderedParent = renderedNodes.get(parentElement);
787
- if (!renderedParent) {
788
- renderedParent = {
789
- placeholder: textContent,
790
- key,
791
- renderAs
792
- };
793
- }
794
- renderedParent.text = text;
795
- for (let newChild of newNode.childNodes) {
796
- renderedNode.element = newChild;
797
- renderedNodes.set(newChild, renderedNode);
798
- }
799
- renderedNodes.delete(node);
800
- node.replaceWith(...newNode.childNodes);
801
- }
945
+ if (valueType !== "html" && !renderedNode.htmlEnd) {
946
+ node.textContent = text;
947
+ } else {
948
+ const content = document.createElement("div");
949
+ if (valueType === "html") content.innerHTML = text;
950
+ else content.textContent = text;
951
+
952
+ if (renderedNode.htmlEnd) {
953
+ // Replace this binding's output while retaining its original template.
954
+ let child = node.nextSibling;
955
+ while (child && child !== renderedNode.htmlEnd) {
956
+ const next = child.nextSibling;
957
+ child.remove();
958
+ child = next;
959
+ }
960
+ renderedNode.htmlEnd.before(...content.childNodes);
961
+ } else {
962
+ const start = document.createComment("render");
963
+ const end = document.createComment("/render");
964
+ renderedNodes.set(start, {
965
+ ...renderedNode,
966
+ element: start,
967
+ htmlEnd: end
968
+ });
969
+ renderedNodes.delete(node);
970
+ node.replaceWith(start, ...content.childNodes, end);
971
+ }
972
+ }
802
973
 
803
974
  if (node.childNodes.length > 0) {
804
975
  for (let childNode of node.childNodes) {
@@ -820,7 +991,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
820
991
 
821
992
  /**
822
993
  * Renders placeholders within a string using asynchronous AST calculations and data context binding.
823
- *
994
+ *
824
995
  * @param {Node} node - The target DOM node (attributes, text nodes, elements).
825
996
  * @param {Object} data - The contextual database record or dataset being rendered.
826
997
  * @param {string} placeholder - The raw template string containing double curly braces (e.g. "{{ $query('#input').value }}").
@@ -828,127 +999,22 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
828
999
  * @param {HTMLElement} renderedNode - Optional target element where rendering applies.
829
1000
  * @param {number} index - Optional current iteration index if rendering a list.
830
1001
  * @returns {Promise<string>} - A promise resolving to the fully processed output string.
1002
+ * @public
1003
+ * @environment client
831
1004
  */
832
- async function renderValue(
833
- node,
834
- data,
835
- placeholder,
836
- renderAs,
837
- renderedNode,
838
- index
839
- ) {
840
- let output = placeholder;
841
-
842
- // Determine the actual HTMLElement for astAsync
843
- // If the node is not an Element (nodeType 1), get its owner or parent element.
844
- // This safely covers Attributes (ownerElement), Text, Comments, etc. (parentElement)
845
- let contextElement = node;
846
- if (node && node.nodeType !== 1) {
847
- contextElement = node.ownerElement || node.parentElement;
848
- }
849
-
850
- // Matches ONLY the innermost {{ }} brackets to prevent greedy-matching nested boundaries.
851
- let regex = /\{\{\s*((?:(?!\{\{|\}\})[\s\S])+?)\s*\}\}/;
852
- let omitted = {};
853
-
854
- let match;
855
- do {
856
- match = output.match(regex);
857
- if (match) {
858
- let value;
859
- let placeholderContent = match[1].trim();
860
-
861
- try {
862
- if (placeholderContent === "index" && index >= 0) {
863
- value = index;
864
- } else {
865
- // --- UNIFIED PARSING PROCESS ---
866
- let preProcessed = placeholderContent;
867
-
868
- // Build unified evaluation sandbox inheriting state & local indices
869
- const sandboxContext = {
870
- data: data,
871
- $data: data,
872
- index: index,
873
- $index: index,
874
- ...data // Expose data fields natively so they are available as local sandbox variables
875
- };
876
-
877
- // 1. PRE-PROCESSOR FOR QUOTED OPERATORS (Saves and processes quoted formulas first)
878
- let quoteRegex = /(['"])(.*?)\1/g;
879
- let quoteMatch;
880
- let replacements = [];
881
- while ((quoteMatch = quoteRegex.exec(preProcessed)) !== null) {
882
- let quoteType = quoteMatch[1];
883
- let content = quoteMatch[2];
884
- if (content.trim().startsWith('\u0024')) {
885
- let resolved = await astAsync({
886
- executeAST: content,
887
- node: contextElement,
888
- sandbox: sandboxContext
889
- });
890
- if (resolved === null || resolved === undefined) resolved = "";
891
- replacements.push({
892
- original: quoteMatch[0],
893
- replacement: `${quoteType}${resolved}${quoteType}`
894
- });
895
- }
896
- }
897
-
898
- for (let r of replacements) {
899
- preProcessed = preProcessed.replace(r.original, r.replacement);
900
- }
901
-
902
- // 2. PRE-PROCESSOR: Resolve unquoted DOM selectors, custom operators, and CRUD JSON formulas
903
- preProcessed = await astAsync({
904
- executeAST: preProcessed,
905
- node: contextElement,
906
- sandbox: sandboxContext
907
- });
908
-
909
- // 3. DATA LOOKUP: Resolve simple keys from the provided data object
910
- // Keep `node` here because getRenderValue needs to look up the attribute cache!
911
- value = getRenderValue(
912
- node,
913
- data,
914
- preProcessed,
915
- renderAs
916
- );
917
- }
918
- } catch (error) {
919
- console.error("[Renderer AST Bridge] Value computation error:", error);
920
- }
921
-
922
- // Replace the placeholder with the resolved value
923
- if (value !== undefined && value !== null) {
924
- if (typeof value === "object") {
925
- value = JSON.stringify(value, null, 2);
926
- }
927
- output = output.replaceAll(match[0], value);
928
- } else if (renderAs) {
929
- if (match[0].startsWith(`{{${renderAs}.`)) {
930
- output = output.replaceAll(match[0], "");
931
- } else if (placeholderContent) {
932
- output = output.replaceAll(
933
- match[0],
934
- `<|${placeholderContent}|>`
935
- );
936
- omitted[`<|${placeholderContent}|>`] = match[0];
937
- }
938
- } else {
939
- match = null;
940
- }
941
- }
942
- } while (match);
943
-
944
- for (let key of Object.keys(omitted)) {
945
- output = output.replaceAll(key, omitted[key]);
946
- }
947
-
948
- return output;
1005
+ async function renderValue(node, data, placeholder, renderAs, renderedNode, index) {
1006
+ return renderValueUtility(
1007
+ node,
1008
+ data,
1009
+ placeholder,
1010
+ renderAs,
1011
+ renderedNode,
1012
+ index,
1013
+ getRenderValue
1014
+ );
949
1015
  }
950
-
951
- function getRenderValue(node, data, key, renderAs) {
1016
+
1017
+ function getRenderValue(node, data, key, renderAs, renderIndex) {
952
1018
  let operator;
953
1019
  if (key.startsWith("$")) {
954
1020
  let keys = key.split(".");
@@ -958,20 +1024,37 @@ function getRenderValue(node, data, key, renderAs) {
958
1024
  }
959
1025
  }
960
1026
 
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;
1027
+ // Field reads can arrive wrapped under a literal dotted or bracketed key.
1028
+ const hasLiteralKey = Object.prototype.hasOwnProperty.call(data, key);
1029
+ const rootKey = key.split(/[.\[]/, 1)[0];
1030
+ const ownsRootScope =
1031
+ hasLiteralKey || Object.prototype.hasOwnProperty.call(data, rootKey);
1032
+ let value = hasLiteralKey
1033
+ ? data[key]
1034
+ : getValueFromObject(data, key);
1035
+
1036
+ // Once the current scope owns a root (for example `object`), a missing
1037
+ // descendant must remain missing instead of leaking in from another
1038
+ // clone's ancestor/source data.
1039
+ if ((value === undefined || value === null) && node && !ownsRootScope) {
1040
+ let parentTemplate = node;
1041
+ do {
1042
+ let parentNode = renderedNodes.get(parentTemplate);
1043
+ if (parentNode) {
1044
+ if (
1045
+ (value === undefined || value === null) &&
1046
+ (parentNode.parent || parentNode.template)
1047
+ ) {
1048
+ let Data,
1049
+ eid,
1050
+ parent = parentNode.parent || parentNode.template;
1051
+ do {
1052
+ const parentNodeContext = getRenderContext(parentNode);
1053
+ const parentContext = getRenderContext(parent);
1054
+ if (key.includes("keyPath")) {
1055
+ value =
1056
+ parentNodeContext?.keyPath ||
1057
+ parentContext?.keyPath;
975
1058
  } else if (key.includes("parentKey")) {
976
1059
  value =
977
1060
  parentNode.parentKey ||
@@ -982,7 +1065,7 @@ function getRenderValue(node, data, key, renderAs) {
982
1065
  } else if (key === "object" || key === "_id")
983
1066
  value = ObjectId().toString();
984
1067
  else if (key === "uuid") {
985
- value = uuid(6);
1068
+ value = uuid(6);
986
1069
  } else if (parent.source) {
987
1070
  Data = parent.source.data;
988
1071
  } else if (parent.parent) {
@@ -994,28 +1077,36 @@ function getRenderValue(node, data, key, renderAs) {
994
1077
  }
995
1078
 
996
1079
  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(".")) {
1080
+ } while (
1081
+ (value === undefined || value === null) &&
1082
+ parent &&
1083
+ !Data
1084
+ );
1085
+
1086
+ if ((value === undefined || value === null) && Data) {
1087
+ value = getValueFromObject(Data, key);
1088
+ }
1089
+ if (
1090
+ (value === undefined || value === null) &&
1091
+ key.includes(".")
1092
+ ) {
1003
1093
  let renderedData = getValueFromObject(
1004
1094
  Data,
1005
1095
  key.split(".")[0]
1006
1096
  );
1007
- if (renderedData) {
1008
- let index;
1097
+ if (renderedData !== undefined && renderedData !== null) {
1098
+ let index = renderIndex;
1009
1099
  if (
1010
- !parent.clones.size ||
1011
- parent.clones.size === 1
1100
+ index === undefined &&
1101
+ (!parent.clones.size ||
1102
+ parent.clones.size === 1)
1012
1103
  ) {
1013
1104
  index = 0;
1014
- } else if (eid) {
1105
+ } else if (index === undefined && eid) {
1015
1106
  index = Array.from(
1016
1107
  parent.clones.keys()
1017
1108
  ).indexOf(eid);
1018
- } else console.log("eid not found");
1109
+ }
1019
1110
 
1020
1111
  if (index >= 0) {
1021
1112
  Data = {
@@ -1028,13 +1119,16 @@ function getRenderValue(node, data, key, renderAs) {
1028
1119
  }
1029
1120
  }
1030
1121
 
1031
- if (!value && parentTemplate.parentElement) {
1122
+ if (
1123
+ (value === undefined || value === null) &&
1124
+ parentTemplate.parentElement
1125
+ ) {
1032
1126
  parentTemplate =
1033
1127
  parentTemplate.parentElement.closest("[render]");
1034
1128
  } else {
1035
1129
  parentTemplate = undefined;
1036
1130
  }
1037
- } while (!value && parentTemplate);
1131
+ } while ((value === undefined || value === null) && parentTemplate);
1038
1132
  }
1039
1133
 
1040
1134
  if (operator) {
@@ -1150,12 +1244,13 @@ Observer.init({
1150
1244
  let parentElement = mutation.target.parentElement.closest("[render]");
1151
1245
  if (!parentElement) return;
1152
1246
 
1153
- let renderedNode = renderedNodes.get(parentElement);
1154
- let data;
1155
- if (renderedNode) {
1156
- if (renderedNode.renderData) {
1157
- data = renderedNode.renderData;
1158
- }
1247
+ let renderedNode = renderedNodes.get(parentElement);
1248
+ let data;
1249
+ if (renderedNode) {
1250
+ const renderContext = getRenderContext(renderedNode);
1251
+ if (renderContext?.renderData) {
1252
+ data = renderContext.renderData;
1253
+ }
1159
1254
  } else {
1160
1255
  let sourceElement = mutation.target.parentElement.closest("[render-query]")
1161
1256
  let sourceData = sources.get(sourceElement);
@@ -1184,4 +1279,185 @@ Observer.init({
1184
1279
 
1185
1280
  init();
1186
1281
 
1187
- export { render, renderValue, sources, renderedNodes };
1282
+ export { render, renderValue, sources, renderedNodes };
1283
+
1284
+ /**
1285
+ *
1286
+ * @name array
1287
+ * @kind member
1288
+ * @attribute
1289
+ * @environment client
1290
+ */
1291
+
1292
+ /**
1293
+ *
1294
+ * @name eid
1295
+ * @kind member
1296
+ * @attribute
1297
+ * @environment client
1298
+ */
1299
+
1300
+ /**
1301
+ *
1302
+ * @name key
1303
+ * @kind member
1304
+ * @attribute
1305
+ * @environment client
1306
+ */
1307
+
1308
+ /**
1309
+ *
1310
+ * @name render
1311
+ * @kind member
1312
+ * @attribute
1313
+ * @environment client
1314
+ */
1315
+
1316
+ /**
1317
+ *
1318
+ * @name render-as
1319
+ * @kind member
1320
+ * @attribute
1321
+ * @environment client
1322
+ */
1323
+
1324
+ /**
1325
+ *
1326
+ * @name render-clone
1327
+ * @kind member
1328
+ * @attribute
1329
+ * @environment client
1330
+ */
1331
+
1332
+ /**
1333
+ *
1334
+ * @name render-data
1335
+ * @kind member
1336
+ * @attribute
1337
+ * @environment client
1338
+ */
1339
+
1340
+ /**
1341
+ *
1342
+ * @name render-exclude
1343
+ * @kind member
1344
+ * @attribute
1345
+ * @environment client
1346
+ */
1347
+
1348
+ /**
1349
+ *
1350
+ * @name render-include
1351
+ * @kind member
1352
+ * @attribute
1353
+ * @environment client
1354
+ */
1355
+
1356
+ /**
1357
+ *
1358
+ * @name render-limit
1359
+ * @kind member
1360
+ * @attribute
1361
+ * @environment client
1362
+ */
1363
+
1364
+ /**
1365
+ *
1366
+ * @name render-query
1367
+ * @kind member
1368
+ * @attribute
1369
+ * @environment client
1370
+ */
1371
+
1372
+ /**
1373
+ *
1374
+ * @name render-reference
1375
+ * @kind member
1376
+ * @attribute
1377
+ * @environment client
1378
+ */
1379
+
1380
+ /**
1381
+ *
1382
+ * @name render-reverse
1383
+ * @kind member
1384
+ * @attribute
1385
+ * @environment client
1386
+ */
1387
+
1388
+ /**
1389
+ *
1390
+ * @name render-type
1391
+ * @kind member
1392
+ * @attribute
1393
+ * @environment client
1394
+ */
1395
+
1396
+ /**
1397
+ *
1398
+ * @name renderedKey
1399
+ * @kind member
1400
+ * @attribute
1401
+ * @environment client
1402
+ */
1403
+
1404
+ /**
1405
+ *
1406
+ * @name template
1407
+ * @kind member
1408
+ * @attribute
1409
+ * @environment client
1410
+ */
1411
+
1412
+ /**
1413
+ *
1414
+ * @name value-type
1415
+ * @kind member
1416
+ * @attribute
1417
+ * @environment client
1418
+ */
1419
+
1420
+ /**
1421
+ * A simple render component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.
1422
+ *
1423
+ * @module render
1424
+ * @environment client
1425
+ */
1426
+
1427
+ /**
1428
+ * Declarative selectors recognized by this module.
1429
+ *
1430
+ * @name declarative-selectors
1431
+ * @kind member
1432
+ * @selectors
1433
+ * @environment client
1434
+ * @example
1435
+ * ```json
1436
+ * [
1437
+ * "[${params}]",
1438
+ * "[render-element]",
1439
+ * "[render-clone]",
1440
+ * "[render-query]"
1441
+ * ]
1442
+ * ```
1443
+ */
1444
+
1445
+ /**
1446
+ * Declarative actions supported or registered by this module.
1447
+ *
1448
+ * @name declarative-actions
1449
+ * @kind member
1450
+ * @actions
1451
+ * @environment client
1452
+ * @example
1453
+ * ```json
1454
+ * [
1455
+ * {
1456
+ * "name": "render"
1457
+ * },
1458
+ * {
1459
+ * "name": "renderKey"
1460
+ * }
1461
+ * ]
1462
+ * ```
1463
+ */