@ram_28/kf-ai-sdk 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/useForm.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # useForm
2
2
 
3
- Form state management with automatic schema validation, computed fields, and react-hook-form integration.
3
+ Form state management with schema validation, computed fields, and react-hook-form integration.
4
4
 
5
5
  ## Imports
6
6
 
@@ -13,122 +13,229 @@ import {
13
13
  clearFormCache,
14
14
  } from "@ram_28/kf-ai-sdk/form";
15
15
  import type {
16
- FieldErrors,
17
16
  UseFormOptionsType,
18
17
  UseFormReturnType,
18
+ FormFieldConfigType,
19
+ FieldErrors,
19
20
  } from "@ram_28/kf-ai-sdk/form/types";
20
21
  ```
21
22
 
22
- ## Product Class Pattern
23
-
24
- The recommended pattern uses a role-based BDO wrapper class (like `Product`) instead of hardcoded source strings:
25
-
26
- ```tsx
27
- // sources/Product.ts - Role-based BDO wrapper
28
- import { Role, Roles } from "./roles";
29
-
30
- export type ProductType<TRole extends Role> = /* role-mapped types */;
31
-
32
- export class Product<TRole extends Role = typeof Roles.Admin> {
33
- constructor(_role: TRole) {}
34
- get _id(): string { return "BDO_AmazonProductMaster"; }
35
- // API methods: list, get, create, update, delete, draft, etc.
36
- }
37
- ```
38
-
39
- This pattern provides type-safe role-based field access, consistent with `useTable` and other hooks.
40
-
41
23
  ## Type Definitions
42
24
 
43
- ```typescript
44
- // Form operation type
45
- type FormOperationType = "create" | "update";
25
+ ### UseFormOptionsType
46
26
 
47
- // Hook options
27
+ ```typescript
28
+ // Hook options for initializing the form
48
29
  interface UseFormOptionsType<T> {
30
+ // Business Object ID (required)
31
+ // Example: product._id
49
32
  source: string;
50
- operation: FormOperationType;
33
+
34
+ // Form operation mode (required)
35
+ // - "create": Creates new record
36
+ // - "update": Edit existing record, loads record data
37
+ operation: "create" | "update";
38
+
39
+ // Record ID - required for update operations (required for operation: update)
40
+ // The form will fetch this record's data on mount
51
41
  recordId?: string;
42
+
43
+ // Initial form values
44
+ // Merged with schema defaults and (for update) fetched record data
52
45
  defaultValues?: Partial<T>;
46
+
47
+ // Validation trigger mode (from react-hook-form)
48
+ // - "onBlur" (default): Validate when field loses focus
49
+ // - "onChange": Validate on every keystroke
50
+ // - "onSubmit": Validate only on form submission
51
+ // - "onTouched": Validate on first blur, then on every change
52
+ // - "all": Validate on both blur and change
53
+ // Note: Computation (draft API) always fires on blur regardless of this setting
53
54
  mode?: "onBlur" | "onChange" | "onSubmit" | "onTouched" | "all";
55
+
56
+ // Enable form initialization (default: true)
57
+ // Set to false to defer schema fetching
54
58
  enabled?: boolean;
55
- userRole?: string;
59
+
60
+ // Callback when schema loading fails
61
+ // Use for error reporting or retry logic
56
62
  onSchemaError?: (error: Error) => void;
57
63
  }
64
+ ```
65
+
66
+ ### UseFormReturnType
58
67
 
