Skip to main content

Renderers Documentation

Overview (Non-Technical)

The Renderers system is the execution engine that takes visual designs (forms, dashboards, reports, dataviews) and converts them into interactive, fully-functional components. Think of it as a "compiler" that reads configuration JSON and produces React components. This separation of configuration from code enables non-technical users to design complex pages while the system handles all the technical details.

Key Purpose: Transform page configurations into interactive, responsive, properly-validated React components that handle data binding, user interaction, and error states automatically.


Standards

Renderer Architecture

Rendering Pipeline:

Configuration JSON

Parser (Validates schema)

Component Factory (Creates React components)

Data Binding Engine (Connects to data sources)

Validation Engine (Attaches validators)

Styling Engine (Applies theme)

Interactive Component
```text

### Renderer Types

```text
┌─────────────────────────────────────────────────────┐
│ RENDERERS │
├─────────────────────────────────────────────────────┤
│ │
│ FormRenderer DashboardRenderer ReportRenderer
│ └─ Validates └─ Visualizes data └─ Formats
│ └─ Binds data └─ Auto-refreshes └─ Exports
│ └─ Submits └─ Filters └─ Paginates
│ └─ Shows errors └─ Aggregates │
│ └─ Prints
│ │
│ ContentRenderer │ DataViewRenderer
│ └─ Rich text └─ Displays tables
│ └─ Markdown └─ Sorting/Filtering
│ └─ HTML render └─ Bulk actions
│ └─ Parent-child expand

└─────────────────────────────────────────────────────┘
```text

### File Organization

```text
renderers/
├── core/
│ ├── RendererRegistry.tsx # Factory for all renderers
│ ├── ConfigurationValidator.ts # Schema validation
│ ├── DataBindingEngine.ts # Variable substitution
│ └── ThemeApplier.ts # CSS variable application
├── form/
│ ├── FormRenderer.tsx # Main form component
│ ├── FieldRenderer.tsx # Individual field mapper
│ ├── FieldComponents/ # Actual field implementations
│ │ ├── TextInput.tsx
│ │ ├── SelectDropdown.tsx
│ │ ├── DatePicker.tsx
│ │ ├── FileUpload.tsx
│ │ ├── RepeaterField.tsx
│ │ └── ... (20+ field types)
│ ├── ValidationEngine.ts # Form validation logic
│ ├── FormState.ts # Form state management
│ └── hooks/
│ ├── useFormState.ts
│ ├── useFieldValidation.ts
│ └── useFormSubmission.ts
├── dashboard/
│ ├── DashboardRenderer.tsx # Main dashboard component
│ ├── WidgetRenderer.tsx # Widget factory
│ ├── widgets/
│ │ ├── ChartWidget.tsx # Chart visualization
│ │ ├── MetricWidget.tsx # Single metric card
│ │ ├── TableWidget.tsx # Data table
│ │ ├── GaugeWidget.tsx # Gauge visualization
│ │ └── ... (10+ widget types)
│ ├── FilterPanel.tsx # Dashboard filters
│ ├── hooks/
│ │ └── useDashboardData.ts
│ └── utils/
│ └── chartConfig.ts # Chart.js configuration
├── report/
│ ├── ReportRenderer.tsx # Main report component
│ ├── ReportSection.tsx # Section renderer
│ ├── sections/
│ │ ├── HeaderSection.tsx
│ │ ├── TableSection.tsx
│ │ ├── ChartSection.tsx
│ │ └── SummarySection.tsx
│ ├── ReportExporter.ts # PDF/Excel export
│ └── printing.ts # Print styling
├── dataview/
│ ├── DataViewRenderer.tsx # Main dataview component
│ ├── TableRenderer.tsx # Table view
│ ├── CardRenderer.tsx # Card view
│ ├── TimelineRenderer.tsx # Timeline view
│ ├── RowActionsMenu.tsx # Action buttons
│ └── hooks/
│ └── useDataViewData.ts
├── content/
│ ├── ContentRenderer.tsx # Content component
│ ├── ElementRenderers/
│ │ ├── RichTextRenderer.tsx
│ │ ├── MarkdownRenderer.tsx
│ │ ├── ImageRenderer.tsx
│ │ └── ... (elements)
│ └── hooks/
│ └── useContentData.ts
└── types/
├── renderer.types.ts # TypeScript interfaces
├── configuration.types.ts # Config schemas
└── component.types.ts # Component patterns
```text

### Renderer Configuration Pattern

**Base Configuration Structure:**

```typescript
interface RendererConfig {
// Identification
pageId: string;
pageName: string;
pageType: "form" | "dashboard" | "report" | "content" | "dataview";
version: string;

// Layout
layout: {
type: "grid" | "flex" | "absolute";
columns?: number;
spacing?: "xs" | "sm" | "md" | "lg";
};

// Components
components: ComponentConfig[];

// Styling
styling: {
theme?: "light" | "dark";
cssClass?: string;
inlineStyles?: Record<string, string>;
};

// Metadata
metadata: {
createdDate: string;
modifiedDate: string;
version: string;
};
}

