@cocreate/render 1.53.0 → 2.0.1

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,13 +28,23 @@ 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
+ /**
38
+ *
39
+ * @public
40
+ * @environment client
41
+ */
37
42
  const sources = new Map();
43
+ /**
44
+ *
45
+ * @public
46
+ * @environment client
47
+ */
38
48
  const renderedNodes = new Map();
39
49
  const elementSelector = "[render-query]";
40
50
 
@@ -91,6 +101,8 @@ function setRenderContext(renderedNode, context = {}) {
91
101
  * - If an HTMLCollection or an array of elements is provided, each element in the collection/array is initialized.
92
102
  * - If a single element is provided, only that element is initialized.
93
103
  * - If null or omitted, the function queries and initializes all elements matching the 'elementSelector'.
104
+ * @private
105
+ * @environment client
94
106
  */
95
107
  function init(element) {
96
108
  if (
@@ -142,6 +154,8 @@ function init(element) {
142
154
  * @param {number} [options.currentIndex] - The current index in a list or array rendering process.
143
155
  * @param {boolean} [options.update] - Flag to indicate if the rendering should update existing content.
144
156
  * @param {boolean} [options.remove] - Flag to indicate if the rendering should remove elements instead of updating or appending.
157
+ * @public
158
+ * @environment client
145
159
  */
146
160
  async function render({
147
161
  source,
@@ -238,6 +252,8 @@ async function render({
238
252
  element = [element];
239
253
  }
240
254
 
255
+ const renderedNodesToClear = new Set();
256
+
241
257
  for (let i = 0; i < element.length; i++) {
242
258
  let renderedNode = renderedNodes.get(element[i]);
243
259
  if (renderedNode) {
@@ -259,6 +275,7 @@ async function render({
259
275
  };
260
276
  renderedNodes.set(element[i], renderedNode);
261
277
  }
278
+ renderedNodesToClear.add(renderedNode);
262
279
 
263
280
  let clones = renderedNode.clones; // || renderedNode.template.clones..
264
281
  if (!clones && renderedNode.template) {
@@ -333,6 +350,10 @@ async function render({
333
350
  await renderValues(element[i], data, key, renderAs);
334
351
  }
335
352
  }
353
+ for (const renderedNode of renderedNodesToClear) {
354
+ clearRendering(renderedNode);
355
+ }
356
+
336
357
  // Dispatch input and change events after rendering is complete
337
358
  if (source instanceof HTMLElement) {
338
359
  source.dispatchEvent(new Event("input"));
@@ -679,9 +700,11 @@ function insertElement(
679
700
  if (!template.clones) template = template.template;
680
701
 
681
702
  let item;
703
+ let isNewClone = false;
682
704
  if (!currentIndex && currentIndex !== 0) {
683
705
  if (template.clones.has(eid)) return;
684
706
  item = [eid, element];
707
+ isNewClone = true;
685
708
  }
686
709
 
687
710
  const clones = Array.from(template.clones);
@@ -694,6 +717,8 @@ function insertElement(
694
717
  index += data.filter.startingIndex;
695
718
  clones.splice(index, 0, item); // Insert item into clones at the specified index
696
719
 
720
+ if (isNewClone) trackRendering(template, element);
721
+
697
722
  if (renderReverse != null && renderReverse !== false && renderReverse !== "false") {
698
723
  // Chat-specific logic: reverse rendering but still inserting based on sorting
699
724
  if (clones[index + 1] && clones[index + 1][1] !== element) {
@@ -719,11 +744,32 @@ function insertElement(
719
744
 
720
745
  template.clones = new Map(clones);
721
746
  } else {
747
+ const isNewClone = !template.clones.has(eid);
748
+ if (isNewClone) trackRendering(template, element);
722
749
  template.clones.set(eid, element);
723
750
  template.element.insertAdjacentElement("beforebegin", element);
724
751
  }
725
752
  }
726
753
 
754
+ function trackRendering(renderedNode, element) {
755
+ element.classList.add("render-pending");
756
+ if (!renderedNode.rendering) renderedNode.rendering = [];
757
+ renderedNode.rendering.push(element);
758
+ }
759
+
760
+ function clearRendering(renderedNode) {
761
+ if (!renderedNode?.rendering) return;
762
+
763
+ const elements = renderedNode.rendering;
764
+ delete renderedNode.rendering;
765
+
766
+ setTimeout(() => {
767
+ for (const element of elements) {
768
+ element.classList.remove("render-pending");
769
+ }
770
+ }, 300);
771
+ }
772
+
727
773
  async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
728
774
  if (!data) return;
729
775
 
@@ -860,6 +906,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
860
906
  await renderTemplate(node, data, renderKey);
861
907
  } else if (node.childNodes.length > 0) {
862
908
  for (let childNode of Array.from(node.childNodes)) {
909
+ if (childNode.parentNode !== node) continue;
863
910
  await renderValues(
864
911
  childNode,
865
912
  data,
@@ -871,7 +918,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
871
918
  );
872
919
  }
873
920
  }
874
- } else if (node.nodeType == 3) {
921
+ } else if (node.nodeType == 3 || (node.nodeType == 8 && renderedNode.htmlEnd)) {
875
922
  let valueType = node.parentElement.getAttribute("value-type");
876
923
 
877
924
  let textContent, text;
@@ -895,35 +942,34 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
895
942
  if (text != renderedNode.text) {
896
943
  renderedNode.text = text;
897
944
 
898
- if (
899
- valueType == "text" ||
900
- valueType == "string" ||
901
- valueType != "html"
902
- ) {
903
- node.textContent = text;
904
- } else {
905
- const newNode = document.createElement("div");
906
- newNode.innerHTML = text;
907
- let parentElement = node.parentElement;
908
-
909
- let renderedParent = renderedNodes.get(parentElement);
910
- if (!renderedParent) {
911
- renderedParent = {
912
- placeholder: textContent,
913
- key,
914
- renderAs
915
- };
916
- }
917
- renderedParent.text = text;
918
- for (let newChild of newNode.childNodes) {
919
- renderedNodes.set(newChild, {
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, {
920
965
  ...renderedNode,
921
- element: newChild
966
+ element: start,
967
+ htmlEnd: end
922
968
  });
969
+ renderedNodes.delete(node);
970
+ node.replaceWith(start, ...content.childNodes, end);
923
971
  }
924
- renderedNodes.delete(node);
925
- node.replaceWith(...newNode.childNodes);
926
- }
972
+ }
927
973
 
928
974
  if (node.childNodes.length > 0) {
929
975
  for (let childNode of node.childNodes) {
@@ -945,7 +991,7 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
945
991
 
946
992
  /**
947
993
  * Renders placeholders within a string using asynchronous AST calculations and data context binding.
948
- *
994
+ *
949
995
  * @param {Node} node - The target DOM node (attributes, text nodes, elements).
950
996
  * @param {Object} data - The contextual database record or dataset being rendered.
951
997
  * @param {string} placeholder - The raw template string containing double curly braces (e.g. "{{ $query('#input').value }}").
@@ -953,127 +999,21 @@ async function renderValues(node, data, key, renderAs, keyPath, parent, index) {
953
999
  * @param {HTMLElement} renderedNode - Optional target element where rendering applies.
954
1000
  * @param {number} index - Optional current iteration index if rendering a list.
955
1001
  * @returns {Promise<string>} - A promise resolving to the fully processed output string.
1002
+ * @public
1003
+ * @environment client
956
1004
  */
957
- async function renderValue(
958
- node,
959
- data,
960
- placeholder,
961
- renderAs,
962
- renderedNode,
963
- index
964
- ) {
965
- let output = placeholder;
966
-
967
- // Determine the actual HTMLElement for astAsync
968
- // If the node is not an Element (nodeType 1), get its owner or parent element.
969
- // This safely covers Attributes (ownerElement), Text, Comments, etc. (parentElement)
970
- let contextElement = node;
971
- if (node && node.nodeType !== 1) {
972
- contextElement = node.ownerElement || node.parentElement;
973
- }
974
-
975
- // Matches ONLY the innermost {{ }} brackets to prevent greedy-matching nested boundaries.
976
- let regex = /\{\{\s*((?:(?!\{\{|\}\})[\s\S])+?)\s*\}\}/;
977
- let omitted = {};
978
-
979
- let match;
980
- do {
981
- match = output.match(regex);
982
- if (match) {
983
- let value;
984
- let placeholderContent = match[1].trim();
985
-
986
- try {
987
- if (placeholderContent === "index" && index >= 0) {
988
- value = index;
989
- } else {
990
- // --- UNIFIED PARSING PROCESS ---
991
- let preProcessed = placeholderContent;
992
-
993
- // Build unified evaluation sandbox inheriting state & local indices
994
- const sandboxContext = {
995
- data: data,
996
- $data: data,
997
- index: index,
998
- $index: index,
999
- ...data // Expose data fields natively so they are available as local sandbox variables
1000
- };
1001
-
1002
- // 1. PRE-PROCESSOR FOR QUOTED OPERATORS (Saves and processes quoted formulas first)
1003
- let quoteRegex = /(['"])(.*?)\1/g;
1004
- let quoteMatch;
1005
- let replacements = [];
1006
- while ((quoteMatch = quoteRegex.exec(preProcessed)) !== null) {
1007
- let quoteType = quoteMatch[1];
1008
- let content = quoteMatch[2];
1009
- if (content.trim().startsWith('\u0024')) {
1010
- let resolved = await astAsync({
1011
- executeAST: content,
1012
- node: contextElement,
1013
- sandbox: sandboxContext
1014
- });
1015
- if (resolved === null || resolved === undefined) resolved = "";
1016
- replacements.push({
1017
- original: quoteMatch[0],
1018
- replacement: `${quoteType}${resolved}${quoteType}`
1019
- });
1020
- }
1021
- }
1022
-
1023
- for (let r of replacements) {
1024
- preProcessed = preProcessed.replace(r.original, r.replacement);
1025
- }
1026
-
1027
- // 2. PRE-PROCESSOR: Resolve unquoted DOM selectors, custom operators, and CRUD JSON formulas
1028
- preProcessed = await astAsync({
1029
- executeAST: preProcessed,
1030
- node: contextElement,
1031
- sandbox: sandboxContext
1032
- });
1033
-
1034
- // 3. DATA LOOKUP: Resolve simple keys from the provided data object
1035
- // Keep `node` here because getRenderValue needs to look up the attribute cache!
1036
- value = getRenderValue(
1037
- node,
1038
- data,
1039
- preProcessed,
1040
- renderAs,
1041
- index
1042
- );
1043
- }
1044
- } catch (error) {
1045
- console.error("[Renderer AST Bridge] Value computation error:", error);
1046
- }
1047
-
1048
- // Replace the placeholder with the resolved value
1049
- if (value !== undefined && value !== null) {
1050
- if (typeof value === "object") {
1051
- value = JSON.stringify(value, null, 2);
1052
- }
1053
- output = output.replaceAll(match[0], value);
1054
- } else if (renderAs) {
1055
- if (match[0].startsWith(`{{${renderAs}.`)) {
1056
- output = output.replaceAll(match[0], "");
1057
- } else if (placeholderContent) {
1058
- output = output.replaceAll(
1059
- match[0],
1060
- `<|${placeholderContent}|>`
1061
- );
1062
- omitted[`<|${placeholderContent}|>`] = match[0];
1063
- }
1064
- } else {
1065
- match = null;
1066
- }
1067
- }
1068
- } while (match);
1069
-
1070
- for (let key of Object.keys(omitted)) {
1071
- output = output.replaceAll(key, omitted[key]);
1072
- }
1073
-
1074
- 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
+ );
1075
1015
  }
1076
-
1016
+
1077
1017
  function getRenderValue(node, data, key, renderAs, renderIndex) {
1078
1018
  let operator;
1079
1019
  if (key.startsWith("$")) {
@@ -1340,3 +1280,167 @@ Observer.init({
1340
1280
  init();
1341
1281
 
1342
1282
  export { render, renderValue, sources, renderedNodes };
1283
+
1284
+ /**
1285
+ *
1286
+ * @name array
1287
+ * @kind attribute
1288
+ * @environment client
1289
+ */
1290
+
1291
+ /**
1292
+ *
1293
+ * @name eid
1294
+ * @kind attribute
1295
+ * @environment client
1296
+ */
1297
+
1298
+ /**
1299
+ *
1300
+ * @name key
1301
+ * @kind attribute
1302
+ * @environment client
1303
+ */
1304
+
1305
+ /**
1306
+ *
1307
+ * @name render
1308
+ * @kind attribute
1309
+ * @environment client
1310
+ */
1311
+
1312
+ /**
1313
+ *
1314
+ * @name render-as
1315
+ * @kind attribute
1316
+ * @environment client
1317
+ */
1318
+
1319
+ /**
1320
+ *
1321
+ * @name render-clone
1322
+ * @kind attribute
1323
+ * @environment client
1324
+ */
1325
+
1326
+ /**
1327
+ *
1328
+ * @name render-data
1329
+ * @kind attribute
1330
+ * @environment client
1331
+ */
1332
+
1333
+ /**
1334
+ *
1335
+ * @name render-exclude
1336
+ * @kind attribute
1337
+ * @environment client
1338
+ */
1339
+
1340
+ /**
1341
+ *
1342
+ * @name render-include
1343
+ * @kind attribute
1344
+ * @environment client
1345
+ */
1346
+
1347
+ /**
1348
+ *
1349
+ * @name render-limit
1350
+ * @kind attribute
1351
+ * @environment client
1352
+ */
1353
+
1354
+ /**
1355
+ *
1356
+ * @name render-query
1357
+ * @kind attribute
1358
+ * @environment client
1359
+ */
1360
+
1361
+ /**
1362
+ *
1363
+ * @name render-reference
1364
+ * @kind attribute
1365
+ * @environment client
1366
+ */
1367
+
1368
+ /**
1369
+ *
1370
+ * @name render-reverse
1371
+ * @kind attribute
1372
+ * @environment client
1373
+ */
1374
+
1375
+ /**
1376
+ *
1377
+ * @name render-type
1378
+ * @kind attribute
1379
+ * @environment client
1380
+ */
1381
+
1382
+ /**
1383
+ *
1384
+ * @name renderedKey
1385
+ * @kind attribute
1386
+ * @environment client
1387
+ */
1388
+
1389
+ /**
1390
+ *
1391
+ * @name template
1392
+ * @kind attribute
1393
+ * @environment client
1394
+ */
1395
+
1396
+ /**
1397
+ *
1398
+ * @name value-type
1399
+ * @kind attribute
1400
+ * @environment client
1401
+ */
1402
+
1403
+ /**
1404
+ * A simple render component in vanilla javascript. Easily configured using HTML5 attributes and/or JavaScript API.
1405
+ *
1406
+ * @module render
1407
+ * @environment client
1408
+ */
1409
+
1410
+ /**
1411
+ * Declarative selectors recognized by this module.
1412
+ *
1413
+ * @name declarative-selectors
1414
+ * @kind member
1415
+ * @selectors
1416
+ * @environment client
1417
+ * @example
1418
+ * ```json
1419
+ * [
1420
+ * "[${params}]",
1421
+ * "[render-element]",
1422
+ * "[render-clone]",
1423
+ * "[render-query]"
1424
+ * ]
1425
+ * ```
1426
+ */
1427
+
1428
+ /**
1429
+ * Declarative actions supported or registered by this module.
1430
+ *
1431
+ * @name declarative-actions
1432
+ * @kind member
1433
+ * @actions
1434
+ * @environment client
1435
+ * @example
1436
+ * ```json
1437
+ * [
1438
+ * {
1439
+ * "name": "render"
1440
+ * },
1441
+ * {
1442
+ * "name": "renderKey"
1443
+ * }
1444
+ * ]
1445
+ * ```
1446
+ */
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }