fix(runtime): bind inserted revealed triggers

req: runtime/007
This commit is contained in:
slhx agent
2026-07-19 21:37:26 +02:00
parent e7ca6d2350
commit 7c98de1e8c
4 changed files with 15 additions and 3 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
- The public component-reuse explanation lives in `docs/recipes/reusable-partials.md`; do not grow a client component framework to explain partial composition.
- The stable public `.heml` authoring surface lives in `docs/hemplate-syntax.md`; Hemlate examples must use that real hemplate syntax, not Vue/Handlebars sketches. hemx-build consumes hemplate Surface facts and must not grow an independent `.heml` parser or CSS-path identity model. Plain CSS/SCSS owns appearance; generated class constants are ergonomic references, not a styling framework or behavior selector system. Generated resources, form/handle metadata, atoms, and event constants come from hemx-build facts, not hand-written app plumbing. Forms remain HTML-shaped, checked against user-authored Rust domain types, parsed through `FormValue`, and manipulated through generated form/control ids rather than selectors. Proc-macros stay local/side-effect-free while build.rs owns global codegen and hard build failures. No-op global codegen must preserve generated artifact timestamps so downstream Rust compilation remains fresh only when canonical output changes. req: boundary/001 req: boundary/002 req: boundary/003 req: boundary/004 req: surface/001 req: surface/002 req: surface/003 req: surface/004 req: surface/005 req: surface/006 req: surface/007 req: surface/008 req: surface/009 req: surface/010 req: codegen/001 req: codegen/003 req: codegen/004 req: codegen/005 req: codegen/006 req: form/001 req: form/004 req: form/007 req: form/008 req: form_effects/001 req: form_effects/002 req: form_effects/003 req: build/001 req: build/002 req: build/003 req: build/004 req: build/005 req: build/006 req: build/007 req: build/008 req: build/009 req: style/001 req: style/002 req: style/003 req: style/004 req: style/005 req: style/006
- Optional `.heml` editor overlays must share authority with `hemx-build` diagnostics and `docs/hemplate-syntax.md`; `hemx-lsp` owns editor protocol glue for diagnostics/completion/hover and derive-known template facts, while VS Code/Cursor/Neovim keep normal HTML/tree-sitter tooling. Simple `h-for` completion facts cover one Rust identifier bound directly to a `self` vector field; malformed bindings and non-vector fields must not fabricate locals. Do not create a second template language, selector model, formatter, Rust type system, or custom editor framework. Compiler diagnostics with directive/target metadata select that source attribute instead of line 0 column 0. Cross-file template/handler references visible to build validation must fail at `cargo check` with useful spans; global completeness checks stay component-scoped unless caught at mount/tests. req: diagnostics/004 req: diagnostics/005 req: diagnostics/006 req: diagnostics/007 req: diagnostics/008 req: diag/009 req: diag/010 req: invariant/004 req: invariant/006 req: check/001 req: check/003
- JS runtime changes must preserve root-scoped lookup, delegated listeners, canonical hemx `EffectBatch` application, fail-closed request handling, transactional/recoverable failure behavior, root-scoped error outlets, and tiny pending/failure/trigger-timing conventions without selectors, handler-name parsing, VDOM, expressions, or per-node listeners. Runtime `.d.ts` types are developer convenience only, not core tooling authority. req: invariant/002 req: runtime/001 req: runtime/002 req: runtime/003 req: runtime/005 req: runtime/006 req: failure/001 req: failure/002 req: failure/003 req: failure/004 req: failure/005 req: failure/006 req: convention/001 req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006 req: convention/007 req: convention/008 req: convention/009 req: convention/010 req: convention/011 req: convention/012 req: convention/013 req: convention/014 req: convention/015 req: convention/016 req: convention/017 req: ts/001
- JS runtime changes must preserve root-scoped lookup, delegated listeners, canonical hemx `EffectBatch` application, fail-closed request handling, transactional/recoverable failure behavior, root-scoped error outlets, and tiny pending/failure/trigger-timing conventions without selectors, handler-name parsing, VDOM, expressions, or per-node listeners. Runtime `.d.ts` types are developer convenience only, not core tooling authority. req: invariant/002 req: runtime/001 req: runtime/002 req: runtime/003 req: runtime/005 req: runtime/006 req: runtime/007 req: failure/001 req: failure/002 req: failure/003 req: failure/004 req: failure/005 req: failure/006 req: convention/001 req: convention/002 req: convention/003 req: convention/004 req: convention/005 req: convention/006 req: convention/007 req: convention/008 req: convention/009 req: convention/010 req: convention/011 req: convention/012 req: convention/013 req: convention/014 req: convention/015 req: convention/016 req: convention/017 req: ts/001
- Opaque island JavaScript is a leaf adapter for high-frequency local behavior only; client-local handlers keep the server-handler shape while `hemx-wasm` owns concrete opt-in syntax. Use native events/generated helpers at the boundary and do not introduce a component runtime, client state graph, VDOM, selector interop, or second UI model. req: canonical_authoring/017 req: client_local/001 req: client_local/003 req: client_local/004 req: interop/001 req: interop/002 req: interop/003 req: interop/006 req: interop/007 req: interop/008 req: interop/009 req: interop/010 req: interop/011 req: interop/012
- Host capability adapters must stay at the `hemx-host` boundary: typed capabilities use fire/request/stream/schedule shapes; adapters may call host APIs and return host events, but they must not mutate DOM or own app/domain state. req: host/001 req: host/002
- Local/offline app behavior should be commands/events/projections; do not add `hemx-local`, stored DOM patches, stored `EffectBatch` truth, or a core client state graph without a proven reusable contract. Atoms are explicit addressable/bootstrap/sync resources, not the default state container or a reactive framework. Replay, reconciliation, export, and deletion rules stay explicit product decisions, and exemplars should show UI effects as app-state output. The local-first multiplayer kanban remains an advanced north-star integration milestone, not beginner/API surface scope. req: canonical_authoring/018 req: canonical_authoring/019 req: state/001 req: state/002 req: state/003 req: state/004 req: state/005 req: state/006 req: state/007 req: local/001 req: local/002 req: local/003 req: local/004 req: milestone/001 req: milestone/002 req: milestone/003
+2
View File
@@ -414,6 +414,8 @@ what a valid business email is. [north_star]
005 Failed hemx HTTP requests fail closed: non-2xx responses are not applied as effects, pending state is restored, root-scoped `data-hemx-error` outlets show transport failure, and runtime emits `hemx:error` with status when available. [north_star]
007 The runtime binds polling and revealed triggers inserted by effects so replacement sentinels and live regions keep their authored behavior without app JavaScript.
---
## failure
+10
View File
@@ -1127,6 +1127,16 @@
if (node.hasAttribute(ROOT)) cleanupRemovedRoot(node);
descendantRoots(node).forEach(cleanupRemovedRoot);
});
record.addedNodes.forEach((node) => {
if (!(node instanceof Element)) return;
if (node.hasAttribute(ROOT)) bindRoot(node);
descendantRoots(node).forEach(bindRoot);
const owner = rootOf(node.parentElement);
if (owner) {
bindPolling(owner);
bindRevealed(owner);
}
});
});
}).observe(document.documentElement, { childList: true, subtree: true });
try {
+2 -2
View File
@@ -86,7 +86,7 @@ fn run_html_examples_smoke() -> ExitCode {
("delete row", DELETE_ROW_SMOKE),
("lazy load", LAZY_LOAD_SMOKE),
("click-to-load load more", CLICK_TO_LOAD_SMOKE),
("infinite/reveal rows", INFINITE_SCROLL_SMOKE),
("infinite/reveal rows", INFINITE_SCROLL_SMOKE), // req: runtime/007 test
("value select", VALUE_SELECT_SMOKE),
("reset user input", RESET_INPUT_SMOKE),
];
@@ -274,7 +274,7 @@ const ACTIVE_SEARCH_SMOKE: &str = r#"(async()=>{const input=document.querySelect
const DELETE_ROW_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const del=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.trim()==="Delete"&&Array.from(f.elements).some(e=>e.name==="id"&&e.value==="1")); del.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); if(text().includes("Review content")) throw new Error("delete row did not remove row"); return true;})()"#;
const LAZY_LOAD_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Load lazy content")); const panel=()=>f.nextElementSibling; const before=panel().textContent.trim(); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); const after=panel().textContent.trim(); if(!(after.includes("Lazy content loaded by server update #")&&after!==before)) throw new Error("lazy load did not visibly update"); return true;})()"#;
const CLICK_TO_LOAD_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Load more")); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); if(!(text().includes("Loaded row 3")&&text().includes("Loaded row 4"))) throw new Error("click-to-load did not append rows"); return true;})()"#;
const INFINITE_SCROLL_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Reveal more rows")); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); if(!(text().includes("Loaded row 4")&&text().includes("Loaded row 6"))) throw new Error("infinite scroll did not append rows"); return true;})()"#;
const INFINITE_SCROLL_SMOKE: &str = r##"(async()=>{document.documentElement.style.scrollBehavior="auto"; const section=document.querySelector("#infinite-scroll"); const old=Array.from(section.querySelectorAll("form")).find(f=>f.textContent.includes("Reveal more rows")); const replacement=old.cloneNode(true); replacement.removeAttribute("data-hemx-locked"); old.remove(); scrollTo(0,0); await new Promise(r=>setTimeout(r,300)); const before=section.querySelectorAll("li").length; section.append(replacement); await new Promise(r=>setTimeout(r,300)); replacement.scrollIntoView({block:"center"}); for(let i=0;i<30;i++){if(section.querySelectorAll("li").length>before) return true; await new Promise(r=>setTimeout(r,150));} throw new Error("inserted infinite-scroll sentinel did not append rows");})()"##;
const REVEALED_FALLBACK_SMOKE: &str = r#"(async()=>{const text=()=>document.body.textContent.replace(/\s+/g," "); const runtime=document.querySelector('script[src*="hemx"][src$=".js"]'); if(!runtime) throw new Error("runtime script not found"); Object.defineProperty(window,"IntersectionObserver",{value:undefined,configurable:true}); const reloaded=document.createElement("script"); reloaded.src=runtime.src; document.head.appendChild(reloaded); for(let i=0;i<20;i++){if(text().includes("Lazy content loaded by server update #")&&text().includes("Loaded row 4")&&text().includes("Loaded row 6")) return true; await new Promise(r=>setTimeout(r,150));} throw new Error("revealed fallback did not dispatch lazy/infinite forms without a click");})()"#;
const PROGRESS_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.textContent.includes("Tick progress")); if(f.hasAttribute("data-hemx-interval")) throw new Error("progress form should wait for an explicit click"); const text=()=>document.querySelector("progress").parentElement.textContent.replace(/\s+/g," ").trim(); const before=text(); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); const after=text(); if(!(before.includes("0% complete")&&after.includes("25% complete"))) throw new Error("progress click did not visibly tick from 0% to 25%"); return true;})()"#;
const VALUE_SELECT_SMOKE: &str = r#"(async()=>{const f=Array.from(document.querySelectorAll("form")).find(f=>f.elements.category); f.elements.category.value="numbers"; f.elements.category.dispatchEvent(new Event("change",{bubbles:true,cancelable:true})); f.querySelector("button,input[type=submit]").click(); await new Promise(r=>setTimeout(r,700)); const options=Array.from(document.querySelectorAll("select[name=value] option")).map(option=>option.textContent.trim()); if(!(options.includes("One")&&options.includes("Two")&&!options.includes("Alpha"))) throw new Error("value select did not replace options"); return true;})()"#;