59
- // Hook return type
68
+ ```typescript
69
+ // Hook return type with all form state and methods
60
70
  interface UseFormReturnType<T> {
61
- // React Hook Form methods
62
- register: UseFormRegister<T>;
63
- handleSubmit: (onSuccess?, onError?) => (e?) => Promise<void>;
64
- watch: (name?) => any;
65
- setValue: (name, value) => void;
66
- reset: (values?) => void;
67
-
68
- // Form state
71
+ // ============================================================
72
+ // FORM METHODS (from react-hook-form)
73
+ // ============================================================
74
+
75
+ // Register a field for validation and form handling
76
+ // Returns props to spread on input elements
77
+ register: (name: keyof T, options?: RegisterOptions) => InputProps;
78
+
79
+ // Handle form submission
80
+ // - onSuccess: Called with response data on successful save
81
+ // - onError: Called with FieldErrors (validation) or Error (API)
82
+ handleSubmit: (
83
+ onSuccess?: (data: T) => void,
84
+ onError?: (error: FieldErrors<T> | Error) => void,
85
+ ) => (e?: React.BaseSyntheticEvent) => Promise<void>;
86
+
87
+ // Watch field values reactively
88
+ // watch("Price") returns current value, re-renders on change
89
+ watch: (name?: keyof T) => any;
90
+
91
+ // Set field value programmatically
92
+ // Useful for computed previews, templates, external data
93
+ setValue: (name: keyof T, value: any, options?: SetValueConfig) => void;
94
+
95
+ // Reset form to initial/provided values
96
+ reset: (values?: T) => void;
97
+
98
+ // ============================================================
99
+ // FORM STATE (flattened - no nested formState object)
100
+ // ============================================================
101
+
102
+ // Current validation errors by field name
69
103
  errors: FieldErrors<T>;
104
+
105
+ // True when all fields pass validation
70
106
  isValid: boolean;
107
+
108
+ // True when any field has been modified
71
109
  isDirty: boolean;
110
+
111
+ // True during form submission
72
112
  isSubmitting: boolean;
113
+
114
+ // True after successful submission
73
115
  isSubmitSuccessful: boolean;
74
116
 
75
- // Loading states
76
- isLoadingInitialData: boolean;
77
- isLoadingRecord: boolean;
117
+ // ============================================================
118
+ // LOADING STATES
119
+ // ============================================================
120
+
121
+ // True during initial load
78
122
  isLoading: boolean;
79
123
 
80
- // Schema info
124
+ // True during background operations
125
+ isFetching: boolean;
126
+
127
+ // ============================================================
128
+ // ERROR HANDLING
129
+ // ============================================================
130
+
131
+ // Error from schema/record loading (not submission errors)
132
+ loadError: Error | null;
133
+
134
+ // True when loadError is present
135
+ hasError: boolean;
136
+
137
+ // ============================================================
138
+ // SCHEMA INFORMATION
139
+ // ============================================================
140
+
141
+ // Raw BDO schema (for advanced use cases)
81
142
  schema: BDOSchemaType | null;
143
+
144
+ // Processed schema with field configs ready for rendering
82
145
  schemaConfig: FormSchemaConfigType | null;
146
+
147
+ // List of computed field names
83
148
  computedFields: Array<keyof T>;
149
+
150
+ // List of required field names
84
151
  requiredFields: Array<keyof T>;
85
152
 
86
- // Field helpers
87
- getField: (fieldName) => FormFieldConfigType | null;
153
+ // ============================================================
154
+ // FIELD HELPERS
155
+ // ============================================================
156
+
157
+ // Get configuration for a specific field
158
+ // Returns null if field doesn't exist
159
+ getField: (fieldName: keyof T) => FormFieldConfigType | null;
160
+
161
+ // Get all field configurations as a record
88
162
  getFields: () => Record<keyof T, FormFieldConfigType>;
89
- isFieldRequired: (fieldName) => boolean;
90
- isFieldComputed: (fieldName) => boolean;
163
+
164
+ // Check if a field exists in the schema
165
+ hasField: (fieldName: keyof T) => boolean;
166
+
167
+ // Check if a field is required
168
+ isFieldRequired: (fieldName: keyof T) => boolean;
169
+
170
+ // Check if a field is computed (read-only, server-calculated)
171
+ isFieldComputed: (fieldName: keyof T) => boolean;
172
+
173
+ // ============================================================
174
+ // OPERATIONS
175
+ // ============================================================
176
+
177
+ // Refresh schema from server (clears cache)
178
+ refreshSchema: () => Promise<void>;
179
+
180
+ // Clear all validation errors
181
+ clearErrors: () => void;
91
182
  }
183
+ ```
184
+
185
+ ### FormFieldConfigType
92
186
 
93
- // Field configuration
187
+ ```typescript
188
+ // Field configuration from processed schema
94
189
  interface FormFieldConfigType {
190
+ // Field identifier (matches BDO field name)
95
191
  name: string;
96
- type:
97
- | "text"
98
- | "number"
99
- | "email"
100
- | "date"
101
- | "checkbox"
102
- | "select"
103
- | "textarea";
192
+
193
+ // Input type for rendering
194
+ // "text" | "number" | "email" | "date" | "datetime-local"
195
+ // "checkbox" | "select" | "textarea" | "reference"
196
+ type: FormFieldTypeType;
197
+
198
+ // Display label (from schema or derived from name)
104
199
  label: string;
200
+
201
+ // Whether field is required for submission
105
202
  required: boolean;
203
+
204
+ // Whether field is computed (read-only, server-calculated)
106
205
  computed: boolean;
206
+
207
+ // Default value from schema
107
208
  defaultValue?: any;
108
- options?: { value: any; label: string }[];
109
- permission: { editable: boolean; readable: boolean; hidden: boolean };
209
+
210
+ // Options for select/reference fields
211
+ // { value: any, label: string }[]
212
+ options?: SelectOptionType[];
213
+
214
+ // Field description for help text
215
+ description?: string;
216
+
217
+ // User permissions for this field
218
+ permission: {
219
+ editable: boolean; // Can user edit this field
220
+ readable: boolean; // Can user see this field
221
+ hidden: boolean; // Should field be completely hidden
222
+ };
110
223
  }
111
224
  ```
112
225
 
113
226
  ## Basic Example
114
227
 
