Framework Integration Implementation Guide
Effective: March 14, 2026
Target Audience: Frontend Developers, Integrators
📋 Table of Contents
🚀 Quick Start
For FormRenderer Users
import { FormRenderer } from '@/components/workspace/viewer/page-renderer';
export const MyForm = () => {
const schema = {
fields: [
{ name: 'input', type: 'text' },
{ name: 'calc', type: 'calculated', expression: 'SUM(input)' }
]
};
return (
<FormRenderer
schema={schema}
variant="full"
onSubmit={handleSubmit}
/>
);
};
That's it! All frameworks are enabled by default.
🔧 Step-by-Step Integration
Step 1: Verify Schema Support
Your schema should already support frameworks if created with Page Designer. Check for:
type: 'calculated'fieldscascadeConfigproperties on selectsvalidationRuleswith async validatorstype: 'repeater'fields withentityName
Step 2: Ensure Renderer Compatibility
// ✅ Works - All frameworks enabled
<FormRenderer schema={schema} data={data} />
// ✅ Works - Explicit framework flags (optional)
<FormRenderer
schema={schema}
data={data}
enableCalculatedFields={true}
enableCascadeSelects={true}
enableAsyncValidation={true}
enableRepeaterDiscovery={true}
/>
// ⚠️ May not work - Missing schema
<FormRenderer schema={undefined} data={data} />
Step 3: Add Event Handlers
import { FormRenderer } from '@/components/workspace/viewer/page-renderer';
export const MyForm = () => {
const [formData, setFormData] = useState({});
const [errors, setErrors] = useState({});
const handleChange = (data) => {
// Called when form data changes
// Includes calculated field updates
setFormData(data);
};
const handleSubmit = async (data) => {
// Called when user clicks submit
// All async validation already completed
// All calculated fields already computed
try {
await api.saveForm(data);
} catch (err) {
setErrors(err.errors);
}
};
return (
<FormRenderer
schema={schema}
data={formData}
onChange={handleChange}
onSubmit={handleSubmit}
/>
);
};
Step 4: Handle Validation Errors
const [validationErrors, setValidationErrors] = useState({});
// FormRenderer will call onError when validation fails
const handleError = (errors, fieldErrors) => {
setValidationErrors(fieldErrors);
// Show toast: "Please fix validation errors"
};
<FormRenderer
schema={schema}
data={data}
onError={handleError}
/>
Step 5: Monitor Async Operations
const [isValidating, setIsValidating] = useState(false);
const [isCalculating, setIsCalculating] = useState(false);
// Track framework processing
const handleFrameworkStateChange = (state) => {
setIsValidating(state.validating);
setIsCalculating(state.calculating);
// Show loading spinner if any operation pending
if (isValidating || isCalculating) {
// Show spinner
}
};
<FormRenderer
schema={schema}
data={data}
onFrameworkStateChange={handleFrameworkStateChange}
/>
🎯 Framework-Specific Integration
Calculated Fields
Schema Definition:
{
"type": "calculated",
"name": "totalPrice",
"label": "Total Price",
"expression": "MULTIPLY(quantity, unitPrice)",
"resultType": "number",
"decimals": 2
}
What Happens:
- User changes
quantityorunitPrice - FormRenderer detects calculated field dependency
- Expression evaluated:
MULTIPLY(2, 100)→ 200 totalPricefield updated to 200onChangecallback called with new data
Integration Code:
const handleFieldChange = (fieldName, value) => {
// FormRenderer handles calculation automatically
// Just update your local state
setFormData(prev => ({
...prev,
[fieldName]: value
}));
};
Cascade Selects
Schema Definition:
{
"type": "select",
"name": "state",
"label": "State",
"cascadeConfig": {
"parentField": "country",
"optionsUrl": "/api/states?country={country}",
"cacheTime": 300000
}
}
What Happens:
- User selects Country
- FormRenderer loads State options from API
- State select becomes enabled
- Options filtered to selected country
- User can select State
Integration Code:
// Just render the select - frameworks handle options loading
const [cascadeOptions, setCascadeOptions] = useState({});
const handleCascadeLoaded = (fieldName, options) => {
setCascadeOptions(prev => ({
...prev,
[fieldName]: options
}));
};
<FormRenderer
schema={schema}
data={data}
onCascadeOptionsLoaded={handleCascadeLoaded}
/>
Async Validation
Schema Definition:
{
"type": "email",
"name": "email",
"label": "Email",
"validationRules": [
{ "type": "required" },
{ "type": "email" },
{
"type": "async",
"trigger": "blur",
"debounce": 500,
"message": "Email already registered"
}
]
}
What Happens:
- User enters email
- On blur event, async validation starts (debounced 500ms)
- API called to check if email exists
- User sees loading spinner
- Error shown if email taken
- Allowed to proceed if email available
Integration Code:
const [isValidating, setIsValidating] = useState({});
const handleValidationStateChange = (fieldName, validating) => {
setIsValidating(prev => ({
...prev,
[fieldName]: validating
}));
};
// Show spinner while email validation in progress
{isValidating.email && <Spinner />}
<FormRenderer
schema={schema}
data={data}
onValidationStateChange={handleValidationStateChange}
/>
Repeater Discovery
Schema Definition:
{
"type": "repeater",
"name": "items",
"label": "Order Items",
"entityName": "order_items"
}
What Happens:
- FormRenderer mounts
- Discovers
order_itemsschema from database - Generates item template with fields auto-mapped
- Repeater initialized with discovered template
- User can add/edit/delete items
Integration Code:
const [discoveredTemplates, setDiscoveredTemplates] = useState({});
const handleRepeaterDiscovered = (fieldName, template) => {
setDiscoveredTemplates(prev => ({
...prev,
[fieldName]: template
}));
// Show "Template loaded" notification
};
<FormRenderer
schema={schema}
data={data}
onRepeaterDiscovered={handleRepeaterDiscovered}
/>
✅ Testing & Validation
Unit Testing
import { render, screen, waitFor } from '@testing-library/react';
import { FormRenderer } from '@/components/workspace/viewer/page-renderer';
describe('FormRenderer Framework Integration', () => {
test('calculated fields update automatically', async () => {
const schema = {
fields: [
{ name: 'qty', type: 'number' },
{ name: 'price', type: 'number' },
{ name: 'total', type: 'calculated', expression: 'MULTIPLY(qty, price)' }
]
};
const { getByDisplayValue } = render(
<FormRenderer schema={schema} data={{ qty: 2, price: 100 }} />
);
// Total should be 200
await waitFor(() => {
expect(getByDisplayValue('200')).toBeInTheDocument();
});
});
test('cascade selects load options', async () => {
const schema = {
fields: [
{ name: 'country', type: 'select', options: [{id: 'US', label: 'USA'}] },
{
name: 'state',
type: 'select',
cascadeConfig: {
parentField: 'country',
optionsUrl: '/api/states?country={country}'
}
}
]
};
render(<FormRenderer schema={schema} data={{}} />);
// Simulate selecting country
const countrySelect = screen.getByDisplayValue('USA');
fireEvent.change(countrySelect, { target: { value: 'US' } });
// State options should load
await waitFor(() => {
expect(screen.getByText(/California|Texas/i)).toBeInTheDocument();
});
});
test('async validation blocks submission on error', async () => {
const schema = {
fields: [
{
name: 'email',
type: 'email',
validationRules: [
{ type: 'async', trigger: 'blur', message: 'Email taken' }
]
}
]
};
const handleSubmit = jest.fn();
render(
<FormRenderer schema={schema} data={{}} onSubmit={handleSubmit} />
);
const emailInput = screen.getByRole('textbox');
fireEvent.change(emailInput, { target: { value: 'taken@example.com' } });
fireEvent.blur(emailInput); // Trigger async validation
const submitBtn = screen.getByRole('button', { name: /submit/i });
// Wait for validation error
await waitFor(() => {
fireEvent.click(submitBtn);
expect(handleSubmit).not.toHaveBeenCalled();
});
});
});
Integration Testing
describe('FormRenderer All Frameworks Together', () => {
test('complete order form with all 4 frameworks', async () => {
const schema = {
fields: [
// Cascade selects
{ name: 'country', type: 'select', options: [...] },
{
name: 'state',
type: 'select',
cascadeConfig: { parentField: 'country', optionsUrl: '/api/states' }
},
// Calculated fields
{ name: 'qty', type: 'number' },
{ name: 'price', type: 'number' },
{ name: 'subtotal', type: 'calculated', expression: 'MULTIPLY(qty, price)' },
// Async validation
{
name: 'email',
type: 'email',
validationRules: [{ type: 'async', trigger: 'blur' }]
},
// Repeater discovery
{ name: 'items', type: 'repeater', entityName: 'order_items' }
]
};
// Render form
const { getByRole } = render(<FormRenderer schema={schema} data={{}} />);
// Select country
fireEvent.change(getByRole('combobox', { name: /country/i }), {
target: { value: 'US' }
});
// Wait for states to load
await waitFor(() => {
expect(getByRole('combobox', { name: /state/i })).not.toBeDisabled();
});
// Change quantities → total auto-calculates
fireEvent.change(getByRole('spinbutton', { name: /qty/i }), {
target: { value: '5' }
});
await waitFor(() => {
const subtotal = getByDisplayValue('500'); // 5 * 100
expect(subtotal).toBeInTheDocument();
});
// Enter email → async validation
fireEvent.change(getByRole('textbox', { name: /email/i }), {
target: { value: 'user@example.com' }
});
fireEvent.blur(getByRole('textbox', { name: /email/i }));
// Wait for validation result
await waitFor(() => {
// No error message if email available
expect(screen.queryByText(/already registered/i)).not.toBeInTheDocument();
});
// All 4 frameworks working together!
});
});
🐛 Troubleshooting
Calculated Fields Not Updating
Symptom: Changed field value, but calculated field unchanged
Causes:
- Expression syntax error
- Field names don't match exactly
- Fields not in schema
Solution:
// Check schema field names match expression
{
name: 'unitPrice', // Use camelCase
type: 'number'
}
// NOT unit_price (snake_case)
// Check expression uses correct names
expression: 'MULTIPLY(unitPrice, quantity)' // ✅
expression: 'MULTIPLY(unit_price, qty)' // ❌
Cascade Options Not Loading
Symptom: Parent field selected, but child options don't load
Causes:
- API endpoint unreachable
- Parent field value not being passed
- Cache issue
Solution:
// Verify API endpoint
cascadeConfig: {
parentField: 'country',
optionsUrl: '/api/states?country={country}' // {country} replaced with value
}
// Check network tab in DevTools
// Should see request: GET /api/states?country=US
// Clear cache if testing
// Cache TTL: 5 minutes by default
Async Validation Always Failing
Symptom: Validation always shows error, even valid data
Causes:
- API endpoint returning error
- Validator implementation issue
- Custom validator throwing exception
Solution:
// Check browser console for errors
// Async validators should handle errors gracefully
// Test validator manually
const testValidator = async (value) => {
try {
const resp = await fetch(`/api/check?email=${value}`);
return resp.ok; // true = valid, false = invalid
} catch (err) {
console.error('Validation error:', err);
return false; // Fail safely on error
}
};
Repeater Template Not Auto-Discovered
Symptom: Repeater shows no fields
Causes:
- Entity name doesn't match database table
- Services not initialized
- Schema not returned from API
Solution:
// Use actual database table name
entityName: 'order_items' // ✅ Matches table name
entityName: 'orderItem' // ❌ Camel case, won't match
entityName: 'order-items' // ❌ Hyphens, won't match
// Check API returns schema
GET /api/schema/order_items
// Should return: { fields: [...] }
Performance Issues (Slow Cascades/Validations)
Symptom: UI freezes while loading options or validating
Causes:
- Large result sets (1000+ options)
- Validation debounce too short
- Unoptimized API
Solution:
// Increase debounce time
{
type: 'async',
trigger: 'blur',
debounce: 1000 // 1 second instead of 500ms
}
// Use server-side filtering
optionsUrl: '/api/states?country={country}&limit=50&search={search}'
// Add pagination for large lists
cascadeConfig: {
optionsUrl: '/api/states?country={country}&page=1&pageSize=50'
}
// Cache results longer
cacheTime: 600000 // 10 minutes instead of 5
🚀 Advanced Scenarios
Custom Calculated Functions
// Define custom function in expression
expression: 'CUSTOM_TAX_RATE(price, state)'
// Implement in calculated fields engine
engine.registerFunction('CUSTOM_TAX_RATE', (price, state) => {
const taxRates = {
'CA': 0.0725,
'NY': 0.08,
'TX': 0.0625
};
return price * (taxRates[state] || 0);
});
Multi-Level Cascades
{
type: 'select',
name: 'country',
options: [...]
}
{
type: 'select',
name: 'state',
cascadeConfig: {
parentField: 'country',
optionsUrl: '/api/states?country={country}'
}
}
{
type: 'select',
name: 'city',
cascadeConfig: {
parentField: 'state',
optionsUrl: '/api/cities?state={state}'
}
}
{
type: 'select',
name: 'zipcode',
cascadeConfig: {
parentField: 'city',
optionsUrl: '/api/zipcodes?city={city}'
}
}
Conditional Validation
{
type: 'text',
name: 'phone',
validationRules: [
{
type: 'async',
trigger: 'blur',
// Only validate if country requires it
condition: (formData) => formData.country === 'US',
validator: async (value) => {
// Validate US phone format
const isValid = /^\d{3}-\d{3}-\d{4}$/.test(value);
return isValid;
}
}
]
}
Dynamic Repeater Items
{
type: 'repeater',
name: 'items',
entityName: 'order_items',
// Max items based on order type
maxItems: (formData) => {
return formData.orderType === 'bulk' ? 999 : 10;
}
}
📚 Related Guides
✅ Checklist: Before Deploying
- All calculated fields tested with sample data
- Cascade options load from correct APIs
- Async validation doesn't block valid submissions
- Repeater templates discovered correctly
- Form submission works end-to-end
- Error messages display properly
- Performance acceptable (no UI freezes)
- Works on mobile/tablet/desktop
- Accessibility features working
- Documentation updated
Last Updated: March 14, 2026
Status: ✅ Complete and Ready for Production