Skip to content

Test: Add Template Coverage#174

Merged
jlukic merged 43 commits intomainfrom
test/templating
May 2, 2026
Merged

Test: Add Template Coverage#174
jlukic merged 43 commits intomainfrom
test/templating

Conversation

@jlukic
Copy link
Copy Markdown
Member

@jlukic jlukic commented May 1, 2026

This PR improves coverage of templating package. Any bugs discovered during testing were added as fixes in this PR.

Changes

Templates

Events

  • Default/bind handlers ignore events from outside the template
  • Default selectors match slotted content (projected through your slot)
  • deep events fire on slotted content (was filtered out)
  • Component-wide events (no selector) fire on the host not shadow root. This fixes issues where it didnt adopt the parents paddingbox
  • 'deepclick' no longer parses as deep + click (word boundary)
  • Fixes attachEvent ignoring passed-through listener options
  • Fixes value callback arg clobbering empty-string and other falsy values

Keys

  • Stop bindKey/unbindKey cycles from stacking duplicate document listeners
  • Fixes whitespace handling in keys: { 'up, down': h } comma-lists
  • keys: { ',': h } no longer fires on every keystroke

Tree traversal & wiring

  • find* helpers accept kebab tag-names: findParent('ui-panels')
  • findParent/findChild expose parent state and data alongside the instance
  • findParent/findChildren no longer hang on self-referential cycles
  • Destroyed parent's findChild returns nothing instead of stale children
  • setParent is the sole authority for parent wiring; idempotent and detaches first

Lifecycle

  • $() in onCreated no longer throws (queries document)
  • lifecyclePromise no longer hangs on hydration paths or late awaiters
  • User onUpdated callback now actually fires
  • Fixes Template.renderedTemplates map growing forever on unique-name churn

Data

  • Fixes defaultState overriding falsy values (0, '', false) passed in as data

Renderer

  • Lit engine wires setParent before attach so subtemplate settings init correctly
  • Subtemplate prototypes' own subTemplates are no longer overridden by the parent's registry

Query

  • $$ no longer ignores the slot root when matching

Feat

  • 'global scroll' (no selector) now defaults to window

Refactor

  • Replace test stubs with real Renderer / ServerRenderer
  • Hoist destroy-time flag resets in onDestroyed; extract markDestroyed() mirroring markRendered()
  • Style: rename hasKeybindings; trim over-explanatory comments

Tests

  • ~470 tests across the eight Template surfaces (events, lifecycle, callback params, key bindings, DOM scoping, data context, subtemplate settings, tree traversal)
  • Recursive and cyclic subtemplate composition
  • Signal auto-unwrap and state.x.push/toggle/increment propagation to rendered DOM

Docs

  • Update docs with changes from this PR

Harness

  • Adjust testing skill to explain how to clean up stale test runners
  • Finalize tachometer plans
  • Guestbook :)

Risk

7/10. Touches Template event/lifecycle/find* and the Renderer clone path. Most fixes have direct pin tests; a few cross-engine paths (lit subtemplate ordering, slotted querySelectorAllDeep) ride through component tests. Potential snags worth a careful read:

  • setParent no longer gets added in constructor. This is a contract change for templates.

Adds 454 tests across 8 surfaces of the Template runtime
(packages/templating/src/template.js): events DSL, lifecycle,
callback params, key bindings, DOM scoping, data context,
subtemplate settings, tree traversal.

30 tests are expected-fail pins documenting confirmed bugs
surfaced by the coverage campaign. Subsequent commits fix
each bug; the corresponding pin tests turn green commit by
commit.

Includes shared scaffolding at packages/templating/test/_helpers/:
stub engine, fresh-Template fixture, browser shadow-DOM
mounting, registry cleanup, synthetic event/key dispatch.

Surface 2 also revives the previously-commented lifecycle
integration tests in packages/component/test/browser/component.test.js
(replacing lines 461-608's TODO block with a 14-test
Lifecycle Events describe).
@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
semantic-next Ready Ready Preview, Comment May 2, 2026 7:07pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
mcp Ignored Ignored Preview May 2, 2026 7:07pm

Request Review

@semantic-performance-bot
Copy link
Copy Markdown

semantic-performance-bot Bot commented May 1, 2026

