Loop.ts 4.0 KB

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