Files
postgres/docs/COMPONENT_TREES.md
2026-01-08 13:39:34 +00:00

14 KiB

Component Trees in features.json

Define entire UI hierarchies in JSON - build complete interfaces declaratively!

The componentTrees section in features.json allows you to define complete component hierarchies in a declarative JSON format. This enables you to build entire pages and complex UIs without writing JSX code.

Overview

Component trees support:

  • Nested component hierarchies
  • Props passing with interpolation
  • Conditional rendering
  • Loops/iterations with forEach
  • Data binding with dataSource
  • Event handlers
  • Dynamic values with template syntax {{variable}}

Basic Structure

{
  "componentTrees": {
    "MyPage": {
      "component": "Box",
      "props": {
        "sx": { "p": 3 }
      },
      "children": [
        {
          "component": "Typography",
          "props": {
            "variant": "h4",
            "text": "Hello World"
          }
        }
      ]
    }
  }
}

Component Node Schema

{
  "component": string,          // Component name (e.g., "Box", "Button", "DataGrid")
  "props"?: object,             // Component props
  "children"?: ComponentNode[], // Child components
  "condition"?: string,         // Render condition (e.g., "hasPermission('create')")
  "forEach"?: string,           // Loop over data (e.g., "items", "users")
  "dataSource"?: string,        // Bind to data source (e.g., "tableData", "navItems")
  "comment"?: string            // Documentation comment
}

Template Syntax

Use {{variable}} for dynamic values:

{
  "component": "Typography",
  "props": {
    "text": "Welcome, {{user.name}}!"
  }
}

Accessing Nested Properties

{
  "component": "Typography",
  "props": {
    "text": "{{user.profile.firstName}} {{user.profile.lastName}}"
  }
}

Using Expressions

{
  "component": "Icon",
  "props": {
    "name": "{{card.change > 0 ? 'TrendingUp' : 'TrendingDown'}}"
  }
}

Conditional Rendering

Use the condition property to conditionally render components:

{
  "component": "Button",
  "condition": "hasPermission('create')",
  "props": {
    "text": "Create New",
    "onClick": "openCreateDialog"
  }
}

Multiple Conditions

{
  "condition": "features.enableSearch && userRole === 'admin'",
  "component": "TextField",
  "props": {
    "placeholder": "Search..."
  }
}

Loops with forEach

Iterate over arrays using forEach:

{
  "component": "Grid",
  "forEach": "users",
  "props": {
    "item": true,
    "xs": 12,
    "sm": 6
  },
  "children": [
    {
      "component": "Card",
      "children": [
        {
          "component": "Typography",
          "props": {
            "text": "{{user.name}}"
          }
        }
      ]
    }
  ]
}

In the loop, the current item is available as the singular form of the array name:

  • forEach: "users" → current item is {{user}}
  • forEach: "products" → current item is {{product}}
  • forEach: "items" → current item is {{item}}

Data Sources

Bind components to data sources:

{
  "component": "NavList",
  "dataSource": "navItems",
  "children": [
    {
      "component": "NavItem",
      "props": {
        "icon": "{{item.icon}}",
        "label": "{{item.label}}",
        "href": "/admin/{{item.id}}"
      }
    }
  ]
}

Event Handlers

Reference event handler functions by name:

{
  "component": "Button",
  "props": {
    "text": "Save",
    "onClick": "handleSave"
  }
}

Multiple handlers:

{
  "component": "TextField",
  "props": {
    "value": "{{searchTerm}}",
    "onChange": "handleSearch",
    "onKeyPress": "handleKeyPress"
  }
}

Complete Examples

Admin Dashboard Layout

{
  "AdminDashboard": {
    "component": "Box",
    "props": {
      "sx": { "display": "flex", "minHeight": "100vh" }
    },
    "children": [
      {
        "component": "Sidebar",
        "props": { "width": 240 },
        "children": [
          {
            "component": "NavList",
            "dataSource": "navItems",
            "children": [
              {
                "component": "NavItem",
                "props": {
                  "icon": "{{item.icon}}",
                  "label": "{{item.label}}",
                  "href": "/admin/{{item.id}}"
                }
              }
            ]
          }
        ]
      },
      {
        "component": "Box",
        "props": { "sx": { "flexGrow": 1 } },
        "children": [
          {
            "component": "AppBar",
            "children": [
              {
                "component": "Toolbar",
                "children": [
                  {
                    "component": "Typography",
                    "props": {
                      "variant": "h6",
                      "text": "{{pageTitle}}"
                    }
                  }
                ]
              }
            ]
          },
          {
            "component": "Outlet",
            "comment": "Child routes render here"
          }
        ]
      }
    ]
  }
}

