View Nodes

Complete reference for all view node types in Constela

Overview

View nodes are the building blocks of Constela UIs. There are 8 node types, each serving a specific purpose in describing your interface.

Element Node

Renders an HTML element with optional props and children.

json
{
  "kind": "element",
  "tag": "div",
  "props": { "class": "container" },
  "children": [ ... ]
}
NameTypeRequiredDefaultDescription
kind"element"Yes-Node type identifier.
tagstringYes-HTML tag name (e.g., "div", "span", "button").
propsRecord<string, Expression | string>No-HTML attributes and event handlers. Event handlers use action names as strings.
childrenViewNode[]No-Child nodes to render inside this element.

Event Handling

Event handlers are specified as objects with event, action, and optional payload fields in props:

json
{
  "kind": "element",
  "tag": "button",
  "props": {
    "onClick": { "event": "click", "action": "handleClick" },
    "onMouseOver": { "event": "mouseover", "action": "showTooltip" }
  },
  "children": [...]
}

Text Node

Renders dynamic or static text content.

json
{
  "kind": "text",
  "value": { "expr": "state", "name": "message" }
}
NameTypeRequiredDefaultDescription
kind"text"Yes-Node type identifier.
valueExpressionYes-Expression that evaluates to the text content.

Examples

json
// Static text
{ "kind": "text", "value": { "expr": "lit", "value": "Hello, World!" } }

// Dynamic text from state
{ "kind": "text", "value": { "expr": "state", "name": "userName" } }

// Computed text
{
  "kind": "text",
  "value": {
    "expr": "bin",
    "op": "+",
    "left": { "expr": "lit", "value": "Count: " },
    "right": { "expr": "state", "name": "count" }
  }
}

If Node

Conditionally renders content based on an expression.

json
{
  "kind": "if",
  "condition": { "expr": "state", "name": "isLoggedIn" },
  "then": { ... },
  "else": { ... }
}
NameTypeRequiredDefaultDescription
kind"if"Yes-Node type identifier.
conditionExpressionYes-Expression that evaluates to a boolean.
thenViewNodeYes-Node to render when condition is truthy.
elseViewNodeNo-Node to render when condition is falsy.

Example

json
{
  "kind": "if",
  "condition": {
    "expr": "bin",
    "op": ">",
    "left": { "expr": "state", "name": "count" },
    "right": { "expr": "lit", "value": 0 }
  },
  "then": {
    "kind": "text",
    "value": { "expr": "lit", "value": "Positive!" }
  },
  "else": {
    "kind": "text",
    "value": { "expr": "lit", "value": "Zero or negative" }
  }
}

Each Node

Iterates over an array and renders content for each item.

json
{
  "kind": "each",
  "items": { "expr": "state", "name": "todos" },
  "as": "todo",
  "index": "i",
  "key": "id",
  "body": { ... }
}
NameTypeRequiredDefaultDescription
kind"each"Yes-Node type identifier.
itemsExpressionYes-Expression that evaluates to an array.
asstringYes-Variable name for the current item in each iteration.
indexstringNo-Variable name for the current index.
keyExpressionNo-Expression that evaluates to a unique key for each item (e.g., item.id).
bodyViewNodeYes-Node to render for each item.

Example

json
{
  "kind": "each",
  "items": { "expr": "state", "name": "users" },
  "as": "user",
  "index": "idx",
  "key": { "expr": "var", "name": "user", "path": "id" },
  "body": {
    "kind": "element",
    "tag": "li",
    "children": [
      {
        "kind": "text",
        "value": { "expr": "var", "name": "user", "path": "name" }
      }
    ]
  }
}

The Key Property

The key property provides a stable identity for each item in the list. This is essential for the reconciliation algorithm to efficiently update the DOM when the list changes.

Why Keys Matter

When a list is re-rendered, Constela needs to determine which elements have been added, removed, or moved. Without keys, the only option is to compare elements by their position (index), which leads to inefficient updates and bugs.

ScenarioWithout KeyWith Key
Item added at startRe-renders all elementsOnly inserts new element
Item removed from middleRe-renders all following elementsOnly removes that element
Items reorderedRe-renders all changed positionsMoves existing DOM nodes
Input state preservationInput values shift to wrong itemsInput values stay with their items

Key Best Practices

  1. Use unique, stable identifiers: Database IDs, UUIDs, or other persistent identifiers work best.
json
// Good: Stable ID from data
"key": { "expr": "var", "name": "todo", "path": "id" }

// Bad: Index changes when items are reordered
"key": { "expr": "var", "name": "index" }
  1. Avoid using index as key unless the list is static and will never be reordered.
  2. Keys must be unique among siblings. Duplicate keys cause undefined behavior.

How Reconciliation Works

