Skip to content

Add FormRenderer component for @objectstack/spec schema-driven forms - #179

Merged
hotlong merged 6 commits into
mainfrom
copilot/refactor-object-form-component
Jan 24, 2026
Merged

Add FormRenderer component for @objectstack/spec schema-driven forms#179
hotlong merged 6 commits into
mainfrom
copilot/refactor-object-form-component

Conversation

CopilotAI commented Jan 24, 2026

Copy link
Copy Markdown
Contributor

Implements server-driven UI form rendering using FormViewSchema from @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-form
  • FieldFactory: Maps widget types to field implementations (text, number, checkbox, date, etc.)

Layout System

  • Responsive grid with 1-4 column support (md:, lg: breakpoints)
  • Column spanning via colSpan property
  • Collapsible sections with toggle state

Field Features

  • Legacy string fields and FormFieldSchema objects
  • Properties: label, placeholder, helpText, required, readonly, hidden
  • Basic validation (required fields)

Usage

import{FormRenderer}from'@object-ui/react';importtype{FormView}from'@objectstack/spec/ui';constschema: FormView={type: 'simple',sections: [{label: 'Contact Info',columns: 2,fields: [{field: 'firstName',label: 'First Name',required: true,widget: 'text'},{field: 'email',label: 'Email',widget: 'email',colSpan: 2}]}]};<FormRendererschema={schema}data={initialData}onSubmit={handleSubmit}/>

Limitations

  • Select widget lacks options support (requires schema extension)
  • Conditional visibility (visibleOn/dependsOn) not implemented
  • Basic validation only (required fields)

Dependencies

  • @objectstack/spec: ^0.3.2
  • react-hook-form: ^7.71.1
Original prompt

基于@objectstack/spec/src/view.zod,重构object-form组件

AI Instruction for Component Development

Prompt:

Create a React component named FormRenderer that renders a form based on the FormViewSchema and FormFieldSchema defined in packages/spec/src/ui/view.zod.ts.

Requirements:

  1. Component Structure:
    • Create packages/react/src/components/form/FormRenderer.tsx.
    • It should accept a schema prop of type FormView and a data prop for the initial form values.
  2. Layout Handling:
    • Support FormSectionSchema to render sections with a grid layout.
    • Respect the columns property (1-4 columns) of the section.
  3. Field Rendering:
    • Iterate through the fields array in each section.
    • Handle both simple string fields (legacy) and FormFieldSchema objects.
    • For FormFieldSchema objects, respect properties like label, placeholder, helpText, readonly, required, and hidden.
    • Implement colSpan support to allow fields to span multiple columns in the grid.
  4. Widget Support:
    • Implement a basic FieldFactory that renders different inputs based on the field type or the widget override property in FormFieldSchema.
    • Start with basic widgets: Text Input, Number Input, Checkbox.
  5. Validation:
    • Use react-hook-form and zod for form state management and validation.

Reference Schemas:
Use the definitions from packages/spec/src/ui/view.zod.ts, specifically FormViewSchema, FormSectionSchema, and FormFieldSchema.


Do you want me to execute this instruction now and create the FormRenderer component for you?


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel

vercelBot commented Jan 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentReviewUpdated (UTC)
objectui-docsErrorErrorJan 24, 2026 7:13am
objectui-siteReadyReadyPreview, CommentJan 24, 2026 7:13am

Request Review

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>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

PackageSizeGzipped
components (index.js)1588.71KB373.06KB
core (index.js)0.53KB0.28KB
plugin-aggrid (AgGridImpl-DKkq6v1B.js)5.09KB1.84KB
plugin-aggrid (index-B6NPAFZx.js)15.66KB4.51KB
plugin-aggrid (index.js)0.18KB0.15KB
plugin-charts (AdvancedChartImpl-DazADGz5.js)74.89KB15.82KB
plugin-charts (BarChart-CRc8MAtI.js)551.60KB127.51KB
plugin-charts (ChartImpl-DVw_7KEd.js)3.17KB1.10KB
plugin-charts (index-CdgY2AuM.js)12.39KB3.83KB
plugin-charts (index.js)0.21KB0.16KB
plugin-editor (MonacoImpl-B7ZgZJJG.js)18.15KB5.59KB
plugin-editor (index-Dl3HAAqu.js)10.07KB3.31KB
plugin-editor (index.js)0.19KB0.15KB
plugin-kanban (KanbanImpl-CUWM-JC-.js)76.50KB20.46KB
plugin-kanban (index-BV3FWhCb.js)11.86KB3.67KB
plugin-kanban (index.js)0.18KB0.15KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js)256.79KB64.50KB
plugin-markdown (index-D_CdfEXQ.js)9.59KB3.16KB
plugin-markdown (index.js)0.19KB0.15KB
react (SchemaRenderer.js)1.44KB0.73KB
react (index.js)0.36KB0.23KB
react (index.test.js)0.34KB0.26KB
types (api-types.js)0.20KB0.18KB
types (app.js)0.20KB0.18KB
types (base.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data.js)0.20KB0.18KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)0.34KB0.25KB
types (layout.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (registry.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

* 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

Unused import vi.
],
};