⚪ No Meaningful Change for e60c51e on Benchmark Suite 📊

Base: main · Action: #25259523730 · Raw: bench-report.json

Test: Add Template Coverage

Note

This PR did not move any measured metrics.

✅ 0 faster · ❌ 0 slower · 🔍 6 unsure · ⚪ 23 no change · 📜 25 reopened


📜 Regressions from peak (25)

These metrics were better on a prior commit than they are now. The peak CI dominates current CI — not attributable to per-sample noise. Bisect candidates are the commits between the peak and HEAD; nearest-to-peak is usually the best bet.

metric current peak vs peak bisect candidates
create-1k 140.1ms 96.8ms @ 74c6bd0 +45% 6f9ea44, 9d35359, 4abfcac +7 more
filter-cycle-20 463.4ms 335.8ms @ e2a22da +38% fb49e7d
hydrate-each-100 16.7ms 12.1ms @ 0aac705 +38% 1745dfc, a15fb96, e2a22da +1 more
replace-1k 108.2ms 78.8ms @ 74c6bd0 +37% 6f9ea44, 9d35359, 4abfcac +7 more
append-1k 122.3ms 89.5ms @ 74c6bd0 +37% 6f9ea44, 9d35359, 4abfcac +7 more
create-10k 1176.2ms 866.5ms @ 74c6bd0 +36% 6f9ea44, 9d35359, 4abfcac +7 more
remove-row-front-20 708.1ms 545.0ms @ 74c6bd0 +30% 6f9ea44, 9d35359, 4abfcac +7 more
swap-rows-20 1053.2ms 846.1ms @ 74c6bd0 +24% 6f9ea44, 9d35359, 4abfcac +7 more
update-10th-10 202.1ms 164.4ms @ 74c6bd0 +23% 6f9ea44, 9d35359, 4abfcac +7 more
remove-row-middle-20 406.8ms 332.7ms @ 74c6bd0 +22% 6f9ea44, 9d35359, 4abfcac +7 more
clear-10k 166.0ms 141.4ms @ a6ec43a +17% 0aac705, 1745dfc, a15fb96 +2 more
remove-first-10 180.9ms 155.6ms @ e2a22da +16% fb49e7d
clear-completed-250 57.5ms 50.2ms @ 4abfcac +15% 6cf2e7e, a6ec43a, 0aac705 +4 more
select-40 752.4ms 684.2ms @ 74c6bd0 +10% 6f9ea44, 9d35359, 4abfcac +7 more
remove-5-front 89.0ms 81.9ms @ 4abfcac +9% 6cf2e7e, a6ec43a, 0aac705 +4 more
remove-5-back 74.9ms 69.3ms @ 9d35359 +8% 4abfcac, 6cf2e7e, a6ec43a +5 more
bulk-add-500 239.5ms 222.1ms @ 4abfcac +8% 6cf2e7e, a6ec43a, 0aac705 +4 more
edit-cycle-5 163.3ms 151.6ms @ 0aac705 +8% 1745dfc, a15fb96, e2a22da +1 more
remove-middle-10 162.8ms 151.9ms @ 0aac705 +7% 1745dfc, a15fb96, e2a22da +1 more
remove-last-10 161.8ms 155.0ms @ 9d35359 +4% 4abfcac, 6cf2e7e, a6ec43a +5 more
toggle-first-10 161.3ms 155.3ms @ 6f9ea44 +4% 9d35359, 4abfcac, 6cf2e7e +6 more
toggle-middle-10 161.2ms 155.4ms @ 6f9ea44 +4% 9d35359, 4abfcac, 6cf2e7e +6 more
toggle-last-10 157.8ms 153.3ms @ fb49e7d +3%
edit-start-10 160.8ms 157.4ms @ 6f9ea44 +2% 9d35359, 4abfcac, 6cf2e7e +6 more
toggle-10 159.6ms 158.5ms @ a6ec43a +1% 0aac705, 1745dfc, a15fb96 +2 more
⚪ No Change (23)

Metrics where this PR measured within ±2% of main — no meaningful performance change detected.

