Technical Implementation & Developer Guide
Comprehensive technical documentation for developers working with Page Designer, including architecture, renderer patterns, component structure, and extension guidelines.
Architecture Overview
File Structure
page-designer/
├── core/
│ ├── PageDesignerCore.tsx # Main orchestrator component
│ ├── PageDesignerStudio.tsx # Studio wrapper
│ └── JsonSchemaGenerator.ts # Schema validation
├── editors/
│ ├── FieldEditor.tsx # Two-pane field editor
│ ├── FieldPreview.tsx # Left pane preview
│ ├── field-editor.css # Editor styling
│ ├── AdvancedStylingEditor.tsx # Advanced CSS editor
│ ├── AdvancedStylingEditor.tsx # Advanced CSS editor
│ └── [15+ field-specific editors] # Specialized editors
├── renderers/
│ ├── FormRenderer.tsx # Form rendering engine
│ ├── DashboardRenderer.tsx # Dashboard widget rendering
│ ├── ReportRenderer.tsx # Report document rendering
│ └── PageRenderer.tsx # Content page rendering
├── sidebar/
│ ├── FieldPalette.tsx # Drag-drop field palette
│ ├── PageTreeView.tsx # Page structure tree
│ └── CompactSidebar.tsx # Collapsed sidebar
├── dialogs/
│ ├── SettingsDialog.tsx # Field settings dialog
│ ├── PreviewDialog.tsx # Live preview
│ ├── VersionHistoryDialog.tsx # Version management
│ └── [5+ other dialogs] # Configuration dialogs
├── canvas/
│ ├── PageCanvas.tsx # Design canvas
│ └── useDragAndDrop.ts # Drag-drop logic
├── managers/
│ ├── DataSourceManager.tsx # Data source config
│ ├── VariablesManager.tsx # Form variables
│ ├── ActionsManager.tsx # Form actions
│ └── ChangeLogManager.ts # Change tracking
├── types.ts # TypeScript definitions
├── utils/
│ ├── field-templates.ts # Field presets
│ ├── schema-validator.ts # Schema validation
│ └── [helper utilities]
└── hooks/
├── useUndoRedo.ts # Undo/redo state
└── useDragAndDrop.ts # Drag-drop hook
Core Components
PageDesignerCore
Main orchestrator managing state, interactions, and sub-components.
Responsibilities:
- Form state management (fields, data sources, variables, actions)
- Field selection and editing
- Undo/redo history
- Mode switching (design/preview)
- Device preview
- Save/publish coordination
Key Props:
interface PageDesignerProps {
open: boolean;
onClose?: () => void;
pageId: string;
entityCode: string;
initialSchema: FormSchema;
initialDataSources?: ManagedDataSource[];
initialVariables?: FormVariable[];
initialActions?: FormAction[];
onSave?: (schema: FormSchema) => Promise<void>;
onChange?: (schema: FormSchema) => void;
viewMode?: "design" | "preview";
deviceType?: "mobile" | "tablet" | "laptop" | "desktop";
}
FieldEditor
Two-pane field property editor with live preview.
Layout:
- Left Pane (380px): Live field preview
- Right Pane (flex): Property tabs
Tabs:
- Basic - Label, placeholder, options, data source
- Display - Visibility, styling, decorations
- Data - Default value, persistence, binding
- Validation - Rules, constraints, error messages
- Conditional - Show/hide logic
- Calculated - Formula-based computation
- Layout - Container-specific properties
- Workflows - Trigger actions
- Help - User documentation
- Permissions - Access control
- Advanced - CSS, custom code
Renderers
Three specialized renderers handle different page types:
FormRenderer - Interactive form display
export const FormRenderer: React.FC<FormRendererProps> = ({
schema: FormSchema;
mode?: 'design' | 'view' | 'edit';
onSubmit?: (data: any) => Promise<void>;
onFieldChange?: (fieldId: string, value: any) => void;
readOnly?: boolean;
})
DashboardRenderer - Widget composition
export const DashboardRenderer: React.FC<DashboardRendererProps> = ({
schema: FormSchema;
dataSource?: any;
refreshInterval?: number;
onWidgetInteraction?: (widgetId: string, action: string) => void;
})
ReportRenderer - Document generation
export const ReportRenderer: React.FC<ReportRendererProps> = ({
schema: FormSchema;
data: any;
format?: 'pdf' | 'html' | 'excel';
onExport?: (format: string) => void;
})
Field System
Field Interface
export interface FormField {
// Identity
id: string;
name?: string;
type: FieldType;
// Display
label: string;
placeholder?: string;
description?: string;
tooltip?: string;
prefix?: string;
suffix?: string;
// State
defaultValue?: any;
required?: boolean;
disabled?: boolean;
hidden?: boolean;
readOnly?: boolean;
// Validation
validation?: ValidationConfig;
validationRules?: ValidationRule[];
// Logic
conditional?: ConditionalLogic;
calculated?: CalculatedValue;
// Data
options?: FieldOption[];
dataSource?: DataSource;
multiple?: boolean;
// Specific Properties
rows?: number; // textarea
min?: number; // number, slider
max?: number; // number, slider
step?: number; // slider
currency?: string; // currency field
[key: string]: any; // Field-specific props
// Container
children?: FormField[];
containerProps?: ContainerProps;
// Advanced
helpContent?: HelpConfig;
fieldPermissions?: PermissionConfig;
workflows?: WorkflowConfig[];
style?: StyleConfig;
}
Field Categories
Input Fields (25+)
- Basic: text, textarea, number, email, password
- Selection: select, radio, checkbox, checkbox-group, autocomplete
- Temporal: date, daterange, time, datetime
- File: file, multifile
- Special: currency, phone, url, tags, rating, slider, signature, html, switch
Layout (20+) Containers for grouping and organizing fields
- Grid-based: grid, columns, responsive-grid
- Tabbed: tabs, accordion, wizard, stepper
- Other: panel, section, card, divider, spacer, repeater, table, datagrid, hero, card-container, fieldset, split-pane
Widgets (10+) Data visualization and dashboards
- Charts: chart, kpi, gauge, metric
- Interactive: calendar-widget, map-widget, timeline, kanban, progress-tracker, activity-log
- Tables: table-widget, list-widget
Content (15+) Static and dynamic content display
- Text: heading, paragraph, text-content, blockquote
- Media: image, video, carousel, gallery
- Navigation: navigation, header, footer, sidebar, breadcrumb
- Other: alert, badge, chip, code-block, content-card
Interactive (5+) User interaction elements
- dialog, popup, drawer, dataview, tree-view
Validation System
Validation Rule Engine
export interface ValidationRule {
type: 'required' | 'minLength' | 'maxLength' | 'min' | 'max' |
'pattern' | 'email' | 'url' | 'comparison' | 'custom' | 'async';
// Common
message: string; // Error message
trigger: 'blur' | 'change' | 'submit' | 'realtime';
// Type-specific
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
pattern?: string | RegExp;
compareField?: string; // For comparison
operator?: 'equals' | 'notEquals' | 'lessThan' | ...;
customValidation?: string; // JavaScript function
asyncValidationUrl?: string; // For async
asyncMethod?: 'GET' | 'POST';
debounceMs?: number;
}
Validation Execution
// Validate single field
const validateField = (field: FormField, value: any): ValidationError[] => {
const errors: ValidationError[] = [];
// Built-in validation
if (field.required && !value) {
errors.push({ fieldId: field.id, message: "Required" });
}
// Validation rules
for (const rule of field.validationRules || []) {
const ruleError = validateRule(rule, value, formData);
if (ruleError) errors.push(ruleError);
}
return errors;
};
// Validate entire form
const validateForm = (schema: FormSchema, data: any): ValidationResult => {
const errors: Record<string, ValidationError[]> = {};
for (const field of flattenFields(schema.fields)) {
const fieldErrors = validateField(
field,
data[field.propertyKey || field.id],
);
if (fieldErrors.length > 0) {
errors[field.id] = fieldErrors;
}
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
};
Conditional Logic System
Conditional Evaluation
export interface ConditionalLogic {
rules: ConditionalRule[];
conjunction: 'AND' | 'OR';
}
export interface ConditionalRule {
when: string; // Field ID to watch
operator: 'equals' | 'notEquals' | 'contains' | 'greaterThan' | ...;
value: any; // Expected value
action: 'show' | 'hide' | 'require' | 'disable' | 'enable';
childRules?: ConditionalRule[]; // Nested rules
}
Evaluation Engine
const evaluateCondition = (rule: ConditionalRule, data: any): boolean => {
const fieldValue = data[rule.when];
switch (rule.operator) {
case "equals":
return fieldValue === rule.value;
case "notEquals":
return fieldValue !== rule.value;
case "contains":
return String(fieldValue).includes(rule.value);
case "greaterThan":
return Number(fieldValue) > Number(rule.value);
case "lessThan":
return Number(fieldValue) < Number(rule.value);
case "isEmpty":
return !fieldValue || fieldValue === "";
case "isNotEmpty":
return !!fieldValue && fieldValue !== "";
default:
return false;
}
};
const evaluateConditionalLogic = (
logic: ConditionalLogic,
data: any,
): boolean => {
const results = logic.rules.map((rule) => evaluateCondition(rule, data));
if (logic.conjunction === "AND") {
return results.every((r) => r);
} else {
return results.some((r) => r);
}
};
Rendering Strategy
Component Renderer Pattern
Each field type has a corresponding renderer component:
interface FieldRendererProps {
field: FormField;
value: any;
onChange: (value: any) => void;
errors?: ValidationError[];
disabled?: boolean;
readOnly?: boolean;
context?: RenderContext;
}
type FieldRenderer<T extends FormField = FormField> =
React.FC<FieldRendererProps & T>;
// Registry pattern
const fieldRenderers: Record<FieldType, FieldRenderer> = {
text: TextFieldRenderer,
email: EmailFieldRenderer,
select: SelectFieldRenderer,
date: DateFieldRenderer,
// ... 60+ renderers
};
// Dispatch factory
const renderField = (field: FormField, props: FieldRendererProps) => {
const Renderer = fieldRenderers[field.type];
if (!Renderer) {
return <UnsupportedFieldWarning field={field} />;
}
return <Renderer {...props} field={field} />;
};
Composition Pattern for Containers
Layout fields render their children recursively:
const ContainerRenderer: React.FC<FieldRendererProps> = ({ field, ...props }) => {
if (!field.children) return null;
return (
<Container field={field}>
{field.children.map(child => (
<RenderField
key={child.id}
field={child}
{...props}
/>
))}
</Container>
);
};
State Management
Redux Store Structure
store/
├── slices/
│ ├── form.slice.ts # Form schema state
│ ├── selection.slice.ts # Selected field
│ ├── clipboard.slice.ts # Copy/paste state
│ ├── history.slice.ts # Undo/redo
│ ├── dataSource.slice.ts # Data sources
│ ├── preview.slice.ts # Preview mode
│ └── ui.slice.ts # UI state
└── hooks/
├── useFormState.ts
├── useFieldSelection.ts
└── useUndo history.ts
Form State
interface FormState {
schema: FormSchema;
selectedFieldId: string | null;
editingFieldId: string | null;
hasChanges: boolean;
dataSources: ManagedDataSource[];
variables: FormVariable[];
actions: FormAction[];
mode: PageMode;
viewMode: "design" | "preview";
device: DeviceType;
}
Extensibility
Custom Field Types
Create new field types by implementing the FieldRenderer interface:
// 1. Define field type
export type MyCustomFieldType = {
type: 'my-custom-field';
customProperty: string;
};
// 2. Create renderer
export const MyCustomFieldRenderer: FieldRenderer = ({
field,
value,
onChange,
errors,
...props
}) => {
return (
<div className="my-custom-field">
<input
value={value}
onChange={(e) => onChange(e.target.value)}
data-custom={field.customProperty}
/>
{errors && <ErrorMessage>{errors[0].message}</ErrorMessage>}
</div>
);
};
// 3. Register renderer
fieldRenderers['my-custom-field'] = MyCustomFieldRenderer;
// 4. Add editor component
export const MyCustomFieldEditor: FieldEditor = ({ field, onUpdate }) => {
return (
<>
<TextField
label="Custom Property"
value={field.customProperty}
onChange={(e) => onUpdate({ customProperty: e.target.value })}
/>
</>
);
};
// 5. Add to palette
<FieldPaletteItem
title="My Custom Field"
type="my-custom-field"
category="custom"
icon={<CustomIcon />}
/>
Custom Validation Rules
const customValidators: Record<string, Validator> = {
"credit-card": (value: string) => {
// Luhn algorithm validation
return isValidCreditCard(value);
},
"phone-us": (value: string) => {
return /^\+?1?\d{9,15}$/.test(value);
},
"username-available": async (value: string) => {
const response = await fetch(`/api/users/check?username=${value}`);
return response.ok;
},
};
Custom Data Sources
const customDataSources: Record<string, DataSourceAdapter> = {
graphql: new GraphQLDataSourceAdapter(),
mongodb: new MongoDBDataSourceAdapter(),
elasticsearch: new ElasticsearchAdapter(),
firebase: new FirebaseAdapter(),
};
Performance Considerations
Memoization
// Memoize field renderer
const MemoizedFieldRenderer = React.memo(FieldRenderer, (prev, next) => {
return (
prev.field === next.field &&
prev.value === next.value &&
prev.disabled === next.disabled
);
});
Virtual Scrolling for Large Forms
import { List, AutoSizer } from 'react-virtualized';
const FormRenderer = ({ fields }) => {
return (
<AutoSizer>
{({ height, width }) => (
<List
width={width}
height={height}
rowCount={fields.length}
rowRenderer={({ index }) => renderField(fields[index])}
rowHeight={60}
/>
)}
</AutoSizer>
);
};
Lazy Loading Data Sources
interface DataSource {
lazyLoad?: boolean; // Load only when field becomes visible
requestDelay?: number; // Debounce API calls
cache?: "memory" | "session" | "none"; // Caching strategy
}
Testing
Unit Testing Fields
import { render, screen, fireEvent } from '@testing-library/react';
describe('TextFieldRenderer', () => {
it('renders with label', () => {
render(
<TextFieldRenderer
field={{ id: '1', type: 'text', label: 'Name' }}
value=""
onChange={jest.fn()}
/>
);
expect(screen.getByText('Name')).toBeInTheDocument();
});
it('calls onChange on input change', () => {
const onChange = jest.fn();
render(
<TextFieldRenderer
field={{ id: '1', type: 'text', label: 'Name' }}
value=""
onChange={onChange}
/>
);
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'John' } });
expect(onChange).toHaveBeenCalledWith('John');
});
it('displays validation errors', () => {
render(
<TextFieldRenderer
field={{ id: '1', type: 'text', label: 'Name' }}
value=""
onChange={jest.fn()}
errors={[{ fieldId: '1', message: 'Required' }]}
/>
);
expect(screen.getByText('Required')).toBeInTheDocument();
});
});
Integration Testing
describe('FormRenderer', () => {
it('submits form with valid data', async () => {
const onSubmit = jest.fn();
render(
<FormRenderer
schema={mockSchema}
onSubmit={onSubmit}
/>
);
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' }
});
fireEvent.click(screen.getByText('Submit'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com'
});
});
});
});
Standards & Best Practices
Naming Conventions
-
Field IDs: camelCase, no spaces
- ✅
firstName,orderTotal,shippingAddress - ❌
first_name,First Name,FirstName
- ✅
-
Property Keys: snake_case for database
- ✅
first_name,order_total,user_id - ❌
firstName,orderTotal,userId
- ✅
-
CSS Classes: kebab-case
- ✅
.custom-field,.form-group,.field-error - ❌
.customField,.FormGroup,.fieldError
- ✅
-
Component Names: PascalCase
- ✅
TextFieldRenderer,FormRenderer - ❌
textFieldRenderer,form_renderer
- ✅
Documentation Standards
Every field should have:
- Label and description
- Placeholder for input fields
- Help content for complex fields
- Validation error messages
{
"type": "text",
"label": "Email Address",
"placeholder": "user@example.com",
"description": "Your primary contact email",
"tooltip": "Used for account notifications",
"helpContent": {
"title": "Email Address Help",
"content": "Provide a valid email address..."
},
"validationRules": [
{
"type": "email",
"message": "Please enter a valid email address"
}
]
}
Default Values
Always provide sensible defaults:
{
"defaultValue": null,
"required": false,
"disabled": false,
"hidden": false,
"readOnly": false,
"min": 0,
"max": 100,
"step": 1
}