115
- A minimal create form with field registration and submission.
116
-
117
228
  ```tsx
118
229
  import { useForm } from "@ram_28/kf-ai-sdk/form";
119
- import type {
120
- UseFormOptionsType,
121
- UseFormReturnType,
122
- } from "@ram_28/kf-ai-sdk/form/types";
123
- import { Product, ProductType } from "../sources";
230
+ import { Product, type ProductForRole } from "../sources";
124
231
  import { Roles } from "../sources/roles";
125
232
 
126
- type SellerProduct = ProductType<typeof Roles.Seller>;
233
+ type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
127
234
 
128
235
  function CreateProductForm() {
129
- const product = new Product(Roles.Seller);
236
+ const product = new Product(Roles.Buyer);
130
237
 
131
- const options: UseFormOptionsType<SellerProduct> = {
238
+ const form = useForm<BuyerProduct>({
132
239
  source: product._id,
133
240
  operation: "create",
134
241
  defaultValues: {
@@ -136,22 +243,12 @@ function CreateProductForm() {
136
243
  Price: 0,
137
244
  Category: "",
138
245
  },
139
- };
140
-
141
- const form: UseFormReturnType<SellerProduct> = useForm<SellerProduct>(options);
142
-
143
- const onSuccess = (data: SellerProduct) => {
144
- console.log("Product created:", data);
145
- };
146
-
147
- const onError = (error: Error) => {
148
- console.error("Failed to create:", error.message);
149
- };
246
+ });
150
247
 
151
- if (form.isLoading) return <div>Loading form...</div>;
248
+ if (form.isLoading) return <div>Loading...</div>;
152
249
 
153
250
  return (
154
- <form onSubmit={form.handleSubmit(onSuccess, onError)}>
251
+ <form onSubmit={form.handleSubmit((data) => console.log("Created:", data))}>
155
252
  <div>
156
253
  <label>Title</label>
157
254
  <input {...form.register("Title")} />
@@ -161,7 +258,6 @@ function CreateProductForm() {
161
258
  <div>
162
259
  <label>Price</label>
163
260
  <input type="number" {...form.register("Price")} />
164
- {form.errors.Price && <span>{form.errors.Price.message}</span>}
165
261
  </div>
166
262
 
167
263
  <div>
@@ -177,311 +273,141 @@ function CreateProductForm() {
177
273
  }
178
274
  ```
179
275
 
180
- ---
181
-
182
- ## Create vs Update
276
+ ## Examples
183
277
 
184
- ### Create Form
278
+ ### Product Listing Form
185
279
 
186
- Initialize a form for creating new records.
280
+ Create form with validation and success handling.
187
281
 
