The expression system bridges visual low-code editing with code-based flexibility. Key principles:
- No Magic Syntax: Expressions are just JavaScript - no special {{ }} or template languages
- Context-Aware: Visual editor knows what's available (props, state, globals)
- Type-Safe: Validate expressions against expected types
- Debuggable: Expressions visible in step debugger
Every component property can be set in three ways:
Use case: Fixed values that never change
In Visual Editor:
Property: text
Type: [Static ▼]
Value: "Welcome to Dashboard"
In Manifest:
{
"props": {
"text": {
"type": "static",
"value": "Welcome to Dashboard"
}
}
}Generated Code:
<Text>Welcome to Dashboard</Text>Use case: Dynamic values computed from available context
In Visual Editor:
Property: text
Type: [Expression ▼]
┌──────────────────────────────────────┐
│ user.firstName + ' ' + user.lastName │ ← JavaScript editor
└──────────────────────────────────────┘
Available context:
• props.user (object)
• state.isHovered (boolean)
• global.theme (string)
In Manifest:
{
"props": {
"text": {
"type": "expression",
"value": "user.firstName + ' ' + user.lastName"
}
}
}Generated Code:
<Text>{user.firstName + ' ' + user.lastName}</Text>Editor Features:
- Syntax highlighting
- Auto-completion for available context
- Inline error checking
- Type validation
Use case: Direct connection to another component's state or output
In Visual Editor:
Property: searchTerm
Type: [Binding ▼]
Source: [Select Component ▼]
↳ SearchBox
↳ [Select Property ▼]
↳ state.query
Or visually draw wire from SearchBox's query output to this component's searchTerm input.
In Manifest:
{
"props": {
"searchTerm": {
"type": "binding",
"source": "searchBox.state.query"
}
}
}Generated Code:
// Handled through props or Context depending on relationship
<SearchResults searchTerm={searchQuery} />Expressions can reference:
// Access props directly by name
props.user
props.onClick
props.children// Internal state of the component
state.isHovered
state.count
state.selectedId// From .lowcode/variables.json
global.currentUser
global.theme
global.isAuthenticated// From other components with exposed state
searchBox.state.query
userList.state.selectedUser
sidebar.state.isOpen// Built-in helpers (auto-imported)
formatDate(date, 'YYYY-MM-DD')
capitalize(string)
filter(array, condition)
// Custom script nodes
validateEmail(email)
calculateTotal(items)// String manipulation
user.name.toUpperCase()
user.email.toLowerCase()
// Number formatting
price.toFixed(2)
(count * 100).toString() + '%'
// Array operations
items.length
items[0].name
items.map(item => item.id)
// Object access
user.profile.avatar
settings.theme.colors.primary// Ternary operator
user.isPremium ? 'gold' : 'gray'
// Logical operators
isLoggedIn && user.name
isLoading || 'Click to load'
// Nullish coalescing
user.nickname ?? user.name// Combining multiple sources
global.currentUser.id === props.userId ? 'Edit' : 'View'
// Array filtering
items.filter(item => item.category === selectedCategory)
// Computed values
items.reduce((sum, item) => sum + item.price, 0)
// Date formatting
new Date(user.createdAt).toLocaleDateString('en-US')When expressions get too complex, extract to script nodes:
Visual Editor: Add "Script Node" from component palette
Configure:
Name: formatUserData
Inputs:
• rawUser (object)
• locale (string)
Outputs:
• displayName (string)
• joinDate (string)
• canEdit (boolean)
Write Script (scripts/formatUserData.js):
export function formatUserData(inputs) {
const { rawUser, locale } = inputs;
return {
displayName: `${rawUser.firstName} ${rawUser.lastName}`,
joinDate: new Date(rawUser.createdAt).toLocaleDateString(locale),
canEdit: rawUser.role === 'admin' || rawUser.id === inputs.currentUserId
};
}
// Metadata for visual editor
export const metadata = {
name: 'Format User Data',
description: 'Transforms raw user object into display-ready format',
inputs: {
rawUser: {
type: 'object',
required: true,
schema: {
firstName: 'string',
lastName: 'string',
createdAt: 'string',
role: 'string',
id: 'string'
}
},
locale: {
type: 'string',
default: 'en-US'
}
},
outputs: {
displayName: { type: 'string' },
joinDate: { type: 'string' },
canEdit: { type: 'boolean' }
}
};In another component's property:
Visual Editor:
Property: text
Type: [Expression ▼]
┌─────────────────────────────┐
│ formatUserData.displayName │
└─────────────────────────────┘
Generated Code:
import { formatUserData } from './scripts/formatUserData';
export function UserProfile({ rawUser, locale }) {
const { displayName, joinDate, canEdit } = formatUserData({ rawUser, locale });
return (
<div>
<h1>{displayName}</h1>
<p>Joined: {joinDate}</p>
{canEdit && <button>Edit</button>}
</div>
);
}The expression editor validates types at edit time:
// ✅ Valid
<Text fontSize={12 + 4}> // → number (fontSize expects number)
// ❌ Invalid
<Text fontSize="large"> // → string (fontSize expects number)Visual Editor Shows:
⚠️ Type mismatch: fontSize expects number, got string
// Convert types explicitly
Number(user.age) // string → number
String(count) // number → string
Boolean(value) // any → booleanThe expression editor provides intelligent suggestions:
Typing user.:
┌─────────────────────────┐
│ user. │
├─────────────────────────┤
│ ↳ id (string) │
│ ↳ name (string) │
│ ↳ email (string) │
│ ↳ role (string) │
│ ↳ isPremium (boolean) │
│ ↳ createdAt (string) │
└─────────────────────────┘
Typing global.:
┌──────────────────────────────┐
│ global. │
├──────────────────────────────┤
│ ↳ currentUser (object) │
│ ↳ theme (string) │
│ ↳ isAuthenticated (boolean) │
└──────────────────────────────┘
Context is inferred from:
- Component prop types
- Global variable definitions
- Parent component state
- TypeScript interfaces (if available)
Manifest:
{
"type": "expression",
"value": "user.name.toUpperCase()"
}Compiled:
{user.name.toUpperCase()}Manifest:
{
"type": "expression",
"value": "global.users.find(u => u.id === props.userId).name"
}Compiled (detects global dependency):
import { useGlobalState } from '../runtime/globalState';
export function Component({ userId }) {
const users = useGlobalState('users');
return <div>{users.find(u => u.id === userId).name}</div>;
}Manifest:
{
"type": "expression",
"value": "items.filter(i => i.price > 100).length"
}Compiled (memoized for performance):
import { useMemo } from 'react';
export function Component({ items }) {
const expensiveItemsCount = useMemo(
() => items.filter(i => i.price > 100).length,
[items]
);
return <div>{expensiveItemsCount} expensive items</div>;
}If expression fails at runtime:
// Expression: user.profile.avatar
// Error: Cannot read property 'avatar' of undefinedVisual Editor Shows:
⚠️ Runtime Error in UserCard.avatar:
Cannot read property 'avatar' of undefined
Expression: user.profile.avatar
Context: user.profile is undefined
Suggestion: Use optional chaining
✓ user.profile?.avatar
When stepping through execution:
┌─────────────────────────────────────────────┐
│ Evaluating Expression │
├─────────────────────────────────────────────┤
│ Component: UserCard │
│ Property: displayName │
│ Expression: user.firstName + ' ' + user... │
│ │
│ Available Context: │
│ • user = { │
│ firstName: "John", │
│ lastName: "Doe" │
│ } │
│ │
│ Result: "John Doe" │
└─────────────────────────────────────────────┘
Keep expressions simple
// Good
user.name
// Good
user.firstName + ' ' + user.lastName
// Good
items.length > 0 ? 'Items available' : 'No items'Use optional chaining
// Good - handles undefined safely
user.profile?.avatar
// Good - provides fallback
user.nickname ?? user.firstNameExtract complex logic to script nodes
// Instead of:
items.filter(i => i.category === cat && i.price < 100).map(i => i.name).join(', ')
// Do:
filterAndFormatItems({ items, category: cat, maxPrice: 100 })Don't write multi-line expressions
// Bad
items.map(item => {
if (item.price > 100) {
return item.name.toUpperCase();
}
return item.name;
}).join(', ')
// Use script node insteadDon't mutate state in expressions
// Bad - side effects
items.push(newItem)
// Bad - modifies array
items.sort()
// Use event handlers insteadDon't access external APIs directly
// Bad
fetch('/api/users')
// Bad
localStorage.getItem('user')
// Use script nodes with proper async handling- Visual Expression Builder: Drag-drop expression construction (like n8n)
- Expression Templates: Library of common patterns
- AI Assistance: Natural language → expression conversion
- Performance Profiling: Identify slow expressions
- Live Expression Testing: Test expressions with sample data
See Also:
- Data Flow Model - How expressions fit into data architecture
- Component Schema - Expression storage format
- Debugger Design - Debugging expression evaluation