interface ComponentConfig {
componentId: string;
componentType: string;
properties: Record<string, any>;
styling?: StyleConfig;
binding?: DataBindingConfig;
validation?: ValidationConfig;
children?: ComponentConfig[];
}
```text

### Naming Conventions

- **Renderer Files:** `[Type]Renderer.tsx` (e.g., `FormRenderer.tsx`, `DashboardRenderer.tsx`)
- **Component Files:** `[ComponentName].tsx` (e.g., `TextInput.tsx`, `DatePicker.tsx`)
- **Hook Files:** `use[HookName].ts` (e.g., `useFormState.ts`, `useDataBinding.ts`)
- **Utility Files:** `[description].ts` (e.g., `chartConfig.ts`, `validators.ts`)
- **Type Files:** `[description].types.ts` (e.g., `renderer.types.ts`, `form.types.ts`)

---

## Do's and Don'ts

### ✅ DO

- **Validate configuration before rendering:** Catch errors early
- **Use TypeScript for type safety:** Strong types prevent bugs
- **Implement error boundaries:** Gracefully handle renderer crashes
- **Use CSS variables for theming:** Enable dark mode switching
- **Memoize expensive components:** Use React.memo for optimization
- **Implement proper loading states:** Show skeleton/spinner
- **Handle missing data gracefully:** Show appropriate empty state
- **Use semantic HTML:** Improves accessibility
- **Test renderers with sample configs:** Verify all features work
- **Log rendering errors with context:** Enable debugging
- **Support keyboard navigation:** Full accessibility support
- **Cache rendered components:** Avoid re-rendering when possible

### ❌ DON'T

- **Tightly couple config to component logic:** Separate concerns
- **Hardcode component types:** Use factory pattern
- **Skip validation on properties:** Always validate config
- **Ignore error states:** Handle failures gracefully
- **Use inline styles everywhere:** Prefer CSS classes + variables
- **Render without error boundaries:** Prevents app crashes
- **Block the UI during rendering:** Use async rendering
- **Create deeply nested JSX:** Keep JSX readable
- **Forget to clean up subscriptions:** Memory leaks
- **Assume data is always valid:** Validate at boundaries
- **Render without loading states:** User confusion
- **Skip accessibility attributes:** ARIA labels, keyboard nav

---

## Technical Details

### Form Rendering Engine

**FormRenderer Component Structure:**

```typescript
export interface FormRendererProps {
pageConfig: PageConfiguration;
initialData?: Record<string, any>;
readonly?: boolean;
onSubmit: (data: any) => Promise<void>;
onCancel?: () => void;
onValidationError?: (errors: ValidationErrors) => void;
}

export function FormRenderer({
pageConfig,
initialData,
readonly,
onSubmit,
onCancel,
onValidationError
}: FormRendererProps) {
// 1. Initialize form state
const [formData, setFormData] = useState(initialData || {});
const [errors, setErrors] = useState<ValidationErrors>({});
const [touched, setTouched] = useState<TouchedFields>({});
const [isSubmitting, setIsSubmitting] = useState(false);

// 2. Setup data binding
const boundConfig = useDataBinding(pageConfig, formData);

// 3. Handle field changes
const handleFieldChange = useCallback((fieldId: string, value: any) => {
setFormData(prev => ({ ...prev, [fieldId]: value }));
setTouched(prev => ({ ...prev, [fieldId]: true }));

// Validate field if touched
if (touched[fieldId]) {
const fieldConfig = findFieldConfig(boundConfig, fieldId);
const error = validateField(value, fieldConfig.validation);
setErrors(prev => ({ ...prev, [fieldId]: error }));
}
}, [touched]);

// 4. Form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

// Validate all fields
const allErrors = validateAllFields(formData, boundConfig);
if (Object.keys(allErrors).length > 0) {
setErrors(allErrors);
onValidationError?.(allErrors);
return;
}

// Transform data
const transformedData = transformFormData(formData, boundConfig);

// Submit
setIsSubmitting(true);
try {
await onSubmit(transformedData);
} finally {
setIsSubmitting(false);
}
};