188
282
  ```tsx
189
- import { Product, ProductType } from "../sources";
283
+ import { useForm } from "@ram_28/kf-ai-sdk/form";
284
+ import type { FieldErrors } from "@ram_28/kf-ai-sdk/form/types";
285
+ import { useNavigate } from "react-router-dom";
286
+ import { Product, type ProductForRole } from "../sources";
190
287
  import { Roles } from "../sources/roles";
191
288
 
192
- type SellerProduct = ProductType<typeof Roles.Seller>;
289
+ type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
193
290
 
194
- function CreateForm() {
195
- const product = new Product(Roles.Seller);
291
+ function ProductListingForm() {
292
+ const product = new Product(Roles.Buyer);
293
+ const navigate = useNavigate();
196
294
 
197
- const form = useForm<SellerProduct>({
295
+ const form = useForm<BuyerProduct>({
198
296
  source: product._id,
199
297
  operation: "create",
200
298
  defaultValues: {
201
299
  Title: "",
300
+ Description: "",
202
301
  Price: 0,
302
+ Category: "Electronics",
203
303
  Stock: 0,
204
304
  },
305
+ mode: "onBlur",
205
306
  });
206
307
 
207
- return (
208
- <form onSubmit={form.handleSubmit()}>
209
- <input {...form.register("Title")} placeholder="Product title" />
210
- <input type="number" {...form.register("Price")} placeholder="Price" />
211
- <button type="submit">Create</button>
212
- </form>
213
- );
214
- }
215
- ```
216
-
217
- ### Edit Form
218
-
219
- Load existing record data for editing.
220
-
221
- ```tsx
222
- function EditForm({ productId }: { productId: string }) {
223
- const product = new Product(Roles.Seller);
224
-
225
- const form = useForm<SellerProduct>({
226
- source: product._id,
227
- operation: "update",
228
- recordId: productId,
229
- });
230
-
231
- if (form.isLoadingRecord) return <div>Loading product...</div>;
232
-
233
- return (
234
- <form onSubmit={form.handleSubmit()}>
235
- <input {...form.register("Title")} />
236
- <input type="number" {...form.register("Price")} />
237
- <button type="submit" disabled={!form.isDirty}>
238
- Save Changes
239
- </button>
240
- </form>
241
- );
242
- }
243
- ```
244
-
245
- ### Toggle Between Create and Edit
246
-
247
- Switch form mode based on selection.
248
-
249
- ```tsx
250
- function ProductForm({ existingProduct }: { existingProduct?: SellerProduct }) {
251
- const product = new Product(Roles.Seller);
252
- const isEditing = !!existingProduct;
253
-
254
- const form = useForm<SellerProduct>({
255
- source: product._id,
256
- operation: isEditing ? "update" : "create",
257
- recordId: existingProduct?._id,
258
- defaultValues: existingProduct ?? { Title: "", Price: 0 },
259
- });
260
-
261
- return (
262
- <form onSubmit={form.handleSubmit()}>
263
- <h2>{isEditing ? "Edit Product" : "New Product"}</h2>
264
- <input {...form.register("Title")} />
265
- <input type="number" {...form.register("Price")} />
266
- <button type="submit">{isEditing ? "Update" : "Create"}</button>
267
- </form>
268
- );
269
- }
270
- ```
271
-
272
- ---
273
-
274
- ## Field Rendering
275
-
276
- ### Using getField for Metadata
277
-
278
- Access field configuration from the schema.
279
-
280
- ```tsx
281
- function DynamicField({
282
- form,
283
- fieldName,
284
- }: {
285
- form: UseFormReturnType<SellerProduct>;
286
- fieldName: keyof SellerProduct;
287
- }) {
288
- const field = form.getField(fieldName);
308
+ const onSuccess = (data: BuyerProduct) => {
309
+ toast.success(`Product "${data.Title}" created!`);
310
+ navigate(`/products/${data._id}`);
311
+ };
289
312
 
290
- if (!field || field.permission.hidden) return null;
313
+ const onError = (error: FieldErrors<BuyerProduct> | Error) => {
314
+ if (error instanceof Error) {
315
+ toast.error(`Failed: ${error.message}`);
316
+ } else {
317
+ toast.error("Please fix the validation errors");
318
+ }
319
+ };
291
320
 
292
- const isDisabled = !field.permission.editable || field.computed;
321
+ if (form.isLoading) return <div>Loading form...</div>;
293
322
 
294
323
  return (
295
- <div className="field">
296
- <label>
297
- {field.label}
298
- {field.required && <span className="required">*</span>}
299
- {field.computed && <span className="computed">(auto)</span>}
300
- </label>
301
-
302
- {field.type === "select" && field.options ? (
303
- <select {...form.register(fieldName)} disabled={isDisabled}>
304
- <option value="">Select {field.label}</option>
305
- {field.options.map((opt) => (
306
- <option key={opt.value} value={opt.value}>
307
- {opt.label}
308
- </option>
309
- ))}
310
- </select>
311
- ) : (
312
- <input
313
- type={field.type === "number" ? "number" : "text"}
314
- {...form.register(fieldName)}
315
- disabled={isDisabled}
316
- />
317
- )}
318
-
319
- {form.errors[fieldName] && (
320
- <span className="error">{form.errors[fieldName]?.message}</span>
321
- )}
322
- </div>
323
- );
324
- }
325
- ```
326
-
327
- ### Handling Computed Fields
328
-
329
- Display auto-calculated fields as read-only.
330
-
331
- ```tsx
332
- function FormWithComputedFields() {
333
- const product = new Product(Roles.Seller);
334
-
335
- const form = useForm<SellerProduct>({
336
- source: product._id,
337
- operation: "create",
338
- });
324
+ <form onSubmit={form.handleSubmit(onSuccess, onError)}>
325
+ <div>
326
+ <label>Title {form.isFieldRequired("Title") && "*"}</label>
327
+ <input {...form.register("Title")} />
328
+ {form.errors.Title && (
329
+ <span className="error">{form.errors.Title.message}</span>
330
+ )}
331
+ </div>
339
332
 
340
- return (
341
- <form onSubmit={form.handleSubmit()}>
342
333
  <div>
343
- <label>MRP</label>
344
- <input type="number" {...form.register("MRP")} />
334
+ <label>Description</label>
335
+ <textarea {...form.register("Description")} rows={4} />
345
336
  </div>
346
337
 
347
338
  <div>
348
- <label>Price</label>
349
- <input type="number" {...form.register("Price")} />
339
+ <label>Price *</label>
340
+ <input type="number" step="0.01" {...form.register("Price")} />
341
+ {form.errors.Price && (
342
+ <span className="error">{form.errors.Price.message}</span>
343
+ )}
350
344
  </div>
351
345
 
352
- {/* Discount is computed from MRP and Price */}
353
- {form.isFieldComputed("Discount") && (
354
- <div>
355
- <label>Discount % (auto-calculated)</label>
356
- <input
357
- type="number"
358
- value={form.watch("Discount") ?? 0}
359
- readOnly
360
- disabled
361
- />
362
- </div>
363
- )}
346
+ <div>
347
+ <label>Stock</label>
348
+ <input type="number" {...form.register("Stock")} />
349
+ </div>
364
350
 
365
- <button type="submit">Save</button>
351
+ <button type="submit" disabled={form.isSubmitting || !form.isValid}>
352
+ {form.isSubmitting ? "Creating..." : "Create Product"}
353
+ </button>
366
354
  </form>
367
355
  );
368
356
  }
369
357
  ```
370
358
 
371
- ### Render All Fields Dynamically
359
+ ### Edit Product
372
360
 
373
- Generate form fields from schema configuration.
361
+ Update mode with record loading state.
374
362
 
375
363
  ```tsx
376
- function DynamicForm() {
377
- const product = new Product(Roles.Seller);
378
-
379
- const form = useForm<SellerProduct>({
380
- source: product._id,
381
- operation: "create",
382
- });
383
-
384
- if (form.isLoading) return <div>Loading...</div>;
385
-
386
- const fields = form.getFields();
387
-
388
- return (
389
- <form onSubmit={form.handleSubmit()}>
390
- {Object.entries(fields).map(([fieldName, field]) => {
391
- if (field.permission.hidden) return null;
392
-
393
- return (
394
- <div key={fieldName}>
395
- <label>{field.label}</label>
396
- <input
397
- {...form.register(fieldName as keyof SellerProduct)}
398
- type={field.type === "number" ? "number" : "text"}
399
- disabled={!field.permission.editable || field.computed}
400
- />
401
- </div>
402
- );
403
- })}
404
-
405
- <button type="submit">Submit</button>
406
- </form>
407
- );
408
- }
409
- ```
410
-
411
- ---
412
-
413
- ## Validation
414
-
415
- ### Displaying Field Errors
364
+ import { useForm } from "@ram_28/kf-ai-sdk/form";
365
+ import { Product, type ProductForRole } from "../sources";
366
+ import { Roles } from "../sources/roles";
416
367
 
417
- Show validation errors inline with fields.
368
+ type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
418
369
 