Resource List Page with CRUD Actions

{
  "ResourceListPage": {
    "component": "Box",
    "children": [
      {
        "component": "Box",
        "props": {
          "sx": { "display": "flex", "justifyContent": "space-between", "mb": 3 }
        },
        "children": [
          {
            "component": "Typography",
            "props": {
              "variant": "h4",
              "text": "{{resourceName}}"
            }
          },
          {
            "component": "Button",
            "condition": "hasPermission('create')",
            "props": {
              "variant": "contained",
              "startIcon": "Add",
              "text": "Create New",
              "onClick": "openCreateDialog"
            }
          }
        ]
      },
      {
        "component": "DataGrid",
        "dataSource": "tableData",
        "props": {
          "columns": "{{columns}}",
          "rows": "{{rows}}",
          "onEdit": "handleEdit",
          "onDelete": "handleDelete"
        }
      },
      {
        "component": "Pagination",
        "condition": "features.enablePagination",
        "props": {
          "count": "{{totalPages}}",
          "page": "{{currentPage}}",
          "onChange": "handlePageChange"
        }
      }
    ]
  }
}

Form Dialog

{
  "FormDialogTree": {
    "component": "Dialog",
    "props": {
      "open": "{{open}}",
      "onClose": "handleClose",
      "maxWidth": "md"
    },
    "children": [
      {
        "component": "DialogTitle",
        "children": [
          {
            "component": "Typography",
            "props": {
              "text": "{{title}}"
            }
          }
        ]
      },
      {
        "component": "DialogContent",
        "children": [
          {
            "component": "Grid",
            "props": { "container": true, "spacing": 2 },
            "children": [
              {
                "component": "Grid",
                "forEach": "formFields",
                "props": {
                  "item": true,
                  "xs": 12,
                  "sm": 6
                },
                "children": [
                  {
                    "component": "DynamicField",
                    "props": {
                      "field": "{{field}}",
                      "value": "{{values[field.name]}}",
                      "onChange": "handleFieldChange"
                    }
                  }
                ]
              }
            ]
          }
        ]
      },
      {
        "component": "DialogActions",
        "children": [
          {
            "component": "Button",
            "props": {
              "text": "Cancel",
              "onClick": "handleClose"
            }
          },
          {
            "component": "Button",
            "props": {
              "variant": "contained",
              "text": "Save",
              "onClick": "handleSubmit",
              "disabled": "{{!isValid}}"
            }
          }
        ]
      }
    ]
  }
}

Dashboard Stats Cards

