diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d35bbf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4ad2f07 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Ladder + +Most libraries give you a framework. This is just a ladder. \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..72a9ade --- /dev/null +++ b/index.ts @@ -0,0 +1,26 @@ +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'; diff --git a/src/Ref.ts b/lib/Capsule.ts similarity index 83% rename from src/Ref.ts rename to lib/Capsule.ts index b1306d7..00730b9 100644 --- a/src/Ref.ts +++ b/lib/Capsule.ts @@ -1,8 +1,6 @@ import { ISubscription } from "./Publisher"; -export type MaybeRef = T | Ref; - -class RefSubscription implements ISubscription { +class CapsuleSubscription implements ISubscription { private unbindCallback?: () => void; constructor(unbindCallback: () => void) { @@ -17,10 +15,10 @@ class RefSubscription implements ISubscription { interface Stringable { toString(): string; } +type Capsable = Stringable | string | null; +export type MaybeCapsule = T | Capsule; -type AllowedRef = { toString(): string } | string | null; - -export default class Ref { +export default class Capsule { private watchers: Array<(newVal: T) => void> | null = null; private afterWatchers: Array<(newVal: T) => void> | null = null; private value: T; @@ -32,11 +30,11 @@ export default class Ref { this.isString = typeof val === "string"; } - static new(val: MaybeRef): Ref { - if (val instanceof Ref) { + static new(val: MaybeCapsule): Capsule { + if (val instanceof Capsule) { return val; } else { - return new Ref(val); + return new Capsule(val); } } @@ -52,7 +50,7 @@ export default class Ref { } this.watchers.push(watcher); } - return new RefSubscription(() => this.unbind(watcher, !!after)); + return new CapsuleSubscription(() => this.unbind(watcher, !!after)); } private unbind(watcher: (newVal: T) => void, after: boolean): void { diff --git a/src/Publisher.ts b/lib/Publisher.ts similarity index 99% rename from src/Publisher.ts rename to lib/Publisher.ts index f5412b0..dadd1b8 100644 --- a/src/Publisher.ts +++ b/lib/Publisher.ts @@ -32,7 +32,6 @@ interface EventSubscriberRecord { set(key: K, subscribers: ISubscriber[]): EventSubscriberRecord; } - export class Publisher implements IPublisher { private subscribers: EventSubscriberRecord; private parent: PublisherType; diff --git a/lib/Rung.ts b/lib/Rung.ts new file mode 100644 index 0000000..aab9a08 --- /dev/null +++ b/lib/Rung.ts @@ -0,0 +1,34 @@ +export type RungOptions = {}; + +export default abstract class Rung { + protected el: HTMLElement | null = null; + + protected constructor(options: RungOptions) {} + + render(): HTMLElement { + if (!this.el) { + this.el = this.build(); + } + return this.el; + } + + protected getEl(): HTMLElement { + return this.render(); + } + + redraw(): void { + const oldNode = this.el; + if (!oldNode || !this.el) { + return; + } + const parent = this.el.parentElement; + if (parent) { + this.el = this.build(); + parent.replaceChild(this.el, oldNode); + } else { + this.render(); + } + } + + protected abstract build(): HTMLElement; +} diff --git a/src/Subscriber.ts b/lib/Subscriber.ts similarity index 100% rename from src/Subscriber.ts rename to lib/Subscriber.ts diff --git a/lib/helpers.ts b/lib/helpers.ts new file mode 100644 index 0000000..c0765c7 --- /dev/null +++ b/lib/helpers.ts @@ -0,0 +1,92 @@ +import Capsule from "./Capsule"; +import {ISubscription} from "./Publisher"; +import Rung from "./Rung"; + +type IRenderAttributes = Partial<{ + [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Capsule +}> & { + classes?: string[], + saveTo?: Capsule, +}; + +type IdSelector = `#${ string }`; + +export function bootstrap(app: Rung, id: IdSelector) { + const rootNode = document.querySelector(id); + if (!rootNode) { + throw new Error(`No node was found with the id ${id} to attach to`); + } else { + rootNode.appendChild(app.render()); + } +} + +export function frag(subs?: Node[]): DocumentFragment { + const frag = document.createDocumentFragment(); + if (subs) { + attachSubs(frag, subs); + } + return frag; +} + +export function q(text: string): Text { + return document.createTextNode(text); +} + +export function h(type: T, attributes?: IRenderAttributes, subNodes?: (Rung | Node | Capsule)[]): HTMLElementTagNameMap[T] { + const element = document.createElement(type); + if (attributes) { + if (attributes.classes) { + element.classList.add(...attributes.classes); + } + if (attributes.saveTo) { + attributes.saveTo.val = element; + } + applyAttributes(element, attributes); + } + if (subNodes) { + attachSubs(element, subNodes); + } + return element; +} + +function nodeCapsuleWatcher(newVal: T extends Capsule ? U : never, textNode: Text, sub: ISubscription): void { + if (!textNode.parentNode) { + sub.unbind(); + textNode.remove(); + } else { + textNode.replaceWith(newVal?.toString() ?? q("[dead ref]")); + } +} + +function attachSubs(node: Element | DocumentFragment, subNodes: (Rung | Node | Capsule)[]): 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) { + const textNode = q(subNode.toString()); + const sub = subNode.watch((newVal) => nodeCapsuleWatcher(newVal, textNode, sub)); + node.append(textNode); + } else { + node.append(subNode); + } + } +} + +function applyAttributes(element: HTMLElement, attributes: IRenderAttributes): void { + for (const key in attributes) { + if (Object.prototype.hasOwnProperty.call(attributes, key)) { + const attribute = (attributes as Record)[key]; + if (attribute) { + if (attribute instanceof Capsule) { + const attributeAsCapsule = attribute as Capsule; + const elementWithAttributeKey = element as unknown as Record; + elementWithAttributeKey[key] = attributeAsCapsule.val; + attribute.watch((newVal) => elementWithAttributeKey[key] = newVal); + } else { + (element as unknown as Record)[key] = attribute; + } + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 61992fa..03070a7 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { - "name": "ladder", + "name": "@djledda/ladder", "version": "1.0.0", "description": "other libraries provide you with a whole framework - this is just a ladder", - "scripts": { }, - "repository": { - "type": "git", - "url": "https://git.djledda.de/djledda/ladder" - }, + "scripts": {}, "author": "Daniel Ledda ", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "typescript": "^4.7.2" + } } diff --git a/src/Rung.ts b/src/Rung.ts deleted file mode 100644 index 17514e4..0000000 --- a/src/Rung.ts +++ /dev/null @@ -1,119 +0,0 @@ -import Ref from "./Ref"; -import { ISubscription } from "./Publisher"; - -export type RungOptions = {}; - -type IRenderAttributes = Partial<{ - [K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Ref -}> & { - classes?: string[], - saveTo?: Ref, -}; - -export default abstract class Rung { - protected el: HTMLElement | null = null; - - constructor(options: RungOptions) {} - - render(): HTMLElement { - if (!this.el) { - this.el = this.build(); - } - return this.el; - } - - protected getEl(): HTMLElement { - if (!this.el) { - return this.render(); - } else { - return this.el; - } - } - - redraw(): void { - const oldNode = this.el; - if (!oldNode || !this.el) { - return; - } - const parent = this.el.parentElement; - if (parent) { - this.el = this.build(); - parent.replaceChild(this.el, oldNode); - } else { - this.render(); - } - } - - protected abstract build(): HTMLElement; -} - -export function frag(subs?: Rung[]): DocumentFragment { - const frag = document.createDocumentFragment(); - if (subs) { - attachSubs(frag, subs); - } - return frag; -} - -export function q(text: string): Text { - return document.createTextNode(text); -} - -export function h(type: T, attributes?: IRenderAttributes, subNodes?: (Rung | Node | Ref)[]): HTMLElementTagNameMap[T] { - const element = document.createElement(type); - if (attributes) { - if (attributes.classes) { - element.classList.add(...attributes.classes); - } - if (attributes.saveTo) { - attributes.saveTo.val = element; - } - applyAttributes(element, attributes); - } - if (subNodes) { - attachSubs(element, subNodes); - } - return element; -} - -function nodeRefWatcher(newVal: T extends Ref ? U : never, textNode: Text, sub: ISubscription): void { - if (!textNode.parentNode) { - sub.unbind(); - textNode.remove(); - } else { - textNode.replaceWith(newVal?.toString() ?? ""); - } -} - -function attachSubs(node: Element | DocumentFragment, subNodes: (Rung | Node | Ref)[]): 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 Ref) { - const textNode = q(subNode.val.toString()); - const sub = subNode.watch((newVal) => nodeRefWatcher(newVal, textNode, sub)); - node.append(textNode); - } else { - node.append(subNode); - } - } -} - -function applyAttributes(element: HTMLElement, attributes: IRenderAttributes): void { - for (const key in attributes) { - if (Object.prototype.hasOwnProperty.call(attributes, key)) { - const attribute = (attributes as Record)[key]; - if (attribute) { - if (attribute instanceof Ref) { - const attributeAsRef = attribute as Ref; - const elementWithAttributeKey = element as unknown as Record; - elementWithAttributeKey[key] = attributeAsRef.val; - attribute.watch((newVal) => elementWithAttributeKey[key] = newVal); - } else { - (element as unknown as ({ [key: string]: typeof attribute }))[key] = attribute; - } - } - } - } -} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1193e0e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "sourceMap": true + }, + "exclude": [ + "node_modules" + ], + "include": [ + "index.ts", + "lib/**/*.ts" + ] +} \ No newline at end of file