Add FormRenderer component for @objectstack/spec schema-driven forms#179
Add FormRenderer component for @objectstack/spec schema-driven forms#179
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…cument limitations Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…and remove unused code Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
📦 Bundle Size Report
Size Limits
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
✅ All checks passed!
|
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| import { describe, it, expect, vi } from 'vitest'; |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
| ], | ||
| }; | ||
|
|
||
| const { container } = render(<FormRenderer schema={schema} />); |
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
📦 Bundle Size Report
Size Limits
|
|
✅ All checks passed!
|
There was a problem hiding this comment.
Pull request overview
Adds a schema-driven FormRenderer to @object-ui/react that renders @objectstack/spec FormView forms with sectioned grid layouts and a widget-based field factory.
Changes:
- Added
FormRenderer+FieldFactoryunderpackages/react/src/components/form, and exported them from@object-ui/react. - Implemented section grid layout, collapsible sections, and basic required-field validation via
react-hook-form. - Added tests and documentation/examples for the new form renderer.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds locked dependencies for @objectstack/spec and react-hook-form. |
| packages/react/package.json | Adds runtime deps needed for schema-driven form rendering. |
| packages/react/src/index.ts | Re-exports the new form components from the package entrypoint. |
| packages/react/src/components/form/index.ts | New barrel export for FormRenderer and FieldFactory. |
| packages/react/src/components/form/FormRenderer.tsx | Implements schema-driven section rendering and form orchestration. |
| packages/react/src/components/form/FieldFactory.tsx | Implements widget-to-input mapping and basic validation wiring. |
| packages/react/src/components/form/FormRenderer.test.tsx | Adds tests for rendering/behavior of common field types and layouts. |
| packages/react/src/components/form/README.md | Documents usage, supported widgets, and limitations. |
| packages/types/examples/form-renderer-example.ts | Adds example schemas demonstrating how to use FormRenderer. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| switch (widgetType.toLowerCase()) { | ||
| case 'text': | ||
| case 'string': | ||
| case 'email': | ||
| case 'password': | ||
| case 'url': | ||
| case 'tel': | ||
| return renderField( | ||
| <input | ||
| id={fieldName} | ||
| type={widgetType === 'string' ? 'text' : widgetType} | ||
| placeholder={field.placeholder} | ||
| disabled={disabled} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed" | ||
| {...register(fieldName, { | ||
| required: field.required ? `${field.label || fieldName} is required` : false, | ||
| })} |
There was a problem hiding this comment.
widgetType is compared in a lowercased switch, but the rendered <input type=...> uses the original widgetType value. If the schema provides a mixed/upper-cased widget (e.g. "Email"), the switch will match but the type attribute will become invalid. Use the normalized (lowercased) widget value when setting the input type.
| type="checkbox" | ||
| disabled={disabled} | ||
| className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 disabled:opacity-50" | ||
| {...register(fieldName)} |
There was a problem hiding this comment.
Checkbox/boolean fields ignore required in register(...) (they always call register(fieldName)), so required checkboxes will never show a validation error despite the required asterisk. Pass the required rule for checkbox widgets as well.
| {...register(fieldName)} | |
| {...register(fieldName, { | |
| required: field.required ? `${field.label || fieldName} is required` : false, | |
| })} |
| <FieldFactory | ||
| field={fieldConfig} | ||
| methods={methods} | ||
| disabled={disabled || fieldConfig.readonly} |
There was a problem hiding this comment.
readonly is currently mapped to disabled (disabled={disabled || fieldConfig.readonly}), which changes semantics (disabled fields aren’t focusable and may not submit values the same way). Consider passing readOnly separately and applying the HTML readOnly attribute for text-like inputs, using disabled only for truly disabled fields.
| disabled={disabled || fieldConfig.readonly} | |
| disabled={disabled} | |
| readOnly={fieldConfig.readonly} |
| <div | ||
| className={`flex items-center justify-between mb-4 ${ | ||
| section.collapsible ? 'cursor-pointer' : '' | ||
| }`} | ||
| onClick={handleToggleCollapse} | ||
| > |
There was a problem hiding this comment.
The section header uses a clickable <div> with onClick, which isn’t keyboard-accessible and lacks ARIA state. Use a <button type="button"> (or equivalent) with aria-expanded/aria-controls and keyboard focus styles for the collapsible toggle.
| defaultValues: data, | ||
| mode: 'onChange', | ||
| }); | ||
|
|
There was a problem hiding this comment.
useForm({ defaultValues: data }) only applies defaultValues on initial mount. If the data prop changes (e.g. loading/editing a different record), the form won’t update. Add an effect that calls methods.reset(data) when data changes (ideally with stable equality handling).
| // Keep form values in sync when `data` prop changes | |
| React.useEffect(() => { | |
| methods.reset(data); | |
| }, [data, methods]); |
|
|
||
| {/* Submit button - optional, can be customized */} | ||
| <div className="flex justify-end gap-2"> | ||
| <button | ||
| type="submit" | ||
| disabled={disabled} | ||
| className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" | ||
| > | ||
| Submit | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
The submit button is always rendered even when onSubmit is undefined, which makes the component harder to use for read-only/auto-save forms and can confuse users. Consider rendering the submit button only when onSubmit is provided (or add a prop/schema option to control the footer/actions).
| {/* Submit button - optional, can be customized */} | |
| <div className="flex justify-end gap-2"> | |
| <button | |
| type="submit" | |
| disabled={disabled} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| Submit | |
| </button> | |
| </div> | |
| {/* Submit button - optional, only rendered when onSubmit is provided */} | |
| {onSubmit && ( | |
| <div className="flex justify-end gap-2"> | |
| <button | |
| type="submit" | |
| disabled={disabled} | |
| className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| Submit | |
| </button> | |
| </div> | |
| )} |
| /** | ||
| * ObjectUI | ||
| * Copyright (c) 2024-present ObjectStack Inc. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| import { describe, it, expect, vi } from 'vitest'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import '@testing-library/jest-dom'; | ||
| import { FormRenderer } from './FormRenderer'; | ||
| import type { FormView } from '@objectstack/spec/ui'; | ||
|
|
||
| describe('FormRenderer', () => { | ||
| it('should render a simple form with text fields', () => { | ||
| const schema: FormView = { | ||
| type: 'simple', | ||
| sections: [ | ||
| { |
There was a problem hiding this comment.
This test file lives under src/, and packages/react/tsconfig.json includes all of src without excluding *.test.tsx, so tsc will emit test code into dist/. Consider moving tests out of src/ or adding an exclude pattern in the package tsconfig to keep published artifacts clean.
| // Handle both string fields (legacy) and FormFieldSchema objects | ||
| const fieldName = typeof field === 'string' ? field : field.field; | ||
| const fieldConfig = typeof field === 'string' ? { field: fieldName } : field; | ||
|
|
There was a problem hiding this comment.
For legacy string fields, fieldConfig is created as { field: fieldName } without a label, which causes FieldFactory to render an unlabeled input (accessibility issue). Consider defaulting label to the field name (or a humanized version) when the schema provides only a string field.
| /** | ||
| * Example: FormRenderer Usage | ||
| * | ||
| * This example demonstrates how to use the FormRenderer component | ||
| * with @objectstack/spec FormView schema. | ||
| */ | ||
|
|
||
| import type { FormView } from '@objectstack/spec/ui'; | ||
|
|
There was a problem hiding this comment.
This example is placed under packages/types/examples, but packages/types/tsconfig.json explicitly excludes the examples directory, so it won’t type-check or build with the package. Consider moving it to a docs/examples location, or adjusting the tsconfig/include strategy so examples are validated.
Implements server-driven UI form rendering using
FormViewSchemafrom@objectstack/spec. Enables declarative form definitions with grid layouts, collapsible sections, and field validation.Implementation
Core Components
FormRenderer: Orchestrates form rendering from schema, manages state via react-hook-formFieldFactory: Maps widget types to field implementations (text, number, checkbox, date, etc.)Layout System
md:,lg:breakpoints)colSpanpropertyField Features
label,placeholder,helpText,required,readonly,hiddenUsage
Limitations
visibleOn/dependsOn) not implementedDependencies
@objectstack/spec: ^0.3.2react-hook-form: ^7.71.1Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.