diff --git a/README.md b/README.md index 4ad2f07..4fbc073 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # Ladder -Most libraries give you a framework. This is just a ladder. \ No newline at end of file +Most libraries give you a framework. This is just a ladder. + +## What's in the box? + +- JSX-friendly \ No newline at end of file diff --git a/index.ts b/index.ts index 72a9ade..b17d0cf 100644 --- a/index.ts +++ b/index.ts @@ -23,4 +23,4 @@ export { frag, h, q, -} from './lib/helpers'; +} from './lib/helpers'; \ No newline at end of file diff --git a/jsx-test.tsx b/jsx-test.tsx new file mode 100644 index 0000000..6fe636b --- /dev/null +++ b/jsx-test.tsx @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..5de0bc5 --- /dev/null +++ b/jsxFactory.d.ts @@ -0,0 +1,13 @@ +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 295b03f..3015e5b 100644 --- a/lib/Capsule.ts +++ b/lib/Capsule.ts @@ -1,5 +1,23 @@ import { ISubscription } from "./Publisher"; +export interface Stringable { + toString(): string; +} + +export type Captable = Stringable | string | null; + +export interface ICapsule { + watch(watcher: (newVal: T) => void, after?: boolean): ISubscription; + toString(): string; + val: T; +} + +export function isCapsule(maybeCapsule: any): maybeCapsule is ICapsule { + return Object.prototype.hasOwnProperty.call(maybeCapsule, 'val') + && typeof maybeCapsule.watch === "function" + && typeof maybeCapsule.toString === "function"; +} + class CapsuleSubscription implements ISubscription { private unbindCallback?: () => void; @@ -12,13 +30,7 @@ class CapsuleSubscription implements ISubscription { } } -interface Stringable { - toString(): string; -} -type Captable = Stringable | string | null; -export type MaybeCapsule = T | Capsule; - -export default class Capsule { +export default class Capsule implements ICapsule { private watchers: Array<(newVal: T) => void> | null = null; private afterWatchers: Array<(newVal: T) => void> | null = null; private value: T; @@ -30,7 +42,7 @@ export default class Capsule { this.isString = typeof val === "string"; } - static new(val: MaybeCapsule): Capsule { + static new(val: T | Capsule): Capsule { if (val instanceof Capsule) { return val; } else { @@ -84,12 +96,11 @@ export default class Capsule { } toString(): string { + if (this.isString) { + return this.value as unknown as string; + } if (!this.asString) { - if (this.isString) { - return this.val as unknown as string; - } else { - this.asString = this.val?.toString() ?? "null"; - } + this.asString = this.val?.toString() ?? "null"; } return this.asString; } diff --git a/lib/Publisher.ts b/lib/Publisher.ts index dadd1b8..d40c4ab 100644 --- a/lib/Publisher.ts +++ b/lib/Publisher.ts @@ -82,7 +82,7 @@ export class Publisher implements IPubl } export interface IPublisher { - addSubscriber(subscriber: ISubscriber, subscribeTo: T | T[]): {unbind: () => void}; + addSubscriber(subscriber: ISubscriber, subscribeTo: T | T[]): { unbind: () => void }; } export interface ISubscription { diff --git a/lib/Rung.ts b/lib/Rung.ts index aab9a08..d71b015 100644 --- a/lib/Rung.ts +++ b/lib/Rung.ts @@ -1,3 +1,5 @@ +import {SubNode} from "./helpers"; + export type RungOptions = {}; export default abstract class Rung { @@ -32,3 +34,5 @@ export default abstract class Rung { protected abstract build(): HTMLElement; } + +export type FunctionalRung, N extends Node> = (attributes: Props, subNodes: SubNode[]) => N; \ No newline at end of file diff --git a/lib/helpers.ts b/lib/helpers.ts index c0765c7..95ff38d 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -1,12 +1,12 @@ -import Capsule from "./Capsule"; +import {isCapsule, ICapsule} from "./Capsule"; import {ISubscription} from "./Publisher"; -import Rung from "./Rung"; +import Rung, {FunctionalRung} from "./Rung"; -type IRenderAttributes = Partial<{ - [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Capsule +export type IRenderAttributes = Partial<{ + [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | ICapsule }> & { classes?: string[], - saveTo?: Capsule, + saveTo?: ICapsule, }; type IdSelector = `#${ string }`; @@ -32,7 +32,32 @@ export function q(text: string): Text { return document.createTextNode(text); } -export function h(type: T, attributes?: IRenderAttributes, subNodes?: (Rung | Node | Capsule)[]): HTMLElementTagNameMap[T] { +type InstantiationType = FunctionalRung | keyof HTMLElementTagNameMap; + +type Props = + T extends FunctionalRung + ? Attributes + : T extends keyof HTMLElementTagNameMap + ? IRenderAttributes + : 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(type: T, attributes?: Props | null, ...subNodes: SubNode[]) { + if (typeof type === "function") { + return type(attributes, subNodes); + } else { + return createStandardElement(type, attributes ?? {}, subNodes); + } +} + +function createStandardElement( + type: T, + attributes: IRenderAttributes | null, + subNodes: SubNode[] +): HTMLElementTagNameMap[T] { const element = document.createElement(type); if (attributes) { if (attributes.classes) { @@ -43,13 +68,11 @@ export function h(type: T, attributes?: I } applyAttributes(element, attributes); } - if (subNodes) { - attachSubs(element, subNodes); - } + attachSubs(element, subNodes); return element; } -function nodeCapsuleWatcher(newVal: T extends Capsule ? U : never, textNode: Text, sub: ISubscription): void { +function nodeCapsuleWatcher(newVal: T extends ICapsule ? U : never, textNode: Text, sub: ISubscription): void { if (!textNode.parentNode) { sub.unbind(); textNode.remove(); @@ -58,14 +81,14 @@ function nodeCapsuleWatcher(newVal: T extends Capsule ? U : never, t } } -function attachSubs(node: Element | DocumentFragment, subNodes: (Rung | Node | Capsule)[]): void { +function attachSubs(node: Element | DocumentFragment, subNodes: SubNode[]): void { for (let i = 0; i < subNodes.length; i++) { const subNode = subNodes[i]; if (subNode instanceof Rung) { node.append(subNode.render()); - } else if (subNode instanceof Capsule) { + } else if (isCapsule(subNode)) { const textNode = q(subNode.toString()); - const sub = subNode.watch((newVal) => nodeCapsuleWatcher(newVal, textNode, sub)); + const sub = subNode.watch((newVal) => nodeCapsuleWatcher(newVal, textNode, sub)); node.append(textNode); } else { node.append(subNode); @@ -78,8 +101,8 @@ function applyAttributes(element: HTMLEle if (Object.prototype.hasOwnProperty.call(attributes, key)) { const attribute = (attributes as Record)[key]; if (attribute) { - if (attribute instanceof Capsule) { - const attributeAsCapsule = attribute as Capsule; + if (isCapsule(attribute)) { + const attributeAsCapsule = attribute as ICapsule; const elementWithAttributeKey = element as unknown as Record; elementWithAttributeKey[key] = attributeAsCapsule.val; attribute.watch((newVal) => elementWithAttributeKey[key] = newVal); diff --git a/tsconfig.json b/tsconfig.json index 1193e0e..7fb514a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2020", - "sourceMap": true + "sourceMap": true, + "strict": true, + "jsx": "react", + "jsxFactory": "h", + "jsxFragmentFactory": "frag" }, "exclude": [ "node_modules"