Skip to content

Commit 2d260dc

Browse files
committed
fix: MutationObserver safety net for custom element upgrades
Add a global MutationObserver that watches for any custom element inserted into the document and calls customElements.upgrade() on it. This catches elements that the browser fails to auto-upgrade after DOM operations like replaceChildren — regardless of timing, View Transitions, or microtask ordering. Also harden new-post form submission: fall back to querySelector for form reference, validate title/body before submitting.
1 parent 5a4468c commit 2d260dc

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

examples/blog/modules/posts/components/new-post.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@ export class NewPost extends WebComponent {
2323

2424
async onSubmit(e: SubmitEvent) {
2525
e.preventDefault();
26-
const form = e.currentTarget as HTMLFormElement;
26+
// Use querySelector as a fallback — e.currentTarget can be null in
27+
// some re-render scenarios with light DOM event delegation.
28+
const form = (e.currentTarget || this.querySelector('form')) as HTMLFormElement;
29+
if (!form) return;
2730
const data = new FormData(form);
31+
const title = String(data.get('title') || '');
32+
const body = String(data.get('body') || '');
33+
if (!title || !body) {
34+
this.setState({ error: 'Title and body are required' });
35+
return;
36+
}
2837
this.setState({ busy: true, error: null });
2938
try {
30-
const result = await createPost({
31-
title: String(data.get('title') || ''),
32-
body: String(data.get('body') || ''),
33-
});
39+
const result = await createPost({ title, body });
3440
if (!result.success) {
3541
this.setState({ busy: false, error: result.error });
3642
return;

packages/core/src/router-client.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,37 @@ function parseHTML(html) {
5050

5151
let enabled = false;
5252

53+
/**
54+
* Global MutationObserver that upgrades any custom element inserted into the
55+
* document. This is the safety net — if replaceChildren, View Transitions,
56+
* or any other DOM operation inserts elements that the browser fails to
57+
* auto-upgrade, this observer catches them.
58+
*/
59+
let upgradeObserver = null;
60+
function ensureUpgradeObserver() {
61+
if (upgradeObserver || typeof MutationObserver === 'undefined' || typeof customElements === 'undefined') return;
62+
upgradeObserver = new MutationObserver((mutations) => {
63+
for (const m of mutations) {
64+
for (const node of m.addedNodes) {
65+
if (node.nodeType !== 1) continue;
66+
const el = /** @type {Element} */ (node);
67+
if (el.tagName?.includes('-')) customElements.upgrade(el);
68+
for (const child of el.querySelectorAll('*')) {
69+
if (child.tagName?.includes('-')) customElements.upgrade(child);
70+
}
71+
}
72+
}
73+
});
74+
upgradeObserver.observe(document.body, { childList: true, subtree: true });
75+
}
76+
5377
/** Enable the client router. Idempotent. */
5478
export function enableClientRouter() {
5579
if (enabled || typeof document === 'undefined') return;
5680
enabled = true;
5781
document.addEventListener('click', onClick, true);
5882
window.addEventListener('popstate', onPopState);
83+
ensureUpgradeObserver();
5984
}
6085

6186
/** Disable the client router. */

0 commit comments

Comments
 (0)