158 lines
4.6 KiB
JavaScript
158 lines
4.6 KiB
JavaScript
// 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 }
|