metric Change
add-20 -0.1% – +0.0%
bulk-add-500 -0.9% – +1.5%
create-10k -0.2% – +0.5%
create-1k -1.2% – +0.5%
edit-cycle-5 -0.8% – +0.2%
filter-cycle-20 -0.6% – +0.4%
hydrate-each-100 -1.9% – +2.0%
remove-10-middle -0.2% – +1.9%
remove-first-10 -0.1% – +1.5%
remove-last-10 -0.2% – +0.1%
remove-middle-10 -0.5% – +0.3%
remove-row-back-10 -1.6% – +0.4%
remove-row-front-20 -0.7% – +1.4%
remove-row-middle-20 -0.8% – +1.3%
replace-1k -1.6% – +0.2%
select-40 -0.6% – +1.3%
swap-rows-20 -1.1% – +1.1%
toggle-10 +0.3% – +1.1%
toggle-all-20 -0.1% – +0.1%
toggle-first-10 -0.3% – +0.1%
toggle-last-10 -1.2% – -0.6%
toggle-middle-10 -0.7% – -0.1%
update-10th-10 -2.0% – +1.5%
🔍 Unsure (6)

Inconclusive (4)

The measured difference is small, and our sampling couldn't confidently place it above or below zero. Running more samples in a future run might settle these metrics.

metric Change Expected Noise
append-1k -3.2% – +1.7% ±1%
clear-10k -1.7% – +4.8% ±1%
edit-start-10 -3.0% – -0.7% ±1%
remove-5-back +1.5% – +7.7% ±2%

Too Fast to Measure Precisely (2)

On benches this short, system jitter (scheduling, GC, JIT) masks sub-4% changes; larger deltas still resolve cleanly.

metric Change Test Time Expected Noise
clear-completed-250 +0.7% – +2.4% ~58ms ±3%
remove-5-front -0.1% – +2.4% ~89ms ±2%

Sample size: 50 · Resolution floor: ±2% · Timeout: 3min · Wall-clock: 18m23s

The previous test commit shipped a `_helpers/` folder with a stubbed
rendering engine, browser fixture, and other faked scaffolding. That
pattern bypasses the real Renderer the user hits, defeating the point
of testing.

Tests now use:
- `new Template({...})` directly for pure-logic surfaces
- Real `Renderer` + `ServerRenderer` from `@semantic-ui/renderer`
  for tests that need `initialize()` / `attach()`
- Light DOM (plain element as renderRoot) by default
- Shadow DOM only where boundary semantics are the test target
  (Surface 5 + the deep keyword in Surface 1, parameterized via
  `RENDER_TARGETS.forEach(({ name, target }) => describe(name, ...))`)

Cross-package test deps are normal for tightly-coupled packages —
templating tests reach into `@semantic-ui/renderer` directly without
needing `defineComponent`.

All 30 bug pins preserved; 0 flips:
- B1 falsy override (4)
- B2a + B2b lifecyclePromise (2 in templating, 2 in component)
- B3 cascade leak (8)
- B4 keys whitespace (2 via it.fails)
- B5 kebab lookup (6)
- B6 setParent/removeParent (4)
- B7 value || vs ?? (1)
- B8 deep keyword (1)

Adds 6 new lifecycle-args tests in callback-params.test.js to pin
that $/state/settings/findParent/etc. reach lifecycle hooks correctly.

Surface 5 (DOM scoping) now parameterized over light/shadow render
targets (48 tests vs 26 — same logic, two modes plus 4 shadow-only).

Surface 1 (events) similarly parameterized for 31 tests × 2 modes
plus 14 unparameterized parser/bubble-map plus 3 shadow-only.

`_helpers/` directory removed entirely — no shared test infrastructure
beyond inline local helpers per file.
jlukic added 8 commits May 1, 2026 16:17
The events guide promises default-mode handlers don't fire on slotted
content. The mechanism — `isNodeInTemplate` range check at line 538 —
only activates when `startNode`/`endNode` markers are set, which the
renderer does for subtemplates but `WebComponentBase.attach()` does
not for top-level components. Top-level filter was inert.

Adds a `Node.contains()` check on `template.renderRoot` for default
and `bind` event types. Slotted content (light-DOM, not a descendant
of the shadow root) is rejected. Subtemplate path is unaffected — the
range filter still does the precise scoping; the new gate passes
because subtemplates share the parent's renderRoot.