const { container } = render(<FormRenderer schema={schema} />);

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused variable container.
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

PackageSizeGzipped
components (index.js)1588.71KB373.06KB
core (index.js)0.53KB0.28KB
plugin-aggrid (AgGridImpl-DKkq6v1B.js)5.09KB1.84KB
plugin-aggrid (index-B6NPAFZx.js)15.66KB4.51KB
plugin-aggrid (index.js)0.18KB0.15KB
plugin-charts (AdvancedChartImpl-DazADGz5.js)74.89KB15.82KB
plugin-charts (BarChart-CRc8MAtI.js)551.60KB127.51KB
plugin-charts (ChartImpl-DVw_7KEd.js)3.17KB1.10KB
plugin-charts (index-CdgY2AuM.js)12.39KB3.83KB
plugin-charts (index.js)0.21KB0.16KB
plugin-editor (MonacoImpl-B7ZgZJJG.js)18.15KB5.59KB
plugin-editor (index-Dl3HAAqu.js)10.07KB3.31KB
plugin-editor (index.js)0.19KB0.15KB
plugin-kanban (KanbanImpl-CUWM-JC-.js)76.50KB20.46KB
plugin-kanban (index-BV3FWhCb.js)11.86KB3.67KB
plugin-kanban (index.js)0.18KB0.15KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js)256.79KB64.50KB
plugin-markdown (index-D_CdfEXQ.js)9.59KB3.16KB
plugin-markdown (index.js)0.19KB0.15KB
react (SchemaRenderer.js)1.44KB0.73KB
react (index.js)0.36KB0.23KB
react (index.test.js)0.34KB0.26KB
types (api-types.js)0.20KB0.18KB
types (app.js)0.20KB0.18KB
types (base.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data.js)0.20KB0.18KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)0.34KB0.25KB
types (layout.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (registry.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

CopilotAI changed the title [WIP] Refactor object-form component based on view schemaAdd FormRenderer component for @objectstack/spec schema-driven formsJan 24, 2026
CopilotAI requested a review from hotlongJanuary 24, 2026 07:16
@github-actions

Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

@hotlong
hotlong marked this pull request as ready for review January 24, 2026 07:16
CopilotAI review requested due to automatic review settings January 24, 2026 07:16
@hotlong
hotlong merged commit 069528f into mainJan 24, 2026
14 of 16 checks passed

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a schema-driven FormRenderer to @object-ui/react that renders @objectstack/specFormView forms with sectioned grid layouts and a widget-based field factory.

Changes:

  • Added FormRenderer + FieldFactory under packages/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
FileDescription
pnpm-lock.yamlAdds locked dependencies for @objectstack/spec and react-hook-form.
packages/react/package.jsonAdds runtime deps needed for schema-driven form rendering.
packages/react/src/index.tsRe-exports the new form components from the package entrypoint.
packages/react/src/components/form/index.tsNew barrel export for FormRenderer and FieldFactory.
packages/react/src/components/form/FormRenderer.tsxImplements schema-driven section rendering and form orchestration.
packages/react/src/components/form/FieldFactory.tsxImplements widget-to-input mapping and basic validation wiring.
packages/react/src/components/form/FormRenderer.test.tsxAdds tests for rendering/behavior of common field types and layouts.
packages/react/src/components/form/README.mdDocuments usage, supported widgets, and limitations.
packages/types/examples/form-renderer-example.tsAdds example schemas demonstrating how to use FormRenderer.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +72 to +88
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,
})}

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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)}

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{...register(fieldName)}
{...register(fieldName,{
required: field.required ? `${field.label||fieldName} is required` : false,
})}

Copilot uses AI. Check for mistakes.
<FieldFactory
field={fieldConfig}
methods={methods}
disabled={disabled || fieldConfig.readonly}

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
disabled={disabled||fieldConfig.readonly}
disabled={disabled}
readOnly={fieldConfig.readonly}

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +167
<div
className={`flex items-center justify-between mb-4 ${
section.collapsible ? 'cursor-pointer' : ''
}`}
onClick={handleToggleCollapse}
>

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
defaultValues: data,
mode: 'onChange',
});

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// Keep form values in sync when `data` prop changes
React.useEffect(()=>{
methods.reset(data);
},[data,methods]);

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +112

{/* 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>

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
{/* Submit button - optional, can be customized */}
<divclassName="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&&(
<divclassName="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>
)}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +20
/**
* 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: [
{

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +180 to +183
// Handle both string fields (legacy) and FormFieldSchema objects
const fieldName = typeof field === 'string' ? field : field.field;
const fieldConfig = typeof field === 'string' ? { field: fieldName } : field;

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +17
/**
* Example: FormRenderer Usage
*
* This example demonstrates how to use the FormRenderer component
* with @objectstack/spec FormView schema.
*/

import type { FormView } from '@objectstack/spec/ui';

CopilotAIJan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesdocumentationImprovements or additions to documentationpackage: reactpackage: typestests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@github-advanced-security@hotlong