@praxisjs/runtime 0.1.0 → 0.2.0

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/children.d.ts +3 -0
  3. package/dist/children.d.ts.map +1 -0
  4. package/dist/children.js +24 -0
  5. package/dist/children.js.map +1 -0
  6. package/dist/component.d.ts +5 -0
  7. package/dist/component.d.ts.map +1 -0
  8. package/dist/component.js +42 -0
  9. package/dist/component.js.map +1 -0
  10. package/dist/context.d.ts +4 -0
  11. package/dist/context.d.ts.map +1 -0
  12. package/dist/context.js +18 -0
  13. package/dist/context.js.map +1 -0
  14. package/dist/dom/constants.d.ts +5 -0
  15. package/dist/dom/constants.d.ts.map +1 -0
  16. package/dist/dom/constants.js +61 -0
  17. package/dist/dom/constants.js.map +1 -0
  18. package/dist/dom/create.d.ts +2 -0
  19. package/dist/dom/create.d.ts.map +1 -0
  20. package/dist/dom/create.js +7 -0
  21. package/dist/dom/create.js.map +1 -0
  22. package/dist/dom/events.d.ts +3 -0
  23. package/dist/dom/events.d.ts.map +1 -0
  24. package/dist/dom/events.js +5 -0
  25. package/dist/dom/events.js.map +1 -0
  26. package/dist/dom/props.d.ts +3 -0
  27. package/dist/dom/props.d.ts.map +1 -0
  28. package/dist/dom/props.js +72 -0
  29. package/dist/dom/props.js.map +1 -0
  30. package/dist/element.d.ts +3 -0
  31. package/dist/element.d.ts.map +1 -0
  32. package/dist/element.js +16 -0
  33. package/dist/element.js.map +1 -0
  34. package/dist/index.d.ts +14 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +28 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/reactive.d.ts +3 -0
  39. package/dist/reactive.d.ts.map +1 -0
  40. package/dist/reactive.js +35 -0
  41. package/dist/reactive.js.map +1 -0
  42. package/dist/scope.d.ts +10 -0
  43. package/dist/scope.d.ts.map +1 -0
  44. package/dist/scope.js +20 -0
  45. package/dist/scope.js.map +1 -0
  46. package/package.json +4 -5
  47. package/src/children.ts +33 -0
  48. package/src/component.ts +60 -0
  49. package/src/context.ts +22 -0
  50. package/src/dom/constants.ts +63 -0
  51. package/src/dom/create.ts +7 -0
  52. package/src/dom/events.ts +11 -0
  53. package/src/dom/props.ts +79 -0
  54. package/src/element.ts +24 -0
  55. package/src/index.ts +36 -1
  56. package/src/reactive.ts +45 -0
  57. package/src/scope.ts +25 -0
  58. package/dist/renderer.d.ts +0 -3
  59. package/dist/renderer.d.ts.map +0 -1
  60. package/dist/renderer.js +0 -380
  61. package/dist/renderer.js.map +0 -1
  62. package/src/renderer.ts +0 -473
