Escaping the Metadata: Advanced Frontend Overrides
Framework M is designed with a "No-Cliff" philosophy. While 90% of your application can be driven by high-level metadata decorator definitions, there are mission-critical scenarios or complex UX workflows where standard forms and lists are insufficient.
This guide explains how to take full control of a DocType's interface using file-based overrides. For general discovery details and structure, see Frontend DX.
1. Naming Conventions
Override files must use the exact PascalCase name of your DocType.
- DocType:
UserProfile-> File:src/overrides/UserProfileForm.tsx - DocType:
SalesInvoice-> File:src/overrides/SalesInvoiceList.tsx
2. The frm Controller
The most powerful tool for custom overrides is the frm object. When your override is mounted, it receives a standardized controller that provides reactive access to the document and system actions.
Basic Usage
import { StandardFormViewProps } from "../pages/FormView";
export default function MyCustomForm({ frm, meta }: StandardFormViewProps) {
return (
<div>
<h1>Customizing {meta.name}</h1>
<p>Current Status: {frm.doc.docstatus}</p>
<button onClick={() => frm.save()} disabled={frm.isSaving}>
{frm.isSaving ? "Saving..." : "Flash Save"}
</button>
</div>
);
}
Frm API Reference
| Property | Type | Description |
|---|---|---|
doc | any | The current reactive document state (draft). |
setDoc | `(data | fn)` |
save | () => Promise | Triggers the useCreate or useUpdate mutation. Redirection is handled automatically. |
cancel | () => void | Silently discards changes and routes back to the list view. |
refetch | () => Promise | Forcibly refreshes data from the server (e.g., after a workflow action). |
isEditing | boolean | true if we are editing an existing record, false for "New". |
isSaving | boolean | Reactive flag for mutation "in-flight" status. |
isLocked | boolean | true if the document is Submitted or Cancelled. |
errors | string[] | Reactive list of validation or system errors. |
workflow | any | The current workflow state (if a workflow is defined for this DocType). |
3. Pattern: The "Progressive Enrichment" Override
You don't always have to build from scratch. You can wrap the StandardFormView to add sidebars, headers, or custom banners while keeping the central AutoForm alive.
HOC Pattern
import { StandardFormView } from "../pages/FormView";
export default function EnrichmentOverride(props: StandardFormViewProps) {
const { frm } = props;
return (
<div style={{ padding: 20, border: '5px solid gold' }}>
<header>
<h2>Advanced Editor Layer</h2>
<p>This layout wraps the standard form.</p>
</header>
{/* Mounting the standard view inside your custom layout */}
<StandardFormView {...props} />
<footer>
<button onClick={() => frm.save()}>Custom Save Button</button>
</footer>
</div>
);
}
4. Pattern: The "Total Control" Override
For specialized views (like an Order Entry screen with a custom POS-style grid), you can ignore the standard views entirely and build a pure React interface using @framework-m/ui components.
import { XStack, YStack, Button, Input } from "@framework-m/ui";
export default function OrderEntryForm({ frm }: StandardFormViewProps) {
const calculateTotal = () => {
return frm.doc.items?.reduce((sum, item) => sum + item.amount, 0) || 0;
};
return (
<YStack gap="$md">
<Input
value={frm.doc.customer}
onChangeText={(val) => frm.setDoc({ customer: val })}
/>
<Display value={calculateTotal()} />
<Button onPress={() => frm.save()}>Complete Order</Button>
</YStack>
);
}
5. When to Escape?
- Complex Inter-field Logic: When changing Field A needs to trigger a remote recalculation of a hidden Sub-table.
- Custom Visuals: When you need a Canvas, a Map, or a specialized Chart integrated into the form.
- Read Models: When the data should be fetched from a high-performance Read Model SQL view rather than the default CRUD API.
- Mission Critical UX: When the standard vertical stacking of
AutoFormdoesn't provide the speed of entry required by the user.