Files
buuh/packages/component/index.js
T
Bendik Aagaard LynghaugandClaude Fable 5 ce1ec9e4a9 rebrand: buuh — a friendly public fork under the uhhm org
Packages renamed to the @uhhm scope (@uhhm/buuh, @uhhm/buuh-html,
@uhhm/buuh-component, @uhhm/buuh-devtools, @uhhm/buuh-migrate,
@uhhm/bankai). Scoping is load-bearing twice over: npm routes registries
per scope so @uhhm/* resolves against project.uhhm.no while everything
else stays on npmjs, and it means this fork never squats upstream's
names anywhere. The codemod now migrates choo v7 apps to the @uhhm
names. README rewritten with the fork framing and full upstream credit;
the choojs RFC moves to docs/upstream-rfc-draft.md, in the drawer for if
this work ever goes home. API unchanged — choo() is still choo().

Also: Gitea Actions CI + release workflows (npm publish to the uhhm
registry on tag push, CDN bundle uploaded as a generic package),
npm run bundle producing dist-cdn/buuh.js (the whole framework as one
minified ES module for import-map use), docs/publishing.md explaining
what Gitea Packages is (a real npm registry) and is not (a CDN — serve
the bundle from a static host with module-safe MIME instead), and
onload.js constructing window.MutationObserver to match its own guard
(surfaced by smoke-testing the bundle outside a full browser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
2026-09-08 19:55:20 +02:00

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 '@uhhm/buuh-html/morph'
import nanotiming from '@uhhm/buuh/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 }