Tests pin both shapes:
- Q3 PIN: slotted button click does NOT fire default handler
- Q3 sanity: shadow-internal button click DOES fire default handler
`targetElement?.value || event.target?.value || event?.detail?.value`
clobbered falsy-but-real values to undefined when an earlier operand
was a falsy non-undefined and a later one would have provided a real
value. Most visibly: empty-string `<input>` (cleared field) delivered
`value: undefined` to handlers instead of `value: ''` — confusing for
form code that expected the empty string.

Switching to `??` short-circuits only on null/undefined; falsy values
(0, false, '') flow through correctly. The 3 cases that passed under
`||`'s last-operand semantics still pass.
createReactiveState used `if (dataValue)` to decide whether passed
data overrides the default for a state key. Falsy values (0, false,
'', null) silently skipped the override — `defaultState: { count: 5 }`
rendered as `{>child count=0}` initialized state.count to 5, not 0.

The same falsy-clobbering pattern lived on the next line in the
fallback resolution `config?.value || config`. For complex configs
(`{ value: 0, options: {} }`) this would have returned the whole
config object instead of the value 0. Same bug class, switched to
`??` for consistency.

Subtemplate settings (line 933) already uses `!== undefined` —
both paths now agree on falsy-override semantics.
`keySequence.split(',')` left whitespace in the alternates, so
`keys: { 'up, down': handler }` produced `['up', ' down']`. The
leading-space alternate would only match after some prior keystroke
left a trailing space in the sliding-window buffer; pressing ArrowDown
from a fresh buffer never matched ' down'.

Trim each split alternate. Same fix handles padded forms like
`'up , down '`.
defineComponent normalizes tagName to camelCase via kebabToCamel before
storing it as templateName. Looking up via the kebab form
(`findParent('ui-panels')`, `findTemplate('ui-panel')`) missed because
the find* statics did strict equality against the camel registry key.

Normalize the input at the entry of each static (findTemplate,
findParentTemplate, findChildTemplates). kebabToCamel is idempotent
on already-camel input, so both forms now reach the same registry key.
Single chokepoint per method; binders stay as plain pass-throughs.
The line-538 range filter only excepted `global`. Deep events whose
target lived outside the template's startNode/endNode range (slotted
content, nested child shadow DOM) were rejected before line 544's
deep-aware filter could let them through. Net effect: `events: { 'deep
click .x': handler }` did NOT fire on slotted matches inside a
subtemplate.

Mirror line 544's idiom — except both `deep` and `global` from the
range check. The semantics now match: range filter applies to default
and `bind` modes; `deep` and `global` opt out by design.
findTemplate, findParent, findChild, findChildren previously spread
the template's instance with its closure data (subtemplate cascade)
or its merged dataContext (DOM cascade). The merged dataContext
exposes state Signals; the closure data leaks values the parent
passed in. Neither was the documented contract.

The parallel `.component()` / `.dataContext()` Query methods are
deliberately separate — `component` returns the createComponent
factory output (the public API surface), `dataContext` is for the
rare case you need internals. find* should match `component`'s
contract: just the instance.

Strip the data/dataContext spreads from all five find* paths
(findTemplate, findParentTemplate DOM cascade, findParentTemplate
subtemplate cascade, findChildTemplates DOM cascade, findChildTemplates
subtemplate cascade). Stale test that pinned the old leaky shape now
asserts the leak is gone.
setParent could be called twice with the same parent and push the
child twice, or called with a new parent without detaching from the
old one — child stayed in BOTH parents' _childTemplates. removeParent
filtered out of the parent's array but never cleared this.parentTemplate,
so isSubtemplate() lied after destroy.

Latent today (the renderer's cloneInstance is the only production
caller and goes through one setParent per Template lifetime), but
subtree caching and in-place subtemplate updates will exercise re-
attachment soon. Heap-leak class — destroyed children holding
parentTemplate keep the parent reachable through the cycle.

setParent now does an O(1) idempotency check and detaches from any
prior parent before wiring the new one. removeParent clears
this.parentTemplate. Constructor stops auto-wiring parentTemplate —
setParent is the single source of truth for both directions of the
parent/child link.

