Implementing Edit Actions with Modal Forms in Xest Data Table
This guide demonstrates how to create an edit action with a modal form to update records in Xest Data Table.
1. Create the Edit Component
First, create a separate component for the edit functionality:
interface EditRecordProps {
record: {
id: number;
name: string;
email: string;
// ... other record fields
};
onUpdate: () => void;
}
function EditRecordAction({ record, onUpdate }: EditRecordProps) {
const [open, setOpen] = useState(false);
const [formData, setFormData] = useState({
name: record.name,
email: record.email,
});
const handleUpdate = async () => {
try {
await updateRecord(record.id, formData);
toast.success("Record updated successfully");
setOpen(false);
onUpdate(); // Refresh table data
} catch (error) {
toast.error("Failed to update record");
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">
<Pencil className="h-4 w-4" />
Edit
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Record</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Name</Label>
<Input
value={formData.name}
onChange={(e) => setFormData(prev => ({
...prev,
name: e.target.value
}))}
/>
</div>
<div>
<Label>Email</Label>
<Input
value={formData.email}
onChange={(e) => setFormData(prev => ({
...prev,
email: e.target.value
}))}
/>
</div>
</div>
<DialogFooter>
<Button onClick={handleUpdate}>
Save Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
2. Add Form Validation
Use Zod to validate the form data:
const editSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address")
});
// In your component:
const handleUpdate = async () => {
const result = editSchema.safeParse(formData);
if (!result.success) {
toast.error(result.error.issues[0].message);
return;
}
try {
await updateRecord(record.id, formData);
// ... rest of the code
} catch (error) {
// ... error handling
}
};
3. Integrate with Xest Data Table
Add the edit action to your table columns:
import { useTable } from '@xest-ui/data-table';
function YourDataTable() {
const { data: { refresh } } = useTable();
const columns: Col<RecordType>[] = [
// ... other columns
{
title: "Actions",
render: (record) => (
<EditRecordAction
record={record}
onUpdate={refresh}
/>
),
}
];
return (
<TableProvider
params={{
// ... your table configuration
}}
columns={columns}
>
<DataTable />
</TableProvider>
);
}
4. Add Loading States
Improve user experience by adding loading states:
function EditRecordAction({ record, onUpdate }: EditRecordProps) {
const [isLoading, setIsLoading] = useState(false);
const handleUpdate = async () => {
setIsLoading(true);
try {
await updateRecord(record.id, formData);
toast.success("Record updated successfully");
setOpen(false);
onUpdate();
} catch (error) {
toast.error("Failed to update record");
} finally {
setIsLoading(false);
}
};
return (
<Dialog>
{/* ... dialog content ... */}
<DialogFooter>
<Button onClick={handleUpdate} disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
"Save Changes"
)}
</Button>
</DialogFooter>
</Dialog>
);
}
5. Handle API Integration
Create a service function for the update operation:
interface UpdateRecordData {
name: string;
email: string;
}
export async function updateRecord(id: number, data: UpdateRecordData) {
const response = await fetch(`/api/records/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to update record');
}
return response.json();
}
Best Practices
Form State Management
- Initialize form with current record data
- Handle form changes efficiently
- Validate before submission
Error Handling
- Display validation errors clearly
- Show API error messages to users
- Maintain form state on error
UX Considerations
- Show loading states during submission
- Disable form while submitting
- Close modal on successful update
- Refresh table data after update
Modal Management
- Handle modal open/close states
- Reset form when modal closes
- Confirm before closing with unsaved changes
Complete Example
Here's a full implementation combining all the above concepts:
import { useState, useEffect } from 'react';
import { z } from 'zod';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Loader2, Pencil } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { updateRecord } from '@/services/records/updateRecord';
const editSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address")
});
interface EditRecordProps {
record: {
id: number;
name: string;
email: string;
};
onUpdate: () => void;
}
export function EditRecordAction({ record, onUpdate }: EditRecordProps) {
const [open, setOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: record.name,
email: record.email,
});
// Reset form when modal closes
useEffect(() => {
if (!open) {
setFormData({
name: record.name,
email: record.email,
});
}
}, [open, record]);
const handleUpdate = async () => {
const result = editSchema.safeParse(formData);
if (!result.success) {
toast.error(result.error.issues[0].message);
return;
}
setIsLoading(true);
try {
await updateRecord(record.id, formData);
toast.success("Record updated successfully");
setOpen(false);
onUpdate();
} catch (error) {
toast.error("Failed to update record");
} finally {
setIsLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<Pencil className="h-4 w-4 mr-2" />
Edit
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Record</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Name</Label>
<Input
value={formData.name}
onChange={(e) => setFormData(prev => ({
...prev,
name: e.target.value
}))}
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label>Email</Label>
<Input
value={formData.email}
onChange={(e) => setFormData(prev => ({
...prev,
email: e.target.value
}))}
disabled={isLoading}
/>
</div>
</div>
<DialogFooter>
<Button
onClick={handleUpdate}
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
"Save Changes"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}