Skip to main content

Form Page: Create and Edit Data

With list pages for viewing and detail pages for editing, the next natural step is creating new records. This article implements form functionality using the SDK create and update APIs, including form state management, frontend validation, submission, and error feedback.

What You'll Learn
  • Using the create API to create new data
  • Using the update API to modify data
  • Form state management and validation
  • Error handling and user feedback

Requirements

Implement a customer creation form: enter name, phone, company, and status, validate on the frontend, submit after validation passes, and return to the list on success.

Final Result:

Create Data Page


Prerequisites

info
  1. Completed Customer List Page
  2. Configured React Router

Implementation Steps

Step 1: Create Form Component

// src/pages/data-form.tsx

/**
* Title: Create Data
*/
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { lovrabetClient } from "../api/client";

interface FormData {
name: string;
phone: string;
company: string;
status: "potential" | "dealed" | "lost";
}

const initialForm: FormData = {
name: "",
phone: "",
company: "",
status: "potential",
};

export default function DataForm() {
const navigate = useNavigate();
const [form, setForm] = useState<FormData>(initialForm);
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>(
{}
);
const [submitting, setSubmitting] = useState(false);

const MODEL_NAME = "dataset_customers"; // Standard method, recommended
// const MODEL_NAME = "customers"; // Alias method

// Form validation
const validate = (): boolean => {
const newErrors: Partial<Record<keyof FormData, string>> = {};

if (!form.name.trim()) {
newErrors.name = "Name cannot be empty";
}

if (!form.phone.trim()) {
newErrors.phone = "Phone cannot be empty";
} else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "Please enter a valid phone number";
}

if (!form.status) {
newErrors.status = "Please select a status";
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

// Submit form
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

if (!validate()) {
return;
}

setSubmitting(true);
try {
const result = await lovrabetClient.models[MODEL_NAME].create({
name: form.name.trim(),
phone: form.phone.trim(),
company: form.company.trim(),
status: form.status,
});

console.log("Created successfully:", result);
alert("Created successfully");
navigate("/data"); // Return to list page
} catch (error) {
console.error("Creation failed:", error);
alert("Creation failed, please try again");
} finally {
setSubmitting(false);
}
};

// Cancel
const handleCancel = () => {
if (form.name || form.phone || form.company) {
if (!confirm("Are you sure you want to cancel? All entered content will be lost.")) {
return;
}
}
navigate("/data");
};

return (
<div className="data-form">
<div className="header">
<h1>Create Data</h1>
</div>

<form onSubmit={handleSubmit}>
<div className="form-group">
<label>
Name <span className="required">*</span>
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="Please enter name"
disabled={submitting}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>

<div className="form-group">
<label>
Phone <span className="required">*</span>
</label>
<input
type="text"
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="Please enter phone number"
disabled={submitting}
/>
{errors.phone && <span className="error">{errors.phone}</span>}
</div>

<div className="form-group">
<label>Company</label>
<input
type="text"
value={form.company}
onChange={(e) => setForm({ ...form, company: e.target.value })}
placeholder="Please enter company name"
disabled={submitting}
/>
</div>

<div className="form-group">
<label>
Status <span className="required">*</span>
</label>
<select
value={form.status}
onChange={(e) =>
setForm({ ...form, status: e.target.value as any })
}
disabled={submitting}
>
<option value="potential">Potential</option>
<option value="dealed">Closed</option>
<option value="lost">Lost</option>
</select>
{errors.status && <span className="error">{errors.status}</span>}
</div>

<div className="actions">
<button type="button" onClick={handleCancel} disabled={submitting}>
Cancel
</button>
<button type="submit" disabled={submitting}>
{submitting ? "Submitting..." : "Save"}
</button>
</div>
</form>
</div>
);
}

Step 2: Add Route Configuration

// src/App.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import DataList from "./pages/data-list";
import DataDetail from "./pages/data-detail";
import DataForm from "./pages/data-form";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/data" element={<DataList />} />
<Route path="/data/new" element={<DataForm />} />
<Route path="/data/:id" element={<DataDetail />} />
</Routes>
</BrowserRouter>
);
}

Step 3: Add Create Button to List Page

// src/pages/data-list.tsx
import { useNavigate } from "react-router-dom";

export default function DataList() {
const navigate = useNavigate();

return (
<div className="data-list">
<div className="header">
<h1>Data List</h1>
<button onClick={() => navigate("/data/new")}>+ Create</button>
</div>
{/* ... other content */}
</div>
);
}

Key Concepts

create API

Create new records:

// Basic usage
const result = await lovrabetClient.models.dataset_xxx.create({
name: "John Doe",
phone: "13800138000",
status: "potential",
});

// Returns the complete created data
console.log(result.id); // ID of the newly created record
console.log(result.create_time); // Creation time
ParameterTypeDescription
dataRecord<string, any>Data object to create

Return Value: Returns the complete record data after creation (including auto-generated fields like id, create_time, etc.).