419
- ```tsx
420
- function FormWithValidation() {
421
- const product = new Product(Roles.Seller);
370
+ function EditProductForm({ productId }: { productId: string }) {
371
+ const product = new Product(Roles.Buyer);
422
372
 
423
- const form = useForm<SellerProduct>({
373
+ const form = useForm<BuyerProduct>({
424
374
  source: product._id,
425
- operation: "create",
426
- mode: "onBlur", // Validate on blur
375
+ operation: "update",
376
+ recordId: productId,
427
377
  });
428
378
 
379
+ if (form.isLoading) {
380
+ return <div>Loading product...</div>;
381
+ }
382
+
383
+ if (form.hasError) {
384
+ return (
385
+ <div>
386
+ <p>Failed to load product: {form.loadError?.message}</p>
387
+ <button onClick={() => form.refreshSchema()}>Retry</button>
388
+ </div>
389
+ );
390
+ }
391
+
429
392
  return (
430
- <form onSubmit={form.handleSubmit()}>
431
- <div className={form.errors.Title ? "field-error" : ""}>
432
- <label>Title *</label>
393
+ <form onSubmit={form.handleSubmit(() => toast.success("Product updated!"))}>
394
+ <div>
395
+ <label>Title</label>
433
396
  <input {...form.register("Title")} />
434
- {form.errors.Title && (
435
- <span className="error-message">{form.errors.Title.message}</span>
436
- )}
437
397
  </div>
438
398
 
439
- <div className={form.errors.Price ? "field-error" : ""}>
440
- <label>Price *</label>
399
+ <div>
400
+ <label>Price</label>
441
401
  <input type="number" {...form.register("Price")} />
442
- {form.errors.Price && (
443
- <span className="error-message">{form.errors.Price.message}</span>
444
- )}
445
402
  </div>
446
403
 
447
- <button type="submit" disabled={!form.isValid}>
448
- Submit
449
- </button>
450
- </form>
451
- );
452
- }
453
- ```
454
-
455
- ### Form State Indicators
456
-
457
- Show form status to users.
458
-
459
- ```tsx
460
- function FormWithStateIndicators() {
461
- const product = new Product(Roles.Seller);
462
-
463
- const form = useForm<SellerProduct>({
464
- source: product._id,
465
- operation: "update",
466
- recordId: "PROD-001",
467
- });
468
-
469
- return (
470
- <form onSubmit={form.handleSubmit()}>
471
- {/* Form fields */}
472
- <input {...form.register("Title")} />
473
- <input type="number" {...form.register("Price")} />
474
-
475
- {/* Status bar */}
476
- <div className="form-status">
477
- {form.isDirty && <span className="unsaved">Unsaved changes</span>}
478
- {!form.isValid && <span className="invalid">Please fix errors</span>}
479
- {form.isSubmitting && <span className="saving">Saving...</span>}
480
- {form.isSubmitSuccessful && <span className="success">Saved!</span>}
404
+ <div>
405
+ <label>Stock</label>
406
+ <input type="number" {...form.register("Stock")} />
481
407
  </div>
482
408
 
483
- <button type="submit" disabled={form.isSubmitting || !form.isDirty}>
484
- Save Changes
409
+ <button type="submit" disabled={!form.isDirty || form.isSubmitting}>
410
+ {form.isSubmitting ? "Saving..." : "Save Changes"}
485
411
  </button>
486
412
 
487
413
  <button
@@ -496,130 +422,127 @@ function FormWithStateIndicators() {
496
422
  }
497
423
  ```
498
424
 
499
- ---
500
-
501
- ## Submission
502
-
503
- ### Handle Submit with Callbacks
425
+ ### Auto-Calculate Discount
504
426
 
505
- Process successful submissions and errors.
427
+ Working with computed fields and the watch function.
506
428
 
507
429
  ```tsx
508
- function FormWithCallbacks() {
509
- const product = new Product(Roles.Seller);
510
-
511
- const form = useForm<SellerProduct>({
512
- source: product._id,
513
- operation: "create",
514
- });
515
-
516
- const onSuccess = (data: SellerProduct) => {
517
- alert(`Product "${data.Title}" created successfully!`);
518
- form.reset();
519
- };
520
-
521
- const onError = (error: FieldErrors<SellerProduct> | Error) => {
522
- if (error instanceof Error) {
523
- // API error
524
- alert(`Server error: ${error.message}`);
525
- } else {
526
- // Validation errors
527
- const fieldNames = Object.keys(error).join(", ");
528
- alert(`Please fix errors in: ${fieldNames}`);
529
- }
530
- };
531
-
532
- return (
533
- <form onSubmit={form.handleSubmit(onSuccess, onError)}>
534
- <input {...form.register("Title")} placeholder="Title" />
535
- <input type="number" {...form.register("Price")} placeholder="Price" />
536
- <button type="submit">Create</button>
537
- </form>
538
- );
539
- }
540
- ```
541
-
542
- ### Programmatic Submission
430
+ import { useForm } from "@ram_28/kf-ai-sdk/form";
431
+ import { Product, type ProductForRole } from "../sources";
432
+ import { Roles } from "../sources/roles";
543
433
 
544
- Submit form without a submit button.
434
+ type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
545
435
 
