first commit
This commit is contained in:
12
package.json
Normal file
12
package.json
Normal 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
91
src/Publisher.ts
Normal 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
99
src/Ref.ts
Normal 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
119
src/Rung.ts
Normal 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
4
src/Subscriber.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type LEvent = string;
|
||||
export default interface ISubscriber<T extends LEvent> {
|
||||
notify(publisher: unknown, event: T): void;
|
||||
}
|
||||
Reference in New Issue
Block a user