Behavioral change: `new Template({ parentTemplate: X })` directly,
without a setParent call, no longer counts as a subtemplate. Pre-1.0
edge path; the renderer's cloneInstance always follows clone with
setParent, so production paths are unaffected.
@github-actions github-actions Bot added the Templating Modifies templating package label May 1, 2026
bindKey checked `Object.keys(this.keys).length == 0` to decide whether
bindKeys() needed to install document listeners. The check was meant
to handle "first key registered" but it also fired after every full
unbind/rebind cycle: unbindKey only deletes from this.keys, doesn't
abort the listeners. So bindKey('a',h1) → unbindKey('a') →
bindKey('b',h2) installed a SECOND keydown listener atop the still-
alive first one, and h2 fired twice per keystroke.

Add a `_keysListenersInstalled` flag that bindKeys() checks for
idempotency. Cleared in removeEvents so a new attach cycle can
install fresh listeners against the new eventController.
jlukic added 4 commits May 1, 2026 16:46
- Rename `_keysListenersInstalled` to `hasKeybindings` (no underscore
  prefix; name describes the meaning, not the tracker)
- Drop `name && kebabToCamel(name)` guard at find* entries —
  kebabToCamel(undefined) returns '' via its default param, and
  downstream `if (templateName && ...)` checks treat '' the same as
  undefined ("match any")
- Trim verbose pin-context comments and restate-the-code comments
  added during the bug fixes
Strip campaign-internal artifacts from the Template test surface so
the files read cleanly as open-source contracts:

- Drop bug IDs (B1–B12, Q1, Q3), pin terminology (PIN, EXPECTED FAIL),
  stage labels, surface numbers, convergent/finding labels, and
  workspace path references from test names and comments.
- Test names now read as release-note-style behavior or contract
  descriptions, not implementation jargon.
- Group tests under concept-level describe blocks; merge duplicate
  describes where they overlapped.
- Apply the three-level comment hierarchy from the code-formatting
  guide; remove restate-the-code comments; keep only non-obvious
  WHY notes (Vite/Svelte voice).
- File headers match the minimal style of test/template.test.js.

No test logic, assertions, or fixtures changed. Same 461 passing,
2 expected-fail (lifecyclePromise — fix in flight), 2 skipped.
~500 lines net reduction across 11 files.
The lit render-template directive called setParent AFTER attach, which
relied on Template's constructor auto-wiring parentTemplate so that
isSubtemplate() was already true by the time attach()'s initialize()
ran. With setParent now the sole authority for parent wiring, the late
setParent call meant initialize() saw isSubtemplate() === false and
skipped createSubtemplateSettings — breaking subtemplate settings
reactivity for any component using the lit engine.

Reorder: setElement → setParent → attach. Matches the native engine's
canonical sequence and the tested clone+wire+initialize flow.
resolveLifecyclePromise was called from inside dispatchEvent. Two
consequences hit consumers:

1. Late awaiters hung. lifecyclePromise(name) lazy-created a Promise +
   resolver pair on first access. If the consumer awaited el.created
   AFTER the event already fired without prior access, the new pair
   was created against an already-fired event — promise never resolved.

2. Hydration suppressed promise resolution. The lifecycle wrappers
   gate dispatchEvent on !isHydrating to avoid re-broadcasting DOM
   events during DSD hydration. Promise resolution was incidentally
   gated too — every awaiter of el.created / el.rendered hung during
   hydration even though the component was rendered.

Fix: call resolveLifecyclePromise from the lifecycle wrappers
(onCreated, onRendered, onUpdated, onDestroyed) directly, not from
dispatchEvent. Cache an immediately-resolved promise on first fire
so late awaiters get a non-hanging result. dispatchEvent is now a
plain DOM event helper.
jlukic added 2 commits May 1, 2026 17:23
The constructor accepted onUpdated but never stored it as
onUpdatedCallback, so user-provided onUpdated functions were silently
dropped. Manual invocation of the wrapper would fire only the DOM
event, not the user callback.

