Epic B (DOM Morphing & Rendering Fidelity, #22) is complete: Idiomorph-based
morphing (#43), focus/scroll/in-flight input preservation (#44), stable
data-key list reconciliation (#46), and a data-no-morph escape hatch for
JS-owned regions (#45). Combined with Epic A (#21, HMAC state signing +
locked fields + CSRF/CSWSH audit), this closes out the 0.6.0b - Hardening
Foundation milestone.
Backfills missing CHANGELOG entries for B1/B2/B4 (only B3 had one) and
corrects README claims left over from before the morph work landed (the
known-limitations bullet, the architecture-overview paragraph, and the
roadmap's 0.6.0b line, which had mis-attributed Epic D's 422-re-render item).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1LT1PQ8qo9GcyLeDLiut
Summary
Implements B2 of Epic B (#22): "Preserve focus / scroll / in-flight input across patches."
| B2 checklist item | How it's addressed | |---|---| | Preserve focus across patches | Already handled by idiomorph's
restoreFocusdefault (true) — verified by readingvendor/idiomorph.js'ssaveAndRestoreFocus()(~line 221); no code change needed, but now covered by a seam-level regression test. | | Preserve in-flight input across patches | Idiomorph's focus restoration alone does not stop the server render from overwriting the focused input'svalue(seeignoreAttribute()/syncInputValue(), ~lines 738-846 invendor/idiomorph.js). Fixed by passingignoreActiveValue: truein theIdiomorph.morph()config used by bothupdate()androllback()(new_morphConfig()helper incomponent-client.js). | | Preserve scroll position across patches | Idiomorph has no concept of scroll at all — this is bespoke bookkeeping the framework now owns. Added_captureScrollPositions()/_restoreScrollPositions(), called around everyIdiomorph.morph()invocation in bothupdate()androllback(). The root element is restored by direct reference (it keeps DOM identity across an outerHTML morph); descendants are re-located byidafter the morph, since idiomorph may recreate rather than patch them. Descendants without a stableidcannot be re-located — a documented limitation, mirroring idiomorph's ownrestoreFocusrequirement that a recreated focused element carry anid. |What changed
src/component_framework/static/component_framework/js/component-client.js:update()/rollback()now callIdiomorph.morph()via a shared_morphConfig()helper that addsignoreActiveValue: trueto the existingmorphStyle: 'outerHTML'config._captureScrollPositions(element)/_restoreScrollPositions(element, positions)private helpers, invoked immediately before/after each morph call inupdate()androllback().tests/js/morph-preservation.test.mjs(new): TDD red→green tests for the seam-levelignoreActiveValueconfig on bothupdate()/rollback(), plus direct tests of the scroll capture/restore helpers (root-by-reference, descendant-by-id, no-id descendant is a documented no-op, and a fast-path no-op when nothing is scrolled).vendor/idiomorph.jswas not touched, per scope.Out of scope (noted, not implemented)
Per B2's literal wording only — the following are separate, later checklist items on #22 and were intentionally left untouched:
data-key)Test plan
node --test "tests/js/**/*.test.mjs"— 22/22 passing (13 pre-existing + 9 new)uv run pytest— 496 passed (no Python surface touched by this change; confirms no regression)uv run ruff check .— all checks passeduv run ruff format --check .— all files already formatted🤖 Generated with Claude Code
https://claude.ai/code/session_01Pi1LT1PQ8qo9GcyLeDLiut