From e5e0c47f68c4edc4a68c0d1d15dc7831493dd68b Mon Sep 17 00:00:00 2001 From: Daniel Ledda Date: Thu, 26 May 2022 22:00:23 +0200 Subject: [PATCH] feat: added jsx support and some tests --- index.html | 11 +++++++ index.ts | 35 ++++++---------------- jsx-test.tsx | 4 --- jsxFactory.d.ts | 13 -------- lib/Capsule.ts | 2 +- lib/Rung.ts | 26 ++++++++-------- lib/helpers.ts | 76 +++++++++++++++++++++++++++++++++++++---------- lib/jsxFactory.ts | 27 +++++++++++++++++ package.json | 5 ++-- test.tsx | 49 ++++++++++++++++++++++++++++++ 10 files changed, 173 insertions(+), 75 deletions(-) create mode 100644 index.html delete mode 100644 jsx-test.tsx delete mode 100644 jsxFactory.d.ts create mode 100644 lib/jsxFactory.ts create mode 100644 test.tsx diff --git a/index.html b/index.html new file mode 100644 index 0000000..6ca205c --- /dev/null +++ b/index.html @@ -0,0 +1,11 @@ + + + + + Ladder Test Playground + + +
+ + + \ No newline at end of file diff --git a/index.ts b/index.ts index b17d0cf..840d54e 100644 --- a/index.ts +++ b/index.ts @@ -1,26 +1,9 @@ -export { - ISubscription, - IPublisher, - Publisher, -} from './lib/Publisher'; - -export { - default as ISubscriber, - LEvent, -} from './lib/Subscriber'; - -export { - default as Rung, - RungOptions, -} from './lib/Rung'; - -export { - default as Capsule, -} from './lib/Capsule'; - -export { - bootstrap, - frag, - h, - q, -} from './lib/helpers'; \ No newline at end of file +import "./lib/jsxFactory"; +export type { ISubscription, IPublisher } from './lib/Publisher'; +export { Publisher } from './lib/Publisher'; +export type { default as ISubscriber, LEvent } from './lib/Subscriber'; +export { default as Rung } from './lib/Rung'; +export type { RungOptions } from './lib/Rung'; +export { default as Capsule } from './lib/Capsule'; +export type { ICapsule } from './lib/Capsule'; +export { bootstrap, frag, h, q } from './lib/helpers'; \ No newline at end of file diff --git a/jsx-test.tsx b/jsx-test.tsx deleted file mode 100644 index 6fe636b..0000000 --- a/jsx-test.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { h, frag, q } from "./index"; -import "./jsxFactory"; - -const MyCoolDiv = () =>
My Cool Div!
; \ No newline at end of file diff --git a/jsxFactory.d.ts b/jsxFactory.d.ts deleted file mode 100644 index 5de0bc5..0000000 --- a/jsxFactory.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IRenderAttributes } from './lib/helpers'; - -declare namespace JSX { - type Element = Node; - export interface AttributeCollection { - [name: string]: string | boolean | (() => any); - className: string; - } - type RenderAttributes = { - [TagName in keyof HTMLElementTagNameMap]: IRenderAttributes; - }; - export interface IntrinsicElements extends RenderAttributes {} -} \ No newline at end of file diff --git a/lib/Capsule.ts b/lib/Capsule.ts index 3015e5b..1f9d604 100644 --- a/lib/Capsule.ts +++ b/lib/Capsule.ts @@ -13,7 +13,7 @@ export interface ICapsule { } export function isCapsule(maybeCapsule: any): maybeCapsule is ICapsule { - return Object.prototype.hasOwnProperty.call(maybeCapsule, 'val') + return typeof maybeCapsule.val !== "undefined" && typeof maybeCapsule.watch === "function" && typeof maybeCapsule.toString === "function"; } diff --git a/lib/Rung.ts b/lib/Rung.ts index d71b015..09c6a98 100644 --- a/lib/Rung.ts +++ b/lib/Rung.ts @@ -3,36 +3,36 @@ import {SubNode} from "./helpers"; export type RungOptions = {}; export default abstract class Rung { - protected el: HTMLElement | null = null; + protected node: Node | null = null; protected constructor(options: RungOptions) {} - render(): HTMLElement { - if (!this.el) { - this.el = this.build(); + render(): Node { + if (!this.node) { + this.node = this.build(); } - return this.el; + return this.node; } - protected getEl(): HTMLElement { + protected getEl(): Node { return this.render(); } redraw(): void { - const oldNode = this.el; - if (!oldNode || !this.el) { + const oldNode = this.node; + if (!oldNode || !this.node) { return; } - const parent = this.el.parentElement; + const parent = this.node.parentElement; if (parent) { - this.el = this.build(); - parent.replaceChild(this.el, oldNode); + this.node = this.build(); + parent.replaceChild(this.node, oldNode); } else { this.render(); } } - protected abstract build(): HTMLElement; + protected abstract build(): Node; } -export type FunctionalRung, N extends Node> = (attributes: Props, subNodes: SubNode[]) => N; \ No newline at end of file +export type FunctionalRung, N extends HTMLElement> = (attributes: Props, subNodes?: SubNode[]) => N; diff --git a/lib/helpers.ts b/lib/helpers.ts index 95ff38d..1a6ed44 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -2,17 +2,21 @@ import {isCapsule, ICapsule} from "./Capsule"; import {ISubscription} from "./Publisher"; import Rung, {FunctionalRung} from "./Rung"; -export type IRenderAttributes = Partial<{ - [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule -}> & { +type CommonRenderAttributes = { classes?: string[], - saveTo?: ICapsule, + saveTo?: ICapsule, }; +export type IRenderAttributes = + T extends DocumentFragment + ? Partial<{ [K in keyof DocumentFragment]: DocumentFragment[K] | ICapsule }> & CommonRenderAttributes + : T extends keyof HTMLElementTagNameMap + ? Partial<{ + [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule + }> & CommonRenderAttributes + : never; -type IdSelector = `#${ string }`; - -export function bootstrap(app: Rung, id: IdSelector) { - const rootNode = document.querySelector(id); +export function bootstrap(app: Rung, id: string) { + const rootNode = document.getElementById(id); if (!rootNode) { throw new Error(`No node was found with the id ${id} to attach to`); } else { @@ -20,8 +24,11 @@ export function bootstrap(app: Rung, id: IdSelector) { } } -export function frag(subs?: Node[]): DocumentFragment { +export function frag(attributes: IRenderAttributes | null, subs?: SubNode[]): DocumentFragment { const frag = document.createDocumentFragment(); + if (attributes) { + applyAttributes(frag, attributes); + } if (subs) { attachSubs(frag, subs); } @@ -32,27 +39,62 @@ export function q(text: string): Text { return document.createTextNode(text); } -type InstantiationType = FunctionalRung | keyof HTMLElementTagNameMap; +type InstantiationType = FunctionalRung | keyof HTMLElementTagNameMap | Rung; type Props = T extends FunctionalRung - ? Attributes + ? Attributes & CommonRenderAttributes : T extends keyof HTMLElementTagNameMap ? IRenderAttributes - : never; + : T extends Rung + ? CommonRenderAttributes + : never; export type SubNode = Rung | Node | ICapsule; export function h(type: T, attributes?: Props, ...subNodes: SubNode[]): HTMLElementTagNameMap[T]; -export function h, U extends Props, V extends ReturnType>(type: T, attributes?: U, ...subNodes: SubNode[]): V; +export function h, U extends Props>(type: T, attributes?: U, ...subNodes: SubNode[]): ReturnType; +export function h(type: T, attributes: CommonRenderAttributes): ReturnType; export function h(type: T, attributes?: Props | null, ...subNodes: SubNode[]) { if (typeof type === "function") { - return type(attributes, subNodes); + return createFunctionalRungElement(type, attributes, subNodes); + } else if (type instanceof Rung) { + const rendered = type.render(); + if (attributes?.classes && rendered instanceof HTMLElement) { + rendered.classList.add(...attributes.classes); + } + if (attributes?.saveTo) { + attributes.saveTo.val = rendered; + } + return rendered; } else { return createStandardElement(type, attributes ?? {}, subNodes); } } +function createFunctionalRungElement, U extends Props>( + type: T, + attributes: U, + subNodes: SubNode[] +): ReturnType { + for (let i = 0; i < subNodes.length; i++) { + const subNode = subNodes[i]; + if (isCapsule(subNode)) { + const textNode = q(subNode.toString()); + const sub = subNode.watch((newVal) => nodeCapsuleWatcher(newVal, textNode, sub)); + subNodes[i] = textNode; + } + } + const rendered = subNodes.length > 0 ? type(attributes, subNodes) : type(attributes); + if (attributes?.classes && rendered instanceof HTMLElement) { + rendered.classList.add(...attributes.classes); + } + if (attributes?.saveTo) { + attributes.saveTo.val = rendered; + } + return rendered; +} + function createStandardElement( type: T, attributes: IRenderAttributes | null, @@ -77,7 +119,7 @@ function nodeCapsuleWatcher(newVal: T extends ICapsule ? U : never, sub.unbind(); textNode.remove(); } else { - textNode.replaceWith(newVal?.toString() ?? q("[dead ref]")); + textNode.textContent = newVal?.toString() ?? "[dead ref]"; } } @@ -96,7 +138,9 @@ function attachSubs(node: Element | DocumentFragment, subNodes: SubNode[]): void } } -function applyAttributes(element: HTMLElement, attributes: IRenderAttributes): void { +function applyAttributes(element: HTMLElementTagNameMap[T], attributes: IRenderAttributes): void; +function applyAttributes(element: DocumentFragment, attributes: IRenderAttributes): void; +function applyAttributes(element: HTMLElementTagNameMap[keyof HTMLElementTagNameMap] | DocumentFragment, attributes: IRenderAttributes): void { for (const key in attributes) { if (Object.prototype.hasOwnProperty.call(attributes, key)) { const attribute = (attributes as Record)[key]; diff --git a/lib/jsxFactory.ts b/lib/jsxFactory.ts new file mode 100644 index 0000000..2fc2224 --- /dev/null +++ b/lib/jsxFactory.ts @@ -0,0 +1,27 @@ +import { IRenderAttributes } from './helpers'; +import Rung from "./Rung"; +import {ICapsule} from "./Capsule"; + +type RenderAttributesMap = { + [TagName in keyof HTMLElementTagNameMap]: IRenderAttributes; +}; + +declare global { + namespace JSX { + interface Element extends Node {} + interface ElementClass extends Rung {} + interface AttributeCollection { + [name: string]: string | boolean | (() => any); + className: string; + } + interface IntrinsicAttributes { + saveTo?: ICapsule; + classes?: string[]; + } + interface IntrinsicClassAttributes { + saveTo?: ICapsule; + classes?: string[]; + } + interface IntrinsicElements extends RenderAttributesMap {} + } +} diff --git a/package.json b/package.json index 03070a7..3401deb 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "author": "Daniel Ledda ", "license": "MIT", "devDependencies": { - "typescript": "^4.7.2" - } + "typescript": "^4.7.2", + "vite": "^2.9.9" + }, } diff --git a/test.tsx b/test.tsx new file mode 100644 index 0000000..8c99134 --- /dev/null +++ b/test.tsx @@ -0,0 +1,49 @@ +import { h, q, frag, bootstrap, Rung, Capsule } from "./index"; +import {SubNode} from "./lib/helpers"; + +const MyCoolDiv = (props: { isRed: boolean }, subNodes?: SubNode[]) => h("div", { classes: props.isRed ? ["red"] : [] }, ...subNodes ?? []); + +class App extends Rung { + private counter = Capsule.new(0); + private rungs = Capsule.new(null); + + constructor() { + super({}); + this.counter.watch((count) => { + if (this.rungs.val) { + this.rungs.val.replaceChildren( + ...new Array(count).fill(null).map((_, i) => { + return
; + }) + ); + } + }); + } + + build() { + return <> + +

Ladder

+ + {this.counter} + +
+ ; + } +} + +const app = new App(); +bootstrap(app, "app");