546
- ```tsx
547
- function FormWithProgrammaticSubmit() {
548
- const product = new Product(Roles.Seller);
436
+ function PricingForm() {
437
+ const product = new Product(Roles.Buyer);
549
438
 
550
- const form = useForm<SellerProduct>({
439
+ const form = useForm<BuyerProduct>({
551
440
  source: product._id,
552
441
  operation: "create",
442
+ defaultValues: {
443
+ MRP: 0,
444
+ Price: 0,
445
+ },
553
446
  });
554
447
 
555
- const handleSaveAndClose = async () => {
556
- const submitHandler = form.handleSubmit(
557
- (data) => {
558
- console.log("Saved:", data);
559
- // Close modal or navigate away
560
- },
561
- (error) => {
562
- console.error("Validation failed:", error);
563
- },
564
- );
448
+ // Watch computed field value from server
449
+ const discount = form.watch("Discount");
565
450
 
566
- await submitHandler();
567
- };
451
+ if (form.isLoading) return <div>Loading...</div>;
568
452
 
569
453
  return (
570
- <div>
571
- <input {...form.register("Title")} />
572
- <input type="number" {...form.register("Price")} />
454
+ <form onSubmit={form.handleSubmit()}>
455
+ <div>
456
+ <label>MRP (Maximum Retail Price)</label>
457
+ <input type="number" {...form.register("MRP")} />
458
+ </div>
573
459
 
574
- <div className="actions">
575
- <button type="button" onClick={() => form.reset()}>
576
- Cancel
577
- </button>
578
- <button type="button" onClick={handleSaveAndClose}>
579
- Save & Close
580
- </button>
460
+ <div>
461
+ <label>Selling Price</label>
462
+ <input type="number" {...form.register("Price")} />
581
463
  </div>
582
- </div>
464
+
465
+ {/* Computed field - read-only, server-calculated */}
466
+ {form.isFieldComputed("Discount") && (
467
+ <div>
468
+ <label>Discount % (auto-calculated)</label>
469
+ <input type="number" value={discount ?? 0} readOnly disabled />
470
+ </div>
471
+ )}
472
+
473
+ <button type="submit">Save Product</button>
474
+ </form>
583
475
  );
584
476
  }
585
477
  ```
586
478
 
587
- ---
479
+ ### Static Dropdown
588
480
 
589
- ## Watch and setValue
481
+ Static fields return options with `Value` and `Label`.
590
482
 
