Atlas Forms
Section Cards Layout
The CardSectionPlayer renders each form section inside its own card panel. This is one of the most common custom player patterns — it gives a visually organised, whitespace-heavy layout that is preferred in admin consoles and multi-section data entry forms.
The Pattern
Instead of rendering controls in a flat list, group them by sectionId, then wrap each group in a card:
// src/players/CardSectionPlayer.tsx
import React, { useMemo } from 'react';
import { FormStateProvider } from '@atlas-forms/state-react';
import { ControlRenderer, useAtlasForm } from '@atlas-forms/player-components-react';
import { useFormVisibility } from '@atlas-forms/player-components-react';
import { FormEngine } from '@atlas-forms/form-engine-js';
import { parseSchema } from '@atlas-forms/schema-js';
import type { FormSchema, FormControl, FormSection } from '@atlas-forms/types-js';
// Group controls by sectionId
function groupBySection(controls: FormControl[], sections: FormSection[]) {
const sectionMap = new Map<string, FormControl[]>();
// Initialise in section order
sections.forEach(s => sectionMap.set(s.id, []));
// Distribute controls
controls.forEach(c => {
const sid = c.sectionId ?? '__root__';
if (!sectionMap.has(sid)) sectionMap.set(sid, []);
sectionMap.get(sid)!.push(c);
});
return sectionMap;
}
const CardSectionPlayerInner: React.FC<{ onSubmit: (v: any) => void }> = ({ onSubmit }) => {
const { engine, values, submitForm, isSubmitting } = useAtlasForm();
const { isVisible } = useFormVisibility();
const schema = engine.getSchema();
const grouped = useMemo(() =>
groupBySection(schema.controls ?? [], schema.sections ?? []),
[schema]
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await submitForm();
onSubmit(values);
};
return (
<form onSubmit={handleSubmit}>
{(schema.sections ?? []).map(section => {
const controls = (grouped.get(section.id) ?? []).filter(c => isVisible(c.id));
if (controls.length === 0) return null;
return (
<div key={section.id} className="card" style={{ marginBottom: 24 }}>
{section.title && (
<div className="card-header">
<h3>{section.title}</h3>
{section.description && <p>{section.description}</p>}
</div>
)}
<div className="card-body">
{controls.map(control => (
<ControlRenderer key={control.id} control={control} />
))}
</div>
</div>
);
})}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
</form>
);
};
export const CardSectionPlayer: React.FC<{
schema: FormSchema | object;
initialValues?: Record<string, any>;
onSubmit: (v: any) => void;
}> = ({ schema: raw, initialValues = {}, onSubmit }) => {
const schema = useMemo(() => parseSchema(raw as object), [raw]);
const engine = useMemo(() => new FormEngine(schema, initialValues), [schema, initialValues]);
return (
<FormStateProvider engine={engine} mode="edit">
<CardSectionPlayerInner onSubmit={onSubmit} />
</FormStateProvider>
);
};
Form Schema for This Pattern
// Each section becomes a card; controls reference section via sectionId
{
"sections": [
{ "id": "personal", "title": "Personal Information", "order": 1 },
{ "id": "contact", "title": "Contact Details", "order": 2 },
{ "id": "documents", "title": "Supporting Documents", "order": 3 }
],
"controls": [
{ "id": "first-name", "type": "text", "sectionId": "personal", "order": 1 },
{ "id": "last-name", "type": "text", "sectionId": "personal", "order": 2 },
{ "id": "email", "type": "email", "sectionId": "contact", "order": 1 },
{ "id": "phone", "type": "text", "sectionId": "contact", "order": 2 },
{ "id": "id-doc", "type": "file-upload", "sectionId": "documents", "order": 1 }
]
}
Performance with useFormVisibility
Call
useFormVisibility() once at the top of the inner component and pass isVisible down, rather than calling the hook inside each card section's render. This prevents redundant context reads and keeps the visibility check fast.