Component.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { reactive } from "../model/Reactivity";
  2. import { Renderer } from "./vdom/Renderer";
  3. import type h from "virtual-dom/h";
  4. import { SlotNode } from "./vdom/nodes/SlotNode";
  5. /**
  6. * Represents a component's data.
  7. */
  8. export interface IComponent<
  9. TData extends Record<string, any>,
  10. TMethods extends Record<string, CallableFunction>
  11. > {
  12. /**
  13. * Component-related
  14. */
  15. /**
  16. * The function that renders the template HTML.
  17. */
  18. render?: (data: {
  19. h: typeof h
  20. }) => VirtualDOM.VTree;
  21. /**
  22. * Any data to be passed to the template.
  23. */
  24. data?: TData | (() => TData);
  25. /**
  26. * Any methods that can be called from the component.
  27. */
  28. methods?: TMethods;
  29. /**
  30. * Events
  31. */
  32. /**
  33. * Called when the component is mounted
  34. */
  35. created?: (this: Component) => any,
  36. /**
  37. * Called when the component is mounted.
  38. */
  39. mounted?: (this: Component) => any;
  40. }
  41. export class Component {
  42. $container: any;
  43. public static create<
  44. TMethods extends Record<string, CallableFunction>,
  45. TData extends Record<string, any>
  46. >(component: IComponent<TData, TMethods>) {
  47. return new Component(component) as (Component & TMethods);
  48. }
  49. /**
  50. * The component parent to this component.
  51. */
  52. public $parent: Component|null = null;
  53. /**
  54. * The state related to this component.
  55. */
  56. public $state = reactive({});
  57. /**
  58. * Any slots references.
  59. */
  60. public $slots: Record<string, SlotNode> = {};
  61. /**
  62. * Any templates references.
  63. */
  64. public $templates: Record<string, CallableFunction> = {};
  65. /**
  66. * Any component references.
  67. */
  68. public $refs: Record<string, Element> = {};
  69. /**
  70. * If it's the first time that the component is being rendered.
  71. */
  72. public firstRender = true;
  73. /**
  74. * The virtual DOM renderer instance.
  75. */
  76. public renderer = new Renderer(this);
  77. /**
  78. * The component container
  79. */
  80. public $rendered: Element;
  81. constructor(
  82. /**
  83. * The component properties.
  84. */
  85. public $component: IComponent<any, any>
  86. ) {
  87. // If has data
  88. if ($component?.data) {
  89. if (typeof $component.data === "function") {
  90. $component.data = $component.data();
  91. }
  92. for(let key in $component.data) {
  93. // If it's already registered
  94. if (key in this.$state) {
  95. throw new Error("There's already a property named " + key + " registered in the component. Property names should be unique.");
  96. }
  97. this.$state[key] = $component.data[key];
  98. }
  99. }
  100. // If has methods
  101. if ($component?.methods) {
  102. for(let method in $component.methods) {
  103. this.$state[method] = $component.methods[method].bind(this);
  104. }
  105. }
  106. // For each generated data
  107. for(let key in this.$state) {
  108. // Prepare a descriptor for the base component
  109. const attributes: PropertyDescriptor = {
  110. get() {
  111. return this.$state[key]
  112. }
  113. };
  114. // If it's not a function
  115. if (typeof this.$state[key] !== "function") {
  116. attributes.set = (value) => this.$state[key] = value;
  117. }
  118. // Define the property inside the component
  119. Object.defineProperty(this, key, attributes);
  120. }
  121. if (this.$component?.created) {
  122. this.$component.created.call(this);
  123. }
  124. }
  125. /**
  126. * The root component.
  127. */
  128. public get $root() {
  129. let parent = this.$parent;
  130. while(parent?.$parent !== null) {
  131. parent = parent?.$parent;
  132. }
  133. return parent;
  134. }
  135. /**
  136. * Enqueues a function to be executed in the next queue tick.
  137. * @param callback — The callback to be executed.
  138. * @returns
  139. */
  140. public $nextTick(callback: CallableFunction) {
  141. return this.renderer.nextTick(callback);
  142. }
  143. /**
  144. * Registers a single template.
  145. * @param templateName The template name.
  146. * @param template The template render function.
  147. */
  148. public registerTemplate(templateName: string, template: CallableFunction) {
  149. this.$templates[templateName] = template;
  150. }
  151. /**
  152. * Renders the template function into a div tag.
  153. */
  154. public async render() {
  155. if (this.firstRender) {
  156. this.firstRender = false;
  157. this.$rendered = await this.renderer.render();
  158. }
  159. return this.$rendered;
  160. }
  161. /**
  162. * Renders and mounts the template into a given element.
  163. * @param target The target element where the element will be mounted.
  164. * @returns
  165. */
  166. public async mount(target: HTMLElement | SlotNode | string) {
  167. const rendered = await this.render();
  168. // If it's targeting a slot
  169. if (target instanceof SlotNode) {
  170. target.replace(rendered);
  171. this.$container = rendered;
  172. } else
  173. // If it's targeting a string (selector)
  174. if (typeof target === "string") {
  175. this.$container = document.querySelector(target);
  176. this.$container?.append(rendered);
  177. } else
  178. // If it's targeint an element
  179. if (target instanceof Element) {
  180. this.$container = target;
  181. target.append(rendered);
  182. } else {
  183. throw new Error("Invalid mounting target " + target);
  184. }
  185. return rendered;
  186. }
  187. }