Required fields cannot be omitted, and field names must match the dataset field names. Both OpenAPI and WebAPI modes are supported (v1.1.14+).


Form Validation Patterns

Validation on Submit

const validate = (): boolean => {
const newErrors: Record<string, string> = {};

if (!form.name.trim()) {
newErrors.name = "Name cannot be empty";
}

if (!form.phone.trim()) {
newErrors.phone = "Phone cannot be empty";
} else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "Please enter a valid phone number";
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
// Submit logic...
};

Real-time Validation (Immediate feedback on input)

const [errors, setErrors] = useState<Record<string, string>>({});

const handleFieldChange = (field: string, value: string) => {
setForm({ ...form, [field]: value });

// Clear error in real-time
if (errors[field]) {
setErrors({ ...errors, [field]: "" });
}
};

const handleFieldBlur = (field: string) => {
const newErrors = { ...errors };

if (field === "name" && !form.name.trim()) {
newErrors.name = "Name cannot be empty";
}

if (field === "phone") {
if (!form.phone.trim()) {
newErrors.phone = "Phone cannot be empty";
} else if (!/^1[3-9]\d{9}$/.test(form.phone)) {
newErrors.phone = "Please enter a valid phone number";
}
}

setErrors(newErrors);
};

Common Validation Rules

Required Field Validation

if (!value || !value.trim()) {
return "This field cannot be empty";
}

Length Validation

if (value.length < 2) {
return "At least 2 characters required";
}
if (value.length > 50) {
return "Cannot exceed 50 characters";
}

Phone Number Validation

// Simple validation
if (!/^1\d{10}$/.test(value)) {
return "Please enter a valid phone number";
}

// Strict validation (carrier prefixes)
if (!/^1[3-9]\d{9}$/.test(value)) {
return "Please enter a valid phone number";
}

Email Validation

if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return "Please enter a valid email address";
}

Number Range Validation

const num = Number(value);
if (isNaN(num)) {
return "Please enter a valid number";
}
if (num < 0 || num > 100) {
return "Please enter a number between 0 and 100";
}

Best Practices

Use Controlled Components

// Recommended: Controlled component
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>

// Not recommended: Uncontrolled component
<input type="text" defaultValue={form.name} ref={nameRef} />

Trim Input Values

await lovrabetClient.models.dataset_xxx.create({
name: form.name.trim(), // Remove leading/trailing spaces
phone: form.phone.trim(), // Remove leading/trailing spaces
company: form.company.trim(), // Remove leading/trailing spaces
});

Disable Form During Submission

<button type="submit" disabled={submitting}>
{submitting ? "Submitting..." : "Save"}
</button>

<input disabled={submitting} />

Confirm on Cancel

const handleCancel = () => {
// Check if there is any entered content
const hasContent = Object.values(form).some((v) => v && v.trim());

if (hasContent && !confirm("Are you sure you want to cancel? All entered content will be lost.")) {
return;
}

navigate("/data");
};

Frequently Asked Questions

Q: How do I get the ID of newly created data after successful creation?

The create return value contains the complete created data:

const result = await lovrabetClient.models.dataset_xxx.create({
name: "John Doe",
phone: "13800138000",
});

console.log(result.id); // Newly created ID
console.log(result.create_time); // Creation time

Q: Where should required field validation be done?

It is recommended to have two layers of validation: frontend validation improves user experience (immediate feedback), while backend validation through dataset "required" settings ensures data integrity.

// Frontend validation
if (!form.name.trim()) {
setErrors({ name: "Name cannot be empty" });
return;
}

// Submit (backend will also validate)
await lovrabetClient.models.dataset_xxx.create(form);

Q: How to handle enum fields?

Use a select dropdown:

<select
value={form.status}
onChange={(e) => setForm({ ...form, status: e.target.value as any })}
>
<option value="potential">Potential</option>
<option value="dealed">Closed</option>
<option value="lost">Lost</option>
</select>

Enum values must match exactly with the enum values in the dataset configuration.

Q: How to implement multi-step forms?

Use state to manage the current step:

const [step, setStep] = useState(1);
const [formData, setFormData] = useState({});

const nextStep = () => {
// Validate current step
if (!validateStep(step)) return;
setStep(step + 1);
};

const prevStep = () => {
setStep(step - 1);
};

// Submit on final step
const handleSubmit = async () => {
await lovrabetClient.models.dataset_xxx.create(formData);
navigate("/data");
};

Summary

Congratulations on completing the create and edit functionality! You have learned:

TopicDescription
create APICreate new records, returns complete data including ID
update APIUpdate records, requires ID
Form ValidationFrontend validation improves user experience
Error HandlingUse try-catch to catch exceptions, provide user-friendly feedback
Best Practices
  • Frontend validation for basic format checks (e.g., phone number format)
  • Business rule validation on the backend (e.g., uniqueness check)
  • Always use try-catch for exception handling
  • Disable buttons during submission to prevent duplicate submissions

Next Steps

Core Documentation

Advanced Topics


Difficulty Level: L1 | Estimated Time: 30 minutes