// 5. Render form
return (
<form onSubmit={handleSubmit} className="form-renderer">
{pageConfig.sections.map(section => (
<FormSection
key={section.sectionId}
section={section}
formData={formData}
errors={errors}
touched={touched}
onFieldChange={handleFieldChange}
readonly={readonly}
/>
))}
<FormActions
onSubmit={handleSubmit}
onCancel={onCancel}
isSubmitting={isSubmitting}
/>
</form>
);
}
```text

**Field Renderer Factory:**

```typescript
export function FieldRenderer({
fieldConfig,
value,
error,
touched,
onChange,
readonly
}: FieldRendererProps) {
// Map fieldType to component
const FieldComponent = fieldComponentMap[fieldConfig.fieldType];

if (!FieldComponent) {
console.error(`Unknown field type: ${fieldConfig.fieldType}`);
return null;
}

return (
<div className={`field-wrapper ${fieldConfig.fieldType}`}>
<label htmlFor={fieldConfig.fieldId}>{fieldConfig.fieldLabel}</label>
<FieldComponent
id={fieldConfig.fieldId}
value={value}
onChange={onChange}
readonly={readonly}
{...fieldConfig.properties}
/>
{touched && error && (
<span className="field-error">{error}</span>
)}
{fieldConfig.help && (
<span className="field-help">{fieldConfig.help}</span>
)}
</div>
);
}

// Component mapping
const fieldComponentMap: Record<string, React.ComponentType<any>> = {
'text': TextInput,
'email': EmailInput,
'date': DatePicker,
'select': SelectDropdown,
'checkbox': CheckboxField,
'radio': RadioGroup,
'textarea': TextArea,
'file': FileUpload,
'repeater': RepeaterField,
'nested': NestedGroup,
'lookup': LookupDropdown,
// ... more field types
};
```text

### Data Binding Engine

**Variable Substitution System:**

```typescript
export function useDataBinding(config: PageConfiguration, context: any) {
return useMemo(() => {
return processDataBindings(config, context);
}, [config, context]);
}

function processDataBindings(config: any, context: any): any {
if (typeof config === "string" && config.includes("${")) {
// Resolve variable reference
return resolveVariable(config, context);
}

if (Array.isArray(config)) {
return config.map((item) => processDataBindings(item, context));
}

if (typeof config === "object" && config !== null) {
const resolved: any = {};
for (const [key, value] of Object.entries(config)) {
resolved[key] = processDataBindings(value, context);
}
return resolved;
}

return config;
}

function resolveVariable(expr: string, context: any): any {
// Match ${variable.path} patterns
const match = expr.match(/\$\{([^}]+)\}/);
if (!match) return expr;

const path = match[1];
const parts = path.split(".");

let value = context;
for (const part of parts) {
if (value == null) return undefined;
value = value[part];
}

return value;
}
```text

### Validation Engine

**Validation Execution Model:**

```typescript
export function validateField(
value: any,
validationRules: ValidationRule[],
): string | null {
if (!validationRules || validationRules.length === 0) {
return null;
}

for (const rule of validationRules) {
const validator = validatorMap[rule.type];
if (!validator) {
console.warn(`Unknown validator: ${rule.type}`);
continue;
}

const error = validator(value, rule.params);
if (error) {
return rule.message || error;
}
}

return null;
}

