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
- Declarative UI: Define UIs in configuration, not code
- Rapid Prototyping: Build pages quickly without JSX
- Non-Technical Edits: Allow non-developers to modify UI structure
- Consistency: Enforce consistent component usage
- Dynamic Generation: Generate UIs from API responses
- A/B Testing: Easily swap component trees
- Version Control: Track UI changes in JSON
- Hot Reloading: Update UIs without code changes
- Multi-Platform: Same tree can target web, mobile, etc.
- Reduced Code: Less boilerplate, more configuration
Best Practices
- Keep trees shallow: Deep nesting is hard to maintain
- Use meaningful names:
UserListPagenotPage1 - Document with comments: Add
commentfields for clarity - Group related trees: Organize by feature or page
- Validate props: Ensure required props are present
- Test conditions: Verify conditional logic works
- Handle missing data: Provide fallbacks for
{{variables}} - Reuse subtrees: Extract common patterns
- Type checking: Use TypeScript for component props
- 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.