Skip to content

Instantly share code, notes, and snippets.

@jinjor
Last active February 6, 2019 02:07
Show Gist options
  • Save jinjor/08cf386c9992ed693ecd45f82f3bbab5 to your computer and use it in GitHub Desktop.
Save jinjor/08cf386c9992ed693ecd45f82f3bbab5 to your computer and use it in GitHub Desktop.
export abstract class DomSync<T, Context extends unknown[]> {
constructor(
private root: Element | Document | ShadowRoot,
private selector?: string
) {}
abstract readonly class: string;
protected identify(obj: T): string | number {
const o = obj as any;
if (o && (typeof o.id === "string" || typeof o.id === "number")) {
return String(o.id).trim();
}
throw new Error("Cannot identify the object.");
}
protected abstract create(obj: T, ...context: Context): Element;
protected abstract update(el: Element, obj: T, ...context: Context): void;
listenToRoot(type: string, callback: (e: Event) => void): () => void {
this.root.addEventListener(type, callback);
return function() {
this.root.removeEventListener(type, callback);
};
}
listenToChild(
type: string,
match: (el: Element) => boolean,
callback: (e: Event, element: Element) => void,
unmatchedCallback?: (e: Event) => void
): () => void {
const handle = (e: Event) => {
let target = e.target as HTMLElement;
while (target && target !== e.currentTarget) {
if (match(target)) {
callback(e, target);
return;
} else {
target = target.parentElement;
}
}
if (unmatchedCallback) {
unmatchedCallback(e);
}
};
return this.listenToRoot(type, handle);
}
listenToContainer(
type: string,
callback: (e: Event, element: Element) => void,
unmatchedCallback?: (e: Event) => void
): () => void {
return this.listenToChild(
type,
el => el.classList.contains(this.class),
callback,
unmatchedCallback
);
}
listenToItem(
type: string,
callback: (e: Event, element: Element, id: string | number) => void,
unmatchedCallback?: (e: Event) => void
): () => void {
return this.listenToChild(
type,
el => el.classList.contains(this.class),
(e, el: HTMLElement) => {
callback(e, el, el.dataset.id || +el.dataset["id:number"]);
},
unmatchedCallback
);
}
private containerElement(): Node {
if (this.selector) {
return this.root.querySelector(this.selector);
}
return this.root;
}
private elementId(objectId: string | number): string {
return this.class + "_" + objectId;
}
private objectId(obj: T): string | number {
return this.identify(obj);
}
add(obj: T, ...context: Context): Element | null {
const objectId = this.objectId(obj);
const elementId = this.elementId(objectId);
const el = this.create(obj, ...context);
if (!el) {
return null;
}
this.update(el, obj, ...context);
if (el.id) {
throw new Error("Element id should not be set.");
}
el.id = elementId;
el.classList.add(this.class);
el.setAttribute(
typeof objectId !== "string" ? `data-id:${typeof objectId}` : "data-id",
String(objectId)
);
this.containerElement().appendChild(el);
return el;
}
addAll(objs: T[], ...context: Context): Element[] {
const els = [];
for (let obj of objs) {
const el = this.add(obj, ...context);
if (el) {
els.push(el);
}
}
return els;
}
modify(obj: T, ...context: Context): Element | null {
const elementId = this.elementId(this.objectId(obj));
const el = this.root.querySelector("#" + elementId);
if (el) {
this.update(el, obj, ...context);
return el;
}
return null;
}
remove(id: string): void {
const el = this.root.querySelector("#" + this.elementId(id));
if (el) {
el.remove();
}
}
sync(id: string, obj: T | null, ...context: Context): Element | null {
if (obj) {
const el = this.modify(obj, ...context);
if (!el) {
this.add(obj, ...context);
}
return el;
} else {
this.remove(id);
return null;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment