UNPKG

panel

Version:

Web Components with Virtual DOM: lightweight composable web apps

125 lines (110 loc) 3.37 kB
/** * Manages Virtual DOM -> DOM rendering cycle * @module dom-patcher * @private */ import { init as initSnabbdom, attributesModule, h, datasetModule, eventListenersModule, propsModule, styleModule, classModule, } from 'snabbdom'; import delayedClassModule from 'snabbdom-delayed-class'; import {Perf} from './component-utils/perf'; import {paramsModule} from './component-utils/snabbdom-params-module'; const patch = initSnabbdom([ datasetModule, attributesModule, classModule, propsModule, styleModule, eventListenersModule, delayedClassModule, paramsModule, ]); export const EMPTY_DIV = h(`div`); export {h}; export class DOMPatcher { constructor(initialState, renderFunc, options = {}) { this.updateMode = options.updateMode || `async`; this.state = Object.assign({}, initialState); this.renderFunc = renderFunc; this.vnode = this.renderFunc(this.state); this.postRenderCallback = options.postRenderCallback; // prepare root element const tagName = this.vnode.sel.split(/[#.]/)[0]; const classMatches = this.vnode.sel.match(/\.[^.#]+/g); const idMatch = this.vnode.sel.match(/#[^.#]+/); this.el = document.createElement(tagName); if (classMatches) { this.el.className = classMatches.map((c) => c.slice(1)).join(` `); // this attribute setting ensures that svg elements behave as expected and will ensure // compatibility with different snabbdom versions this.el.setAttribute(`class`, this.el.className); } if (idMatch) { this.el.id = idMatch[0].slice(1); } patch(this.el, this.vnode); if (this.el === this.vnode.elm) { const insertHook = this.vnode.data.hook && this.vnode.data.hook.insert; if (insertHook) { // since Snabbdom recycled our newly-created root element (this.el) rather // than creating its own element, it doesn't fire the insert hook, so we're // going to fake it out and call it ourselves insertHook(this.vnode); } } this.el = this.vnode.elm; } update(newState) { if (this.rendering) { console.error(`Applying new DOM update while render is already in progress!`); } this.pendingState = newState; switch (this.updateMode) { case `async`: if (!this.pending) { this.pending = true; requestAnimationFrame(() => this.render()); } break; case `sync`: this.render(); break; } } render() { // if disconnected, don't render if (!this.renderFunc) { return; } const startedAt = Perf.getNow(); this.rendering = true; this.pending = false; this.state = this.pendingState; const newVnode = this.renderFunc(this.state); this.rendering = false; patch(this.vnode, newVnode); this.vnode = newVnode; this.el = this.vnode.elm; if (this.postRenderCallback) { this.postRenderCallback(Perf.getNow() - startedAt); } } disconnect() { const vnode = this.vnode; this.renderFunc = null; this.state = null; this.vnode = null; this.el = null; this.postRenderCallback = null; // patch with empty vnode to clear out listeners in tree // this ensures we don't leave dangling DetachedHTMLElements blocking GC patch(vnode, {sel: vnode.sel, key: vnode.key}); } }