Store onUpdated alongside the other lifecycle callbacks; have the
wrapper invoke it inside the microtask debounce. Comment notes that
the state-Reaction only tracks defaultState Signals — fine-grained
block-level reactivity does not flow through this hook, so consumers
needing universal coverage should observe the `updated` DOM event or
invoke the wrapper manually.
@github-actions github-actions Bot added the Docs Modifies documentation label May 1, 2026
jlukic added 17 commits May 2, 2026 13:37
A no-selector handler ('click', 'mouseover', etc) bound at renderRoot
missed events on the host's own surface — clicks on padding/border or
host-dispatched events don't enter the shadow tree's bubble path. The
docs lead with mouseover hovering 'any part of the component'; binding
on the host matches that semantic.
'global hashchange' / 'global scroll' (no selector) now binds to window
implicitly. Authors no longer need to repeat the obvious 'global scroll
window' for the typical page-level event use cases.
The 4th arg destructured only eventSettings/querySettings, silently
dropping passive/capture/once when passed in the natural shape.
Production at inpage-menu.js:269 was in the broken shape today and
only escaped via query.js's autoPassive for scroll/resize.

Reshape: querySettings stays namespaced; everything else (passive,
capture, once, signal, ...) is collected via rest into eventSettings
and forwarded to addEventListener through Query's .on().
A bare-comma descriptor ('keys: { ",": fn }') split into two empty
strings, and endsWith('') is always true — so the handler fired on
every keystroke. Filter empties out of the alternates list.
$ from inside onCreated (which fires before attach() sets renderRoot)
threw 'root.querySelectorAll is not a function' because the globalThis
fallback was Window, which doesn't implement ParentNode. Use document
on the client so authors can query the surrounding page from onCreated
as the design intended.
…hurn

removeTemplate set(name, []) on the empty array — keys never deleted.
With monotonically-increasing names (Anonymous #N, route-driven distinct
components), the Map grew forever. delete the key when its templates
list empties.

Also guards findTemplate(null) at entry — kebabToCamel's default param
covers undefined but null bypasses defaults and threw on .split('-').
findTemplate, findParent, findChild, and findChildren returned just the
instance, breaking real use cases that read state from the result —
e.g. CodePlaygroundPreview's findParent('codePlayground').currentFiles.

