first commit

This commit is contained in:
Daniel Ledda
2022-05-22 22:09:36 +02:00
commit b3ee2af7c5
5 changed files with 325 additions and 0 deletions

12
package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "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"
},
"author": "Daniel Ledda <dan.j.ledda@gmail.com>",
"license": "MIT"
}

91
src/Publisher.ts Normal file
View File

@@ -0,0 +1,91 @@
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;
}

99
src/Ref.ts Normal file
View File

@@ -0,0 +1,99 @@
import { ISubscription } from "./Publisher";
export type MaybeRef<T> = T | Ref<T>;
class RefSubscription implements ISubscription {
private unbindCallback?: () => void;
constructor(unbindCallback: () => void) {
this.unbindCallback = unbindCallback;
}
unbind(): void {
this.unbindCallback?.();
}
}
interface Stringable {
toString(): string;
}
type AllowedRef = { toString(): string } | string | null;
export default class Ref<T extends AllowedRef = Stringable> {
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 AllowedRef>(val: MaybeRef<T>): Ref<T> {
if (val instanceof Ref) {
return val;
} else {
return new Ref<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 RefSubscription(() => 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;
}
}

119
src/Rung.ts Normal file
View File

@@ -0,0 +1,119 @@
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;
}
}
}
}
}

4
src/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;
}