{
  "DashboardStatsCards": {
    "component": "Grid",
    "props": { "container": true, "spacing": 3 },
    "children": [
      {
        "component": "Grid",
        "forEach": "statsCards",
        "props": {
          "item": true,
          "xs": 12,
          "sm": 6,
          "md": 3
        },
        "children": [
          {
            "component": "Card",
            "children": [
              {
                "component": "CardContent",
                "children": [
                  {
                    "component": "Icon",
                    "props": {
                      "name": "{{card.icon}}",
                      "color": "{{card.color}}"
                    }
                  },
                  {
                    "component": "Typography",
                    "props": {
                      "variant": "h4",
                      "text": "{{card.value}}"
                    }
                  },
                  {
                    "component": "Typography",
                    "props": {
                      "variant": "body2",
                      "text": "{{card.label}}"
                    }
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  }
}

Using Component Trees in Code

Get a Component Tree

import { getComponentTree } from '@/utils/featureConfig';

const tree = getComponentTree('AdminDashboard');

Render a Component Tree

import { getComponentTree } from '@/utils/featureConfig';

function ComponentTreeRenderer({ treeName, data, handlers }: Props) {
  const tree = getComponentTree(treeName);
  
  if (!tree) return null;
  
  return renderNode(tree, data, handlers);
}

function renderNode(node: ComponentNode, data: any, handlers: any): JSX.Element {
  const Component = getComponent(node.component);
  
  // Evaluate condition
  if (node.condition && !evaluateCondition(node.condition, data)) {
    return null;
  }
  
  // Handle forEach loops
  if (node.forEach) {
    const items = data[node.forEach] || [];
    return (
      <>
        {items.map((item: any, index: number) => {
          const itemData = { ...data, [getSingular(node.forEach)]: item };
          return renderNode({ ...node, forEach: undefined }, itemData, handlers);
        })}
      </>
    );
  }
  
  // Interpolate props
  const props = interpolateProps(node.props, data, handlers);
  
  // Render children
  const children = node.children?.map((child, idx) =>
    renderNode(child, data, handlers)
  );
  
  return <Component key={index} {...props}>{children}</Component>;
}

Complete Example with React

import React from 'react';
import { getComponentTree } from '@/utils/featureConfig';
import { Box, Button, Typography, Dialog, TextField } from '@mui/material';

const componentMap = {
  Box, Button, Typography, Dialog, TextField,
  // ... other components
};

function DynamicPage({ treeName }: { treeName: string }) {
  const tree = getComponentTree(treeName);
  const [data, setData] = useState({
    pageTitle: 'Users Management',
    resourceName: 'Users',
    rows: [],
    loading: false,
  });
  
  const handlers = {
    handleEdit: (row: any) => console.log('Edit', row),
    handleDelete: (row: any) => console.log('Delete', row),
    openCreateDialog: () => console.log('Create'),
  };
  
  return renderComponentTree(tree, data, handlers);
}

Benefits of Component Trees

  1. Declarative UI: Define UIs in configuration, not code
  2. Rapid Prototyping: Build pages quickly without JSX
  3. Non-Technical Edits: Allow non-developers to modify UI structure
  4. Consistency: Enforce consistent component usage
  5. Dynamic Generation: Generate UIs from API responses
  6. A/B Testing: Easily swap component trees
  7. Version Control: Track UI changes in JSON
  8. Hot Reloading: Update UIs without code changes
  9. Multi-Platform: Same tree can target web, mobile, etc.
  10. Reduced Code: Less boilerplate, more configuration

Best Practices

  1. Keep trees shallow: Deep nesting is hard to maintain
  2. Use meaningful names: UserListPage not Page1
  3. Document with comments: Add comment fields for clarity
  4. Group related trees: Organize by feature or page
  5. Validate props: Ensure required props are present
  6. Test conditions: Verify conditional logic works
  7. Handle missing data: Provide fallbacks for {{variables}}
  8. Reuse subtrees: Extract common patterns
  9. Type checking: Use TypeScript for component props
  10. Version trees: Track changes in version control

Advanced Features

Computed Values

{
  "component": "Typography",
  "props": {
    "text": "{{items.length}} items found"
  }
}

Nested Conditionals

{
  "condition": "user.role === 'admin'",
  "component": "Box",
  "children": [
    {
      "condition": "user.permissions.includes('delete')",
      "component": "Button",
      "props": {
        "text": "Delete All",
        "onClick": "handleDeleteAll"
      }
    }
  ]
}

Dynamic Component Selection

{
  "component": "{{viewType === 'grid' ? 'GridView' : 'ListView'}}",
  "props": {
    "items": "{{items}}"
  }
}

API Reference

getComponentTree(treeName: string): ComponentTree | undefined

Get a component tree by name.

const tree = getComponentTree('AdminDashboard');

getAllComponentTrees(): Record<string, ComponentTree>

Get all defined component trees.

const trees = getAllComponentTrees();
console.log(Object.keys(trees)); // ['AdminDashboard', 'ResourceListPage', ...]

Conclusion

Component trees in features.json enable you to:

  • Build complete UIs without writing JSX
  • Define page layouts declaratively
  • Create dynamic, data-driven interfaces
  • Rapidly prototype and iterate
  • Build half your app from configuration!

With component trees, features.json becomes a complete UI definition language, enabling true configuration-driven development.