Restore the merge: DOM cascade spreads parentNode.dataContext (state
signals exposed via the host's runtime context); subtemplate cascade
spreads template.data (which accumulates the runtime context after
the first render through setDataContext). Pin tests flipped from
'no leak' to 'exposes parent awareness'.
$$ rooted at a shadow root missed slotted nodes when the slotted node
itself matched the selector — the assignedNodes branch recursed into
each slotted node but never tested it via .matches() before recursing.
Only descendants got picked up. Add the explicit .matches() check on
each slotted node so the slotted root is a first-class candidate.
cloneInstance in both engines passed 'subTemplates: parent_registry' to
template.clone(), overriding the prototype's own subTemplates declaration.
A child that referenced its own grandchild via {>grandchild} silently
failed to render because the cloned child inherited the parent's
registry instead of its own.

Drop the override line in both engines. Template.clone() already defaults
to the prototype's own subTemplates; without the override, each cloned
level uses its own declaration. No inheritance — each level owns what it
references.
Hour-long stuck Vitest watcher sessions (and their Playwright/Chromium
children) cause spurious 'Failed to fetch dynamically imported module'
errors via port collisions, plus outright suite hangs. Document the
~1m30s suite budget, the 2-minute timeout heuristic, and the kill steps
so the next agent doesn't burn cycles assuming flakiness.
Full repo suite is ~28s (3624 tests / 82 files). 60s timeout is the
right alarm threshold, not 2m.
Adds two pins exercising self-referencing subTemplates after S7-G1:

- Recursive with base case: a tree node references itself in its own
  subTemplates and uses an {#if children.length} guard. Three-level
  data renders all labels.
- Cyclic without base case: two components reference each other with
  no terminator. The renderer's recursion hits the V8 stack limit and
  throws RangeError — bounded failure, captured via window.error.
  Documents the failure mode so authors know to include a guard.
findParent's parent-walk and findChildren's recursive search trusted
that the parent/child links formed an acyclic tree. A self-referential
parentTemplate caused findParent to infinite-loop; a self-referential
_childTemplates caused findChildren to blow the V8 stack with
RangeError. Misuse-only, but the failure modes are bad enough (page
hang, browser crash) to warrant guards.

Add a visited Set to each: findParent breaks when it re-encounters a
template; findChildren skips already-visited subtrees.
After onDestroyed, the parent's traversal helpers (findChild,
findChildren) still surfaced live children because _childTemplates was
never cleared. Drop the array — children outlive the parent only via
their own refs, not via stale traversal through a dead parent.

Also trims the comments on the cycle guards added in the prior commit;
the visited Sets and break conditions are self-explanatory in context.
Group the boolean/state resets (rendered, destroyed, hasKeybindings,
_childTemplates) together at the top of the destroy sequence before
the side-effecting cleanups (abort, clearReactions, removeEvents,
removeObservers, removeParent). Reads as one unit and matches the
pattern of state-then-effects elsewhere.
The destroy-time flag resets get a named method paired with the
existing markRendered(): markDestroyed() handles rendered/destroyed/
hasKeybindings/_childTemplates as one unit. onDestroyed reads as
'mark destroyed, then run the side-effect cleanups.'
@github-actions github-actions Bot added the Query Modifies query package label May 2, 2026
Removes the multi-line block above onUpdated, the multi-line attachEvent
docstring, the isServer 'mirror of utils' comment, the parentTemplate
'wired through setParent only' note (already obvious from the setParent
method), and the 'event delegation at the shadow root' line that just
restated the next line of code. Trims the naked-event and bindKey-gate
comments to one line each. Per repo convention: source comments are for
non-obvious WHY only — short, load-bearing, vite/svelte voice.
jlukic added 2 commits May 2, 2026 14:35
Catches up the docs to the source state after the campaign:

- events.mdx: el vs target args table, isDeep description, projection
  vs piercing on Deep Events, host binding for component-wide handlers,
  global default to window. Composed callout points at the framework
  dispatchEvent helper which sets composed: true by default.
- keys.mdx + lifecycle.mdx: event param added to callback tables.
- lifecycle.mdx: SSR section distinguishes callbacks (fire same order)
  from DOM events (suppressed during hydration).
- dom.mdx: $ vs $$ example reframed for in-component scope.
- instances.mdx + examples/templates/subtemplates/row.js: findParent
  examples in camelCase form.
- subtemplates.mdx: parent-fallback path for subtemplate settings.
- api/templating/template.mdx: clone() reframed as prototype-to-instance
  manifestation; onUpdated dropped from the Options table.
- authoring/component-events skill: bubble-map adds load and unload
  rows to the Non-Bubbling Event Mapping table.
- authoring/component-state skill: state vs settings precedence
  inverted to match overlaySettingsSignals reality.

Source-of-truth lists: ai/workspace/template-coverage/stage-1-5-batch.md
(D1-D10) and stage-3-5-verified.md (Stage 3.5 extras).
Five pins exercising the Template-tier reactive surface end-to-end:
the rendered DOM contains the unwrapped value (not the Signal object),
state.x.set updates the DOM, and the mutation helpers (.increment,
.toggle, .push) propagate through the renderer to the rendered output.
jlukic added 2 commits May 2, 2026 14:43
Sweeps the doc cleanup batch for AI-tell patterns: cuts the gratuitous
'projection vs piercing' cross-link callout, the camelCase note on
findParent (already shown by the example), the 'manifests a new
instance' verbosity around clone(). Trims the Component-Wide,
Global, and Composed callouts in events.mdx, the SSR section in
lifecycle.mdx, the Inherited Settings section in subtemplates.mdx,
and the state-vs-settings explanation in the component-state skill.
Replaces all added em dashes with periods, semicolons, or rephrased
sentences. The Global example now shows the no-selector default to
window via an inline comment rather than a paragraph above.
Adds the patterns we had to fix in PR #174's first-draft body:

- New 'Internals' layer in the bullet-layer table, with the 'would
  this mean anything to a reader who hasn't read the diff' check.
- Tell #6 'Verb-first mechanism frames' — Let X / Stop Y / Wire Z /
  Make Z openers with bad-good rewrite table.
- Tell #7 'Internal symbols and line numbers' — line-538, internal
  field names, alongside-X cross-references with rewrite table.
- 'Subgroup long sections' rule: bold sub-labels above ~8 bullets,
  not heading levels (navigation generator reserves those).
- Voice-check item in the post-draft checklist: read aloud, imagine
  texting the reviewer, watch for the new tells specifically.
@jlukic jlukic merged commit 5075cb6 into main May 2, 2026
13 checks passed
@jlukic jlukic deleted the test/templating branch May 2, 2026 19:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Docs Modifies documentation Query Modifies query package Templating Modifies templating package Tests Modifies tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant