Recursive Html Context Switching
Design Log 47 - Recursive HTML Context Switching
Written for AI agents. See Log Methodology Note below for details.
Problem Statement
The current recursive HTML design (Design Log 46) has a fundamental limitation when the recursive structure needs to operate on a nested type that differs from the root context type.
The Core Issue
Consider this data structure:
data:
tree:
- id: string
name: string
hasChildren: boolean
isOpen: boolean
children: $/data/tree # References the tree array type
When we try to create a recursive region, we encounter a context mismatch:
<ul class="menu-list" ref="menuItem">
<li forEach="tree" trackBy="id">
<a href="#">
<span class="name">{name}</span>
</a>
<div if="hasChildren && isOpen">
<recurse ref="menuItem" />
</div>
</li>
</ul>
The Problem:
- The
ref="menuItem"is at the root context wheretreeis a property - The
forEach="tree"iterates over the tree items - When we recurse, we need to iterate over
children, nottree - But
childrenis not accessible at the root context - The recursive region needs to work with both
tree(initially) andchildren(on recursion)
Why This Fails:
- At the
ref="menuItem"level, the ViewState type isIndirectRecursion2ViewState(which hastreeproperty) - Inside
forEach="tree", the ViewState type isTreeOfIndirectRecursion2ViewState(which haschildrenproperty) - The recursive region function needs a consistent type to operate on
- We can't write
forEach="tree"ANDforEach="children"in the same template
Current Workaround Attempts
Attempt 1: Put ref inside forEach
<ul class="menu-list">
<li forEach="tree" trackBy="id" ref="menuItem">
<a href="#"><span class="name">{name}</span></a>
<div if="hasChildren && isOpen">
<recurse ref="menuItem" />
</div>
</li>
</ul>
Problem: The ref is now on a forEach element, which creates a collection of refs instead of a single ref. The recursive region would need to iterate, but we're already in an iteration context.
Attempt 2: Use accessor on recurse
<ul class="menu-list" ref="menuItem">
<li forEach="tree" trackBy="id">
<a href="#"><span class="name">{name}</span></a>
<div if="hasChildren && isOpen">
<recurse ref="menuItem" accessor="children" />
</div>
</li>
</ul>
Problem: The accessor="children" would use withData to switch to the children array, but the recursive function still expects to receive tree initially and children on recursion. The mismatch persists.
Root Cause Analysis
The fundamental issue is a context type mismatch between initialization and recursion:
Initialization Phase: The recursive region is called with the root ViewState type
- Needs to access
treearray forEach="tree"makes sense here
- Needs to access
Recursion Phase: The recursive region is called with the tree item type
- Needs to access
childrenarray forEach="children"makes sense here
- Needs to access
The Conflict: We can't write both in a single template because:
ref="menuItem"establishes the recursive region boundary- The region's ViewState type is fixed at compile time
- We need the region to work with different accessor paths depending on how it's invoked
Why This Is a Design Problem
This isn't a bug in the implementation—it's a fundamental limitation in the design:
- Recursive regions assume the type structure is isomorphic (same shape at every level)
- Direct recursion (
children: array<$/data>) creates isomorphic structures - Indirect recursion through different paths (
treevschildren) creates non-isomorphic structures - The current design doesn't have a way to "reset" the context to normalize the type
Proposed Solution: Context Reset Element
Introduce a mechanism to reset the ViewState context to a specific type before entering the recursive region.
Design Goals
- Context Normalization: Allow resetting the context from root type to recursive item type
- Explicit: The context switch should be clearly visible in the HTML
- Type-Safe: The compiler should validate that the accessor resolves to the correct type
- Minimal New Syntax: Reuse existing concepts where possible
Syntax Options
Option 1: <with-data> Element
<with-data accessor="tree">
<ul class="menu-list" ref="menuItem">
<li forEach="." trackBy="id">
<a href="#"><span class="name">{name}</span></a>
<div if="hasChildren && isOpen">
<recurse ref="menuItem" accessor="children" />
</div>
</li>
</ul>
</with-data>
Semantics:
<with-data accessor="tree">switches context to thetreearray type- Inside,
forEach="."means "iterate over the current context" (the array itself) - On recursion,
accessor="children"switches to the children array - Both forEach operate on arrays, just accessed differently
Advantages:
- Clear and explicit context boundary
- Reuses the
withDataconcept from the runtime forEach="."is intuitive ("this" in programming)
Disadvantages:
- Introduces a new HTML element
Option 2: context Attribute
<ul class="menu-list" context="tree" ref="menuItem">
<li forEach="." trackBy="id">
<a href="#"><span class="name">{name}</span></a>
<div if="hasChildren && isOpen">
<recurse ref="menuItem" accessor="children" />
</div>
</li>
</ul>
Advantages:
- No new elements, just an attribute
- More compact
Disadvantages:
- Less explicit about the context boundary
- Mixing
contextandrefon the same element might be confusing
Option 3: <recurse-root> Element
<recurse-root accessor="tree">
<ul class="menu-list" ref="menuItem">
<li forEach="." trackBy="id">
<a href="#"><span class="name">{name}</span></a>
<div if="hasChildren && isOpen">
<recurse ref="menuItem" accessor="children" />
</div>
</li>
</ul>
</recurse-root>
Advantages:
- Explicitly names it as related to recursion
- Clear boundary
Disadvantages:
- More verbose
- The name might imply it's required for all recursion (it's not)
Recommended: <with-data> Element
The <with-data> element provides the clearest semantics and matches the runtime behavior most closely.
Detailed Design
HTML Syntax
<with-data accessor="expression">
<!-- Content here operates in the switched context -->
</with-data>
Rules:
- The
accessorattribute is required and must resolve to a valid type - Inside
<with-data>, the ViewState context is the resolved type forEach="."inside<with-data>means "iterate over the current context"<with-data>can be nested for multiple context switches<with-data>can wrap a recursive region to normalize its entry type
Example: Menu with Nested Submenus
<html>
<head>
<script type="application/jay-data">
data:
tree:
- id: string
name: string
hasChildren: boolean
isOpen: boolean
children: $/data/tree
</script>
</head>
<body>
<nav class="menu">
<!-- Switch context to the tree array -->
<with-data accessor="tree">
<ul class="menu-list" ref="menuItem">
<!-- Now forEach="." iterates over the current context (tree array) -->
<li forEach="." trackBy="id">
<a href="#">
<span class="name">{name}</span>
</a>
<div if="hasChildren && isOpen">
<!-- Recurse with children accessor -->
<recurse ref="menuItem" accessor="children" />
</div>
</li>
</ul>
</with-data>
</nav>
</body>
</html>
How It Works
Initial Render
- Root ViewState is
IndirectRecursion2ViewStatewithtree: Array<TreeItem> <with-data accessor="tree">switches context toArray<TreeItem>- Inside, the recursive region
ref="menuItem"operates onArray<TreeItem> forEach="."iterates over the array (current context)- Each item has type
TreeItemwithchildren: Array<TreeItem>
Recursive Call
- Inside forEach, context is
TreeItem <recurse ref="menuItem" accessor="children"/>useswithDatato switch tochildrenarray- The recursive function receives
Array<TreeItem>(same type as initial) - Inside the recursive function,
forEach="."iterates over this array - Recursion continues with consistent types
Generated TypeScript
export interface TreeOfIndirectRecursion2ViewState {
id: string;
name: string;
hasChildren: boolean;
isOpen: boolean;
children: Array<TreeOfIndirectRecursion2ViewState> | null;
}
export interface IndirectRecursion2ViewState {
tree: Array<TreeOfIndirectRecursion2ViewState>;
}
export function render(options?: RenderElementOptions): IndirectRecursion2ElementPreRender {
const [refManager, [refMenuItem]] = ReferencesManager.for(options, [], ['menuItem'], [], []);
// Recursive function operates on the array type
function renderRecursiveRegion_menuItem(): BaseJayElement<
Array<TreeOfIndirectRecursion2ViewState>
> {
return de(
'ul',
{ class: 'menu-list' },
[
forEach(
// forEach="." becomes identity function
(vs: Array<TreeOfIndirectRecursion2ViewState>) => vs,
(vs1: TreeOfIndirectRecursion2ViewState) => {
return de('li', {}, [
e('a', { href: '#' }, [e('span', { class: 'name' }, [dt((vs1) => vs1.name)])]),
c(
(vs1) => vs1.hasChildren && vs1.isOpen,
() =>
e('div', {}, [
// accessor="children" uses withData
withData(
(vs1) => vs1.children,
() => renderRecursiveRegion_menuItem(),
),
]),
),
]);
},
'id',
),
],
refMenuItem(),
);
}
const render = (viewState: IndirectRecursion2ViewState) =>
ConstructContext.withRootContext(viewState, refManager, () =>
e('nav', { class: 'menu' }, [
// accessor="tree" uses withData
withData(
(vs) => vs.tree,
() => renderRecursiveRegion_menuItem(),
),
]),
) as IndirectRecursion2Element;
return [refManager.getPublicAPI() as IndirectRecursion2ElementRefs, render];
}
Key Insights
- Type Consistency: The recursive function always receives
Array<TreeItem>, whether called initially or recursively - Context Switching:
<with-data>generateswithData()calls that handle the context switch - Identity forEach:
forEach="."compiles to an identity function(vs) => vs - Accessor Chain: Initial call uses
withData((vs) => vs.tree, ...), recursive calls usewithData((vs1) => vs1.children, ...)
Implementation Steps
###1. Parser Changes
Add support for <with-data> element:
// In jay-html-helpers.ts
export function isWithData(element: HTMLElement): boolean {
return element.rawTagName === 'with-data';
}
2. Compiler Changes
Handle <with-data> elements in the compiler:
if (isWithData(htmlElement)) {
const accessor = htmlElement.getAttribute('accessor');
if (!accessor) {
return new RenderFragment('', Imports.none(), [
'<with-data> element must have an "accessor" attribute',
]);
}
// Parse the accessor
const accessorExpr = parseAccessor(accessor, variables);
// Create new variables context with the resolved type
const newVariables = variables.childVariableFor(accessorExpr);
// Render children with new context
const childElement = renderHtmlElement(htmlElement, {
...context,
variables: newVariables,
});
// Wrap in withData call
return new RenderFragment(
`withData(${accessorExpr.render().rendered}, () => ${childElement.rendered})`,
childElement.imports.plus(Import.withData),
[...accessorExpr.validations, ...childElement.validations],
childElement.refs,
childElement.recursiveRegions,
);
}
3. Special Handling for forEach="."
When accessor is just ".", it means the current context:
if (forEach === '.') {
// Identity function - iterate over current context
forEachFragment = new RenderFragment(
`(${variables.currentVar}: ${variables.currentType.name}) => ${variables.currentVar}`,
Imports.none(),
[],
);
} else {
// Normal accessor parsing
const forEachAccessor = parseAccessor(forEach, variables);
forEachFragment = forEachAccessor.render().map((_) => `(${paramName}: ${paramType}) => ${_}`);
}
4. Update Recursive Type Resolution
The parser needs to understand that children: $/data/tree creates a JayRecursiveType that resolves to the array at data.tree:
// In resolveRecursiveReferences
if (type instanceof JayRecursiveType) {
const parts = type.referencePath.split('/').filter((p) => p);
if (parts.length >= 2 && parts[0] === '$' && parts[1] === 'data') {
let resolvedType: JayType = rootType;
// Navigate to the target type
for (let i = 2; i < parts.length; i++) {
const pathSegment = parts[i];
if (resolvedType instanceof JayObjectType && pathSegment in resolvedType.props) {
resolvedType = resolvedType.props[pathSegment];
} else if (resolvedType instanceof JayArrayType) {
if (
resolvedType.itemType instanceof JayObjectType &&
pathSegment in resolvedType.itemType.props
) {
resolvedType = resolvedType.itemType.props[pathSegment];
} else {
return; // Path not found
}
} else {
return; // Path not found
}
}
type.resolvedType = resolvedType;
}
}
Benefits
- Solves Context Mismatch: Allows recursive regions to operate on consistent types
- Explicit: The context switch is clearly visible in the HTML
- Flexible: Can be used for non-recursive context switching too
- Type-Safe: Compiler validates accessor expressions
- Runtime Support: Maps directly to existing
withDataruntime function - Composable: Can nest multiple
<with-data>elements
Migration Path
Existing recursive HTML that doesn't need context switching continues to work unchanged. The <with-data> element is only needed when the recursive structure's entry point differs from its recursive point.
Before (doesn't work):
<ul ref="menuItem">
<li forEach="tree" trackBy="id">
<recurse ref="menuItem" accessor="children" />
</li>
</ul>
After (works):
<with-data accessor="tree">
<ul ref="menuItem">
<li forEach="." trackBy="id">
<recurse ref="menuItem" accessor="children" />
</li>
</ul>
</with-data>
Conclusion
The <with-data> element provides a clean, explicit way to handle context switching in recursive HTML structures. It solves the fundamental problem of type mismatch between initialization and recursion paths, while maintaining type safety and clarity. The syntax maps naturally to the existing withData runtime function, making implementation straightforward.
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.