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

97
lib/Capsule.ts Normal file
View File

@@ -0,0 +1,97 @@
import { ISubscription } from "./Publisher";
class CapsuleSubscription implements ISubscription {
private unbindCallback?: () => void;
constructor(unbindCallback: () => void) {
this.unbindCallback = unbindCallback;
}
unbind(): void {
this.unbindCallback?.();
}
}
interface Stringable {
toString(): string;
}
type Capsable = Stringable | string | null;
export type MaybeCapsule<T> = T | Capsule<T>;
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;
private asString?: string;
private isString: boolean;
private constructor(val: T) {
this.value = val;
this.isString = typeof val === "string";
}
static new<T extends Capsable>(val: MaybeCapsule<T>): Capsule<T> {
if (val instanceof Capsule) {
return val;
} else {
return new Capsule<T>(val);
}
}
watch(watcher: (newVal: T) => void, after?: boolean): ISubscription {
if (after) {
if (this.afterWatchers === null) {
this.afterWatchers = [];
}
this.afterWatchers.push(watcher);
} else {
if (this.watchers === null) {
this.watchers = [];
}
this.watchers.push(watcher);
}
return new CapsuleSubscription(() => this.unbind(watcher, !!after));
}
private unbind(watcher: (newVal: T) => void, after: boolean): void {
if (after) {
if (!this.afterWatchers) {
return;
}
const index = this.afterWatchers.indexOf(watcher);
if (index !== -1) {
this.afterWatchers.splice(index, 1);
}
} else {
if (!this.watchers) {
return;
}
const index = this.watchers.indexOf(watcher);
if (index !== -1) {
this.watchers.splice(index, 1);
}
}
}
get val(): T {
return this.value;
}
set val(val: T) {
this.watchers?.forEach(watcher => watcher(val));
this.value = val;
this.afterWatchers?.forEach(watcher => watcher(val));
}
toString(): string {
if (!this.asString) {
if (this.isString) {
return this.val as unknown as string;
} else {
this.asString = this.val?.toString() ?? "null";
}
}
return this.asString;
}
}

90
lib/Publisher.ts Normal file
View File

@@ -0,0 +1,90 @@
import ISubscriber, {LEvent} from "./Subscriber";
class PublisherSubscription<EventType extends LEvent> implements ISubscription {
private subscriber: ISubscriber<EventType>;
private readonly eventTypes: EventType[];
private unbindCallback?: () => void;
constructor(subscriber: ISubscriber<EventType>, eventTypes: EventType[]) {
this.subscriber = subscriber;
this.eventTypes = eventTypes;
}
setUnbind(unbind: () => void): void {
this.unbindCallback = unbind;
}
unbind(): void {
this.unbindCallback?.();
}
getEventTypes(): EventType[] {
return this.eventTypes;
}
getSubscriber(): ISubscriber<EventType> {
return this.subscriber;
}
}
interface EventSubscriberRecord<T extends LEvent> {
get<K extends T>(key: K): ISubscriber<K>[];
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;
constructor(parent: PublisherType) {
this.parent = parent;
this.subscribers = new Map();
}
addSubscriber(subscriber: ISubscriber<EventType>, subscribeTo: EventType | Readonly<EventType[]>): ISubscription {
let eventTypes: EventType[] = [];
if (typeof subscribeTo === "string") {
eventTypes.push(subscribeTo);
} else {
eventTypes = subscribeTo.slice();
}
for (const key of eventTypes) {
this.getSubscribers(key).push(subscriber);
}
const sub = new PublisherSubscription(subscriber, eventTypes);
sub.setUnbind(() => this.unbind(sub));
return sub;
}
private unbind(subscription: PublisherSubscription<EventType>): void {
for (const key of subscription.getEventTypes()) {
const subs = this.getSubscribers(key);
subs.splice(subs.indexOf(subscription.getSubscriber()), 1);
}
}
private getSubscribers<K extends EventType>(key: K): ISubscriber<K>[] {
const subscribersList = this.subscribers.get(key);
if (subscribersList === undefined) {
const newList: ISubscriber<K>[] = [];
this.subscribers.set(key, newList);
return newList;
} else {
return subscribersList;
}
}
notifySubs<K extends EventType>(eventType: K): void {
for (const sub of this.getSubscribers(eventType)) {
sub.notify(this.parent, eventType);
}
}
}
export interface IPublisher<T extends LEvent> {
addSubscriber(subscriber: ISubscriber<T>, subscribeTo: T | T[]): {unbind: () => void};
}
export interface ISubscription {
unbind(): void;
}

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

4
lib/Subscriber.ts Normal file
View File

@@ -0,0 +1,4 @@
export type LEvent = string;
export default interface ISubscriber<T extends LEvent> {
notify(publisher: unknown, event: T): void;
}

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