From a99e550c6eb7aa05d5d9eab4d89c24cd84f840f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:11:44 +0000 Subject: [PATCH 01/17] Initial plan From a7882a47bd0dfc00fdd04785e626afcfdb4873c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:44:07 +0000 Subject: [PATCH 02/17] Add comprehensive Form component with react-hook-form integration Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- apps/playground/src/data/examples.ts | 138 +++++++- .../components/src/renderers/form/form.tsx | 325 ++++++++++++++++++ .../components/src/renderers/form/index.ts | 1 + 3 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 packages/components/src/renderers/form/form.tsx diff --git a/apps/playground/src/data/examples.ts b/apps/playground/src/data/examples.ts index 9f2e04ff2..bd8ce568b 100644 --- a/apps/playground/src/data/examples.ts +++ b/apps/playground/src/data/examples.ts @@ -608,6 +608,142 @@ export const examples = { ] }`, + 'airtable-form': `{ + "type": "div", + "className": "max-w-4xl space-y-6", + "body": [ + { + "type": "div", + "className": "space-y-2", + "body": [ + { + "type": "text", + "content": "Airtable-Style Feature-Complete Form", + "className": "text-3xl font-bold" + }, + { + "type": "text", + "content": "A comprehensive form component with validation, multi-column layout, and conditional fields", + "className": "text-muted-foreground" + } + ] + }, + { + "type": "card", + "className": "shadow-sm", + "body": { + "type": "form", + "className": "p-6", + "submitLabel": "Create Project", + "cancelLabel": "Reset", + "showCancel": true, + "columns": 2, + "validationMode": "onBlur", + "resetOnSubmit": false, + "defaultValues": { + "projectType": "personal", + "priority": "medium", + "notifications": true + }, + "fields": [ + { + "name": "projectName", + "label": "Project Name", + "type": "input", + "required": true, + "placeholder": "Enter project name", + "validation": { + "minLength": { + "value": 3, + "message": "Project name must be at least 3 characters" + } + } + }, + { + "name": "projectType", + "label": "Project Type", + "type": "select", + "required": true, + "options": [ + { "label": "Personal", "value": "personal" }, + { "label": "Team", "value": "team" }, + { "label": "Enterprise", "value": "enterprise" } + ] + }, + { + "name": "teamSize", + "label": "Team Size", + "type": "input", + "inputType": "number", + "placeholder": "Number of team members", + "condition": { + "field": "projectType", + "in": ["team", "enterprise"] + }, + "validation": { + "min": { + "value": 2, + "message": "Team must have at least 2 members" + } + } + }, + { + "name": "budget", + "label": "Budget", + "type": "input", + "inputType": "number", + "placeholder": "Project budget", + "condition": { + "field": "projectType", + "equals": "enterprise" + } + }, + { + "name": "priority", + "label": "Priority Level", + "type": "select", + "required": true, + "options": [ + { "label": "Low", "value": "low" }, + { "label": "Medium", "value": "medium" }, + { "label": "High", "value": "high" }, + { "label": "Critical", "value": "critical" } + ] + }, + { + "name": "deadline", + "label": "Deadline", + "type": "input", + "inputType": "date", + "condition": { + "field": "priority", + "in": ["high", "critical"] + } + }, + { + "name": "description", + "label": "Project Description", + "type": "textarea", + "placeholder": "Describe your project goals and objectives", + "validation": { + "maxLength": { + "value": 500, + "message": "Description must not exceed 500 characters" + } + } + }, + { + "name": "notifications", + "label": "Enable Notifications", + "type": "checkbox", + "description": "Receive updates about project progress" + } + ] + } + } + ] +}`, + 'simple-page': `{ "type": "div", "className": "space-y-4", @@ -733,6 +869,6 @@ export type ExampleKey = keyof typeof examples; export const exampleCategories = { 'Primitives': ['simple-page', 'input-states', 'button-variants'], 'Layouts': ['grid-layout', 'dashboard', 'tabs-demo'], - 'Forms': ['form-demo'], + 'Forms': ['form-demo', 'airtable-form'], 'Data Display': ['calendar-view'] }; diff --git a/packages/components/src/renderers/form/form.tsx b/packages/components/src/renderers/form/form.tsx new file mode 100644 index 000000000..6ae099086 --- /dev/null +++ b/packages/components/src/renderers/form/form.tsx @@ -0,0 +1,325 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { useForm } from 'react-hook-form'; +import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from '@/ui/form'; +import { Button } from '@/ui/button'; +import { renderChildren } from '../../lib/utils'; +import { Alert, AlertDescription } from '@/ui/alert'; +import { AlertCircle, Loader2 } from 'lucide-react'; +import React from 'react'; + +// Form renderer component - Airtable-style feature-complete form +ComponentRegistry.register('form', + ({ schema, className, onAction, ...props }) => { + const { + defaultValues = {}, + fields = [], + submitLabel = 'Submit', + cancelLabel = 'Cancel', + showCancel = false, + layout = 'vertical', + columns = 1, + onSubmit: onSubmitProp, + onChange: onChangeProp, + resetOnSubmit = false, + validationMode = 'onSubmit', + disabled = false, + } = schema; + + // Initialize react-hook-form + const form = useForm({ + defaultValues, + mode: validationMode, + }); + + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [submitError, setSubmitError] = React.useState(null); + + // Watch for form changes + React.useEffect(() => { + if (onChangeProp && onAction) { + const subscription = form.watch((data) => { + onAction({ + type: 'form_change', + data, + formData: data, + }); + }); + return () => subscription.unsubscribe(); + } + }, [form, onAction, onChangeProp]); + + // Handle form submission + const handleSubmit = form.handleSubmit(async (data) => { + setIsSubmitting(true); + setSubmitError(null); + + try { + if (onAction) { + const result = await onAction({ + type: 'form_submit', + data, + formData: data, + }); + + // Check if submission returned an error + if (result?.error) { + setSubmitError(result.error); + return; + } + } + + if (onSubmitProp && typeof onSubmitProp === 'function') { + await onSubmitProp(data); + } + + if (resetOnSubmit) { + form.reset(); + } + } catch (error: any) { + setSubmitError(error?.message || 'An error occurred during submission'); + console.error('Form submission error:', error); + } finally { + setIsSubmitting(false); + } + }); + + // Handle cancel + const handleCancel = () => { + form.reset(); + if (onAction) { + onAction({ + type: 'form_cancel', + data: form.getValues(), + }); + } + }; + + // Determine grid classes based on columns + const gridClass = columns > 1 + ? `grid gap-4 md:grid-cols-${Math.min(columns, 4)}` + : 'space-y-4'; + + return ( +
+ + {/* Form Error Alert */} + {submitError && ( + + + {submitError} + + )} + + {/* Form Fields */} + {schema.children ? ( + // If children are provided directly, render them +
+ {renderChildren(schema.children)} +
+ ) : ( + // Otherwise render fields from schema +
+ {fields.map((field: any, index: number) => { + const { + name, + label, + description, + type = 'input', + required = false, + disabled: fieldDisabled = false, + validation = {}, + condition, + ...fieldProps + } = field; + + // Handle conditional rendering + if (condition) { + const watchField = condition.field; + const watchValue = form.watch(watchField); + + if (condition.equals && watchValue !== condition.equals) { + return null; + } + if (condition.notEquals && watchValue === condition.notEquals) { + return null; + } + if (condition.in && !condition.in.includes(watchValue)) { + return null; + } + } + + // Build validation rules + const rules: any = { + ...validation, + }; + + if (required) { + rules.required = validation.required || `${label || name} is required`; + } + + return ( + ( + + {label && ( + + {label} + {required && *} + + )} + + {/* Render the actual field component based on type */} + {renderFieldComponent(type, { + ...fieldProps, + ...formField, + disabled: disabled || fieldDisabled || isSubmitting, + schema: { type, ...fieldProps }, + })} + + {description && ( + {description} + )} + + + )} + /> + ); + })} +
+ )} + + {/* Form Actions */} + {(schema.showActions !== false) && ( +
+ {showCancel && ( + + )} + +
+ )} +
+ + ); + }, + { + label: 'Form', + inputs: [ + { + name: 'fields', + type: 'array', + label: 'Fields', + description: 'Array of field configurations with name, label, type, validation, etc.' + }, + { + name: 'defaultValues', + type: 'object', + label: 'Default Values', + description: 'Object with default values for form fields' + }, + { name: 'submitLabel', type: 'string', label: 'Submit Button Label', defaultValue: 'Submit' }, + { name: 'cancelLabel', type: 'string', label: 'Cancel Button Label', defaultValue: 'Cancel' }, + { name: 'showCancel', type: 'boolean', label: 'Show Cancel Button', defaultValue: false }, + { + name: 'layout', + type: 'enum', + enum: ['vertical', 'horizontal'], + label: 'Layout', + defaultValue: 'vertical' + }, + { + name: 'columns', + type: 'number', + label: 'Number of Columns', + defaultValue: 1, + description: 'For multi-column layouts (1-4)' + }, + { + name: 'validationMode', + type: 'enum', + enum: ['onSubmit', 'onBlur', 'onChange', 'onTouched', 'all'], + label: 'Validation Mode', + defaultValue: 'onSubmit' + }, + { name: 'resetOnSubmit', type: 'boolean', label: 'Reset After Submit', defaultValue: false }, + { name: 'disabled', type: 'boolean', label: 'Disabled', defaultValue: false }, + { name: 'className', type: 'string', label: 'CSS Class' }, + { name: 'fieldContainerClass', type: 'string', label: 'Field Container CSS Class' } + ], + defaultProps: { + submitLabel: 'Submit', + cancelLabel: 'Cancel', + showCancel: false, + layout: 'vertical', + columns: 1, + validationMode: 'onSubmit', + resetOnSubmit: false, + disabled: false, + fields: [ + { + name: 'name', + label: 'Name', + type: 'input', + required: true, + placeholder: 'Enter your name', + }, + { + name: 'email', + label: 'Email', + type: 'input', + inputType: 'email', + required: true, + placeholder: 'Enter your email', + }, + ], + }, + } +); + +// Helper function to render field components +function renderFieldComponent(type: string, props: any) { + const { schema, ...fieldProps } = props; + + switch (type) { + case 'input': + return ; + + case 'textarea': + return