Atlas Forms
Minimal Custom Player
The minimal custom player is the simplest possible implementation: a FormStateProvider wrapping a flat list of ControlRenderer calls and a submit button. This is the starting template for all custom players.
Complete Minimal Player
// src/players/MinimalPlayer.tsx
import React, { useMemo } from 'react';
import { FormStateProvider } from '@atlas-forms/state-react';
import { ControlRenderer,
useAtlasForm } from '@atlas-forms/player-components-react';
import { FormEngine } from '@atlas-forms/form-engine-js';
import { parseSchema } from '@atlas-forms/schema-js';
import type { FormSchema,
FormMode } from '@atlas-forms/types-js';
interface MinimalPlayerProps {
schema: FormSchema | object;
initialValues?: Record<string, any>;
mode?: FormMode;
onSubmit: (values: Record<string, any>) => Promise<void>;
}
// Inner component — must be inside FormStateProvider
const MinimalPlayerInner: React.FC<{ onSubmit: (v: any) => Promise<void> }> = ({ onSubmit }) => {
const { values, errors, isValid, isSubmitting, submitForm, engine } = useAtlasForm();
const schema = engine.getSchema();
const controls = schema.controls ?? [];
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await submitForm();
if (isValid) {
await onSubmit(values);
}
};
return (
<form onSubmit={handleSubmit}>
{controls.map(control => (
<ControlRenderer key={control.id} control={control} />
))}
{Object.keys(errors).length > 0 && (
<div className="error-summary">
{Object.values(errors).map((msg, i) => (
<p key={i} className="error">{msg}</p>
))}
</div>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
};
// Outer component — creates FormEngine and wraps in FormStateProvider
export const MinimalPlayer: React.FC<MinimalPlayerProps> = ({
schema: rawSchema,
initialValues = {},
mode = 'edit',
onSubmit
}) => {
const schema = useMemo(() =>
rawSchema instanceof Object && 'controls' in rawSchema
? rawSchema as FormSchema
: parseSchema(rawSchema as object),
[rawSchema]
);
const engine = useMemo(() =>
new FormEngine(schema, initialValues),
[schema, initialValues]
);
return (
<FormStateProvider engine={engine} mode={mode}>
<MinimalPlayerInner onSubmit={onSubmit} />
</FormStateProvider>
);
};
Usage
import { MinimalPlayer } from './players/MinimalPlayer';
import schema from './my-form.schema.json';
<MinimalPlayer
schema={schema}
initialValues={{ country: 'UK' }}
onSubmit={async (values) => {
console.log('Submitted:', values);
}}
/>
What This Player Does Not Have
The minimal player omits several features of FormRenderer — add them incrementally as your custom player needs grow:
| Feature | How to Add |
|---|---|
| Draft auto-save | Subscribe to engine change event; save via StorageManager |
| Mode-based visibility | Use useFormVisibility() to filter controls before rendering |
| Section grouping | Group controls by control.sectionId before rendering |
| Theming | Wrap in ThemeProvider above FormStateProvider |
| Action bar actions | Render form schema.actions below the field list |
useMemo on FormEngine
Always create the
FormEngine instance inside a useMemo — do not create it directly in the render function. A new engine on every render would reset all form state on each parent re-render.