Skip to main content

Framework Integration Implementation Guide

Effective: March 14, 2026
Target Audience: Frontend Developers, Integrators


📋 Table of Contents

  1. Quick Start
  2. Step-by-Step Integration
  3. Testing & Validation
  4. Troubleshooting
  5. Advanced Scenarios

🚀 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' fields
  • cascadeConfig properties on selects
  • validationRules with async validators
  • type: 'repeater' fields with entityName

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:

  1. User changes quantity or unitPrice
  2. FormRenderer detects calculated field dependency
  3. Expression evaluated: MULTIPLY(2, 100) → 200
  4. totalPrice field updated to 200
  5. onChange callback 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:

  1. User selects Country
  2. FormRenderer loads State options from API
  3. State select becomes enabled
  4. Options filtered to selected country
  5. 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:

  1. User enters email
  2. On blur event, async validation starts (debounced 500ms)
  3. API called to check if email exists
  4. User sees loading spinner
  5. Error shown if email taken
  6. 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:

  1. FormRenderer mounts
  2. Discovers order_items schema from database
  3. Generates item template with fields auto-mapped
  4. Repeater initialized with discovered template
  5. 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:

  1. Expression syntax error
  2. Field names don't match exactly
  3. 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:

  1. API endpoint unreachable
  2. Parent field value not being passed
  3. 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:

  1. API endpoint returning error
  2. Validator implementation issue
  3. 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:

  1. Entity name doesn't match database table
  2. Services not initialized
  3. 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:

  1. Large result sets (1000+ options)
  2. Validation debounce too short
  3. 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;
}
}


✅ 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