// Validator implementations
const validatorMap: Record<string, ValidatorFn> = {
required: (value) => (!value ? "This field is required" : null),

minLength: (value, params) =>
value?.length < params.minLength
? `Minimum length: ${params.minLength}`
: null,

maxLength: (value, params) =>
value?.length > params.maxLength
? `Maximum length: ${params.maxLength}`
: null,

pattern: (value, params) =>
!new RegExp(params.pattern).test(value) ? "Invalid format" : null,

email: (value) =>
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? "Invalid email address" : null,

custom: (value, params) => {
// Call custom validator function
const result = params.validator(value);
return result === true ? null : result || "Validation failed";
},
};
```text

### Dashboard Rendering Engine

**DashboardRenderer Structure:**

```typescript
export function DashboardRenderer({
pageConfig,
filters = {},
onFilterChange
}: DashboardRendererProps) {
const [dashboardFilters, setDashboardFilters] = useState(filters);
const [refreshTime, setRefreshTime] = useState(Date.now());

// Auto-refresh logic
useEffect(() => {
const interval = setInterval(
() => setRefreshTime(Date.now()),
pageConfig.dashboardSettings?.refreshInterval || 30000
);
return () => clearInterval(interval);
}, [pageConfig.dashboardSettings?.refreshInterval]);

const handleFilterChange = (newFilters: any) => {
setDashboardFilters(newFilters);
onFilterChange?.(newFilters);
};

return (
<div className="dashboard-renderer">
{pageConfig.dashboardSettings?.showFilters && (
<FilterPanel
filters={pageConfig.filters || []}
values={dashboardFilters}
onChange={handleFilterChange}
/>
)}
<div className="widgets-container">
{pageConfig.widgets.map(widget => (
<WidgetRenderer
key={widget.widgetId}
widget={widget}
filters={dashboardFilters}
refreshTime={refreshTime}
/>
))}
</div>
</div>
);
}
```text

**Widget Renderer Factory:**

```typescript
export function WidgetRenderer({ widget, filters, refreshTime }: any) {
const WidgetComponent = widgetComponentMap[widget.widgetType];

if (!WidgetComponent) {
return <ErrorWidget message={`Unknown widget type: ${widget.widgetType}`} />;
}

return (
<ErrorBoundary>
<div className="widget" style={calculateWidgetLayout(widget)}>
<Suspense fallback={<WidgetSkeleton />}>
<WidgetComponent
widget={widget}
filters={filters}
refreshTime={refreshTime}
/>
</Suspense>
</div>
</ErrorBoundary>
);
}

const widgetComponentMap: Record<string, React.ComponentType<any>> = {
'chart': ChartWidget,
'metric': MetricWidget,
'table': TableWidget,
'gauge': GaugeWidget,
'map': MapWidget,
'timeline': TimelineWidget,
'list': ListWidget,
'text': TextWidget
};
```text

### Report Rendering Engine

**ReportRenderer Structure:**

```typescript
export function ReportRenderer({
pageConfig,
data,
format = 'html'
}: ReportRendererProps) {
const reportRef = useRef<HTMLDivElement>(null);

const handleExport = async (exportFormat: string) => {
if (!reportRef.current) return;

switch (exportFormat) {
case 'pdf':
await exportToPDF(reportRef.current, pageConfig);
break;
case 'excel':
await exportToExcel(data, pageConfig);
break;
case 'html':
await downloadHTML(reportRef.current);
break;
}
};

return (
<div className="report-renderer">
<ReportToolbar
onExport={handleExport}
formats={pageConfig.reportSettings?.exportFormats}
/>
<div ref={reportRef} className="report-content">
{pageConfig.sections.map((section, idx) => (
<ReportSection
key={idx}
section={section}
data={data}
pageConfig={pageConfig}
/>
))}
</div>
</div>
);
}
```text

### DataView Rendering Engine

**DataViewRenderer Structure:**

```typescript
export function DataViewRenderer({
pageConfig,
entityId
}: DataViewRendererProps) {
const [page, setPage] = useState(1);
const [sortField, setSortField] = useState(
pageConfig.sorting?.defaultSortField
);
const [sortOrder, setSortOrder] = useState(
pageConfig.sorting?.defaultSortOrder || 'asc'
);
const [filters, setFilters] = useState({});
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());

// Fetch data with current filters
const { data, isLoading } = useQuery({
queryKey: ['records', entityId, page, sortField, sortOrder, filters],
queryFn: () => fetchRecords(entityId, {
page,
pageSize: pageConfig.displaySettings?.pageSize || 20,
sortField,
sortOrder,
filters
})
});

