From 3c5a82baa0d25d85eb89f4b5d2ce367453770f18 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 10:38:09 +0000 Subject: [PATCH 1/8] fix(renderer): merge x-component-props for container components Container renderer now merges x-component-props before sanitizing, allowing components like FormField to receive props from schema. This matches the behavior of field renderer for consistency. --- packages/core/src/registry/renderer-registry.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/registry/renderer-registry.ts b/packages/core/src/registry/renderer-registry.ts index 041eefb..5526302 100644 --- a/packages/core/src/registry/renderer-registry.ts +++ b/packages/core/src/registry/renderer-registry.ts @@ -37,7 +37,9 @@ export const defaultTypeRenderers: Record = { return runtime.create(spec, propsWithChildren); }, 'container': (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); + const xComponentProps = props['x-component-props'] || {}; + const mergedProps = { ...props, ...xComponentProps }; + const sanitized = sanitizePropsForDOM(mergedProps); const propsWithChildren = children && children.length > 0 ? { ...sanitized, children } : sanitized; From 412f12ec8ccd810a357d0e7f3c1de6fdbd1a0324 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 10:38:11 +0000 Subject: [PATCH 2/8] test(utils): add JSON schema field extraction utilities Add extractFormFields and extractRequiredFields functions to extract field names and required fields from form schemas. This enables dynamic test validation based on schema definitions. --- tests/tsconfig.json | 19 +++++++ tests/utils/extractJsonFields.ts | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 tests/tsconfig.json create mode 100644 tests/utils/extractJsonFields.ts diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..ec16afb --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "jsx": "preserve", + "baseUrl": "..", + "paths": { + "@schepta/core": ["./packages/core/src"], + "@schepta/adapter-vanilla": ["./packages/adapters/vanilla/src"], + "@schepta/adapter-react": ["./packages/adapters/react/src"], + "@schepta/adapter-vue": ["./packages/adapters/vue/src"], + "@schepta/factory-vanilla": ["./packages/factories/vanilla/src"], + "@schepta/factory-react": ["./packages/factories/react/src"], + "@schepta/factory-vue": ["./packages/factories/vue/src"] + } + }, + "include": ["**/*"], + "exclude": ["node_modules", "dist"] + } \ No newline at end of file diff --git a/tests/utils/extractJsonFields.ts b/tests/utils/extractJsonFields.ts new file mode 100644 index 0000000..95631f0 --- /dev/null +++ b/tests/utils/extractJsonFields.ts @@ -0,0 +1,93 @@ +import type { FormSchema } from '@schepta/core'; + +function isInputComponent(component: string): boolean { + return component.startsWith('Input') || + component === 'InputText' || + component === 'InputSelect' || + component === 'InputPhone' || + component === 'InputDate' || + component === 'InputTextarea' || + component === 'InputNumber' || + component === 'InputCheckbox'; +} + +/** + * Recursively extracts all form field names from a schema JSON + * A field is identified by having x-component: "FormField" and containing + * a property with an input component (InputText, InputSelect, etc.) + */ +export function extractFormFields(schema: FormSchema | any): string[] { + const fields: string[] = []; + + function traverse(obj: any, path: string[] = []) { + if (!obj || typeof obj !== 'object') { + return; + } + + // Check if this is a FormField component + if (obj['x-component'] === 'FormField' && obj.properties) { + // Look for the actual input field inside the FormField + for (const [key, value] of Object.entries(obj.properties)) { + if (value && typeof value === 'object' && 'x-component' in value) { + const component = (value as any)['x-component']; + // Check if it's an input component + if (component && isInputComponent(component)) { + fields.push(key); + } + } + } + } + + // Recursively traverse properties + if (obj.properties) { + for (const [key, value] of Object.entries(obj.properties)) { + if (value && typeof value === 'object') { + traverse(value, [...path, key]); + } + } + } + } + + traverse(schema); + return fields; +} + +/** + * Extracts required fields from schema based on x-rules.required or required prop + */ +export function extractRequiredFields(schema: FormSchema | any): string[] { + const requiredFields: string[] = []; + + function traverse(obj: any, currentFieldName: string | null = null) { + if (!obj || typeof obj !== 'object') { + return; + } + + // Check if this is an input component with required validation + if (obj['x-component'] && ( + isInputComponent(obj['x-component']) + )) { + // Check for required in x-rules or x-component-props + const isRequired = + obj['x-component-props']?.required === true; + + if (isRequired && currentFieldName) { + requiredFields.push(currentFieldName); + } + } + + // Recursively traverse properties + if (obj.properties) { + for (const [key, value] of Object.entries(obj.properties)) { + if (value && typeof value === 'object') { + // Use the key as field name if we're inside a FormField + const fieldName = obj['x-component'] === 'FormField' ? key : currentFieldName; + traverse(value, fieldName); + } + } + } + } + + traverse(schema); + return requiredFields; +} \ No newline at end of file From 07a7ded05dcc7e5d89d22e496fe45640ffa6f7d6 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 10:38:13 +0000 Subject: [PATCH 3/8] test(e2e): improve React tests with schema-based validation - Update tests to use extracted fields from JSON schemas - Fix select field interaction using selectOption instead of fill - Simplify test structure and remove redundant assertions - Update Playwright config for better test organization --- tests/e2e/react.spec.ts | 88 ++++++++++++++++++++++++++------------ tests/playwright.config.ts | 64 +-------------------------- 2 files changed, 62 insertions(+), 90 deletions(-) diff --git a/tests/e2e/react.spec.ts b/tests/e2e/react.spec.ts index ee786d2..55c77a2 100644 --- a/tests/e2e/react.spec.ts +++ b/tests/e2e/react.spec.ts @@ -1,55 +1,87 @@ import { test, expect } from '@playwright/test'; import simpleFormSchema from '../../instances/form/simple-form.json'; import complexFormSchema from '../../instances/form/complex-form.json'; +import { extractFormFields, extractRequiredFields } from 'tests/utils/extractJsonFields'; + + test.describe('React Form Factory', () => { test.beforeEach(async ({ page, baseURL }) => { - await page.goto(baseURL || 'http://localhost:3000/'); + await page.goto(`${baseURL || 'http://localhost:3000'}/basic`); }); test('should render simple form', async ({ page }) => { + const fields = extractFormFields(simpleFormSchema); + // Wait for form to be rendered await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - // Check if form fields are present - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); + // Check if all form fields from schema are present + for (const field of fields) { + await expect(page.locator(`[data-test-id*="${field}"]`)).toBeVisible(); + } }); test('should render complex form with all field types', async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3000'}/complex`); + await page.click('[data-test-id*="complex-form-tab"]'); + + const fields = extractFormFields(complexFormSchema); + console.log('Extracted fields from complex schema:', fields); // Wait for form to be rendered await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); - // Check different field types - await expect(page.locator('[data-test-id*="email"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="phone"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="userType"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="acceptTerms"]').first()).toBeVisible(); + // Check if all form fields from schema are present + for (const field of fields) { + await expect(page.locator(`[data-test-id*="${field}"]`).first()).toBeVisible(); + } }); - test('should handle form input', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.fill('John'); - - await expect(firstNameField).toHaveValue('John'); + test('should fill form fields', async ({ page }) => { + const fields = extractFormFields(complexFormSchema); + const firstNameField = fields.find(f => f === 'firstName'); + const lastNameField = fields.find(f => f === 'lastName'); + const emailField = fields.find(f => f === 'email'); + const phoneField = fields.find(f => f === 'phone'); + const birthDateField = fields.find(f => f === 'birthDate'); + const userTypeField = fields.find(f => f === 'userType'); + const bioField = fields.find(f => f === 'bio'); + const acceptTermsField = fields.find(f => f === 'acceptTerms'); + + await page.click('[data-test-id*="complex-form-tab"]'); + await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); + + await page.locator(`[data-test-id*="${emailField}"]`).first().fill('john.doe@example.com'); + await page.locator(`[data-test-id*="${phoneField}"]`).first().fill('(123) 456-7890'); + await page.locator(`[data-test-id*="${firstNameField}"]`).first().fill('John'); + await page.locator(`[data-test-id*="${lastNameField}"]`).first().fill('Doe'); + await page.locator(`[data-test-id*="${userTypeField}"]`).first().selectOption('individual'); + await page.locator(`[data-test-id*="${birthDateField}"]`).first().fill('1990-01-01'); + await page.locator(`[data-test-id*="${bioField}"]`).first().fill('I am a software engineer'); + await page.locator(`[data-test-id*="${acceptTermsField}"]`).first().check(); + + await expect(page.locator(`[data-test-id*="${emailField}"]`).first()).toHaveValue('john.doe@example.com'); + await expect(page.locator(`[data-test-id*="${phoneField}"]`).first()).toHaveValue('(123) 456-7890'); + await expect(page.locator(`[data-test-id*="${firstNameField}"]`).first()).toHaveValue('John'); + await expect(page.locator(`[data-test-id*="${lastNameField}"]`).first()).toHaveValue('Doe'); + await expect(page.locator(`[data-test-id*="${userTypeField}"]`).first()).toHaveValue('individual'); + await expect(page.locator(`[data-test-id*="${birthDateField}"]`).first()).toHaveValue('1990-01-01'); + await expect(page.locator(`[data-test-id*="${bioField}"]`).first()).toHaveValue('I am a software engineer'); + await expect(page.locator(`[data-test-id*="${acceptTermsField}"]`).first()).toBeChecked(); + }); test('should validate required fields', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.focus(); - await firstNameField.blur(); - - // Check if validation error appears (implementation dependent) - // This is a placeholder - adjust based on actual validation UI + const requiredFields = extractRequiredFields(complexFormSchema); + + await page.click('[data-test-id*="complex-form-tab"]'); + await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); + + for (const field of requiredFields) { + const fieldLocator = page.locator(`[data-test-id*="${field}"]`).first(); + await expect(fieldLocator).toHaveAttribute('required', ''); + } + }); }); diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index f35f186..8025c2c 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -20,35 +20,7 @@ export default defineConfig({ ...devices['Desktop Chrome'], baseURL: 'http://localhost:3000', }, - }, - { - name: 'material-ui', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3001', - }, - }, - { - name: 'chakra-ui', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3002', - }, - }, - { - name: 'vue', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3010', - }, - }, - { - name: 'vue-vuetify', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3011', - }, - }, + } ], webServer: [ { @@ -58,39 +30,7 @@ export default defineConfig({ timeout: 120 * 1000, stdout: 'ignore', stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-react-material-ui dev', - url: 'http://localhost:3001', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-react-chakra-ui dev', - url: 'http://localhost:3002', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-vue dev', - url: 'http://localhost:3010', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-vue-vuetify dev', - url: 'http://localhost:3011', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, + } ], }); From a547664275ed5fb6ce597eb663db6ae00ed1e2ec Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 10:38:15 +0000 Subject: [PATCH 4/8] chore(tests): remove obsolete e2e test files Remove Chakra UI, Material UI, and provider-specific test files as they are no longer needed in the current test structure. --- tests/e2e/chakra-ui.spec.ts | 66 ----------------------- tests/e2e/material-ui.spec.ts | 44 ---------------- tests/e2e/provider.spec.ts | 99 ----------------------------------- 3 files changed, 209 deletions(-) delete mode 100644 tests/e2e/chakra-ui.spec.ts delete mode 100644 tests/e2e/material-ui.spec.ts delete mode 100644 tests/e2e/provider.spec.ts diff --git a/tests/e2e/chakra-ui.spec.ts b/tests/e2e/chakra-ui.spec.ts deleted file mode 100644 index e568318..0000000 --- a/tests/e2e/chakra-ui.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { test, expect } from '@playwright/test'; -import simpleFormSchema from '../../instances/form/simple-form.json'; -import complexFormSchema from '../../instances/form/complex-form.json'; - -test.describe('Chakra UI Form Factory', () => { - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3002'}/`); - }); - - test('should render simple form with Chakra UI components', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); - }); - - test('should render complex form with all field types', async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3002'}/complex`); - - await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); - - await expect(page.locator('[data-test-id*="email"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="phone"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="userType"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="acceptTerms"]').first()).toBeVisible(); - }); - - test('should handle form input', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // Chakra UI Input component renders directly as an INPUT element - // Get the input element directly by its data-test-id - const firstNameInput = page.locator('input[data-test-id*="firstName"]').first(); - - // Wait for the element to be visible and enabled - await expect(firstNameInput).toBeVisible(); - await expect(firstNameInput).toBeEnabled(); - - // Get initial value - const initialValue = await firstNameInput.inputValue(); - console.log('Initial value:', initialValue); - - // Try to fill the input - try { - await firstNameInput.fill('John'); - console.log('Fill completed'); - } catch (error) { - console.log('Fill error:', error); - throw error; - } - - // Wait a bit for the value to be set - await page.waitForTimeout(500); - - // Check the value after fill - const valueAfterFill = await firstNameInput.inputValue(); - console.log('Value after fill:', valueAfterFill); - - // This will fail if the input is not filled - expect(valueAfterFill).toBe('John'); - }); -}); - diff --git a/tests/e2e/material-ui.spec.ts b/tests/e2e/material-ui.spec.ts deleted file mode 100644 index 4ba7081..0000000 --- a/tests/e2e/material-ui.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { test, expect } from '@playwright/test'; -import simpleFormSchema from '../../instances/form/simple-form.json'; -import complexFormSchema from '../../instances/form/complex-form.json'; - -test.describe('Material UI Form Factory', () => { - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3001'}/`); - }); - - test('should render simple form with Material UI components', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); - - // Check if Material UI styling is applied - const input = firstNameField.locator('input'); - await expect(input).toBeVisible(); - }); - - test('should render complex form with all field types', async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3001'}/complex`); - - await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); - - await expect(page.locator('[data-test-id*="email"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="phone"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="userType"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="acceptTerms"]').first()).toBeVisible(); - }); - - test('should handle form input', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"] input').first(); - await firstNameField.fill('John'); - - await expect(firstNameField).toHaveValue('John'); - }); -}); - diff --git a/tests/e2e/provider.spec.ts b/tests/e2e/provider.spec.ts deleted file mode 100644 index 709029f..0000000 --- a/tests/e2e/provider.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// Only run provider tests for React (provider example is only in React until we have a provider example for other frameworks) -// TODO: atualizar test pois nao temos mais um exemplo direto de provider, agora ele circunda tudo -test.describe('Provider E2E Tests', () => { - test.use({ - // @ts-ignore - project is a valid option in Playwright config - project: 'react' - }); - - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3000'}/provider`); - }); - - test('should render form using components from provider', async ({ page }) => { - // Wait for form to be rendered - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // Verify form container exists (from provider) - const formContainer = page.locator('[data-test-id="FormContainer"]'); - await expect(formContainer).toBeVisible(); - - // Verify form fields are present (components from provider) - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); - }); - - test('should apply middleware from provider to form fields', async ({ page }) => { - // Wait for form to be rendered - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // The middleware adds "[Provider]" prefix to labels - // Verify that labels contain the prefix - const firstNameLabel = page.locator('label[for*="firstName"]'); - await expect(firstNameLabel).toBeVisible(); - - const labelText = await firstNameLabel.textContent(); - expect(labelText).toContain('[Provider]'); - }); - - test('should use externalContext from provider', async ({ page }) => { - // Wait for form to be rendered - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // The schema has: "label": "{{ $externalContext.user.name }}" - // Which should be replaced with "Provider User" from the provider's externalContext - // Note: The middleware adds "[Provider]" prefix, so the label will be "[Provider] Provider User" - const firstNameLabel = page.locator('label[for*="firstName"]'); - await expect(firstNameLabel).toBeVisible(); - - const labelText = await firstNameLabel.textContent(); - // Verify that the template expression was resolved to "Provider User" - expect(labelText).toContain('Provider User'); - // Verify that the middleware prefix is also applied - expect(labelText).toContain('[Provider]'); - }); - - test('should handle form input with provider components', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.fill('Provider Test'); - - await expect(firstNameField).toHaveValue('Provider Test'); - }); - - test('should submit form and display submitted values', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // Fill form fields - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.fill('Jane'); - - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - await lastNameField.fill('Smith'); - - // Submit form - const submitButton = page.locator('[data-test-id="submit-button"]'); - await submitButton.click(); - - // Wait for submitted values section - await page.waitForSelector('text=Valores Submetidos', { timeout: 5000 }); - - // Verify submitted values are displayed - const submittedSection = page.locator('text=Valores Submetidos'); - await expect(submittedSection).toBeVisible(); - - // Verify values in the JSON output - const preElement = page.locator('pre'); - const jsonContent = await preElement.textContent(); - expect(jsonContent).toContain('Jane'); - expect(jsonContent).toContain('Smith'); - }); - -}); - From 3f4afb4130c655569ca0581ec4424c07128a85c7 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 10:38:18 +0000 Subject: [PATCH 5/8] refactor(examples): remove debugger statements from container components Clean up container components by removing debugger statements from FormField, FormSectionContainer, FormSectionGroup, FormSectionGroupContainer, and FormSectionTitle components. --- examples/react/src/basic-ui/components/Containers/FormField.tsx | 2 +- .../src/basic-ui/components/Containers/FormSectionContainer.tsx | 2 +- .../src/basic-ui/components/Containers/FormSectionGroup.tsx | 2 +- .../components/Containers/FormSectionGroupContainer.tsx | 2 +- .../src/basic-ui/components/Containers/FormSectionTitle.tsx | 2 +- .../react/src/chakra-ui/components/Containers/FormField.tsx | 2 +- .../components/Containers/FormSectionGroupContainer.tsx | 2 +- .../react/src/material-ui/components/Containers/FormField.tsx | 2 +- .../components/Containers/FormSectionGroupContainer.tsx | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/react/src/basic-ui/components/Containers/FormField.tsx b/examples/react/src/basic-ui/components/Containers/FormField.tsx index 24e5975..07f551b 100644 --- a/examples/react/src/basic-ui/components/Containers/FormField.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormField.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormField = ({ children, ...props }: any) => { - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx index f5f125f..4da09ab 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormSectionContainer = ({ children, ...props }: any) => { - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx index 59a9d65..e32fc5b 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx @@ -2,5 +2,5 @@ import React from 'react'; export const FormSectionGroup = ({ children, columns, ...props }: any) => { const gridColumns = columns || 'repeat(auto-fit, minmax(200px, 1fr))'; - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx index 3306a08..3d7140e 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormSectionGroupContainer = ({ children, ...props }: any) => { - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx index 4fed989..b0db5aa 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormSectionTitle = ({ 'x-content': content, children, ...props }: any) => { - return

{content || children}

; + return

{content || children}

; }; \ No newline at end of file diff --git a/examples/react/src/chakra-ui/components/Containers/FormField.tsx b/examples/react/src/chakra-ui/components/Containers/FormField.tsx index a4c7270..c38912e 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormField.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormField.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@chakra-ui/react"; export const FormField = ({ children, ...props }: any) => { - return {children}; + return {children}; }; \ No newline at end of file diff --git a/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx b/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx index 45194be..d6ac7bf 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@chakra-ui/react"; export const FormSectionGroupContainer = ({ children, ...props }: any) => { - return {children}; + return {children}; }; \ No newline at end of file diff --git a/examples/react/src/material-ui/components/Containers/FormField.tsx b/examples/react/src/material-ui/components/Containers/FormField.tsx index 32c3ef5..aba9d70 100644 --- a/examples/react/src/material-ui/components/Containers/FormField.tsx +++ b/examples/react/src/material-ui/components/Containers/FormField.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@mui/material"; export const FormField = ({ children, ...props }: any) => { - return {children}; + return {children}; }; diff --git a/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx b/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx index f02690b..397bb78 100644 --- a/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx +++ b/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@mui/material"; export const FormSectionGroupContainer = ({ children, ...props }: any) => { - return {children}; + return {children}; }; From 7a20b9fa16cdb5523bf74e360a6a8f9449c3ffce Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 10:38:19 +0000 Subject: [PATCH 6/8] refactor: update form schemas and styling - Update complex-form.json structure - Refine schema type definitions - Adjust form page layout and styling - Improve CSS organization --- .../src/basic-ui/pages/BasicFormPage.tsx | 12 +- examples/react/src/index.css | 4 + instances/form/complex-form.json | 82 ++-- packages/core/src/schema/schema-types.ts | 418 +++++++++--------- 4 files changed, 277 insertions(+), 239 deletions(-) diff --git a/examples/react/src/basic-ui/pages/BasicFormPage.tsx b/examples/react/src/basic-ui/pages/BasicFormPage.tsx index 9152a21..d3bd562 100644 --- a/examples/react/src/basic-ui/pages/BasicFormPage.tsx +++ b/examples/react/src/basic-ui/pages/BasicFormPage.tsx @@ -22,12 +22,12 @@ export function BasicFormPage() { <> - - - - - - + + + + + + diff --git a/examples/react/src/index.css b/examples/react/src/index.css index ad5c070..83bfa71 100644 --- a/examples/react/src/index.css +++ b/examples/react/src/index.css @@ -12,3 +12,7 @@ code { monospace; } +*, *::before, *::after { + box-sizing: border-box; +} + diff --git a/instances/form/complex-form.json b/instances/form/complex-form.json index 70d7102..bde79a7 100644 --- a/instances/form/complex-form.json +++ b/instances/form/complex-form.json @@ -23,15 +23,12 @@ "basicInfo": { "type": "object", "x-component": "FormSectionGroup", - "x-component-props": { - "columns": "1fr 1fr" - }, "properties": { "email": { "type": "object", "x-component": "FormField", "x-ui": { - "order": 1 + "order": 3 }, "properties": { "email": { @@ -40,11 +37,10 @@ "x-component-props": { "type": "email", "label": "Email Address", - "placeholder": "example@email.com" - }, - "x-rules": { + "placeholder": "example@email.com", "required": true } + } } }, @@ -52,7 +48,7 @@ "type": "object", "x-component": "FormField", "x-ui": { - "order": 2 + "order": 4 }, "properties": { "phone": { @@ -60,7 +56,8 @@ "x-component": "InputPhone", "x-component-props": { "label": "Phone Number", - "placeholder": "(123) 456-7890" + "placeholder": "(123) 456-7890", + "required": true } } } @@ -90,10 +87,43 @@ "basicInfo": { "type": "object", "x-component": "FormSectionGroup", - "x-component-props": { - "columns": "1fr 1fr" - }, "properties": { + "firstName": { + "type": "object", + "x-component": "FormField", + "x-ui": { + "order": 1 + }, + "properties": { + "firstName": { + "type": "string", + "x-component": "InputText", + "x-component-props": { + "label": "First Name", + "placeholder": "Enter your first name", + "required": true + } + } + } + }, + "lastName": { + "type": "object", + "x-component": "FormField", + "x-ui": { + "order": 2 + }, + "properties": { + "lastName": { + "type": "string", + "x-component": "InputText", + "x-component-props": { + "label": "Last Name", + "placeholder": "Enter your last name", + "required": true + } + } + } + }, "userType": { "type": "object", "x-component": "FormField", @@ -136,9 +166,6 @@ "additionalInfo": { "type": "object", "x-component": "FormSectionGroup", - "x-component-props": { - "columns": "1fr" - }, "properties": { "bio": { "type": "object", @@ -157,28 +184,15 @@ } } }, - "age": { - "type": "object", - "x-component": "FormField", - "x-ui": { - "order": 6 - }, - "properties": { - "age": { - "type": "number", - "x-component": "InputNumber", - "x-component-props": { - "label": "Age", - "placeholder": "Enter your age", - "min": 0, - "max": 120 - } - } - } - }, "acceptTerms": { "type": "object", "x-component": "FormField", + "x-component-props": { + "style": { + "display": "flex", + "alignItems": "center" + } + }, "x-ui": { "order": 7 }, diff --git a/packages/core/src/schema/schema-types.ts b/packages/core/src/schema/schema-types.ts index 4ab8b35..801d8ca 100644 --- a/packages/core/src/schema/schema-types.ts +++ b/packages/core/src/schema/schema-types.ts @@ -1,201 +1,221 @@ /** - * Schema Types - * - * Type definitions for form and menu schemas. - * Compatible with JSON Schema format. - */ - -/** - * Base schema property - */ -export interface BaseSchemaProperty { - type: string; - 'x-component'?: string; - 'x-component-props'?: Record; - 'x-rules'?: Record; - 'x-reactions'?: Record; - 'x-ui'?: { - order?: number; - [key: string]: any; - }; - 'x-content'?: any; - 'x-slots'?: Record; -} - -/** - * Form schema structure - */ -export interface FormSchema extends BaseSchemaProperty { - type: 'object'; - $id?: string; - $schema?: string; - properties?: Record; - required?: string[]; -} - -/** - * Form section container - */ -export interface FormSectionContainer extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionContainer'; - properties?: Record; -} - -/** - * Form section title - */ -export interface FormSectionTitle extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionTitle'; - 'x-content': string; -} - -/** - * Form section group container - */ -export interface FormSectionGroupContainer extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionGroupContainer'; - properties?: Record; -} - -/** - * Form section group - */ -export interface FormSectionGroup extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionGroup'; - properties?: Record; -} - -/** - * Form field - */ -export interface FormField extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormField'; - properties?: Record; -} - -/** - * Field component types - */ -export type FieldComponent = - | InputText - | InputSelect - | InputCheckbox - | InputDate - | InputCpf - | InputPhone - | InputTextarea - | InputNumber - | InputAutocomplete; - -/** - * Input text - */ -export interface InputText extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputText'; -} - -/** - * Input select - */ -export interface InputSelect extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputSelect'; -} - -/** - * Input checkbox - */ -export interface InputCheckbox extends BaseSchemaProperty { - type: 'boolean'; - 'x-component': 'InputCheckbox'; -} - -/** - * Date picker - */ -export interface InputDate extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputDate'; -} - -/** - * Input CPF - */ -export interface InputCpf extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputCpf'; -} - -/** - * Input phone - */ -export interface InputPhone extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputPhone'; -} - -/** - * Input autocomplete - */ -export interface InputAutocomplete extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputAutocomplete'; -} - -/** - * Input textarea - */ -export interface InputTextarea extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputTextarea'; -} - -/** - * Input number - */ -export interface InputNumber extends BaseSchemaProperty { - type: 'number'; - 'x-component': 'InputNumber'; -} - -/** - * Menu schema structure - */ -export interface MenuSchema extends BaseSchemaProperty { - type: 'object'; - properties?: Record; -} - -/** - * Menu container - */ -export interface MenuContainer extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'MenuContainer'; - 'x-content'?: { - items?: MenuItem[]; - }; -} - -/** - * Menu item - */ -export interface MenuItem extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'MenuLink' | 'MenuButton'; - 'x-component-props'?: { - href?: string; - onClick?: string; - [key: string]: any; - }; - 'x-content'?: { - text?: string; - }; + * Schema de formulário + */ +export interface FormSchema { + $id: string + $schema?: string + type: "object" + properties: { + [k: string]: FormSectionContainer + } + "x-component": "FormContainer" +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ +export interface FormSectionContainer { + type: "object" + "x-ui": { + /** + * Order of the section in the form (lower values appear first) + */ + order: number + } + properties: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ + [k: string]: FormSectionTitle | FormSectionGroupContainer + } + "x-component": "FormSectionContainer" + "x-component-props"?: { + [k: string]: unknown + } +} +export interface FormSectionTitle { + type: "object" + "x-slots"?: { + [k: string]: unknown + } + "x-content": string + "x-component": "FormSectionTitle" + "x-component-props"?: { + [k: string]: unknown + } +} +export interface FormSectionGroupContainer { + type: "object" + properties: { + [k: string]: FormSectionGroup + } + "x-component": "FormSectionGroupContainer" + "x-component-props"?: { + [k: string]: unknown + } +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ +export interface FormSectionGroup { + type: "object" + properties: { + [k: string]: FormField + } + "x-component": "FormSectionGroup" + "x-component-props"?: { + [k: string]: unknown + } +} +/** + * FormField wrapper (Grid) reutilizável para qualquer tipo de input + * + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ +export interface FormField { + type: "object" + "x-component": "FormField" + "x-ui": { + /** + * Order of the field in the form (lower values appear first) + */ + order: number + } + properties: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^[a-zA-Z_][a-zA-Z0-9_]*$". + */ + [k: string]: + | InputText + | InputSelect + | InputCheckbox + | InputDate + | InputPhone + | InputAutocomplete + | InputTextarea + | InputNumber + } + "x-component-props"?: { + [k: string]: unknown + } +} +export interface InputText { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputText" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputSelect { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputSelect" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputCheckbox { + type: "boolean" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputCheckbox" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputDate { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputDate" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputPhone { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputPhone" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputAutocomplete { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputAutocomplete" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputTextarea { + type: "string" + "x-component": "InputTextarea" + "x-rules"?: { + [k: string]: unknown + } + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + [k: string]: unknown + } +} +export interface InputNumber { + type: "number" + "x-component": "InputNumber" + "x-rules"?: { + [k: string]: unknown + } + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + [k: string]: unknown + } } - From b1807d5d20e0effc17111819d15fe876d43c2aa7 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 11:17:26 +0000 Subject: [PATCH 7/8] fix(inputs): use value prop instead of checked for checkbox components Change InputCheckbox components in Chakra UI and Material UI to use the standard 'value' prop instead of 'checked' for consistency with other input components and the form adapter API. --- .../react/src/chakra-ui/components/Inputs/InputCheckbox.tsx | 4 ++-- .../react/src/material-ui/components/Inputs/InputCheckbox.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx b/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx index cc70e2b..fdf0341 100644 --- a/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx +++ b/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx @@ -2,12 +2,12 @@ import React from "react"; import { Checkbox } from "@chakra-ui/react"; export const InputCheckbox = React.forwardRef((props, ref) => { - const { label, name, checked, onChange, ...rest } = props; + const { label, name, value, onChange, ...rest } = props; return ( onChange?.(e.target.checked)} data-test-id={name} {...rest} diff --git a/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx b/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx index cb83cc7..e152c6a 100644 --- a/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx +++ b/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx @@ -4,14 +4,14 @@ import { Checkbox } from "@mui/material"; export const InputCheckbox = React.forwardRef( (props, ref) => { - const { label, name, checked, onChange, ...rest } = props; + const { label, name, value, onChange, ...rest } = props; return ( onChange?.(e.target.checked)} data-test-id={name} {...rest} From 52d22bab90d4f3860efe7c7dc79a26058a193b29 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 26 Jan 2026 11:17:29 +0000 Subject: [PATCH 8/8] refactor(containers): simplify FormSectionGroup implementation - Remove GridItem wrapper in Chakra UI version - Replace Grid container/item pattern with Box in Material UI version - Use CSS Grid directly for simpler layout - Add data-test-id attribute for better testability - Maintain same visual layout with cleaner code --- .../Containers/FormSectionGroup.tsx | 8 +++---- .../Containers/FormSectionGroup.tsx | 24 +++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx b/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx index 7bb2079..f365584 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx @@ -1,15 +1,13 @@ import React from "react"; -import { Grid, GridItem } from "@chakra-ui/react"; +import { Grid } from "@chakra-ui/react"; export const FormSectionGroup = ({ children, 'x-component-props': xComponentProps, ...props }: any) => { const columns = xComponentProps?.columns || '1fr 1fr'; const gridColumns = columns === '1fr' ? 1 : 2; return ( - - {React.Children.map(children, (child, index) => ( - {child} - ))} + + {children} ); }; \ No newline at end of file diff --git a/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx b/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx index bb357dc..2017d52 100644 --- a/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx +++ b/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Grid } from "@mui/material"; +import { Box, Grid } from "@mui/material"; export const FormSectionGroup = ({ children, @@ -7,23 +7,17 @@ export const FormSectionGroup = ({ ...props }: any) => { const columns = xComponentProps?.columns || "1fr 1fr"; - const gridColumns = columns === "1fr" ? 12 : 6; + const gridColumns = columns === "1fr" ? 1 : 2; return ( - - {React.Children.map(children, (child) => ( - - {child} - - ))} - + {children} + ); };