feat: some refactoring and cleanup
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.idea/
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Ladder
|
||||||
|
|
||||||
|
Most libraries give you a framework. This is just a ladder.
|
||||||
26
index.ts
Normal file
26
index.ts
Normal 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';
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { ISubscription } from "./Publisher";
|
import { ISubscription } from "./Publisher";
|
||||||
|
|
||||||
export type MaybeRef<T> = T | Ref<T>;
|
class CapsuleSubscription implements ISubscription {
|
||||||
|
|
||||||
class RefSubscription implements ISubscription {
|
|
||||||
private unbindCallback?: () => void;
|
private unbindCallback?: () => void;
|
||||||
|
|
||||||
constructor(unbindCallback: () => void) {
|
constructor(unbindCallback: () => void) {
|
||||||
@@ -17,10 +15,10 @@ class RefSubscription implements ISubscription {
|
|||||||
interface Stringable {
|
interface Stringable {
|
||||||
toString(): string;
|
toString(): string;
|
||||||
}
|
}
|
||||||
|
type Capsable = Stringable | string | null;
|
||||||
|
export type MaybeCapsule<T> = T | Capsule<T>;
|
||||||
|
|
||||||
type AllowedRef = { toString(): string } | string | null;
|
export default class Capsule<T extends Capsable = Capsable> {
|
||||||
|
|
||||||
export default class Ref<T extends AllowedRef = Stringable> {
|
|
||||||
private watchers: Array<(newVal: T) => void> | null = null;
|
private watchers: Array<(newVal: T) => void> | null = null;
|
||||||
private afterWatchers: Array<(newVal: T) => void> | null = null;
|
private afterWatchers: Array<(newVal: T) => void> | null = null;
|
||||||
private value: T;
|
private value: T;
|
||||||
@@ -32,11 +30,11 @@ export default class Ref<T extends AllowedRef = Stringable> {
|
|||||||
this.isString = typeof val === "string";
|
this.isString = typeof val === "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
static new<T extends AllowedRef>(val: MaybeRef<T>): Ref<T> {
|
static new<T extends Capsable>(val: MaybeCapsule<T>): Capsule<T> {
|
||||||
if (val instanceof Ref) {
|
if (val instanceof Capsule) {
|
||||||
return val;
|
return val;
|
||||||
} else {
|
} 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);
|
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 {
|
private unbind(watcher: (newVal: T) => void, after: boolean): void {
|
||||||
@@ -32,7 +32,6 @@ interface EventSubscriberRecord<T extends LEvent> {
|
|||||||
set<K extends T>(key: K, subscribers: ISubscriber<K>[]): EventSubscriberRecord<T>;
|
set<K extends T>(key: K, subscribers: ISubscriber<K>[]): EventSubscriberRecord<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Publisher<EventType extends LEvent, PublisherType> implements IPublisher<EventType> {
|
export class Publisher<EventType extends LEvent, PublisherType> implements IPublisher<EventType> {
|
||||||
private subscribers: EventSubscriberRecord<EventType>;
|
private subscribers: EventSubscriberRecord<EventType>;
|
||||||
private parent: PublisherType;
|
private parent: PublisherType;
|
||||||
34
lib/Rung.ts
Normal file
34
lib/Rung.ts
Normal 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
92
lib/helpers.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
package.json
11
package.json
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "ladder",
|
"name": "@djledda/ladder",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "other libraries provide you with a whole framework - this is just a ladder",
|
"description": "other libraries provide you with a whole framework - this is just a ladder",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.djledda.de/djledda/ladder"
|
|
||||||
},
|
|
||||||
"author": "Daniel Ledda <dan.j.ledda@gmail.com>",
|
"author": "Daniel Ledda <dan.j.ledda@gmail.com>",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^4.7.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
119
src/Rung.ts
119
src/Rung.ts
@@ -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
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"index.ts",
|
||||||
|
"lib/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user