Record inline edit
A record's Details tab, edited in place with a pencil, Cancel and Save. The app does the saving.
Installation
The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, icon, record-detail, record-form.
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- A record page's Details tab where people change fields in place: Lightning's inline edit, a pencil on each field, then Cancel and Save at the bottom.
- The fields, which are editable and the picklist values come from the object's metadata and page layout, and the app saves through
updateRecordor the REST API. - What
lightning-record-formdraws in view mode with its edit pencils.
Don't use it
- The tab is read-only, or a pencil opens a form elsewhere. Use Record detail.
- People create a record, or edit it in a modal. Use Record form.
- Many records side by side. Use Data table.
Usage
import {
RecordInlineEdit,
RecordInlineEditCancel,
RecordInlineEditField,
RecordInlineEditFooter,
RecordInlineEditMessages,
RecordInlineEditSection,
} from "@/components/wisty/record-inline-edit"
import { RecordFormSubmit } from "@/components/wisty/record-form"RecordInlineEdit
├── RecordInlineEditMessages
├── RecordInlineEditSection
│ └── RecordInlineEditField
└── RecordInlineEditFooter
├── RecordInlineEditCancel
└── RecordFormSubmitfields describes every field, keyed by API name: Record form's description, which extends Record detail's, with editable: false for a field people can't change and href for a lookup's link. values is the record. The same two render a read-only Record detail. Each RecordInlineEditField draws one field by name. Saving is the app's, so control mode: onSubmit gets the values, and the app passes pending and a failed save's errors back in, or the new values with mode set to "view". defaultMode only sets the mode it starts in.
const [mode, setMode] = useState<RecordInlineEditMode>("view")
const [values, setValues] = useState(record)
const [errors, setErrors] = useState<RecordFormErrors>()
const [pending, startSaving] = useTransition()
<RecordInlineEdit
fields={fields}
values={values}
mode={mode}
onModeChange={setMode}
pending={pending}
errors={errors}
onSubmit={(_, { changed }) =>
startSaving(async () => {
const result = await updateOpportunity(changed)
setErrors(result.errors)
if (result.errors) return
setValues(result.record)
setMode("view")
})
}
>
…
</RecordInlineEdit>Send changed, the values that differ from values, to updateRecord. When it's empty, set the mode back to "view" without a request. To lay out a page layout loaded at runtime, map its sections to RecordInlineEditSection and their field names to RecordInlineEditField, as Record form's Usage does.
RecordInlineEditFooter draws Cancel and Save on its own. To order the buttons or add one of the page's, pass them as children: RecordInlineEditCancel returns to view mode, and Save is Record form's RecordFormSubmit:
<RecordInlineEditFooter>
<RecordInlineEditCancel />
<Button type="button" variant="outline" onClick={openFieldHistory}>
Field History
</Button>
<RecordFormSubmit>Save</RecordFormSubmit>
</RecordInlineEditFooter>A field whose control Record form doesn't draw, like a multi-select picklist, takes a custom control as RecordInlineEditField's child, written with useRecordFormControl as in Record form. Edit mode draws it; view mode shows the value by the field's type.
The footer sticks to the bottom of its scroll container. For Lightning's footer, fixed to the bottom of the window, set it with className:
<RecordInlineEditFooter className="fixed inset-x-0 bottom-0 z-50" />Examples
Editing
With `defaultMode="edit"`: Owner and Expected Revenue stay read-only with a lock, and a field you change turns yellow with an undo button.
After a failed save
The app's `errors`: the account lookup's filter error on its field, and a validation rule's message for the record above the sections.
Saving
With `pending`, Save shows a spinner and is disabled, so it can't save twice, while the app saves the changed values.
Compact density
With `layout="horizontal"` and `columns={1}`, labels sit beside values, as in Lightning's compact density, shown in edit mode with its locks.
Read-only record
With `readOnly`, for a record people can see but not edit: the same fields, and no pencils.
Loading
With `loading` and no values yet, the layout's labels hold their places and every value is a placeholder.
API reference
| Part | Built on | Description |
|---|---|---|
RecordInlineEdit | <div> (opens in a new tab) | |
RecordInlineEditMessages | RecordFormMessages | The errors for the whole record, above the sections in edit mode. Renders nothing without any. |
RecordInlineEditSection | RecordDetailSection | A collapsible section of the page layout, drawn the same in both modes. |
RecordInlineEditField | <div> (opens in a new tab) | One field by API name: its value with a pencil in view mode, the form field or a locked value in edit mode. |
RecordInlineEditFooter | <div> (opens in a new tab) | The buttons docked at the bottom of the block in edit mode: Cancel and Save, or its children. |
RecordInlineEditCancel | Button | Cancel: back to view mode, dropping the edits. Disabled while the save runs. |
RecordInlineEdit
| Prop | Type | Default | Description |
|---|---|---|---|
fields* | RecordInlineEditFields | Every field the layout can show, keyed by API name. | |
values | RecordInlineEditValues | {} | The record's values. After a save, pass the new values with mode "view". |
mode | RecordInlineEditMode | View or edit, controlled. A save needs it controlled, so the app can return to view. | |
defaultMode | RecordInlineEditMode | "view" | The mode it starts in, uncontrolled. Default "view". |
onModeChange | (mode: RecordInlineEditMode) => void | Called when a pencil asks for edit mode, or Cancel for view mode. | |
pending | boolean | false | The save is running: Save shows a spinner and is disabled. |
errors | RecordFormErrors | A failed save's errors. Pass a new object for each save, from state. | |
onSubmit | ( values: RecordFormValues, details: RecordInlineEditSubmitDetails ) => void | Called with valid values when Save is pressed. | |
readOnly | boolean | false | The user can't edit the record: no pencils, and view mode only. |
columns | 1 | 2 | 2 | Fields per row. Default 2. Two columns fall to one in a narrow container. |
layout | "auto" | "stacked" | "horizontal" | "auto" | Label above the value, or beside it from a 48rem screen. Default "auto": beside it when the page is compact (data-density="compact"), above it otherwise. |
headingLevel | 2 | 3 | 4 | 2 | The level of each section's heading. Default 2, under a record page's h1. |
loading | boolean | false | Holds every value's place while the record loads, with no pencils. Without children, draws placeholder fields. |
locale | string | "en-US" | A BCP 47 locale for numbers and dates. Default "en-US". |
currency | string | "USD" | An ISO 4217 code for currency fields. Default "USD". |
timeZone | string | "UTC" | An IANA time zone whose clock datetime fields show and are edited in. Default "UTC". |
RecordInlineEditSection
A collapsible section of the page layout, drawn the same in both modes.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | A stable key for its open state, like the layout section's id. Without it, the title keys it. | |
defaultOpen | boolean | true | Whether the section starts open, uncontrolled. Default true. |
RecordInlineEditField
One field by API name: its value with a pencil in view mode, the form field or a locked value in edit mode.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | A custom control for edit mode that calls useRecordFormControl; omit it for the type's own. | |
name* | string | The field's API name, a key of the root's fields. |
RecordInlineEditFooter
The buttons docked at the bottom of the block in edit mode: Cancel and Save, or its children.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | Buttons in place of Cancel and Save; leave it out for the defaults. | |
cancelLabel | string | "Cancel" | The Cancel button's text, without children. Default "Cancel". |
saveLabel | string | "Save" | The Save button's text, without children. Default "Save". |
RecordInlineEditCancel
Cancel: back to view mode, dropping the edits. Disabled while the save runs.
| Prop | Type | Default |
|---|---|---|
children | ReactNode | "Cancel" |
variant | "default" | "outline" | "secondary" | "ghost" | "destructive" | "link" | "outline-brand" | "destructive-text" | "success" | "inverse" | "outline" |
RecordInlineEditFieldDescriptiontype
How a field is drawn, read and edited: Record form's description, or a relative time, which is only shown.
type RecordInlineEditFieldDescription =
| RecordFormFieldDescription
| Extract<RecordDetailFieldDescription, { type: "relative" }>RecordInlineEditFieldstype
The record's fields, keyed by API name.
type RecordInlineEditFields = Record<string, RecordInlineEditFieldDescription>RecordInlineEditValuestype
The record's values, keyed by API name: a lookup's is its record, a compound field's its parts, an instant ISO 8601.
type RecordInlineEditValues = RecordDetailValuesRecordInlineEditModetype
type RecordInlineEditMode = "view" | "edit"RecordInlineEditSubmitDetailstype
| Member | Type | Default | Description |
|---|---|---|---|
changed* | RecordFormValues | The values that differ from values, as updateRecord takes them. |
SLDS mapping
RecordInlineEditSectionis Record detail's section, an expandableslds-sectionover the record detail form layout (slds-form,slds-form__row,slds-form__item), drawn the same in both modes.- In view mode,
RecordInlineEditFieldis Record detail's field:slds-form-element_readonlywithslds-form-element_editand its pencil. In edit mode it's Record form's field, and a field people can't edit stays read-only withutility:lockwhere the pencil was. - A changed field is
slds-is-edited, withslds-form-element__undo'sutility:undobutton. RecordInlineEditMessagesis Record form's error Alert.RecordInlineEditFooterisslds-docked-form-footer, sticky at the bottom of its scroll container rather than fixed to the window.
Known gaps:
- No multi-select picklist or rich text controls in edit mode, as in Record form. Draw them with a custom control. A relative time ("3 hours ago") stays read-only.
- No error popover next to the footer's buttons; the record's errors show above the sections.
- A pencil opens every editable field, as Lightning's record detail does. Editing one field alone, as a list view's cell does, isn't drawn.
Keyboard interactions
| Key | Action |
|---|---|
| Enter on a pencil | Switches to edit mode and focuses that field's control. |
| Enter in a field | Saves, as Save does. In a lookup or picklist with its list open, Enter picks the option. |
Accessibility
- Each section's title is a heading with its toggle inside:
h2by default, for a record page whose header is theh1. Under another heading, setheadingLevelone level below it. - A pencil is named after its field, like "Edit Amount", an undo button "Undo Amount", and a lock "Locked Opportunity Owner".
- Cancel, or a save the app finishes by setting the mode to
"view", returns focus to the pencil that started the edit, or to its section's toggle when that section was collapsed meanwhile. - While
pending, Save and Cancel are disabled, so the save's answer can't land on a newer edit. Fields stay editable. - A save that fails moves focus to the first field with an error, or to the record's errors when none belongs to a field on the page, and opens every collapsed section until the edit ends.
Responsive behavior
- Two columns need about 584px. In a narrower container the fields stack in one column, in the same order.
- Labels follow Salesforce's display density by default (
layout="auto"): above the values, and beside them when the page is compact (data-density="compact"on<html>).layout="stacked"or"horizontal"fixes it for one block. - Beside means from a 48rem screen, whatever the block's own width; below that they stack.
Rules
- Control
modefor a save: the block never leaves edit mode on its own, and never reads success frompendinggoing false. - Pass
errorsfrom state, a new object for each failed save. Errors left over from an earlier edit don't show when a new one starts. - Pass
nullfor an empty value, never""or 0. A percent is a fraction: 0.6 for 60%. A lookup's value is its record ({ id, name }). - A
relativefield must beeditable: false. Adatetimeis an instant, shown and edited ontimeZone's clock, so pass the user's time zone; atimeis Salesforce's"HH:mm:ss.SSSZ", which no time zone moves. - Give sections with the same title a
value, or they share their open state. - Keep the footer inside
RecordInlineEdit: Save is a submit button. It and its children show only in edit mode. With children, keep aRecordFormSubmitand aRecordInlineEditCancelamong them, or people can't save or leave edit mode. - Never pass
asChildorrender.