From 7dc70daa8373b991964072251d62d0df77cbcd4f Mon Sep 17 00:00:00 2001 From: Ronan Berder Date: Sun, 28 Jun 2026 09:54:05 +0800 Subject: [PATCH] Add force init lifecycle option --- AGENTS.md | 1 + CHANGELOG.md | 8 ++++++++ README.md | 1 + site/src/components/StyleHead.astro | 26 +++++++++++++++++++++++ site/src/docs/installation.mdx | 8 ++++++++ src/js/basecoat.js | 32 +++++++++++++++++++++++------ 6 files changed, 70 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd2100c..8713afe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,7 @@ ## JavaScript - JavaScript is behavior only; do not use JS to paper over CSS architecture issues. - Register JS components with `window.basecoat.register()` and make initialization idempotent with `data-*-initialized` flags. +- Component init functions must return before attaching listeners or mutating DOM when the root already has its initialized flag. - Keep generated DOM minimal and aligned with documented HTML. - Prefer native browser APIs and events. - Keep scroll management scoped to component containers. Do not use `scrollIntoView()` when it can scroll the page unexpectedly. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4019253..05c5d74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Added + +- Added explicit `force: true` support to `window.basecoat.init()` and `window.basecoat.initAll()` for rehydrating restored DOM after framework or navigation cache restores. + +### Fixed + +- Reinitialized Basecoat components after HTMX history restores in the docs site. + ## [1.0.0] - 2026-06-27 ### Breaking Changes diff --git a/README.md b/README.md index 34ea03a..b6c0683 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ These entrypoints are generated by `scripts/generate-css-entrypoints.js` and by - `window.basecoat.init(name)` initializes uninitialized components for one registered component. - `window.basecoat.initAll()` initializes all uninitialized registered components. +- `window.basecoat.init(name, { force: true })` and `window.basecoat.initAll({ force: true })` destroy and reinitialize matching components. Use this when a framework or navigation library restores previously initialized DOM from a cache. Force mode resets transient component state such as open menus, focus state, and active interactions. - `window.basecoat.refresh(element)` asks an initialized component to rescan dynamic children when it supports refresh. - Components use internal destroy hooks to clean up listeners when initialized roots are removed from the DOM. diff --git a/site/src/components/StyleHead.astro b/site/src/components/StyleHead.astro index 73cb557..f0eeeef 100644 --- a/site/src/components/StyleHead.astro +++ b/site/src/components/StyleHead.astro @@ -159,7 +159,33 @@ const styleUrls = { } }; + const clearSerializedRuntimeState = (root) => { + for (const element of root.querySelectorAll?.("*") || []) { + delete element.dataset.basecoatComponent; + for (const attribute of Array.from(element.attributes)) { + if (/^data-[a-z0-9-]+-initialized$/.test(attribute.name)) { + element.removeAttribute(attribute.name); + } + } + } + }; + + const cleanHistoryItem = (event) => { + const item = event.detail?.item; + if (typeof item?.content !== "string") return; + + const template = document.createElement("template"); + template.innerHTML = item.content; + clearSerializedRuntimeState(template.content); + item.content = template.innerHTML; + }; + document.addEventListener("DOMContentLoaded", syncSidebar); + document.addEventListener("htmx:historyItemCreated", cleanHistoryItem); + document.addEventListener("htmx:historyRestore", () => { + window.basecoat?.initAll?.({ force: true }); + syncSidebar(); + }); document.addEventListener("htmx:afterSettle", () => { window.basecoat?.initAll?.(); syncSidebar(); diff --git a/site/src/docs/installation.mdx b/site/src/docs/installation.mdx index ef9076e..1c4dd50 100644 --- a/site/src/docs/installation.mdx +++ b/site/src/docs/installation.mdx @@ -243,6 +243,14 @@ Chart uses a separate helper and is not included in `basecoat-css/all`. Each component page shows the minimal scripts or imports required for that component. +### JavaScript lifecycle + +Basecoat initializes registered components on page load and when new DOM is inserted. + +Use `window.basecoat.initAll()` after manually inserting HTML that contains Basecoat components. It only initializes components that are not already initialized. + +Use `window.basecoat.initAll({ force: true })` when a framework or navigation library restores previously initialized DOM from a cache, such as an HTMX history restore. Force mode destroys existing component instances before reinitializing them, so it also resets transient state such as open menus, focus state, and active interactions. + ## Compatibility Basecoat 1.0 introduces new style packs and a smaller public API based on root classes plus semantic HTML and documented attributes. If you need pre-1.0 class aliases while migrating, load the compatibility stylesheet after your Basecoat stylesheet. diff --git a/src/js/basecoat.js b/src/js/basecoat.js index 724dcb6..6175f9d 100644 --- a/src/js/basecoat.js +++ b/src/js/basecoat.js @@ -63,9 +63,24 @@ node.querySelectorAll('[data-basecoat-component]').forEach(destroyComponent); }; - const initAllComponents = () => { + const uniqueElements = (elements) => Array.from(new Set(elements)); + + const getComponentElements = (componentName, selector, force = false) => { + const elements = Array.from(document.querySelectorAll(selector)); + if (force) { + elements.push(...document.querySelectorAll(`[data-basecoat-component="${componentName}"]`)); + } + return uniqueElements(elements); + }; + + const initAllComponents = (options = {}) => { + const force = options.force === true; Object.entries(componentRegistry).forEach(([name, { selector }]) => { - document.querySelectorAll(selector).forEach(element => initComponent(element, name)); + getComponentElements(name, selector, force).forEach((element) => { + const wasComponent = element.dataset?.basecoatComponent === name; + if (force) destroyComponent(element); + if (wasComponent || element.matches(selector)) initComponent(element, name); + }); }); }; @@ -111,18 +126,23 @@ observer = null; }; - const initRegisteredComponent = (componentName) => { + const initRegisteredComponent = (componentName, options = {}) => { const component = componentRegistry[componentName]; if (!component) { console.warn(`Component '${componentName}' not found in registry`); return; } - document.querySelectorAll(component.selector).forEach(element => initComponent(element, componentName)); + const force = options.force === true; + getComponentElements(componentName, component.selector, force).forEach((element) => { + const wasComponent = element.dataset?.basecoatComponent === componentName; + if (force) destroyComponent(element); + if (wasComponent || element.matches(component.selector)) initComponent(element, componentName); + }); }; - const initAllRegisteredComponents = () => { - initAllComponents(); + const initAllRegisteredComponents = (options = {}) => { + initAllComponents(options); }; const setTheme = (mode) => {