Skip to content

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>
`;
  }
}

Pre-v1.0 — internal monorepo.