591
- ### Watch Field Values
483
+ ```tsx
484
+ import { useState } from "react";
485
+ import { useForm } from "@ram_28/kf-ai-sdk/form";
486
+ import { Product, type ProductForRole } from "../sources";
487
+ import { Roles } from "../sources/roles";
592
488
 
593
- React to field value changes in real-time.
489
+ type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
594
490
 
595
- ```tsx
596
- function FormWithWatcher() {
597
- const product = new Product(Roles.Seller);
491
+ function ProductCategoryForm({ recordId }: { recordId: string }) {
492
+ const product = new Product(Roles.Buyer);
493
+ const [categoryOptions, setCategoryOptions] = useState<
494
+ { Value: string; Label: string }[]
495
+ >([]);
496
+ const [isOpen, setIsOpen] = useState(false);
497
+ const [loadingOptions, setLoadingOptions] = useState(false);
598
498
 
599
- const form = useForm<SellerProduct>({
499
+ const form = useForm<BuyerProduct>({
600
500
  source: product._id,
601
- operation: "create",
501
+ operation: "update",
502
+ recordId,
602
503
  });
603
504
 
604
- const mrp = form.watch("MRP");
605
- const price = form.watch("Price");
606
- const discount = mrp > 0 ? (((mrp - price) / mrp) * 100).toFixed(1) : 0;
505
+ // Fetch options only when dropdown is opened
506
+ const handleDropdownOpen = async () => {
507
+ if (categoryOptions.length > 0) {
508
+ setIsOpen(true);
509
+ return; // Already loaded
510
+ }
511
+
512
+ setLoadingOptions(true);
513
+ setIsOpen(true);
514
+
515
+ try {
516
+ const options = await product.fetchField(recordId, "Category");
517
+ setCategoryOptions(options);
518
+ } catch (error) {
519
+ console.error(error);
520
+ } finally {
521
+ setLoadingOptions(false);
522
+ }
523
+ };
524
+
525
+ if (form.isLoading) return <div>Loading...</div>;
607
526
 
608
527
  return (
609
528
  <form onSubmit={form.handleSubmit()}>
610
529
  <div>
611
- <label>MRP</label>
612
- <input type="number" {...form.register("MRP")} />
613
- </div>
614
-
615
- <div>
616
- <label>Price</label>
617
- <input type="number" {...form.register("Price")} />
618
- </div>
619
-
620
- <div className="preview">
621
- <p>Discount: {discount}%</p>
622
- <p>You save: ${(mrp - price).toFixed(2)}</p>
530
+ <label>Category</label>
531
+ <select
532
+ {...form.register("Category")}
533
+ onFocus={handleDropdownOpen}
534
+ >
535
+ <option value="">Select category</option>
536
+ {loadingOptions ? (
537
+ <option disabled>Loading...</option>
538
+ ) : (
539
+ categoryOptions.map((opt) => (
540
+ <option key={opt.Value} value={opt.Value}>
541
+ {opt.Label}
542
+ </option>
543
+ ))
544
+ )}
545
+ </select>
623
546
  </div>
624
547
 
625
548
  <button type="submit">Save</button>
@@ -628,61 +551,103 @@ function FormWithWatcher() {
628
551
  }
629
552
  ```
630
553
 
631
- ### Set Field Values Programmatically
554
+ ### Dynamic Reference Dropdown
632
555
 
633
- Update field values from external actions.
556
+ Reference fields return the full object structure. Use a custom dropdown to display rich cards.
634
557
 
635
558
  ```tsx
636
- function FormWithSetValue() {
637
- const product = new Product(Roles.Seller);
559
+ import { useState } from "react";
560
+ import { useForm } from "@ram_28/kf-ai-sdk/form";
561
+ import { Product, type ProductForRole } from "../sources";
562
+ import { Roles } from "../sources/roles";
563
+
564
+ type BuyerProduct = ProductForRole<typeof Roles.Buyer>;
638
565
 
639
- const form = useForm<SellerProduct>({
566
+ type SupplierOption = {
567
+ _id: string;
568
+ SupplierName: string;
569
+ Rating: number;
570
+ };
571
+
572
+ function ProductSupplierForm({ recordId }: { recordId: string }) {
573
+ const product = new Product(Roles.Buyer);
574
+ const [supplierOptions, setSupplierOptions] = useState<SupplierOption[]>([]);
575
+ const [selectedSupplier, setSelectedSupplier] = useState<SupplierOption | null>(null);
576
+ const [isOpen, setIsOpen] = useState(false);
577
+ const [loadingOptions, setLoadingOptions] = useState(false);
578
+
579
+ const form = useForm<BuyerProduct>({
640
580
  source: product._id,
641
- operation: "create",
581
+ operation: "update",
582
+ recordId,
642
583
  });
643
584
 
644
- const applyTemplate = (template: string) => {
645
- switch (template) {
646
- case "electronics":
647
- form.setValue("Category", "Electronics");
648
- form.setValue("Price", 99.99);
649
- break;
650
- case "books":
651
- form.setValue("Category", "Books");
652
- form.setValue("Price", 19.99);
653
- break;
585
+ // Fetch options only when dropdown is clicked
586
+ const handleDropdownClick = async () => {
587
+ setIsOpen(!isOpen);
588
+
589
+ if (supplierOptions.length > 0 || loadingOptions) return;
590
+
591
+ setLoadingOptions(true);
592
+ try {
593
+ const options = await product.fetchField(recordId, "SupplierInfo");
594
+ setSupplierOptions(options);
595
+ } catch (error) {
596
+ console.error(error);
597
+ } finally {
598
+ setLoadingOptions(false);
654
599
  }
655
600
  };
656
601
 
602
+ const handleSelect = (supplier: SupplierOption) => {
603
+ setSelectedSupplier(supplier);
604
+ form.setValue("SupplierInfo", { _id: supplier._id, SupplierName: supplier.SupplierName });
605
+ setIsOpen(false);
606
+ };
607
+
608
+ if (form.isLoading) return <div>Loading...</div>;
609
+
657
610
  return (
658
611
  <form onSubmit={form.handleSubmit()}>
659
- <div className="templates">
660
- <button type="button" onClick={() => applyTemplate("electronics")}>
661
- Electronics Template
662
- </button>
663
- <button type="button" onClick={() => applyTemplate("books")}>
664
- Books Template
612
+ <div className="dropdown">
613
+ <label>Supplier</label>
614
+
615
+ {/* Selected value display */}
616
+ <button type="button" onClick={handleDropdownClick}>
617
+ {selectedSupplier ? selectedSupplier.SupplierName : "Select supplier"}
665
618
  </button>
666
- </div>
667
619
 
668
- <input {...form.register("Title")} placeholder="Title" />
669
- <input {...form.register("Category")} placeholder="Category" />
670
- <input type="number" {...form.register("Price")} placeholder="Price" />
620
+ {/* Dropdown options with cards */}
621
+ {isOpen && (
622
+ <div className="dropdown-menu">
623
+ {loadingOptions ? (
624
+ <div className="dropdown-item">Loading...</div>
625
+ ) : (
626
+ supplierOptions.map((supplier) => (
627
+ <div
628
+ key={supplier._id}
629
+ className="dropdown-card"
630
+ onClick={() => handleSelect(supplier)}
631
+ >
632
+ <span className="supplier-name">{supplier.SupplierName}</span>
633
+ <span className="supplier-rating">
634
+ {"★".repeat(supplier.Rating)}{"☆".repeat(5 - supplier.Rating)}
635
+ </span>
636
+ </div>
637
+ ))
638
+ )}
639
+ </div>
640
+ )}
641
+ </div>
671
642
 
672
- <button type="submit">Create</button>
643
+ <button type="submit">Save</button>
673
644
  </form>
674
645
  );
675
646
  }
676
647
  ```
677
648
 
678
- ---
679
-
680
649
  ## Error Utilities
681
650
 
682
- The SDK provides utilities to help handle and categorize errors from form operations.
683
-
684
- ### Available Utilities
685
-
686
651
  ```typescript
687
652
  import {
688
653
  parseApiError,
@@ -692,149 +657,43 @@ import {
692
657
  } from "@ram_28/kf-ai-sdk/form";
693
658
  ```
694
659
 
695
- | Utility | Description |
696
- | -------------------------- | ---------------------------------------------------------------- |
697
- | `parseApiError(error)` | Extracts a user-friendly message from any error type |
698
- | `isNetworkError(error)` | Returns `true` if error is network-related (connection, timeout) |
699
- | `isValidationError(error)` | Returns `true` if error is a validation/schema error |
700
- | `clearFormCache()` | Clears the schema cache (30-minute TTL by default) |
701
-
702
- ### Parse and Handle API Errors
660
+ ### parseApiError
703
661
 
704
- Use built-in utilities for comprehensive error handling.
662
+ Extract user-friendly message from any error type.
705
663
 
706
- ```tsx
707
- import {
708
- parseApiError,
709
- isNetworkError,
710
- isValidationError,
711
- clearFormCache,
712
- } from "@ram_28/kf-ai-sdk/form";
713
-
714
- function FormWithErrorHandling() {
715
- const product = new Product(Roles.Seller);
716
-
717
- const form = useForm<SellerProduct>({
718
- source: product._id,
719
- operation: "create",
720
- });
721
-
722
- const onError = (error: Error) => {
723
- const message = parseApiError(error);
664
+ ```typescript
665
+ const onError = (error: Error) => {
666
+ const message = parseApiError(error);
667
+ toast.error(message);
668
+ };
669
+ ```
724
670
 
725
- if (isNetworkError(error)) {
726
- alert("Network error. Please check your connection.");
727
- } else if (isValidationError(error)) {
728
- alert("Validation failed. Please check your input.");
729
- } else {
730
- alert(message);
731
- }
732
- };
671
+ ### isNetworkError
733
672
 
734
- const handleClearCache = () => {
735
- clearFormCache();
736
- window.location.reload();
737
- };
673
+ Check if error is network-related (connection, timeout).
738
674
 
739
- return (
740
- <form onSubmit={form.handleSubmit(undefined, onError)}>
741
- {/* form fields */}
742
- <button type="submit">Save</button>
743
- <button type="button" onClick={handleClearCache}>
744
- Clear Cache & Reload
745
- </button>
746
- </form>
747
- );
675
+ ```typescript
676
+ if (isNetworkError(error)) {
677
+ toast.error("Network error. Please check your connection.");
748
678
  }
749
679
  ```
750
680
 
751
- ### Handling Different Error Types
752
-
753
- The `handleSubmit` callback receives different error types depending on the failure stage:
681
+ ### isValidationError
754
682
 
755
- ```tsx
756
- function FormWithDetailedErrorHandling() {
757
- const product = new Product(Roles.Seller);
758
-
759
- const form = useForm<SellerProduct>({
760
- source: product._id,
761
- operation: "create",
762
- });
763
-
764
- const onSubmit = (data: SellerProduct) => {
765
- console.log("Success:", data);
766
- };
767
-
768
- const onError = (
769
- error: FieldErrors<SellerProduct> | Error,
770
- event?: React.BaseSyntheticEvent,
771
- ) => {
772
- // Check if it's a validation error (from react-hook-form)
773
- if (!(error instanceof Error)) {
774
- // Field validation errors - error is FieldErrors<SellerProduct>
775
- const invalidFields = Object.keys(error);
776
- console.log("Validation failed for fields:", invalidFields);
777
-
778
- // Access specific field errors
779
- if (error.Title) {
780
- console.log("Title error:", error.Title.message);
781
- }
782
- return;
783
- }
784
-
785
- // API/Network error - error is Error
786
- if (isNetworkError(error)) {
787
- // Handle offline, timeout, connection refused
788
- console.error("Network issue:", error.message);
789
- } else if (isValidationError(error)) {
790
- // Server-side validation error
791
- console.error("Server validation:", parseApiError(error));
792
- } else {
793
- // Other server errors (500, etc.)
794
- console.error("Server error:", parseApiError(error));
795
- }
796
- };
683
+ Check if error is a server-side validation error.
797
684
 
798
- return (
799
- <form onSubmit={form.handleSubmit(onSubmit, onError)}>
800
- {/* form fields */}
801
- </form>
802
- );
685
+ ```typescript
686
+ if (isValidationError(error)) {
687
+ toast.error("Validation failed. Please check your input.");
803
688
  }
804
689
  ```
805
690
 
806
- ### Schema Loading Errors
691
+ ### clearFormCache
807
692
 
808
- Handle errors when the form schema fails to load.
693
+ Clear the schema cache (30-minute TTL by default).
809
694
 
810
- ```tsx
811
- function FormWithSchemaErrorHandling() {
812
- const product = new Product(Roles.Seller);
813
-
814
- const form = useForm<SellerProduct>({
815
- source: product._id,
816
- operation: "create",
817
- onSchemaError: (error) => {
818
- console.error("Failed to load form schema:", error.message);
819
- // Optionally clear cache and retry
820
- clearFormCache();
821
- },
822
- });
823
-
824
- // Check for load errors
825
- if (form.loadError) {
826
- return (
827
- <div className="error-state">
828
- <p>Failed to load form: {form.loadError.message}</p>
829
- <button onClick={() => window.location.reload()}>Retry</button>
830
- </div>
831
- );
832
- }
833
-
834
- if (form.isLoading) {
835
- return <div>Loading form...</div>;
836
- }
837
-
838
- return <form onSubmit={form.handleSubmit()}>{/* form fields */}</form>;
839
- }
695
+ ```typescript
696
+ // Clear cache and reload to get fresh schema
697
+ clearFormCache();
698
+ window.location.reload();
840
699
  ```