Common Patterns
This guide shows real-world patterns for building complete CRUD pages using @xond/ui components together. These patterns are extracted from production applications like icbs-ui and cmt-ui.
Complete CRUD Page Pattern
The most common pattern is a full CRUD page with Table, RemoteDataSource, Toolbar, Form, and ContextMenu.
Basic Structure
import {
Form,
PageContainer,
Paging,
Spinning,
Table,
Toolbar,
createColumnModel,
createFormModel,
createViewModel,
useRemoteDataSource,
} from "@xond/ui";
import { PersonModel } from "@your-app/generated/models";
function UsersPage() {
// 1. Get fields from generated model
const fields = PersonModel.fields;
// 2. Customize fields (show/hide, reorder, modify)
// ... customization code ...
// 3. Create models for Table, Form, View
const columnModel = createColumnModel(fields);
const formModel = createFormModel(fields);
const viewModel = createViewModel(fields);
// 4. Create data source
const dataSource = useRemoteDataSource({
resourceName: "users",
entityName: "user",
endpoint: "/api/users",
pageSize: 20,
});
// 5. Render page
return (
<PageContainer title="Users">
<Table
columnModel={columnModel}
dataSource={dataSource}
header={<Toolbar dataSource={dataSource} />}
footer={<Paging dataSource={dataSource} />}
form={<Form formModel={formModel} viewModel={viewModel} dataSource={dataSource} />}
contextMenu={(rec) => <UserContextMenu rec={rec} dataSource={dataSource} />}
/>
</PageContainer>
);
}
Field Customization
Before creating models, customize fields using utility functions.
Showing/Hiding Fields
import { show } from "@xond/ui";
const fields = PersonModel.fields;
// Show specific columns in table
show("columns", fields, ["name", "email", "phone"]);
// Show specific fields in form
show("fields", fields, ["name", "email", "phone", "address"]);
// Show specific attributes in view
show("attributes", fields, ["name", "email", "photoUrl"]);
// With custom widths
show("fields", fields, ["name", "email"], ["wide", "narrow"]);
Updating Field Properties
import { updateFieldProperties } from "@xond/ui";
// Update single field
updateFieldProperties(fields, "name", {
header: "Full Name", // Custom header
label: "Full Name", // Custom label
fieldWidth: "wide", // Field width
isColumnVisible: true,
isFieldVisible: true,
});
// Update multiple fields
updateFieldProperties(fields, ["email", "phone"], {
fieldWidth: "narrow",
isRequired: true,
});
// Add custom renderer for table column
updateFieldProperties(fields, "name", {
imageRenderer: (value, record) => (
<ImageIcon
src={record.photoUrl}
fallbackSrc={emptyPerson}
size="sm"
shape="circle"
/>
),
});
// Add custom renderer for table cell
updateFieldProperties(fields, "email", {
renderer: (value, record) => (
<div className="flex items-center gap-1">
<EnvelopeIcon className="h-4 w-4" />
<span>{value}</span>
<Copy text={value} />
</div>
),
});
// Add custom view renderer
updateFieldProperties(fields, "photoUrl", {
viewRenderer: (field, value, record) => (
<ImageIcon
src={record.photoUrl || emptyPerson}
fallbackSrc={emptyPerson}
size="xl"
shape="circle"
/>
),
});
Reordering Fields
import { moveField } from "@xond/ui";
// Move field above another
moveField(fields, "email", "above", "name");
// Move field below another
moveField(fields, "phone", "below", "email");
Changing Field Types
// Change field type for form
updateFieldProperties(fields, ["idPhoto", "facePhoto"], {
fieldType: "ImageFileInput",
fieldWidth: "narrow",
});
Custom Toolbar
Create custom toolbars with additional filters and actions.
Basic Custom Toolbar
import { Toolbar, SearchSelect, DateInput, dataSearchItem } from "@xond/ui";
function CustomToolbar({ dataSource }) {
const [rigId, setRigId] = useState(null);
return (
<Toolbar
dataSource={dataSource}
leftItemList={[
dataSearchItem(dataSource), // Built-in search
{
id: "rigFilter",
component: (
<SearchSelect
name="rigId"
entity="rig"
pk="rigId"
displayField="name"
placeholder="Choose Rig..."
onChange={(e) => {
const rigRecord = e?.target?.value;
setRigId(rigRecord?.rigId ?? rigRecord ?? "");
}}
/>
),
},
{
id: "wellFilter",
component: (
<SearchSelect
name="wellId"
entity="well"
pk="wellId"
displayField="name"
filter={rigId ? [{ field: "rigId", value: rigId, comparison: "equals" }] : []}
onChange={(e) => {
const wellRecord = e?.target?.value;
const wellId = wellRecord?.wellId ?? wellRecord ?? "";
dataSource.setFilterConfig([
...dataSource.filterConfig.filter((f) => f.field !== "wellId"),
...(wellId ? [{ field: "wellId", value: wellId, comparison: "equals" }] : []),
]);
}}
/>
),
},
]}
/>
);
}
Using Custom Toolbar
<Table
columnModel={columnModel}
dataSource={dataSource}
header={<CustomToolbar dataSource={dataSource} />}
// ... other props
/>
Custom Forms
Create custom forms when you need special logic or custom inputs.
Custom Form Example
import { Button, HiddenInput, PhoneInput, useNotification } from "@xond/ui";
import { useForm } from "react-hook-form";
import { useEffect } from "react";
function CustomPersonnelForm({ formModel, viewModel, dataSource, onClose }) {
const { showNotification } = useNotification();
const { register, handleSubmit, setValue, reset, formState: { errors } } = useForm({
defaultValues: dataSource.formData || {},
});
// Sync form data when dataSource changes
useEffect(() => {
if (dataSource.formData) {
reset(dataSource.formData);
}
}, [dataSource.formData, reset]);
// Handle save state
useEffect(() => {
if (dataSource.isSaving) {
showNotification({
title: "Saving...",
statusIcon: "info",
timeout: 5000,
});
}
}, [dataSource.isSaving]);
// Handle success
useEffect(() => {
if (dataSource.isSuccess) {
showNotification({
title: "Success",
statusIcon: "success",
description: "Data saved successfully.",
timeout: 2000,
});
if (onClose) onClose();
}
}, [dataSource.isSuccess]);
// Handle errors
useEffect(() => {
if (dataSource.isError) {
showNotification({
title: "Error",
statusIcon: "error",
description: dataSource.errorMessage,
timeout: 2000,
});
}
}, [dataSource.isError]);
// Show view mode
if (dataSource.formMode === "view" && viewModel) {
return <View viewModel={viewModel} record={dataSource.formData || {}} />;
}
const onSubmit = async (data) => {
dataSource.startSave(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Custom form fields */}
<PeoplePickerInput
name="person"
label="Personnel"
onPersonChange={(person) => {
setValue("personId", person.id);
setValue("email", person.mail || "");
setValue("name", person.displayName);
}}
/>
<HiddenInput {...register("personId")} />
<HiddenInput {...register("email")} />
<HiddenInput {...register("name")} />
<PhoneInput {...register("phone")} label="Phone" />
<div className="mt-4 flex justify-end">
<Button type="submit" variant="primary">Save</Button>
</div>
</form>
);
}
Using Custom Form
<Table
columnModel={columnModel}
dataSource={dataSource}
form={<CustomPersonnelForm formModel={formModel} viewModel={viewModel} dataSource={dataSource} />}
// ... other props
/>
Context Menus
Context menus provide row-level actions.
Basic Context Menu
import { ContextMenu, useNotification } from "@xond/ui";
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
function UserContextMenu({ id, rec, dataSource }) {
const { showNotification, hideNotification } = useNotification();
const menuItems = [
{
menuTitle: "Edit",
menuAction: () => {
dataSource.startEdit(rec);
},
menuIcon: <PencilIcon className="h-5 w-5" />,
},
{
menuTitle: "Delete",
menuAction: () => {
showNotification({
title: "Confirmation",
description: "Are you sure you want to delete this record?",
statusIcon: "question",
timeout: 0,
showMask: true,
buttonPosition: "right",
firstButton: {
title: "Yes",
variant: "primary",
onClick: () => {
dataSource.startDelete(rec);
hideNotification();
},
},
secondButton: {
title: "Cancel",
variant: "secondary",
onClick: () => {
hideNotification();
},
},
});
},
menuIcon: <TrashIcon className="h-5 w-5" />,
},
];
return <ContextMenu id={id} menuItems={menuItems} />;
}
Using Context Menu
<Table
columnModel={columnModel}
dataSource={dataSource}
contextMenu={(rec) => <UserContextMenu id={rec.userId} rec={rec} dataSource={dataSource} />}
// ... other props
/>
Row Click Handling
Handle row clicks for navigation or editing.
import { useNavigate } from "react-router-dom";
function UsersPage() {
const navigate = useNavigate();
// ... setup code ...
return (
<Table
columnModel={columnModel}
dataSource={dataSource}
rowClick={(rec, ds) => {
// Navigate to detail page
navigate(`/app/user/detail?id=${rec.userId}`);
// Or open edit form
// ds.startEdit(rec);
}}
// ... other props
/>
);
}
Complete Example
Here's a complete example combining all patterns:
import * as React from "react";
import {
Form,
PageContainer,
Paging,
Spinning,
Table,
Toolbar,
createColumnModel,
createFormModel,
createViewModel,
show,
updateFieldProperties,
moveField,
useRemoteDataSource,
} from "@xond/ui";
import { PersonModel, PersonRecord } from "@your-app/generated/models";
import { UserContextMenu } from "./UserContextMenu";
import { apiURL, crudEndpoint } from "../../lib/env";
function UsersPage() {
const resourceName = "person";
const searchColumns = ["name", "email"];
// Get fields from model
const fields = [...PersonModel.fields];
// Reorder fields
moveField(fields, "email", "above", "name");
// Show specific columns
show("columns", fields, ["name", "email", "phone"]);
// Customize field properties
updateFieldProperties(fields, "name", {
header: "Full Name",
imageRenderer: (value, record) => (
<ImageIcon src={record.photoUrl} fallbackSrc={emptyPerson} size="sm" shape="circle" />
),
});
updateFieldProperties(fields, "email", {
header: "Contact",
renderer: (value, record) => (
<div className="flex items-center gap-1">
<EnvelopeIcon className="h-4 w-4" />
<span>{value}</span>
<Copy text={value} />
</div>
),
});
// Show form fields
show("fields", fields, ["name", "email", "phone", "address"], ["narrow", "narrow", "narrow", "wide"]);
// Create models
const columnModel = createColumnModel(fields);
const formModel = createFormModel(fields);
const viewModel = createViewModel(fields);
// Create data source
const dataSource = useRemoteDataSource({
resourceName,
entityName: "Person",
baseUrl: apiURL,
endPoint: crudEndpoint,
initialIncludeConfig: PersonModel.fkIncludes,
initialSortConfig: [{ field: "name", direction: "asc" }],
pageSize: 20,
textSearchFields: searchColumns,
formContainer: "modal",
formHeaders: {
edit: { title: "Edit User", description: "Editing user information" },
add: { title: "Add User", description: "Adding new user" },
view: { title: "View User", description: "Viewing user details" },
},
});
if (!columnModel) {
return (
<div className="flex h-screen items-center justify-center">
<Spinning />
</div>
);
}
return (
<PageContainer title="Users" frameStyle="full">
<Table
columnModel={columnModel}
dataSource={dataSource}
header={<Toolbar dataSource={dataSource} />}
footer={<Paging dataSource={dataSource} containerWidth="full" />}
form={<Form formModel={formModel} viewModel={viewModel} dataSource={dataSource} />}
contextMenu={(rec) => <UserContextMenu id={rec.personId} rec={rec} dataSource={dataSource} />}
theme="paper"
containerWidth="full"
rowClick={(rec) => {
dataSource.startEdit(rec);
}}
/>
</PageContainer>
);
}
export default UsersPage;
Best Practices
- Always copy fields array – Use
const fields = [...Model.fields]to avoid mutating the original - Customize before creating models – Apply
show(),updateFieldProperties(), etc. before callingcreateColumnModel() - Use generated models – Always start from
@xond/apigenerated models - Handle loading states – Show
SpinningwhilecolumnModelis being created - Sync form data – Use
useEffectto sync form data withdataSource.formData - Handle dataSource states – Monitor
isSaving,isSuccess,isErrorfor user feedback - Customize selectively – Only customize fields you need to change, let defaults handle the rest
Next Steps
- Learn about data sources in detail
- Explore forms and validation
- See integration patterns