// Ported from nanocomponent 6.6.0 (MIT) — https://github.com/choojs/nanocomponent // Native DOM components: render once, morph on update, lifecycle hooks via // onload. In v8 this class is also the intended island/hydration boundary. import morph from '@choojs/html/morph' import nanotiming from '@choojs/core/timing' import onload, { KEY_ATTR } from './onload.js' function makeID () { return 'ncid-' + Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) } export default class Component { static makeID = makeID constructor (name) { this._hasWindow = typeof window !== 'undefined' this._id = null // represents the id of the root node this._ncID = null // internal component id this._olID = null this._proxy = null this._loaded = false // Used to debounce on-load when child-reordering this._rootNodeName = null this._name = name || 'component' this._rerender = false this._handleLoad = this._handleLoad.bind(this) this._handleUnload = this._handleUnload.bind(this) this._arguments = [] } get element () { if (!this._hasWindow) return undefined const el = document.getElementById(this._id) if (el) return el.dataset.nanocomponent === this._ncID ? el : undefined } render (...args) { const renderTiming = nanotiming(this._name + '.render') let el if (!this._hasWindow) { const createTiming = nanotiming(this._name + '.create') el = this.createElement(...args) createTiming() renderTiming() return el } else if (this.element) { el = this.element // retain reference, as the ID might change on render const updateTiming = nanotiming(this._name + '.update') const shouldUpdate = this._rerender || this.update(...args) updateTiming() if (this._rerender) this._rerender = false if (shouldUpdate) { const desiredHtml = this._handleRender(args) const morphTiming = nanotiming(this._name + '.morph') morph(el, desiredHtml) morphTiming() if (this.afterupdate) this.afterupdate(el) } if (!this._proxy) { this._proxy = this._createProxy() } renderTiming() return this._proxy } else { this._reset() el = this._handleRender(args) if (this.beforerender) this.beforerender(el) if (this.load || this.unload || this.afterreorder) { onload(el, this._handleLoad, this._handleUnload, this._ncID) this._olID = el.getAttribute(KEY_ATTR) } renderTiming() return el } } rerender () { if (!this.element) throw new Error('component: cant rerender on an unmounted dom node') this._rerender = true this.render(...this._arguments) } _handleRender (args) { const createElementTiming = nanotiming(this._name + '.createElement') const el = this.createElement(...args) createElementTiming() if (!this._rootNodeName) this._rootNodeName = el.nodeName if (!(el instanceof window.Element)) { throw new Error('component: createElement should return a single DOM node') } if (this._rootNodeName !== el.nodeName) { throw new Error('component: root node types cannot differ between re-renders') } this._arguments = args return this._brandNode(this._ensureID(el)) } _createProxy () { const proxy = document.createElement(this._rootNodeName) const self = this this._brandNode(proxy) proxy.id = this._id proxy.setAttribute('data-proxy', '') proxy.isSameNode = function (el) { return (el && el.dataset.nanocomponent === self._ncID) } return proxy } _reset () { this._ncID = makeID() this._olID = null this._id = null this._proxy = null this._rootNodeName = null } _brandNode (node) { node.setAttribute('data-nanocomponent', this._ncID) if (this._olID) node.setAttribute(KEY_ATTR, this._olID) return node } _ensureID (node) { if (node.id) this._id = node.id else node.id = this._id = this._ncID // Update proxy node ID if it changed if (this._proxy && this._proxy.id !== this._id) this._proxy.id = this._id return node } _handleLoad (el) { if (this._loaded) { if (this.afterreorder) this.afterreorder(el) return // Debounce child-reorders } this._loaded = true if (this.load) this.load(el) } _handleUnload (el) { if (this.element) return // Debounce child-reorders this._loaded = false if (this.unload) this.unload(el) } createElement () { throw new Error('component: createElement should be implemented!') } update () { throw new Error('component: update should be implemented!') } } export { Component, makeID }