Renderer.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { Component } from "../Component";
  2. import Pupper from "../..";
  3. import { walk } from "../../model/NodeWalker";
  4. import { RendererNode } from "../../model/vdom/RendererNode";
  5. import { diff, patch, create } from "virtual-dom";
  6. import h from "virtual-dom/h";
  7. import Debugger from "../../util/Debugger";
  8. import { ConditionalNode } from "./nodes/ConditionalNode";
  9. import { LoopNode } from "./nodes/LoopNode";
  10. import VNode from "virtual-dom/vnode/vnode";
  11. const debug = Debugger.extend("vdom");
  12. export type TRendererNodes = RendererNode | ConditionalNode | LoopNode;
  13. /**
  14. * Most of the evaluation functions were taken from alpine.js
  15. * Thanks, alpine.js!
  16. */
  17. export class Renderer {
  18. vnode: string | VirtualDOM.VTree | (string | VirtualDOM.VTree)[];
  19. rendererNode: ConditionalNode | LoopNode | RendererNode<VirtualDOM.VText | VirtualDOM.VComment | VirtualDOM.VNode | VirtualDOM.Widget | VirtualDOM.Thunk>;
  20. /**
  21. * Creates a renderer node from a virtual DOM node.
  22. * @param node The original virtual DOM node.
  23. * @param parent The parent node.
  24. * @param renderer The renderer related to this node.
  25. * @returns
  26. */
  27. public static createNode(node: VirtualDOM.VTree | string, parent: RendererNode, renderer: Renderer) {
  28. if (node instanceof VNode) {
  29. if ("properties" in node && "attrs" in node.properties) {
  30. if ("x-if" in node.properties.attrs) {
  31. return new ConditionalNode(node, parent, renderer);
  32. } else
  33. if ("x-for" in node.properties.attrs) {
  34. return new LoopNode(node, parent, renderer);
  35. }
  36. }
  37. }
  38. return new RendererNode(node, parent, renderer);
  39. }
  40. public diff = diff;
  41. public patch = patch;
  42. /**
  43. * The stack of states that formulates the context for rendering elements.
  44. */
  45. protected stateStack: Record<string, any>[] = [];
  46. /**
  47. * The container that will receive the renderer contents.
  48. */
  49. public container: Element;
  50. /**
  51. * The rendering queue.
  52. */
  53. private queue: {
  54. callback: CallableFunction,
  55. listeners: CallableFunction[]
  56. }[] = [];
  57. /**
  58. * Determines if the renderer queue is currently running.
  59. */
  60. private inQueue: boolean;
  61. constructor(
  62. public component: Component
  63. ) {
  64. this.stateStack.push(
  65. // Globals
  66. Pupper.$global,
  67. // Magics
  68. Pupper.$magics,
  69. // Component state
  70. component.$state,
  71. // Renderer-related
  72. {
  73. $component: component
  74. }
  75. );
  76. }
  77. /**
  78. * Starts the queue if not executing it already.
  79. */
  80. private maybeStartQueue() {
  81. if (!this.inQueue) {
  82. this.processQueue();
  83. }
  84. }
  85. /**
  86. * Processes the renderer queue.
  87. */
  88. private async processQueue() {
  89. this.inQueue = this.queue.length > 0;
  90. // If doesn't have more items to process.
  91. if (!this.inQueue) {
  92. // Go out of the current queue.
  93. return;
  94. }
  95. // Retrieve the first queue job.
  96. const { callback, listeners } = this.queue.shift();
  97. // Do the job.
  98. await callback();
  99. // If has any listeners
  100. if (listeners && listeners.length) {
  101. for(let listener of listeners) {
  102. await listener();
  103. }
  104. }
  105. // Wait for a new job.
  106. window.requestAnimationFrame(this.processQueue.bind(this));
  107. }
  108. /**
  109. * Generates a state from the state stack.
  110. * @returns
  111. */
  112. public generateScope() {
  113. return this.stateStack.reduce((carrier, curr) => {
  114. for(let key in curr) {
  115. carrier[key] = curr[key];
  116. }
  117. return carrier;
  118. }, {});
  119. }
  120. public rendered = false;
  121. /**
  122. * Renders the virtual dom into a virtual DOM node.
  123. * @returns
  124. */
  125. public async renderToNode() {
  126. const tick = this.nextTick(async () => {
  127. const vdom = this.component.$component.render({ h });
  128. const node = Renderer.createNode(vdom, null, this);
  129. this.rendererNode = await walk(node, this.generateScope());
  130. this.rendererNode.addEventListener("$created", () => {
  131. this.component.$rendered = this.rendererNode.element;
  132. this.component.prepareDOM();
  133. });
  134. });
  135. await this.waitForTick(tick);
  136. return this.rendererNode;
  137. }
  138. /**
  139. * Renders the virtual dom for the first time.
  140. * @returns
  141. */
  142. public async render() {
  143. this.vnode = (await this.renderToNode()).toVNode();
  144. try {
  145. this.container = create(this.vnode as VirtualDOM.VNode, {
  146. warn: true
  147. });
  148. this.rendered = true;
  149. debug("first render ended");
  150. } catch(e) {
  151. Debugger.error("an exception ocurred while rendering component %O", this.vnode);
  152. throw e;
  153. }
  154. return this.container;
  155. }
  156. /**
  157. * Enqueues a function to be executed in the next queue tick.
  158. * @param callback The callback to be executed.
  159. */
  160. public nextTick(callback: CallableFunction) {
  161. const tick = this.queue.push({
  162. callback,
  163. listeners: []
  164. });
  165. window.requestAnimationFrame(() => this.maybeStartQueue());
  166. return tick;
  167. }
  168. /**
  169. * Enqueues a function to be executed in the next queue tick only if it hasn't been enqueued yet.
  170. * @param callback The callback to be executed.
  171. */
  172. public singleNextTick(callback: CallableFunction) {
  173. if (this.queue.find((c) => c.callback === callback)) {
  174. return;
  175. }
  176. this.nextTick(callback);
  177. }
  178. /**
  179. * Waits for the given tick or the last added tick to be executed.
  180. * @returns
  181. */
  182. public waitForTick(tick: number = null) {
  183. return new Promise((resolve) => {
  184. this.queue[tick !== null ? (tick - 1) : this.queue.length - 1].listeners.push(resolve);
  185. });
  186. }
  187. }