Loop.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { directive } from "../../../model/Directive";
  2. import { evaluateLater } from "../../../model/Evaluator";
  3. import { walk } from "../../../model/NodeWalker";
  4. import { effect } from "../../../model/Reactivity";
  5. import { IsNumeric, IsObject } from "../../../util/ObjectUtils";
  6. import { PupperNode } from "../Node";
  7. /**
  8. * @directive x-for
  9. * @description Recursively renders a node's children nodes.
  10. */
  11. directive("for", async (node, { expression, scope }) => {
  12. const loopData = parseForExpression(expression);
  13. const evaluate = evaluateLater(loopData.items);
  14. // Save and remove the children
  15. const children = node.children;
  16. node = node.replaceWithComment();
  17. node.setIgnored();
  18. node.setRenderable(false);
  19. let clones: PupperNode[] = [];
  20. const removeEffect = await effect(async () => {
  21. let loopScope;
  22. try {
  23. let items = await evaluate(scope);
  24. // Support number literals, eg.: x-for="i in 100"
  25. if (IsNumeric(items)) {
  26. items = Array.from(Array(items).keys(), (i) => i + 1);
  27. } else
  28. // If it's an object
  29. if (IsObject(items)) {
  30. // Retrieve the entries from it
  31. items = Object.entries(items);
  32. } else
  33. // If nothing is found, default to an empty array.
  34. if (items === undefined) {
  35. items = [];
  36. }
  37. // Clear the older nodes if needed
  38. if (clones.length) {
  39. clones.forEach((clone) => clone.delete());
  40. clones = [];
  41. }
  42. // Iterate over all evaluated items
  43. for(let index = 0; index < items.length; index++) {
  44. // Clone the scope
  45. loopScope = { ...scope };
  46. // Push the current item to the state stack
  47. if ("item" in loopData) {
  48. loopScope[loopData.item] = items[index];
  49. }
  50. if ("index" in loopData) {
  51. loopScope[loopData.index] = index;
  52. }
  53. if ("collection" in loopData) {
  54. loopScope[loopData.collection] = items;
  55. }
  56. for(let child of children) {
  57. child = child.clone()
  58. .setIgnored(false)
  59. .setParent(node.parent)
  60. .setDirty(true, false)
  61. .setChildrenDirty(true, false)
  62. .setChildrenIgnored(false);
  63. node.insertBefore(child);
  64. child = await walk(child, loopScope);
  65. clones.push(child);
  66. }
  67. }
  68. node.parent.setDirty();
  69. } catch(e) {
  70. console.warn("[pupperjs] The following information can be useful for debugging:");
  71. console.warn("last scope:", loopScope);
  72. console.error(e);
  73. }
  74. });
  75. node.addEventListener("removed", removeEffect);
  76. });
  77. /**
  78. * Parses a "for" expression
  79. * @note This was taken from VueJS 2.* core. Thanks Vue!
  80. * @param expression The expression to be parsed.
  81. * @returns
  82. */
  83. function parseForExpression(expression: string | number | boolean | CallableFunction) {
  84. let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
  85. let stripParensRE = /^\s*\(|\)\s*$/g;
  86. let forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
  87. let inMatch = String(expression).match(forAliasRE);
  88. if (!inMatch) {
  89. return;
  90. }
  91. let res: {
  92. items?: string;
  93. index?: string;
  94. item?: string;
  95. collection?: string;
  96. } = {};
  97. res.items = inMatch[2].trim();
  98. let item = inMatch[1].replace(stripParensRE, "").trim();
  99. let iteratorMatch = item.match(forIteratorRE);
  100. if (iteratorMatch) {
  101. res.item = item.replace(forIteratorRE, "").trim();
  102. res.index = iteratorMatch[1].trim();
  103. if (iteratorMatch[2]) {
  104. res.collection = iteratorMatch[2].trim();
  105. }
  106. } else {
  107. res.item = item;
  108. }
  109. return res;
  110. }