@@ -0,0 +1,60 @@
1
+ import { initSlots } from "@praxisjs/decorators";
2
+ import { isComponent, type ComponentConstructor } from "@praxisjs/shared/internal";
3
+
4
+ import { mountChildren } from "./children";
5
+ import { runInScope } from "./context";
6
+
7
+ import type { Scope } from "./scope";
8
+
9
+ export function mountComponent(
10
+ ctor: ComponentConstructor,
11
+ props: Record<string, unknown>,
12
+ parentScope: Scope,
13
+ ): Node[] {
14
+ const scope = parentScope.fork();
15
+
16
+ const instance = new ctor({ ...props });
17
+
18
+ const rawChildren = props.children;
19
+ if (rawChildren != null) {
20
+ initSlots(instance, rawChildren);
21
+ }
22
+
23
+ const start = document.createComment(`[${ctor.name}]`);
24
+ const end = document.createComment(`[/${ctor.name}]`);
25
+
26
+ // Expose anchor so decorators like @Virtual can find the parent element
27
+ instance._anchor = end;
28
+
29
+ instance.onBeforeMount?.();
30
+
31
+ const container = document.createDocumentFragment();
32
+ container.appendChild(start);
33
+
34
+ let dom: Node | Node[] | null = null;
35
+ runInScope(scope, () => {
36
+ try {
37
+ dom = instance.render();
38
+ } catch (e) {
39
+ instance.onError?.(e instanceof Error ? e : new Error(String(e)));
40
+ }
41
+ });
42
+
43
+ mountChildren(container, dom, scope);
44
+ container.appendChild(end);
45
+
46
+ queueMicrotask(() => {
47
+ instance._mounted = true;
48
+ instance.onMount?.();
49
+ });
50
+
51
+ scope.add(() => {
52
+ instance.onUnmount?.();
53
+ instance._mounted = false;
54
+ });
55
+
56
+ // Return the nodes from the fragment as an array so the caller can append them
57
+ return Array.from(container.childNodes);
58
+ }
59
+
60
+ export { isComponent };
package/src/context.ts ADDED
@@ -0,0 +1,22 @@
1
+ import type { Scope } from "./scope";
2
+
3
+ let _currentScope: Scope | null = null;
4
+
5
+ export function getCurrentScope(): Scope {
6
+ if (!_currentScope) {
7
+ throw new Error(
8
+ "[PraxisJS] jsx() called outside of a render context. Make sure to call render() with a factory function.",
9
+ );
10
+ }
11
+ return _currentScope;
12
+ }
13
+
14
+ export function runInScope<T>(scope: Scope, fn: () => T): T {
15
+ const prev = _currentScope;
16
+ _currentScope = scope;
17
+ try {
18
+ return fn();
19
+ } finally {
20
+ _currentScope = prev;
21
+ }
22
+ }
@@ -0,0 +1,63 @@
1
+ export const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
2
+
3
+ export const SVG_TAGS = new Set([
4
+ "svg",
5
+ "path",
6
+ "circle",
7
+ "rect",
8
+ "line",
9
+ "polyline",
10
+ "polygon",
11
+ "ellipse",
12
+ "text",
13
+ "g",
14
+ "defs",
15
+ "use",
16
+ "symbol",
17
+ "marker",
18
+ "clipPath",
19
+ "mask",
20
+ "pattern",
21
+ "image",
22
+ "linearGradient",
23
+ "radialGradient",
24
+ "stop",
25
+ "filter",
26
+ "feGaussianBlur",
27
+ "tspan",
28
+ "textPath",
29
+ "foreignObject",
30
+ ]);
31
+
32
+ export const EVENT_MAP: Record<string, string> = {
33
+ onClick: "click",
34
+ onDblClick: "dblclick",
35
+ onChange: "change",
36
+ onInput: "input",
37
+ onSubmit: "submit",
38
+ onReset: "reset",
39
+ onKeyDown: "keydown",
40
+ onKeyUp: "keyup",
41
+ onKeyPress: "keypress",
42
+ onFocus: "focus",
43
+ onBlur: "blur",
44
+ onMouseDown: "mousedown",
45
+ onMouseUp: "mouseup",
46
+ onMouseEnter: "mouseenter",
47
+ onMouseLeave: "mouseleave",
48
+ onMouseMove: "mousemove",
49
+ onContextMenu: "contextmenu",
50
+ onScroll: "scroll",
51
+ onWheel: "wheel",
52
+ onDragStart: "dragstart",
53
+ onDragEnd: "dragend",
54
+ onDragOver: "dragover",
55
+ onDrop: "drop",
56
+ onTouchStart: "touchstart",
57
+ onTouchEnd: "touchend",
58
+ onTouchMove: "touchmove",
59
+ onAnimationEnd: "animationend",
60
+ onTransitionEnd: "transitionend",
61
+ };
62
+
63
+ export const VALUE_PROPS = new Set(["checked", "value", "disabled", "selected"]);
@@ -0,0 +1,7 @@
1
+ import { SVG_NAMESPACE, SVG_TAGS } from "./constants";
2
+
3
+ export function createElement(tag: string): HTMLElement | SVGElement {
4
+ return SVG_TAGS.has(tag)
5
+ ? (document.createElementNS(SVG_NAMESPACE, tag))
6
+ : document.createElement(tag);
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { Scope } from "../scope";
2
+
3
+ export function addEvent(
4
+ el: Element,
5
+ eventName: string,
6
+ handler: EventListener,
7
+ scope: Scope,
8
+ ): void {
9
+ el.addEventListener(eventName, handler);
10
+ scope.add(() => { el.removeEventListener(eventName, handler); });
11
+ }
@@ -0,0 +1,79 @@
1
+ import { EVENT_MAP, VALUE_PROPS } from "./constants";
2
+ import { addEvent } from "./events";
3
+
4
+ import type { Scope } from "../scope";
5
+
6
+ function applyClass(el: Element, value: unknown): void {
7
+ if (value === null || value === undefined) {
8
+ el.removeAttribute("class");
9
+ } else {
10
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
11
+ el.setAttribute("class", String(value));
12
+ }
13
+ }
14
+
15
+ function applyStyle(el: Element, value: unknown): void {
16
+ if (value === null || value === undefined) {
17
+ el.removeAttribute("style");
18
+ } else if (typeof value === "object") {
19
+ const htmlEl = el as HTMLElement;
20
+ htmlEl.removeAttribute("style");
21
+ Object.assign(htmlEl.style, value);
22
+ } else {
23
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
24
+ el.setAttribute("style", String(value));
25
+ }
26
+ }
27
+
28
+ function applyAttr(el: Element, key: string, value: unknown): void {
29
+ if (value === false || value === null || value === undefined) {
30
+ el.removeAttribute(key);
31
+ } else if (value === true) {
32
+ el.setAttribute(key, "");
33
+ } else {
34
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
35
+ el.setAttribute(key, String(value));
36
+ }
37
+ }
38
+
39
+ function setProp(el: Element, key: string, value: unknown): void {
40
+ if (key === "class" || key === "className") {
41
+ applyClass(el, value);
42
+ } else if (key === "style") {
43
+ applyStyle(el, value);
44
+ } else if (VALUE_PROPS.has(key)) {
45
+ (el as unknown as Record<string, unknown>)[key] = value;
46
+ } else {
47
+ applyAttr(el, key, value);
48
+ }
49
+ }
50
+
51
+ export function applyProp(
52
+ el: Element,
53
+ key: string,
54
+ value: unknown,
55
+ scope: Scope,
56
+ ): void {
57
+ const normalizedKey = key === "htmlFor" ? "for" : key;
58
+
59
+ if (normalizedKey === "key" || normalizedKey === "children") return;
60
+
61
+ if (normalizedKey === "ref") {
62
+ (value as (el: Element) => void)(el);
63
+ return;
64
+ }
65
+
66
+ if (normalizedKey in EVENT_MAP) {
67
+ addEvent(el, EVENT_MAP[normalizedKey], value as EventListener, scope);
68
+ return;
69
+ }
70
+
71
+ if (typeof value === "function") {
72
+ scope.effect(() => {
73
+ setProp(el, normalizedKey, (value as () => unknown)());
74
+ });
75
+ return;
76
+ }
77
+
78
+ setProp(el, normalizedKey, value);
79
+ }
package/src/element.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { mountChildren } from "./children";
2
+ import { createElement } from "./dom/create";
3
+ import { applyProp } from "./dom/props";
4
+
5
+ import type { Scope } from "./scope";
6
+
7
+ export function mountElement(
8
+ tag: string,
9
+ props: Record<string, unknown>,
10
+ scope: Scope,
11
+ ): HTMLElement | SVGElement {
12
+ const el = createElement(tag);
13
+
14
+ for (const [key, val] of Object.entries(props)) {
15
+ if (key === "children") continue;
16
+ applyProp(el, key, val, scope);
17
+ }
18
+
19
+ if (props.children !== undefined) {
20
+ mountChildren(el, props.children, scope);
21
+ }
22
+
23
+ return el;
24
+ }
package/src/index.ts CHANGED
@@ -1 +1,36 @@
1
- export { render } from "./renderer";
1
+ import { mountChildren } from "./children";
2
+ import { runInScope } from "./context";
3
+ import { Scope } from "./scope";
4
+
5
+ /**
6
+ * Renders a component tree into a container element.
7
+ *
8
+ * The factory function is called once inside the root scope,
9
+ * so `jsx()` can access the current scope via `getCurrentScope()`.
10
+ *
11
+ * @example
12
+ * render(() => <App />, document.getElementById('app'));
13
+ */
14
+ export function render(
15
+ factory: () => Node | Node[] | null,
16
+ container: HTMLElement,
17
+ ): () => void {
18
+ const rootScope = new Scope();
19
+
20
+ container.innerHTML = "";
21
+
22
+ runInScope(rootScope, () => {
23
+ const result = factory();
24
+ mountChildren(container, result, rootScope);
25
+ });
26
+
27
+ return () => {
28
+ rootScope.dispose();
29
+ container.innerHTML = "";
30
+ };
31
+ }
32
+
33
+ export { Scope } from "./scope";
34
+ export { runInScope, getCurrentScope } from "./context";
35
+ export { mountElement } from "./element";
36
+ export { mountComponent } from "./component";
@@ -0,0 +1,45 @@
1
+ import { runInScope } from "./context";
2
+
3
+ import type { Scope } from "./scope";
4
+
5
+ function normalizeToNodes(value: unknown): Node[] {
6
+ if (value === null || value === undefined || value === false) return [];
7
+ if (value instanceof Node) return [value];
8
+ if (Array.isArray(value)) return value.flatMap(normalizeToNodes);
9
+ if (typeof value === "string" || typeof value === "number") {
10
+ return [document.createTextNode(String(value))];
11
+ }
12
+ return [];
13
+ }
14
+
15
+ export function mountReactive(
16
+ parent: Node,
17
+ fn: () => unknown,
18
+ parentScope: Scope,
19
+ ): void {
20
+ const end = document.createComment("reactive");
21
+ parent.appendChild(end);
22
+
23
+ let currentNodes: Node[] = [];
24
+ let childScope = parentScope.fork();
25
+
26
+ parentScope.effect(() => {
27
+ childScope.dispose();
28
+ childScope = parentScope.fork();
29
+
30
+ for (const n of currentNodes) {
31
+ n.parentNode?.removeChild(n);
32
+ }
33
+ currentNodes = [];
34
+
35
+ const result = runInScope(childScope, fn);
36
+ const newNodes = normalizeToNodes(result);
37
+
38
+ for (const n of newNodes) {
39
+ parent.insertBefore(n, end);
40
+ }
41
+ currentNodes = newNodes;
42
+ });
43
+
44
+ parentScope.add(() => { childScope.dispose(); });
45
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { effect as coreEffect } from "@praxisjs/core/internal";
2
+ import type { Cleanup } from "@praxisjs/shared/internal";
3
+
4
+ export class Scope {
5
+ #cleanups: Cleanup[] = [];
6
+
7
+ effect(fn: Parameters<typeof coreEffect>[0]): void {
8
+ this.#cleanups.push(coreEffect(fn));
9
+ }
10
+
11
+ add(cleanup: Cleanup): void {
12
+ this.#cleanups.push(cleanup);
13
+ }
14
+
15
+ fork(): Scope {
16
+ const child = new Scope();
17
+ this.#cleanups.push(() => { child.dispose(); });
18
+ return child;
19
+ }
20
+
21
+ dispose(): void {
22
+ this.#cleanups.forEach((fn) => { fn(); });
23
+ this.#cleanups = [];
24
+ }
25
+ }
@@ -1,3 +0,0 @@
1
- import type { VNode } from "@praxisjs/shared";
2
- export declare function render(vnode: VNode, container: HTMLElement): () => void;
3
- //# sourceMappingURL=renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,KAAK,EAGN,MAAM,kBAAkB,CAAC;AAkc1B,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,cAY1D"}