Portal Community

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

  1. User changes a field value in a control component.
  2. The control component calls setFieldValue(fieldId, newValue) from useAtlasForm().
  3. setFieldValue calls engine.setValue(fieldId, newValue).
  4. FormEngine updates its internal value map and fires the change event.
  5. FormStateProvider receives the event, recalculates computed fields, and triggers a React re-render.
  6. All useAtlasForm() subscribers receive the new values snapshot.
  7. 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);