Skip to content

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

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

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

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 24, 2026

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';
import type { FormView } from '@objectstack/spec/ui';

const schema: 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 }
    ]
  }]
};

<FormRenderer 
  schema={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
Copy link
Copy Markdown

vercel bot commented Jan 24, 2026

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

Project Deployment Review Updated (UTC)
objectui-docs Error Error Jan 24, 2026 7:13am
objectui-site Ready Ready Preview, Comment Jan 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

Package Size Gzipped
components (index.js) 1588.71KB 373.06KB
core (index.js) 0.53KB 0.28KB
plugin-aggrid (AgGridImpl-DKkq6v1B.js) 5.09KB 1.84KB
plugin-aggrid (index-B6NPAFZx.js) 15.66KB 4.51KB
plugin-aggrid (index.js) 0.18KB 0.15KB
plugin-charts (AdvancedChartImpl-DazADGz5.js) 74.89KB 15.82KB
plugin-charts (BarChart-CRc8MAtI.js) 551.60KB 127.51KB
plugin-charts (ChartImpl-DVw_7KEd.js) 3.17KB 1.10KB
plugin-charts (index-CdgY2AuM.js) 12.39KB 3.83KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-editor (MonacoImpl-B7ZgZJJG.js) 18.15KB 5.59KB
plugin-editor (index-Dl3HAAqu.js) 10.07KB 3.31KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-kanban (KanbanImpl-CUWM-JC-.js) 76.50KB 20.46KB
plugin-kanban (index-BV3FWhCb.js) 11.86KB 3.67KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js) 256.79KB 64.50KB
plugin-markdown (index-D_CdfEXQ.js) 9.59KB 3.16KB
plugin-markdown (index.js) 0.19KB 0.15KB
react (SchemaRenderer.js) 1.44KB 0.73KB
react (index.js) 0.36KB 0.23KB
react (index.test.js) 0.34KB 0.26KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 0.20KB 0.18KB
types (base.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data.js) 0.20KB 0.18KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 0.34KB 0.25KB
types (layout.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (registry.js) 0.20KB 0.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

Package Size Gzipped
components (index.js) 1588.71KB 373.06KB
core (index.js) 0.53KB 0.28KB
plugin-aggrid (AgGridImpl-DKkq6v1B.js) 5.09KB 1.84KB
plugin-aggrid (index-B6NPAFZx.js) 15.66KB 4.51KB
plugin-aggrid (index.js) 0.18KB 0.15KB
plugin-charts (AdvancedChartImpl-DazADGz5.js) 74.89KB 15.82KB
plugin-charts (BarChart-CRc8MAtI.js) 551.60KB 127.51KB
plugin-charts (ChartImpl-DVw_7KEd.js) 3.17KB 1.10KB
plugin-charts (index-CdgY2AuM.js) 12.39KB 3.83KB
plugin-charts (index.js) 0.21KB 0.16KB
plugin-editor (MonacoImpl-B7ZgZJJG.js) 18.15KB 5.59KB
plugin-editor (index-Dl3HAAqu.js) 10.07KB 3.31KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-kanban (KanbanImpl-CUWM-JC-.js) 76.50KB 20.46KB
plugin-kanban (index-BV3FWhCb.js) 11.86KB 3.67KB
plugin-kanban (index.js) 0.18KB 0.15KB
plugin-markdown (MarkdownImpl-BRkYjVWf.js) 256.79KB 64.50KB
plugin-markdown (index-D_CdfEXQ.js) 9.59KB 3.16KB
plugin-markdown (index.js) 0.19KB 0.15KB
react (SchemaRenderer.js) 1.44KB 0.73KB
react (index.js) 0.36KB 0.23KB
react (index.test.js) 0.34KB 0.26KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 0.20KB 0.18KB
types (base.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data.js) 0.20KB 0.18KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 0.34KB 0.25KB
types (layout.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (registry.js) 0.20KB 0.18KB

Size Limits

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

Copilot AI changed the title [WIP] Refactor object-form component based on view schema Add FormRenderer component for @objectstack/spec schema-driven forms Jan 24, 2026
Copilot AI requested a review from hotlong January 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
Copilot AI review requested due to automatic review settings January 24, 2026 07:16
@hotlong hotlong merged commit 069528f into main Jan 24, 2026
14 of 16 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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/spec FormView 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
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

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,
})}
Copy link

Copilot AI Jan 24, 2026

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)}
Copy link

Copilot AI Jan 24, 2026

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}
Copy link

Copilot AI Jan 24, 2026

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}
>
Copy link

Copilot AI Jan 24, 2026

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',
});

Copy link

Copilot AI Jan 24, 2026

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>
Copy link

Copilot AI Jan 24, 2026

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 */}
<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>
)}

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: [
{
Copy link

Copilot AI Jan 24, 2026

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;

Copy link

Copilot AI Jan 24, 2026

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';

Copy link

Copilot AI Jan 24, 2026

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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants