Skip to main content

Edit Layout Widget

An Edit Layout widget appears as a section or tab on a record's edit page. It is similar to the View Layout widget, but it is expected to be interactive — the user comes here to make changes.


pageContext shape

Source: EditWidgetRenderer.jsx

{
// Record identity
instanceType,
instanceId,
instanceDetails, // Full record object

// Attribute data
attributeValues, // { [attrName]: attrObj } — saved values
editedValues, // { [attrName]: attrObj } — in-progress edits
getAttributeWorkingValue, // function(attrName) → edited if pending, else saved

// Edit integration
onAttributeEdit, // function(attrName, attrObj, errorMsg) — sync to form
saveInstance, // function() — trigger save
saveOperationState, // { isLoading, isError, data }
reloadInstanceDetails, // function() — reload the record

// Layout config
editMode, // "Inline" | "Full"
layoutConfig,
instanceEditDispatch, // Low-level reducer dispatch (advanced)

// Permissions
canConnect,
allowedOperations, // Array of Enums.AllowedOperations strings

// Lifecycle events
onLinkSuccess,
onPromoteSuccess,
onPromoteError,
isPromoteInProgress,

// Common (always present)
settings,
userData,
}

Note: Edit Layout does not include createInstance (use Create Layout for that).


How editing works

Call onAttributeEdit any time the user changes a value in your widget. This keeps your widget in sync with the rest of the form. When the user clicks the layout's Save button, or you call saveInstance(), all changes (including yours) are saved together.

pageContext.onAttributeEdit(
'AttributeName',
{
name: 'AttributeName',
type: 'Text',
canUpdate: true,
textValue: 'new value',
},
'' // error message, or '' if none
);

Example 1 — A simple text field that syncs with the form

The widget shows a plain text input. When the user types, it calls onAttributeEdit so the value is included in the save.

function DescriptionEditorWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Box, Label, TextBox } = protrakComponents;

const saved = pageContext.attributeValues['Description']?.textValue ?? '';
const [value, setValue] = React.useState(saved);

const handleChange = (newValue) => {
setValue(newValue);
pageContext.onAttributeEdit(
'Description',
{
name: 'Description',
type: 'Text',
canUpdate: true,
textValue: newValue,
},
''
);
};

return (
<Box direction="column" style={{ padding: '1rem' }}>
<Label>Description</Label>
<TextBox value={value} onEdit={handleChange} />
</Box>
);
}

Example 2 — Add items to a JSON list attribute

The widget manages a list stored as JSON in a single text attribute. Each time the user adds an item, it calls onAttributeEdit with the updated JSON.

function TaskListWidget(pageContext) {
const { protrakComponents } = React.useContext(customWidgetContext);
const { Box, Button, ButtonEnums, TextBox, Label } = protrakComponents;

const saved = JSON.parse(
pageContext.attributeValues['TaskList']?.textValue || '[]'
);
const [tasks, setTasks] = React.useState(saved);
const [newTask, setNewTask] = React.useState('');

const addTask = () => {
const updated = [...tasks, { name: newTask }];
setTasks(updated);
setNewTask('');
pageContext.onAttributeEdit(
'TaskList',
{
name: 'TaskList',
type: 'Text',
canUpdate: true,
textValue: JSON.stringify(updated),
},
''
);
};

return (
<Box direction="column" style={{ padding: '1rem', gap: '0.5rem' }}>
{tasks.map((t, i) => (
<div key={i}>{t.name}</div>
))}
<TextBox value={newTask} onEdit={setNewTask} />
<Button
text="Add Task"
onClick={addTask}
disabled={!newTask}
appearance={ButtonEnums.Appearance.Primary}
/>
</Box>
);
}

Preview:

target_type_edit_layout.png


Example 3 — Dropdown populated from the type definition

Fetch picklist options from the type definition and render a dropdown.

function ComponentSelectorWidget(pageContext) {
const { protrakUtils, protrakComponents } =
React.useContext(customWidgetContext);
const { useProtrakApi } = protrakUtils;
const { Box, Dropdown, Label, Spinner } = protrakComponents;

const [selected, setSelected] = React.useState('');

const getTypeOptions = React.useCallback(
() => ({
endpoint: 'types/Task',
config: { method: 'GET' },
}),
[]
);

const { state } = useProtrakApi({ requestConfig: getTypeOptions });

if (state.isLoading) return <Spinner />;

const options =
state.data?.attributes.find((a) => a.attributeName === 'Component')
?.options ?? [];

const picklistOptions = options.map((o) => ({
label: o.displayName,
value: o.name,
}));

const handleChange = (val) => {
setSelected(val);
pageContext.onAttributeEdit(
'Component',
{
name: 'Component',
type: 'Picklist',
canUpdate: true,
textValue: val?.value,
},
''
);
};

return (
<Box direction="column" style={{ padding: '1rem' }}>
<Label>Component</Label>
<Dropdown
defaultValues={selected}
picklistOptions={picklistOptions}
onValueChange={handleChange}
/>
</Box>
);
}

Tips

  • Always pass the correct value field for the attribute type (e.g. textValue for Text, numericValue for Numeric).
  • Do not call saveInstance() on every keystroke — let the user control when to save, or save after a deliberate action.
  • If the save button shows a loading spinner (saveOperationState.isLoading), disable your Save button too.
  • For deep-dive patterns, see: