Appearance
TreeNode
A minimal recursive component. Demonstrates <components> block self-import and the inline <TreeNode :node="child" /> recursion inside its own <template>. Each target gets the idiomatic self-reference form: Vue's defineOptions({ name }) + setup-scope import, React's hoisted named function declaration, Svelte's import TreeNode from './TreeNode.svelte' (with extension), Angular's forwardRef(() => TreeNode), Solid's named function declaration.
Live demo
Three levels deep — Vue's defineOptions({ name: 'TreeNode' }) lets the component reference itself by name inside its own <template>.
Source — TreeNode.rozie
rozie
<!--
TreeNode.rozie
Demonstrates Phase 06.2 component composition + recursion (D-119):
- <components> block declaring a SELF-import (TreeNode → './TreeNode.rozie').
The explicit self-entry is redundant per CONTEXT.md <specifics>: outer-name
self-ref path covers it (D-114). Kept here on purpose to exercise both the
explicit-self-entry skip logic (React/Angular) AND the outer-name precedence.
- Recursive template: <TreeNode :node="child" /> inside the component itself.
tagKind: 'self' on the inner tag (P1 lowering); P2 emit dispatches per target:
Vue → defineOptions({ name: 'TreeNode' }) + setup-scope import
React → named function declaration (hoisted self-ref)
Svelte → import TreeNode from './TreeNode.svelte' (D-117 self-import idiom)
Angular → forwardRef(() => TreeNode) in @Component({ imports: [...] })
- Test data shape: { id, label, children: TreeNode[] }; the browser-mount
integration test (Phase 06.2 P3 Task 2) feeds a 3-level fixture and
asserts all 3 labels render in DOM.
-->
<rozie name="TreeNode">
<components>
{
TreeNode: './TreeNode.rozie',
}
</components>
<props>
{
node: { type: Object, default: () => ({ id: '', label: '', children: [] }) },
}
</props>
<template>
<div class="tree-node">
<span class="tree-node__label">{{ $props.node.label }}</span>
<ul r-if="$props.node.children && $props.node.children.length > 0" class="tree-node__children">
<li r-for="child in $props.node.children" :key="child.id">
<TreeNode :node="child" />
</li>
</ul>
</div>
</template>
<style>
.tree-node { font-family: system-ui; padding-left: 0.5rem; }
.tree-node__label { display: inline-block; }
.tree-node__children { list-style: none; margin: 0.25rem 0 0 0; padding-left: 1rem; border-left: 1px dashed currentColor; }
</style>
</rozie>Vue output
vue
<template>
<div class="tree-node">
<span class="tree-node__label">{{ props.node.label }}</span>
<ul v-if="props.node.children && props.node.children.length > 0" class="tree-node__children">
<li v-for="child in props.node.children" :key="child.id">
<TreeNode :node="child"></TreeNode>
</li>
</ul></div>
</template>
<script setup lang="ts">
import TreeNode from './TreeNode.vue';
defineOptions({ name: 'TreeNode' });
const props = withDefaults(
defineProps<{ node?: unknown }>(),
{ node: () => ({
id: '',
label: '',
children: []
}) }
);
</script>
<style scoped>
.tree-node { font-family: system-ui; padding-left: 0.5rem; }
.tree-node__label { display: inline-block; }
.tree-node__children { list-style: none; margin: 0.25rem 0 0 0; padding-left: 1rem; border-left: 1px dashed currentColor; }
</style>React output
tsx
import styles from './TreeNode.module.css';
interface TreeNodeProps {
node?: Record<string, unknown>;
}
export default function TreeNode(_props: TreeNodeProps): JSX.Element {
const props: TreeNodeProps = {
..._props,
node: _props.node ?? (() => ({
id: '',
label: '',
children: []
}))(),
};
return (
<>
<div className={styles["tree-node"]}>
<span className={styles["tree-node__label"]}>{props.node.label}</span>
{(props.node.children && props.node.children.length > 0) && <ul className={styles["tree-node__children"]}>
{props.node.children.map((child) => <li key={child.id}>
<TreeNode node={child} />
</li>)}
</ul>}</div>
</>
);
}Svelte output
svelte
<script lang="ts">
import TreeNode from './TreeNode.svelte';
interface Props {
node?: unknown;
}
let { node = () => ({
id: '',
label: '',
children: []
}) }: Props = $props();
</script>
<div class="tree-node">
<span class="tree-node__label">{node.label}</span>
{#if node.children && node.children.length > 0}<ul class="tree-node__children">
{#each node.children as child (child.id)}<li>
<TreeNode node={child}></TreeNode>
</li>{/each}
</ul>{/if}</div>
<style>
.tree-node { font-family: system-ui; padding-left: 0.5rem; }
.tree-node__label { display: inline-block; }
.tree-node__children { list-style: none; margin: 0.25rem 0 0 0; padding-left: 1rem; border-left: 1px dashed currentColor; }
</style>Angular output
ts
import { Component, ViewEncapsulation, forwardRef, input } from '@angular/core';
@Component({
selector: 'rozie-tree-node',
standalone: true,
imports: [forwardRef(() => TreeNode)],
template: `
<div class="tree-node">
<span class="tree-node__label">{{ node().label }}</span>
@if (node().children && node().children.length > 0) {
<ul class="tree-node__children">
@for (child of node().children; track child.id) {
<li>
<rozie-tree-node [node]="child"></rozie-tree-node>
</li>
}
</ul>
}</div>
`,
styles: [`
.tree-node { font-family: system-ui; padding-left: 0.5rem; }
.tree-node__label { display: inline-block; }
.tree-node__children { list-style: none; margin: 0.25rem 0 0 0; padding-left: 1rem; border-left: 1px dashed currentColor; }
`],
})
export class TreeNode {
node = input<Record<string, any>>((() => ({
id: '',
label: '',
children: []
}))());
}
export default TreeNode;Solid output
tsx
import type { JSX } from 'solid-js';
import { For, Show, mergeProps, splitProps } from 'solid-js';
interface TreeNodeProps {
node?: Record<string, any>;
}
export default function TreeNode(_props: TreeNodeProps): JSX.Element {
const _merged = mergeProps({ node: (() => ({
id: '',
label: '',
children: []
}))() }, _props);
const [local, rest] = splitProps(_merged, ['node']);
return (
<>
<style>{`.tree-node { font-family: system-ui; padding-left: 0.5rem; }
.tree-node__label { display: inline-block; }
.tree-node__children { list-style: none; margin: 0.25rem 0 0 0; padding-left: 1rem; border-left: 1px dashed currentColor; }`}</style>
<>
<div class={"tree-node"}>
<span class={"tree-node__label"}>{local.node.label}</span>
{<Show when={local.node.children && local.node.children.length > 0}><ul class={"tree-node__children"}>
<For each={local.node.children}>{(child) => <li>
<TreeNode node={child} />
</li>}</For>
</ul></Show>}</div>
</>
</>
);
}Lit output
ts
import { LitElement, css, html, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { SignalWatcher } from '@lit-labs/preact-signals';
import { repeat } from 'lit/directives/repeat.js';
@customElement('rozie-tree-node')
export default class TreeNode extends SignalWatcher(LitElement) {
static styles = css`
.tree-node { font-family: system-ui; padding-left: 0.5rem; }
.tree-node__label { display: inline-block; }
.tree-node__children { list-style: none; margin: 0.25rem 0 0 0; padding-left: 1rem; border-left: 1px dashed currentColor; }
`;
@property({ type: Object }) node: object = {
id: '',
label: '',
children: []
};
private _disconnectCleanups: Array<() => void> = [];
disconnectedCallback(): void {
super.disconnectedCallback();
for (const fn of this._disconnectCleanups) fn();
this._disconnectCleanups = [];
}
render() {
return html`
<div class="tree-node">
<span class="tree-node__label">${this.node.label}</span>
${this.node.children && this.node.children.length > 0 ? html`<ul class="tree-node__children">
${repeat(this.node.children, (child) => child.id, (child, _idx) => html`<li key=${child.id}>
<rozie-tree-node .node=${child}></rozie-tree-node>
</li>`)}
</ul>` : nothing}</div>
`;
}
}