return (
<div className="dataview-renderer">
{pageConfig.displaySettings?.showFilters && (
<FilterBar
filters={pageConfig.filters || []}
onChange={setFilters}
/>
)}
{selectedRows.size > 0 && (
<BulkActionBar
selectedCount={selectedRows.size}
actions={pageConfig.actions?.filter(a => a.bulkApplicable)}
/>
)}
<ViewTypeComponent
viewType={pageConfig.displaySettings?.viewType || 'table'}
records={data?.records || []}
columns={pageConfig.columns || []}
config={pageConfig}
onSelectRow={(id) => setSelectedRows(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
})}
/>
<Pagination
current={page}
total={data?.total}
onChange={setPage}
/>
</div>
);
}
```text

### Content Rendering Engine

**ContentRenderer Structure:**

```typescript
export function ContentRenderer({
pageConfig
}: ContentRendererProps) {
return (
<div className="content-renderer">
{pageConfig.elements.map((element, idx) => (
<ContentElementRenderer
key={idx}
element={element}
index={idx}
/>
))}
</div>
);
}

function ContentElementRenderer({ element }: any) {
const ElementComponent = contentElementMap[element.type];

if (!ElementComponent) {
console.warn(`Unknown element type: ${element.type}`);
return null;
}

return (
<div className={`content-element ${element.type}`}>
<ElementComponent element={element} />
</div>
);
}

const contentElementMap: Record<string, React.ComponentType<any>> = {
'heading': HeadingElement,
'paragraph': ParagraphElement,
'image': ImageElement,
'richtext': RichTextElement,
'markdown': MarkdownElement,
'video': VideoElement,
'divider': DividerElement,
'blockquote': BlockquoteElement,
'list': ListElement
};
```text

### Error Handling & Recovery

**Error Boundary Pattern:**

```typescript
export class RendererErrorBoundary extends React.Component {
state = { hasError: false, error: null };

static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: any) {
logger.error('renderer.error', {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}

render() {
if (this.state.hasError) {
return (
<ErrorFallback
error={this.state.error}
onRetry={() => this.setState({ hasError: false })}
/>
);
}

return this.props.children;
}
}
```text

---

## Key Files & Imports

| Purpose | File | Notes |
| ----------------------- | ----------------------------------------- | ------------------- |
| Form renderer | `components/viewer/FormRenderer.tsx` | Main form component |
| Dashboard renderer | `components/viewer/DashboardRenderer.tsx` | Dashboard component |
| DataView renderer | `components/viewer/DataViewRenderer.tsx` | Table/card/timeline |
| Report renderer | `components/viewer/ReportRenderer.tsx` | Report component |
| Content renderer | `components/viewer/ContentRenderer.tsx` | Content/markdown |
| Configuration validator | `lib/componentValidator.ts` | Schema validation |
| Data binding | `lib/dataBinding.ts` | Variable resolution |
| Validation engine | `lib/validators.ts` | Field validators |

---

## Common Issues & Solutions

| Issue | Cause | Solution |
| ------------------------- | --------------------------------- | ------------------------------------------------ |
| Form not rendering | Invalid config schema | Check config against RendererConfig interface |
| Field not appearing | Field config not in sections | Add field to configuration |
| Validation not triggering | Validation rules missing or wrong | Check validation rule types |
| Data binding not working | Wrong variable syntax | Verify ${variable.path} syntax |
| Styling not applied | CSS variable undefined | Check ThemeContext, ensure variable declared |
| Component crashes | Missing error boundary | Wrap in `<RendererErrorBoundary>` |
| Performance issues | Re-rendering components | Use React.memo, useMemo on expensive ops |
| Filters not working | Filter schema mismatch | Verify filter field exists in entity |
| Export fails | Missing data | Check data structure matches export expectations |

---

## Performance Optimization Tips

1. **Memoize Field Components:** Prevent re-renders

```typescript
const TextInput = React.memo(({ value, onChange }) => (...));
  1. Use Suspense for Lazy Components:

    const TableWidget = lazy(() => import('./TableWidget'));
    <Suspense fallback={<Loader />}>
    <TableWidget />
    </Suspense>
  2. Batch State Updates: Reduce re-renders

    const updateMultiple = useCallback((updates) => {
    setFormData((prev) => ({ ...prev, ...updates }));
    }, []);
  3. Implement Virtual Scrolling: For large lists

    import { FixedSizeList } from "react-window";
  4. Cache Query Results: Use React Query staleTime

    const { data } = useQuery({
    queryKey: [...],
    queryFn: fetchFn,
    staleTime: 5 * 60 * 1000 // 5 minutes
    });