feat: some refactoring and cleanup

This commit is contained in:
Daniel Ledda
2022-05-25 23:49:20 +02:00
parent b3ee2af7c5
commit 7aa8941227
11 changed files with 184 additions and 137 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
.idea/

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# Ladder
Most libraries give you a framework. This is just a ladder.

26
index.ts Normal file
View File

@@ -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';

View File

@@ -1,8 +1,6 @@
import { ISubscription } from "./Publisher";
export type MaybeRef<T> = T | Ref<T>;
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> = T | Capsule<T>;
type AllowedRef = { toString(): string } | string | null;
export default class Ref<T extends AllowedRef = Stringable> {
export default class Capsule<T extends Capsable = Capsable> {
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<T extends AllowedRef = Stringable> {
this.isString = typeof val === "string";
}
static new<T extends AllowedRef>(val: MaybeRef<T>): Ref<T> {
if (val instanceof Ref) {
static new<T extends Capsable>(val: MaybeCapsule<T>): Capsule<T> {
if (val instanceof Capsule) {
return val;
} else {
return new Ref<T>(val);
return new Capsule<T>(val);
}
}
@@ -52,7 +50,7 @@ export default class Ref<T extends AllowedRef = Stringable> {
}
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 {

View File

@@ -32,7 +32,6 @@ interface EventSubscriberRecord<T extends LEvent> {
set<K extends T>(key: K, subscribers: ISubscriber<K>[]): EventSubscriberRecord<T>;
}
export class Publisher<EventType extends LEvent, PublisherType> implements IPublisher<EventType> {
private subscribers: EventSubscriberRecord<EventType>;
private parent: PublisherType;

34
lib/Rung.ts Normal file
View File

@@ -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;
}

92
lib/helpers.ts Normal file
View File

@@ -0,0 +1,92 @@
import Capsule from "./Capsule";
import {ISubscription} from "./Publisher";
import Rung from "./Rung";
type IRenderAttributes<T extends keyof HTMLElementTagNameMap> = Partial<{
[K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Capsule<HTMLElementTagNameMap[T][K]>
}> & {
classes?: string[],
saveTo?: Capsule<HTMLElementTagNameMap[T] | null>,
};
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<T extends keyof HTMLElementTagNameMap>(type: T, attributes?: IRenderAttributes<T>, 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<T>(newVal: T extends Capsule<infer U> ? 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<Capsule>(newVal, textNode, sub));
node.append(textNode);
} else {
node.append(subNode);
}
}
}
function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLElement, attributes: IRenderAttributes<T>): void {
for (const key in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const attribute = (attributes as Record<string, unknown>)[key];
if (attribute) {
if (attribute instanceof Capsule) {
const attributeAsCapsule = attribute as Capsule;
const elementWithAttributeKey = element as unknown as Record<string, typeof attributeAsCapsule.val>;
elementWithAttributeKey[key] = attributeAsCapsule.val;
attribute.watch((newVal) => elementWithAttributeKey[key] = newVal);
} else {
(element as unknown as Record<string, typeof attribute>)[key] = attribute;
}
}
}
}
}

View File

@@ -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 <dan.j.ledda@gmail.com>",
"license": "MIT"
"license": "MIT",
"devDependencies": {
"typescript": "^4.7.2"
}
}

View File

@@ -1,119 +0,0 @@
import Ref from "./Ref";
import { ISubscription } from "./Publisher";
export type RungOptions = {};
type IRenderAttributes<T extends keyof HTMLElementTagNameMap> = Partial<{
[K in keyof HTMLElementTagNameMap[T]]: HTMLElementTagNameMap[T][K] | Ref<HTMLElementTagNameMap[T][K]>
}> & {
classes?: string[],
saveTo?: Ref<HTMLElementTagNameMap[T] | null>,
};
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<T extends keyof HTMLElementTagNameMap>(type: T, attributes?: IRenderAttributes<T>, 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<T>(newVal: T extends Ref<infer U> ? U : never, textNode: Text, sub: ISubscription): void {
if (!textNode.parentNode) {
sub.unbind();
textNode.remove();
} else {
textNode.replaceWith(newVal?.toString() ?? "<dead ref>");
}
}
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<Ref>(newVal, textNode, sub));
node.append(textNode);
} else {
node.append(subNode);
}
}
}
function applyAttributes<T extends keyof HTMLElementTagNameMap>(element: HTMLElement, attributes: IRenderAttributes<T>): void {
for (const key in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, key)) {
const attribute = (attributes as Record<string, unknown>)[key];
if (attribute) {
if (attribute instanceof Ref) {
const attributeAsRef = attribute as Ref;
const elementWithAttributeKey = element as unknown as Record<string, typeof attributeAsRef.val>;
elementWithAttributeKey[key] = attributeAsRef.val;
attribute.watch((newVal) => elementWithAttributeKey[key] = newVal);
} else {
(element as unknown as ({ [key: string]: typeof attribute }))[key] = attribute;
}
}
}
}
}

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"sourceMap": true
},
"exclude": [
"node_modules"
],
"include": [
"index.ts",
"lib/**/*.ts"
]
}