From 525d90d55dedfcc155abf39d466d4d7a982982f6 Mon Sep 17 00:00:00 2001 From: Karsten Schmidt Date: Mon, 10 Sep 2018 01:22:32 +0100 Subject: [PATCH] feat(hdom): generalize diffElement() (#4) - add HDOMOps interface - add DEFAULT_OPS implementation - update diffElement() & diffAttributes() to delegate to ops - refactor diffElement() to be more legible - update createDOM(), add support for `__ops` node attrib to use custom ops for subtrees - add getChild(), replaceChild(), setContent() --- packages/hdom/src/api.ts | 15 ++++ packages/hdom/src/diff.ts | 150 ++++++++++++++++++++++++-------------- packages/hdom/src/dom.ts | 20 ++++- 3 files changed, 130 insertions(+), 55 deletions(-) diff --git a/packages/hdom/src/api.ts b/packages/hdom/src/api.ts index 0c889e7894..e78a255488 100644 --- a/packages/hdom/src/api.ts +++ b/packages/hdom/src/api.ts @@ -51,4 +51,19 @@ export interface HDOMOpts { normalize?: boolean; } +/** + * This interface defines the underlying DOM update operations used by + * `diffElement()`. See `DEFAULT_OPS` (diff.ts) for the default + * implementations. + */ +export interface HDOMOps { + createTree(element: T, tree: any, insert?: number): T | T[]; + getChild(element: T, child: number): T; + removeChild(element: T, child: number); + replaceChild(element: T, child: number, newTree: any); + setAttrib(element: T, id: string, value: any, attribs?: any); + removeAttribs(element: T, attribs: string[], prevAttribs: any); + setContent(element: T, value: any); +} + export const DEBUG = false; diff --git a/packages/hdom/src/diff.ts b/packages/hdom/src/diff.ts index 1e4ab0ea95..158290fe73 100644 --- a/packages/hdom/src/diff.ts +++ b/packages/hdom/src/diff.ts @@ -1,19 +1,35 @@ -import { SEMAPHORE } from "@thi.ng/api/api"; +import { IObjectOf, SEMAPHORE } from "@thi.ng/api/api"; import * as isa from "@thi.ng/checks/is-array"; import * as iss from "@thi.ng/checks/is-string"; import { DiffLogEntry } from "@thi.ng/diff/api"; import { diffArray } from "@thi.ng/diff/array"; import { diffObject } from "@thi.ng/diff/object"; import { equiv } from "@thi.ng/equiv"; + +import { HDOMOps } from "./api"; import { createDOM, + getChild, removeAttribs, removeChild, - setAttrib + replaceChild, + setAttrib, + setContent, } from "./dom"; const isArray = isa.isArray; const isString = iss.isString; +const max = Math.max; + +const DEFAULT_OPS: HDOMOps = { + createTree: createDOM, + getChild, + replaceChild, + removeChild, + setContent, + removeAttribs, + setAttrib, +}; /** * Takes a DOM root element and two hiccup trees, `prev` and `curr`. @@ -33,47 +49,69 @@ const isString = iss.isString; * @param root * @param prev previous tree * @param curr current tree + * @param ops hdom implementation */ -export const diffElement = (root: Element, prev: any, curr: any) => - _diffElement(root, prev, curr, 0); +export const diffElement = ( + root: T, + prev: any[], + curr: any[], + ops: HDOMOps = DEFAULT_OPS) => + _diffElement(ops, root, prev, curr, 0); + +const _diffElement = ( + ops: HDOMOps, + parent: T, + prev: any[], + curr: any[], + child: number) => { -const _diffElement = (parent: Element, prev: any, curr: any, child: number) => { if (curr[1].__diff === false) { releaseDeep(prev); - removeChild(parent, child); - createDOM(parent, curr, child); + ops.replaceChild(parent, child, curr); return; } + // TODO use optimized equiv? const delta = diffArray(prev, curr, equiv, true); if (delta.distance === 0) { return; } const edits = delta.linear; - const el = parent.children[child]; - let i, j, k, eq, e, status, idx, val; - if (edits[0][0] !== 0 || (i = prev[1]).key !== (j = curr[1]).key) { + const el = ops.getChild(parent, child); + let i: number; + let j: number; + let idx: number; + let k: string; + let eq: any[]; + let e: DiffLogEntry; + let status: number; + let val: any; + if (edits[0][0] !== 0 || prev[1].key !== curr[1].key) { // DEBUG && console.log("replace:", prev, curr); releaseDeep(prev); - removeChild(parent, child); - createDOM(parent, curr, child); + ops.replaceChild(parent, child, curr); return; } - if ((i = prev.__release) && i !== curr.__release) { + if ((val = (prev).__release) && val !== (curr).__release) { releaseDeep(prev); } if (edits[1][0] !== 0) { - diffAttributes(el, prev[1], curr[1]); + diffAttributes(ops, el, prev[1], curr[1]); } const equivKeys = extractEquivElements(edits); - const n = edits.length; - const noff = prev.length - 1; - const offsets = new Array(noff + 1); - for (i = noff; i >= 2; i--) { + const numEdits = edits.length; + const prevLength = prev.length - 1; + const offsets = new Array(prevLength + 1); + for (i = prevLength; i >= 2; i--) { offsets[i] = i - 2; } - for (i = 2; i < n; i++) { - e = edits[i], status = e[0], val = e[2]; + for (i = 2; i < numEdits; i++) { + e = edits[i]; + status = e[0]; + val = e[2]; + // DEBUG && console.log(`edit: o:[${offsets.toString()}] i:${idx} s:${status}`, val); + + // element removed? if (status === -1) { if (isArray(val)) { k = val[1].key; @@ -81,67 +119,57 @@ const _diffElement = (parent: Element, prev: any, curr: any, child: number) => { eq = equivKeys[k]; k = eq[0]; // DEBUG && console.log(`diff equiv key @ ${k}:`, prev[k], curr[eq[2]]); - _diffElement(el, prev[k], curr[eq[2]], offsets[k]); + _diffElement(ops, el, prev[k], curr[eq[2]], offsets[k]); } else { idx = e[1]; // DEBUG && console.log("remove @", offsets[idx], val); releaseDeep(val); - removeChild(el, offsets[idx]); - for (j = noff; j >= idx; j--) { - offsets[j] = Math.max(offsets[j] - 1, 0); + ops.removeChild(el, offsets[idx]); + for (j = prevLength; j >= idx; j--) { + offsets[j] = max(offsets[j] - 1, 0); } } } else if (isString(val)) { - el.textContent = ""; + ops.setContent(el, ""); } + + // element added/inserted? } else if (status === 1) { if (isString(val)) { - el.textContent = val; + ops.setContent(el, val); } else if (isArray(val)) { k = val[1].key; if (k === undefined || (k && equivKeys[k][0] === undefined)) { idx = e[1]; // DEBUG && console.log("insert @", offsets[idx], val); - createDOM(el, val, offsets[idx]); - for (j = noff; j >= idx; j--) { + ops.createTree(el, val, offsets[idx]); + for (j = prevLength; j >= idx; j--) { offsets[j]++; } } } } } - if ((i = curr.__init) && i != prev.__init) { + // call __init after all children have been added/updated + if ((val = (curr).__init) && val != (prev).__init) { // DEBUG && console.log("call __init", curr); - i.apply(curr, [el, ...(curr.__args)]); - } -}; - -const releaseDeep = (tag: any) => { - if (isArray(tag)) { - if ((tag).__release) { - // DEBUG && console.log("call __release", tag); - (tag).__release.apply(tag, (tag).__args); - delete (tag).__release; - } - for (let i = tag.length; --i >= 2;) { - releaseDeep(tag[i]); - } + val.apply(curr, [el, ...((curr).__args)]); } }; -const diffAttributes = (el: Element, prev: any, curr: any) => { +const diffAttributes = (ops: HDOMOps, el: T, prev: any, curr: any) => { let i, e, edits; const delta = diffObject(prev, curr); - removeAttribs(el, delta.dels, prev); + ops.removeAttribs(el, delta.dels, prev); let value = SEMAPHORE; for (edits = delta.edits, i = edits.length; --i >= 0;) { e = edits[i]; const a = e[0]; if (a.indexOf("on") === 0) { - el.removeEventListener(a.substr(2), prev[a]); + ops.removeAttribs(el, [a], prev); } if (a !== "value") { - setAttrib(el, a, e[1], curr); + ops.setAttrib(el, a, e[1], curr); } else { value = e[1]; } @@ -149,23 +177,39 @@ const diffAttributes = (el: Element, prev: any, curr: any) => { for (edits = delta.adds, i = edits.length; --i >= 0;) { e = edits[i]; if (e !== "value") { - setAttrib(el, e, curr[e], curr); + ops.setAttrib(el, e, curr[e], curr); } else { value = curr[e]; } } if (value !== SEMAPHORE) { - setAttrib(el, "value", value, curr); + ops.setAttrib(el, "value", value, curr); + } +}; + +const releaseDeep = (tag: any) => { + if (isArray(tag)) { + if ((tag).__release) { + // DEBUG && console.log("call __release", tag); + (tag).__release.apply(tag, (tag).__args); + delete (tag).__release; + } + for (let i = tag.length; --i >= 2;) { + releaseDeep(tag[i]); + } } }; const extractEquivElements = (edits: DiffLogEntry[]) => { - let k, v, e, ek; - const equiv = {}; + let k: string; + let val: any; + let e: DiffLogEntry; + let ek: any[]; + const equiv: IObjectOf = {}; for (let i = edits.length; --i >= 0;) { e = edits[i]; - v = e[2]; - if (isArray(v) && (k = v[1].key) !== undefined) { + val = e[2]; + if (isArray(val) && (k = val[1].key) !== undefined) { ek = equiv[k]; !ek && (equiv[k] = ek = [, ,]); ek[e[0] + 1] = e[1]; diff --git a/packages/hdom/src/dom.ts b/packages/hdom/src/dom.ts index bc06b5b1ea..5b88213e9c 100644 --- a/packages/hdom/src/dom.ts +++ b/packages/hdom/src/dom.ts @@ -4,6 +4,7 @@ import * as isi from "@thi.ng/checks/is-iterable"; import * as iss from "@thi.ng/checks/is-string"; import { SVG_NS, SVG_TAGS } from "@thi.ng/hiccup/api"; import { css } from "@thi.ng/hiccup/css"; +import { HDOMOps } from "./api"; const isArray = isa.isArray; const isFunction = isf.isFunction; @@ -29,7 +30,11 @@ export const createDOM = (parent: Element, tag: any, insert?: number) => { if (isFunction(t)) { return createDOM(parent, t.apply(null, tag.slice(1))); } - const el = createElement(parent, t, tag[1], insert); + const attribs = tag[1]; + if (attribs.__ops) { + return (>attribs.__ops).createTree(parent, tag, insert); + } + const el = createElement(parent, t, attribs, insert); if ((tag).__init) { (tag).__init.apply((tag).__this, [el, ...(tag).__args]); } @@ -70,7 +75,7 @@ export const hydrateDOM = (parent: Element, tree: any, i = 0) => { if (isArray(tree)) { const el = parent.children[i]; if (isFunction(tree[0])) { - return hydrateDOM(parent, tree[0].apply(null, tree.slice(1)), i); + hydrateDOM(parent, tree[0].apply(null, tree.slice(1)), i); } if ((tree).__init) { (tree).__init.apply((tree).__this, [el, ...(tree).__args]); @@ -121,6 +126,14 @@ export const createTextElement = (parent: Element, content: string, insert?: num return el; }; +export const getChild = (parent: Element, child: number) => + parent.children[child]; + +export const replaceChild = (parent: Element, child: number, tree: any) => { + removeChild(parent, child); + createDOM(parent, tree, child); +}; + export const cloneWithNewAttribs = (el: Element, attribs: any) => { const res = el.cloneNode(true); setAttribs(res, attribs); @@ -128,6 +141,9 @@ export const cloneWithNewAttribs = (el: Element, attribs: any) => { return res; }; +export const setContent = (el: Element, body: any) => + el.textContent = body; + export const setAttribs = (el: Element, attribs: any) => { for (let k in attribs) { setAttrib(el, k, attribs[k], attribs);