Constela's reconciliation algorithm uses keys to minimize DOM operations:

  1. Build key maps: Creates a mapping of keys to existing DOM elements
  2. Identify changes: Compares old and new key sets to find additions, removals, and moves
  3. Apply minimal updates: Reorders existing elements, inserts new ones, and removes deleted ones

This key-based approach ensures that:

  • Existing DOM nodes are reused when possible
  • Form input values and focus states are preserved
  • Animations and transitions work correctly across updates
  • Performance remains optimal even with large lists

Example: Preserving Input State

Without proper keys, input values can shift incorrectly when items are added or removed:

json
{
  "kind": "each",
  "items": { "expr": "state", "name": "todos" },
  "as": "todo",
  "index": "i",
  "key": { "expr": "var", "name": "todo", "path": "id" },
  "body": {
    "kind": "element",
    "tag": "li",
    "children": [
      {
        "kind": "element",
        "tag": "input",
        "props": {
          "type": { "expr": "lit", "value": "text" },
          "value": { "expr": "var", "name": "todo", "path": "title" },
          "onInput": { "event": "input", "action": "updateTodo", "payload": { "index": { "expr": "var", "name": "i" } } }
        }
      }
    ]
  }
}

With the id-based key, even if a todo is removed from the middle of the list, the input values for the remaining items stay correctly associated with their respective todos.

Component Node

Renders a reusable component with props.

json
{
  "kind": "component",
  "name": "Button",
  "props": {
    "label": { "expr": "lit", "value": "Click me" }
  },
  "children": [ ... ]
}
NameTypeRequiredDefaultDescription
kind"component"Yes-Node type identifier.
namestringYes-Name of the component to render (must be defined in components).
propsRecord<string, Expression>No-Props to pass to the component.
childrenViewNode[]No-Children to pass into the component's slot.

Example

json
{
  "kind": "component",
  "name": "Card",
  "props": {
    "title": { "expr": "lit", "value": "Welcome" },
    "variant": { "expr": "state", "name": "cardStyle" }
  },
  "children": [
    { "kind": "text", "value": { "expr": "lit", "value": "Card content here" } }
  ]
}

Slot Node

Placeholder for children passed to a component.

json
{
  "kind": "slot"
}
NameTypeRequiredDefaultDescription
kind"slot"Yes-Node type identifier. No additional props.

Usage in Component Definition

json
{
  "components": {
    "Card": {
      "params": {
        "title": { "type": "string" }
      },
      "view": {
        "kind": "element",
        "tag": "div",
        "props": { "class": { "expr": "lit", "value": "card" } },
        "children": [
          {
            "kind": "element",
            "tag": "h2",
            "children": [
              { "kind": "text", "value": { "expr": "param", "name": "title" } }
            ]
          },
          { "kind": "slot" }
        ]
      }
    }
  }
}

When using this component:

json
{
  "kind": "component",
  "name": "Card",
  "props": { "title": { "expr": "lit", "value": "Hello" } },
  "children": [
    { "kind": "text", "value": { "expr": "lit", "value": "This goes into the slot" } }
  ]
}

Markdown Node

Renders Markdown content as HTML with GFM (GitHub Flavored Markdown) support.

json
{
  "kind": "markdown",
  "content": { "expr": "lit", "value": "# Hello\n\nThis is **bold** text." }
}
NameTypeRequiredDefaultDescription
kind"markdown"Yes-Node type identifier.
contentExpressionYes-Expression that evaluates to a Markdown string.

Features

  • GFM support: Tables, strikethrough, task lists, and more
  • XSS protection: Sanitized via DOMPurify
  • SSR compatible: Works with server-rendered HTML
  • CSS class: .constela-markdown for styling

Examples

json
// Static markdown
{
  "kind": "markdown",
  "content": { "expr": "lit", "value": "## Features\n\n- Item 1\n- Item 2" }
}

// Dynamic markdown from state
{
  "kind": "markdown",
  "content": { "expr": "state", "name": "articleBody" }
}

Code Node

Renders code with VS Code-quality syntax highlighting powered by Shiki.

json
{
  "kind": "code",
  "language": { "expr": "lit", "value": "typescript" },
  "content": { "expr": "lit", "value": "const x: number = 1;" }
}
NameTypeRequiredDefaultDescription
kind"code"Yes-Node type identifier.
languageExpressionYes-Expression that evaluates to the language name (e.g., "typescript", "javascript", "json").
contentExpressionYes-Expression that evaluates to the code string.

Features

  • Shiki highlighting: VS Code-quality syntax highlighting
  • SSR compatible: Pre-highlighted on the server
  • CSS class: .constela-code for styling

Examples

json
// Static code block
{
  "kind": "code",
  "language": { "expr": "lit", "value": "json" },
  "content": { "expr": "lit", "value": "{ \"key\": \"value\" }" }
}

// Dynamic code from state
{
  "kind": "code",
  "language": { "expr": "state", "name": "selectedLanguage" },
  "content": { "expr": "state", "name": "codeSnippet" }
}