Atlas Forms
Player Architecture
Understanding the relationship between FormStateProvider, FormEngine, and ControlRenderer is essential before building a custom player. These three components form the indivisible core that every custom player — including the default FormRenderer — is built from.
Component Hierarchy
// The invariant hierarchy that every custom player must follow:
//
// <ThemeProvider> — optional: applies theme tokens
// <FormStateProvider> — REQUIRED: owns FormEngine, provides context to hooks
// <YourCustomLayout> — your layout code
// <ControlRenderer /> — renders individual controls
// <ControlRenderer />
// <ControlRenderer />
// </FormStateProvider>
// </ThemeProvider>
FormStateProvider
// packages/state-react/src/FormStateProvider.tsx
interface FormStateProviderProps {
engine: FormEngine; // A configured FormEngine instance
mode?: FormMode; // Operating mode (default: "edit")
children: React.ReactNode;
}
// FormStateProvider creates the React context that all hooks read from.
// It subscribes to FormEngine change events and re-renders when state changes.
// It MUST be the ancestor of all ControlRenderer instances and hook calls.
FormEngine
// packages/form-engine-js/src/FormEngine.ts
class FormEngine {
constructor(schema: FormSchema, initialValues?: Record<string, any>) {}
// Value management
getValue(fieldId: string): any;
getValues(): Record<string, any>;
setValue(fieldId: string, value: any): void;
setValues(values: Record<string, any>): void;
// Validation
validate(): Promise<Record<string, string>>;
validateField(fieldId: string): Promise<string | null>;
// Events
on(event: 'change' | 'submit' | 'error', handler: Function): void;
off(event: string, handler: Function): void;
// Schema access
getSchema(): FormSchema;
getControl(fieldId: string): FormControl | undefined;
getSection(sectionId: string): FormSection | undefined;
}
ControlRenderer
// packages/player-components-react/src/controls/ControlRenderer.tsx
interface ControlRendererProps {
control: FormControl; // The control definition from the schema
// All other context (values, setFieldValue, mode) comes from FormStateProvider
}
// Usage:
<ControlRenderer control={schema.controls[0]} />
Data Flow
- User changes a field value in a control component.
- The control component calls
setFieldValue(fieldId, newValue)fromuseAtlasForm(). setFieldValuecallsengine.setValue(fieldId, newValue).- FormEngine updates its internal value map and fires the
changeevent. FormStateProviderreceives the event, recalculates computed fields, and triggers a React re-render.- All
useAtlasForm()subscribers receive the newvaluessnapshot. - Controls that depend on the changed value re-render with updated data.
parseSchema
Always normalise the raw JSON schema before passing it to FormEngine:
import { parseSchema } from '@atlas-forms/schema-js';
import { FormEngine } from '@atlas-forms/form-engine-js';
const schema = parseSchema(rawSchemaJson);
// parseSchema: validates structure, fills in defaults, normalises control order
// Throws on invalid schema — catches problems before rendering
const engine = new FormEngine(schema, initialValues);