From eded177500c8639d73c19b06306d621218d0cb96 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 10:03:26 +0000 Subject: [PATCH 1/8] refactor: rename orchestrator function to more apropriate name --- packages/core/README.md | 6 +++--- packages/core/src/index.ts | 2 +- .../component-orchestrator.ts} | 6 +++--- packages/core/src/validation/schema-traversal.ts | 4 ++-- packages/factories/react/src/form-factory.tsx | 4 ++-- packages/factories/vanilla/src/form-factory.ts | 4 ++-- packages/factories/vue/src/form-factory.ts | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) rename packages/core/src/{orchestrator/renderer-orchestrator.ts => orchestrators/component-orchestrator.ts} (98%) diff --git a/packages/core/README.md b/packages/core/README.md index 8026bdc..252773b 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -14,7 +14,7 @@ npm install @schepta/core import { ComponentRegistry, RendererRegistry, - RendererOrchestrator, + componentOrchestrator, createComponentSpec } from '@schepta/core'; @@ -30,8 +30,8 @@ registry.register( }) ); -// Create renderer orchestrator -const orchestrator = new RendererOrchestrator(registry); +// Create component orchestrator +const orchestrator = new componentOrchestrator(registry); ``` ## Documentation diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b0147f3..3a685de 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,7 +17,7 @@ export * from './registries/renderer-registry'; export * from './defaults/register-default-renderers' // Orchestrator -export * from './orchestrator/renderer-orchestrator'; +export * from './orchestrators/component-orchestrator'; // Middleware system export * from './middleware/types'; diff --git a/packages/core/src/orchestrator/renderer-orchestrator.ts b/packages/core/src/orchestrators/component-orchestrator.ts similarity index 98% rename from packages/core/src/orchestrator/renderer-orchestrator.ts rename to packages/core/src/orchestrators/component-orchestrator.ts index 4342b98..7ff073a 100644 --- a/packages/core/src/orchestrator/renderer-orchestrator.ts +++ b/packages/core/src/orchestrators/component-orchestrator.ts @@ -1,5 +1,5 @@ /** - * Renderer Orchestrator + * Component Orchestrator * * Framework-agnostic orchestrator that processes schemas, resolves components, * applies middlewares, and coordinates rendering. @@ -86,10 +86,10 @@ export function resolveSpec( } /** - * Renderer orchestrator factory + * Component orchestrator factory * Returns a renderer function that processes schemas */ -export function createRendererOrchestrator( +export function createComponentOrchestrator( getFactorySetup: () => FactorySetupResult, runtime: RuntimeAdapter ) { diff --git a/packages/core/src/validation/schema-traversal.ts b/packages/core/src/validation/schema-traversal.ts index bd2ab05..7827c20 100644 --- a/packages/core/src/validation/schema-traversal.ts +++ b/packages/core/src/validation/schema-traversal.ts @@ -129,7 +129,7 @@ export type FieldVisitor = (field: FieldNode) => void; * Traverse a FormSchema and call visitor for each input field found. * Tracks the full path to each field matching the orchestrator's name path logic. * - * Path building rules (matching renderer-orchestrator.ts): + * Path building rules (matching component-orchestrator.ts): * - Direct children of FormContainer (root properties) are included * - Field components (type: 'field') add their key to the path * - Other containers just pass through the parent path @@ -201,7 +201,7 @@ export function traverseFormSchema(schema: FormSchema, visitor: FieldVisitor): v const childComponent = (value as any)['x-component']; // Determine if this key should be added to the path - // This matches the orchestrator's logic in renderer-orchestrator.ts: + // This matches the orchestrator's logic in component-orchestrator.ts: // - Direct children of FormContainer (root properties) are included // - Only FormSectionContainer adds to path (it's a root property container) // - Other containers (FormSectionGroup, FormSectionGroupContainer, etc.) don't add to path diff --git a/packages/factories/react/src/form-factory.tsx b/packages/factories/react/src/form-factory.tsx index 00655e0..4c7126f 100644 --- a/packages/factories/react/src/form-factory.tsx +++ b/packages/factories/react/src/form-factory.tsx @@ -13,7 +13,7 @@ import type { } from "@schepta/core"; import { createReactRuntimeAdapter } from "@schepta/adapter-react"; import { - createRendererOrchestrator, + createComponentOrchestrator, type FactorySetupResult, setFactoryDefaultComponents, } from "@schepta/core"; @@ -206,7 +206,7 @@ export const FormFactory = forwardRef( }; }; - return createRendererOrchestrator(getFactorySetup, runtime); + return createComponentOrchestrator(getFactorySetup, runtime); }, [ mergedConfig.components, mergedConfig.customComponents, diff --git a/packages/factories/vanilla/src/form-factory.ts b/packages/factories/vanilla/src/form-factory.ts index cb25f95..23da3bd 100644 --- a/packages/factories/vanilla/src/form-factory.ts +++ b/packages/factories/vanilla/src/form-factory.ts @@ -7,7 +7,7 @@ import { createVanillaRuntimeAdapter } from '@schepta/adapter-vanilla'; import { createVanillaFormAdapter } from '@schepta/adapter-vanilla'; import { getScheptaContext } from '@schepta/adapter-vanilla'; import { - createRendererOrchestrator, + createComponentOrchestrator, setFactoryDefaultComponents, createComponentSpec, } from '@schepta/core'; @@ -105,7 +105,7 @@ export function createFormFactory(options: FormFactoryOptions): FormFactoryResul }; }; - const renderer = createRendererOrchestrator(getFactorySetup, runtime); + const renderer = createComponentOrchestrator(getFactorySetup, runtime); const rootComponentKey = (options.schema as any)['x-component'] || 'FormContainer'; // Render form diff --git a/packages/factories/vue/src/form-factory.ts b/packages/factories/vue/src/form-factory.ts index b3e7007..d0da6f8 100644 --- a/packages/factories/vue/src/form-factory.ts +++ b/packages/factories/vue/src/form-factory.ts @@ -8,7 +8,7 @@ import { createVueRuntimeAdapter } from '@schepta/adapter-vue'; import { createVueFormAdapter } from '@schepta/adapter-vue'; import { useScheptaContext } from '@schepta/adapter-vue'; import { - createRendererOrchestrator, + createComponentOrchestrator, type FactorySetupResult, setFactoryDefaultComponents, createComponentSpec, @@ -211,7 +211,7 @@ export function createFormFactory(defaultProps: FormFactoryProps) { }; const renderer = computed(() => - createRendererOrchestrator(getFactorySetup, runtime.value) + createComponentOrchestrator(getFactorySetup, runtime.value) ); const rootComponentKey = computed(() => (props.schema as any)['x-component'] || 'FormContainer'); From b3fb25c62b2c3304e2e7a4aff2e7c8e099429541 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 10:06:06 +0000 Subject: [PATCH 2/8] refactor: resolve full hierarquical merge logic to renderers in useMergedScheptaConfig --- packages/factories/react/src/hooks/use-merged-config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/factories/react/src/hooks/use-merged-config.ts b/packages/factories/react/src/hooks/use-merged-config.ts index 2f25a49..0b4da7e 100644 --- a/packages/factories/react/src/hooks/use-merged-config.ts +++ b/packages/factories/react/src/hooks/use-merged-config.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { getFactoryDefaultComponents, type ComponentSpec, type MiddlewareFn } from '@schepta/core'; +import { defaultRenderers, getFactoryDefaultComponents, type ComponentSpec, type MiddlewareFn } from '@schepta/core'; import { useScheptaContext } from '@schepta/adapter-react'; export interface MergedConfigInput { @@ -36,6 +36,7 @@ export interface MergedConfig { export function useMergedScheptaConfig(props: MergedConfigInput): MergedConfig { const providerConfig = useScheptaContext(); + // local > provider > default return useMemo(() => ({ components: { ...getFactoryDefaultComponents(), @@ -47,6 +48,7 @@ export function useMergedScheptaConfig(props: MergedConfigInput): MergedConfig { ...(props.customComponents || {}), }, renderers: { + ...defaultRenderers, ...(providerConfig?.renderers || {}), ...(props.renderers || {}), }, From acc4f7d884828a4e426b5cf0d94c89f129b23cf7 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 13:13:59 +0000 Subject: [PATCH 3/8] refactor(core): add renderer orchestrator and update renderer registry - Add createRendererOrchestrator and RendererSpec - Refactor renderer registry for orchestrator-based flow - Update component orchestrator and provider Co-authored-by: Cursor --- .../defaults/register-default-renderers.ts | 3 +- .../orchestrators/component-orchestrator.ts | 26 ++--- .../orchestrators/renderer-orchestrator.ts | 55 +++++++++ packages/core/src/provider/provider.ts | 9 +- .../core/src/registries/component-registry.ts | 6 +- .../core/src/registries/renderer-registry.ts | 107 ++++++++---------- packages/core/src/runtime/types.ts | 15 ++- 7 files changed, 130 insertions(+), 91 deletions(-) create mode 100644 packages/core/src/orchestrators/renderer-orchestrator.ts diff --git a/packages/core/src/defaults/register-default-renderers.ts b/packages/core/src/defaults/register-default-renderers.ts index 567d82e..3cd1b6d 100644 --- a/packages/core/src/defaults/register-default-renderers.ts +++ b/packages/core/src/defaults/register-default-renderers.ts @@ -20,7 +20,7 @@ export const defaultRenderers: Record = { : props; return runtime.create(spec, propsWithChildren); }, - 'field-wrapper': (spec, props, runtime, children) => { + button: (spec, props, runtime, children) => { const propsWithChildren = children && children.length > 0 ? { ...props, children } : props; @@ -50,5 +50,4 @@ export const defaultRenderers: Record = { : props; return runtime.create(spec, propsWithChildren); }, - } \ No newline at end of file diff --git a/packages/core/src/orchestrators/component-orchestrator.ts b/packages/core/src/orchestrators/component-orchestrator.ts index 7ff073a..3e8e6a6 100644 --- a/packages/core/src/orchestrators/component-orchestrator.ts +++ b/packages/core/src/orchestrators/component-orchestrator.ts @@ -5,20 +5,20 @@ * applies middlewares, and coordinates rendering. */ -import type { RuntimeAdapter, ComponentSpec, DebugContextValue } from '../runtime/types'; +import type { RuntimeAdapter, ComponentSpec, DebugContextValue, RenderResult } from '../runtime/types'; import type { FormAdapter } from '../forms/types'; import type { MiddlewareFn, MiddlewareContext } from '../middleware/types'; -import { getRendererForType } from '../registries/renderer-registry'; import { applyMiddlewares } from '../middleware/types'; import { processValue } from '../expressions/template-processor'; import { createDefaultResolver } from '../expressions/variable-resolver'; +import { createRendererOrchestrator } from './renderer-orchestrator'; /** * Resolution result - successful component resolution */ export interface ResolutionSuccess { componentSpec: ComponentSpec; - rendererFn: ReturnType; + rendererFn: (componentSpec: ComponentSpec, props: Record, runtime: RuntimeAdapter, children?: any[]) => RenderResult; } /** @@ -49,7 +49,7 @@ export function resolveSpec( componentKey: string, components: Record, customComponents?: Record, - localRenderers?: Partial>, + renderers?: Partial>, debugEnabled?: boolean ): ResolutionResult { const componentName = schema['x-component'] || componentKey; @@ -58,9 +58,9 @@ export function resolveSpec( let componentSpec = null; if (isCustomComponent && customComponents) { - componentSpec = customComponents[componentKey]; + componentSpec = customComponents[componentKey]; } else { - componentSpec = components[componentName]; + componentSpec = components[componentName]; } if (!componentSpec) { @@ -72,12 +72,7 @@ export function resolveSpec( } const componentType = componentSpec.type || 'field'; - const rendererFn = getRendererForType( - componentType, - undefined, - localRenderers as any, - debugEnabled - ); + const rendererFn = createRendererOrchestrator(renderers?.[componentType]); return { componentSpec, @@ -103,7 +98,7 @@ export function createComponentOrchestrator( const { components, customComponents, - renderers: localRenderers, + renderers, externalContext, state, middlewares, @@ -144,7 +139,7 @@ export function createComponentOrchestrator( componentKey, components, customComponents, - localRenderers, + renderers, debug?.isEnabled ); @@ -234,5 +229,4 @@ export function createComponentOrchestrator( // Final Rendering using renderer function with children return rendererFn(componentSpec, mergedProps, runtime, children.length > 0 ? children : undefined); }; -} - +} \ No newline at end of file diff --git a/packages/core/src/orchestrators/renderer-orchestrator.ts b/packages/core/src/orchestrators/renderer-orchestrator.ts new file mode 100644 index 0000000..c2a298f --- /dev/null +++ b/packages/core/src/orchestrators/renderer-orchestrator.ts @@ -0,0 +1,55 @@ +/** + * Renderer Orchestrator + * + * Orchestrates the rendering of a renderer component. + * + */ + +import type { ComponentSpec, RuntimeAdapter, RendererSpec } from '../runtime/types'; + +export function createRendererOrchestrator( + rendererSpec: RendererSpec, +) { + return function render( + componentSpec: ComponentSpec, + props: Record, + runtime: RuntimeAdapter, + children?: any[] + ) { + // Extract name from props + const name = props.name || ''; + + // If this is a field component and we have a name, use the renderer component + if (name && componentSpec.type === 'field') { + const Component = componentSpec.component(props, runtime); + + const xComponentProps = props['x-component-props'] || {}; + + const componentProps = { + ...xComponentProps, + name, // Ensure name is passed + ...(props.externalContext ? { externalContext: props.externalContext } : {}), + ...(props.schema ? { schema: props.schema } : {}), + ...(props.componentKey ? { componentKey: props.componentKey } : {}), + ...(props['data-test-id'] ? { 'data-test-id': props['data-test-id'] } : {}), + }; + return runtime.create(rendererSpec, { + name, + component: Component as any, + componentProps, + children, + }); + } + + // For non-field components or fields without name, use default rendering + const xComponentProps = props['x-component-props'] || {}; + const mergedProps = { ...props, ...xComponentProps }; + + const propsWithChildren = + children && children.length > 0 + ? { ...mergedProps, children } + : mergedProps; + + return runtime.create(componentSpec, propsWithChildren); + } +} \ No newline at end of file diff --git a/packages/core/src/provider/provider.ts b/packages/core/src/provider/provider.ts index 9364a39..f7ffab8 100644 --- a/packages/core/src/provider/provider.ts +++ b/packages/core/src/provider/provider.ts @@ -6,7 +6,6 @@ import type { ProviderConfig } from './types'; import { defaultDebugConfig } from './types'; -import { getRendererRegistry } from '../registries/renderer-registry'; /** * Merge two provider configurations hierarchically @@ -28,10 +27,10 @@ export function mergeProviderConfigs( }; // Merge renderers using registry function (handles hierarchical merging) - const mergedRenderers = getRendererRegistry( - global.renderers, - local.renderers - ); + const mergedRenderers = { + ...global.renderers, + ...local.renderers + } // Merge middlewares (local appended after global) const mergedMiddlewares = [ diff --git a/packages/core/src/registries/component-registry.ts b/packages/core/src/registries/component-registry.ts index 703a2b4..22e3271 100644 --- a/packages/core/src/registries/component-registry.ts +++ b/packages/core/src/registries/component-registry.ts @@ -12,7 +12,7 @@ import type { ComponentSpec, ComponentType } from '../runtime/types'; */ export const defaultTypeProps: Record> = { field: { fullWidth: true }, - 'field-wrapper': {}, + button: {}, 'container': {}, content: {}, addon: {}, @@ -55,14 +55,14 @@ export function getFactoryDefaultComponents(): Record { */ export function createComponentSpec(config: { id: string; - factory: ComponentSpec['factory']; + component: ComponentSpec['component']; type: ComponentType; displayName?: string; defaultProps?: Record; }): ComponentSpec { return { id: config.id, - factory: config.factory, + component: config.component, type: config.type, displayName: config.displayName || config.id, defaultProps: config.defaultProps || (config.type ? defaultTypeProps[config.type] : {}), diff --git a/packages/core/src/registries/renderer-registry.ts b/packages/core/src/registries/renderer-registry.ts index aaf254d..dcc95ea 100644 --- a/packages/core/src/registries/renderer-registry.ts +++ b/packages/core/src/registries/renderer-registry.ts @@ -5,75 +5,58 @@ * Renderers are functions that wrap components with additional rendering logic. */ -import { defaultRenderers, RendererFn } from '../defaults/register-default-renderers'; -import type { ComponentType } from '../runtime/types'; - +import type { RendererSpec } from '../runtime/types'; /** - * Get unified renderer registry with hierarchical merging - * - * Priority order: local > global > default + * Props passed to field renderer components (framework-agnostic interface) + * Each framework (React, Vue) will type the component appropriately. */ -export function getRendererRegistry( - globalRenderers?: Partial>, - localRenderers?: Partial> -): Record { - // Start with built-in renderers - let merged = { ...defaultRenderers }; +export interface FieldRendererProps { + /** Field name (supports dot notation for nested fields) */ + name: string; + component: any; + /** Props to pass to the field component */ + componentProps: Record; + /** Optional children */ + children?: any; +} - // Apply global renderers (from provider) - if (globalRenderers) { - Object.keys(globalRenderers).forEach(type => { - const renderer = globalRenderers[type as ComponentType]; - if (renderer) { - merged[type as ComponentType] = renderer; - } - }); - } +let factoryDefaultRenderers: Record = {}; - // Apply local renderers (maximum priority) - if (localRenderers) { - Object.keys(localRenderers).forEach(type => { - const renderer = localRenderers[type as ComponentType]; - if (renderer) { - merged[type as ComponentType] = renderer; - } - }); - } +export function setFactoryDefaultRenderers(renderers: Record): void { + factoryDefaultRenderers = renderers; +} - return merged; +export function getFactoryDefaultRenderers(): Record { + return factoryDefaultRenderers; } /** - * Get effective renderer for a component type - * Includes debug logging + * Create a renderer spec from a component. + * Similar API to createComponentSpec - user just passes the component. + * + * @example Using with React + * ```tsx + * import { createRendererSpec } from '@schepta/core'; + * import { RHFFieldRenderer } from './rhf/RHFFieldRenderer'; + * + * const renderers = { + * field: createRendererSpec({ + * id: 'rhf-field-renderer', + * type: 'field', + * component: RHFFieldRenderer, + * }), + * }; + * + * + * ``` */ -export function getRendererForType( - type: ComponentType, - globalRenderers?: Partial>, - localRenderers?: Partial>, - debugEnabled?: boolean -): RendererFn { - // Local renderers have maximum priority - if (localRenderers?.[type]) { - if (debugEnabled) { - console.log(`Renderer resolved from local: ${type}`); - } - return localRenderers[type]!; - } - - // Global renderers second priority - if (globalRenderers?.[type]) { - if (debugEnabled) { - console.log(`Renderer resolved from global: ${type}`); - } - return globalRenderers[type]!; - } - - // Default renderer last - if (debugEnabled) { - console.log(`Renderer resolved from default: ${type}`); - } - return defaultRenderers[type]; -} - +export function createRendererSpec( + config: RendererSpec +): RendererSpec { + return { + id: config.id, + type: config.type, + component: config.component, + }; +} \ No newline at end of file diff --git a/packages/core/src/runtime/types.ts b/packages/core/src/runtime/types.ts index b9c4789..32d32b2 100644 --- a/packages/core/src/runtime/types.ts +++ b/packages/core/src/runtime/types.ts @@ -23,7 +23,16 @@ export interface ComponentSpec { /** Default props to apply */ defaultProps?: Record; /** Factory function to create component instance */ - factory: ComponentFactory; + component: ComponentFactory; +} + +export interface RendererSpec { + /** Renderer identifier */ + id: string; + /** Component type this renderer handles */ + type: ComponentType; + /** The renderer component */ + component: ComponentFactory; } /** @@ -51,7 +60,7 @@ export interface ElementSpec { */ export interface RuntimeAdapter { /** Create an element from a component spec and props */ - create(spec: ComponentSpec, props: Record): RenderResult; + create(spec: ComponentSpec | RendererSpec, props: Record): RenderResult; /** Create a fragment/container for children */ fragment(children: RenderResult[]): RenderResult; @@ -102,7 +111,7 @@ export interface Context { */ export type ComponentType = | 'field' - | 'field-wrapper' + | 'button' | 'container' | 'content' | 'addon' From 9c71658dbb32ee72ec18716b5579f4f8b219d2be Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 13:14:06 +0000 Subject: [PATCH 4/8] refactor(react-factory): replace DefaultFieldWrapper with DefaultFieldRenderer - Add DefaultFieldRenderer and register-default-renderers - Remove DefaultFieldWrapper and field-renderer - Update form-factory and exports for renderer flow Co-authored-by: Cursor --- .../src/components/DefaultFieldWrapper.tsx | 124 ------------------ .../factories/react/src/components/index.ts | 1 - .../defaults/register-default-components.ts | 38 +++--- .../defaults/register-default-renderers.ts | 10 ++ packages/factories/react/src/form-factory.tsx | 40 +----- .../react/src/hooks/use-merged-config.ts | 3 +- packages/factories/react/src/index.ts | 11 +- .../src/renderers/DefaultFieldRenderer.tsx | 98 ++++++++++++++ .../react/src/renderers/field-renderer.ts | 91 ------------- .../factories/react/src/renderers/index.ts | 7 +- 10 files changed, 136 insertions(+), 287 deletions(-) delete mode 100644 packages/factories/react/src/components/DefaultFieldWrapper.tsx create mode 100644 packages/factories/react/src/defaults/register-default-renderers.ts create mode 100644 packages/factories/react/src/renderers/DefaultFieldRenderer.tsx delete mode 100644 packages/factories/react/src/renderers/field-renderer.ts diff --git a/packages/factories/react/src/components/DefaultFieldWrapper.tsx b/packages/factories/react/src/components/DefaultFieldWrapper.tsx deleted file mode 100644 index 4225d88..0000000 --- a/packages/factories/react/src/components/DefaultFieldWrapper.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Default Field Wrapper Component - * - * Wraps field components with native form adapter binding. - * No external dependencies (react-hook-form, formik, etc.) - * - * Users can create custom FieldWrappers for RHF, Formik, etc. - * by implementing the FieldWrapperProps interface. - */ - -import React from 'react'; -import { useScheptaFormAdapter, useScheptaFieldValue } from '../context/schepta-form-context'; - -/** - * Props interface for FieldWrapper components. - * - * Export this type for users creating custom wrappers (RHF, Formik, etc.) - * - * @example Creating a custom RHF FieldWrapper - * ```tsx - * import { FieldWrapperProps } from '@schepta/factory-react'; - * import { Controller, useFormContext } from 'react-hook-form'; - * - * export const RHFFieldWrapper: React.FC = ({ - * name, - * component: Component, - * componentProps = {}, - * children, - * }) => { - * const { control } = useFormContext(); - * return ( - * ( - * {children} - * )} - * /> - * ); - * }; - * ``` - */ -export interface FieldWrapperProps { - /** Field name (supports dot notation for nested fields) */ - name: string; - /** The field component to wrap */ - component: React.ComponentType; - /** Props to pass to the field component */ - componentProps?: Record; - /** Optional children */ - children?: React.ReactNode; -} - -/** - * Type for custom FieldWrapper components. - * Use this when registering a custom FieldWrapper in components. - * - * @example - * ```tsx - * const components = { - * FieldWrapper: createComponentSpec({ - * id: 'FieldWrapper', - * type: 'wrapper', - * factory: () => MyCustomFieldWrapper as FieldWrapperType, - * }), - * }; - * ``` - */ -export type FieldWrapperType = React.ComponentType; - -/** - * Default FieldWrapper - uses native adapter only. - * No external dependencies (RHF, Formik, etc.) - * - * This is the built-in field wrapper that uses the ScheptaFormContext - * to bind field values. For custom form libraries, create your own - * FieldWrapper and register it via the components prop. - * - * @example Using default (automatic via FormFactory) - * ```tsx - * - * ``` - * - * @example Using custom FieldWrapper - * ```tsx - * import { createComponentSpec } from '@schepta/core'; - * import { RHFFieldWrapper } from './my-rhf-wrapper'; - * - * const components = { - * FieldWrapper: createComponentSpec({ - * id: 'FieldWrapper', - * type: 'wrapper', - * factory: () => RHFFieldWrapper, - * }), - * }; - * - * - * ``` - */ -export const DefaultFieldWrapper: React.FC = ({ - name, - component: Component, - componentProps = {}, - children, -}) => { - const adapter = useScheptaFormAdapter(); - // Use the hook for reactivity - re-renders when this field's value changes - const value = useScheptaFieldValue(name); - - const handleChange = (newValue: any) => { - adapter.setValue(name, newValue); - }; - - return ( - - {children} - - ); -}; diff --git a/packages/factories/react/src/components/index.ts b/packages/factories/react/src/components/index.ts index 4c5a5be..0d71fba 100644 --- a/packages/factories/react/src/components/index.ts +++ b/packages/factories/react/src/components/index.ts @@ -6,7 +6,6 @@ export * from './DefaultFormContainer'; export * from './DefaultSubmitButton'; -export * from './DefaultFieldWrapper'; // Input components export * from './DefaultInputText'; diff --git a/packages/factories/react/src/defaults/register-default-components.ts b/packages/factories/react/src/defaults/register-default-components.ts index ee0ae99..cf8fbf9 100644 --- a/packages/factories/react/src/defaults/register-default-components.ts +++ b/packages/factories/react/src/defaults/register-default-components.ts @@ -1,6 +1,5 @@ import { ComponentSpec, createComponentSpec } from "@schepta/core"; import { - DefaultFieldWrapper, DefaultFormContainer, DefaultFormField, DefaultFormSectionContainer, @@ -22,84 +21,79 @@ export const defaultComponents: Record = { FormContainer: createComponentSpec({ id: 'FormContainer', type: 'container', - factory: () => DefaultFormContainer, + component: () => DefaultFormContainer, }), SubmitButton: createComponentSpec({ id: 'SubmitButton', - type: 'content', - factory: () => DefaultSubmitButton, - }), - FieldWrapper: createComponentSpec({ - id: 'FieldWrapper', - type: 'field-wrapper', - factory: () => DefaultFieldWrapper, + type: 'button', + component: () => DefaultSubmitButton, }), // Input components InputText: createComponentSpec({ id: 'InputText', type: 'field', - factory: () => DefaultInputText, + component: () => DefaultInputText, }), InputSelect: createComponentSpec({ id: 'InputSelect', type: 'field', - factory: () => DefaultInputSelect, + component: () => DefaultInputSelect, }), InputCheckbox: createComponentSpec({ id: 'InputCheckbox', type: 'field', - factory: () => DefaultInputCheckbox, + component: () => DefaultInputCheckbox, }), InputDate: createComponentSpec({ id: 'InputDate', type: 'field', - factory: () => DefaultInputDate, + component: () => DefaultInputDate, }), InputPhone: createComponentSpec({ id: 'InputPhone', type: 'field', - factory: () => DefaultInputPhone, + component: () => DefaultInputPhone, }), InputAutocomplete: createComponentSpec({ id: 'InputAutocomplete', type: 'field', - factory: () => DefaultInputAutocomplete, + component: () => DefaultInputAutocomplete, }), InputTextarea: createComponentSpec({ id: 'InputTextarea', type: 'field', - factory: () => DefaultInputTextarea, + component: () => DefaultInputTextarea, }), InputNumber: createComponentSpec({ id: 'InputNumber', type: 'field', - factory: () => DefaultInputNumber, + component: () => DefaultInputNumber, }), // Container components FormField: createComponentSpec({ id: 'FormField', type: 'container', - factory: () => DefaultFormField, + component: () => DefaultFormField, }), FormSectionContainer: createComponentSpec({ id: 'FormSectionContainer', type: 'container', - factory: () => DefaultFormSectionContainer, + component: () => DefaultFormSectionContainer, }), FormSectionTitle: createComponentSpec({ id: 'FormSectionTitle', type: 'content', - factory: () => DefaultFormSectionTitle, + component: () => DefaultFormSectionTitle, }), FormSectionGroup: createComponentSpec({ id: 'FormSectionGroup', type: 'container', - factory: () => DefaultFormSectionGroup, + component: () => DefaultFormSectionGroup, }), FormSectionGroupContainer: createComponentSpec({ id: 'FormSectionGroupContainer', type: 'container', - factory: () => DefaultFormSectionGroupContainer, + component: () => DefaultFormSectionGroupContainer, }), } \ No newline at end of file diff --git a/packages/factories/react/src/defaults/register-default-renderers.ts b/packages/factories/react/src/defaults/register-default-renderers.ts new file mode 100644 index 0000000..9f88cf9 --- /dev/null +++ b/packages/factories/react/src/defaults/register-default-renderers.ts @@ -0,0 +1,10 @@ +import { createRendererSpec } from "@schepta/core"; +import { DefaultFieldRenderer } from "../renderers/DefaultFieldRenderer"; + +export const defaultRenderers = { + field: createRendererSpec({ + id: 'field', + type: 'field', + component: () => DefaultFieldRenderer as any, + }), +} \ No newline at end of file diff --git a/packages/factories/react/src/form-factory.tsx b/packages/factories/react/src/form-factory.tsx index 4c7126f..09cbd67 100644 --- a/packages/factories/react/src/form-factory.tsx +++ b/packages/factories/react/src/form-factory.tsx @@ -16,6 +16,7 @@ import { createComponentOrchestrator, type FactorySetupResult, setFactoryDefaultComponents, + setFactoryDefaultRenderers, } from "@schepta/core"; import { createTemplateExpressionMiddleware } from "@schepta/core"; import { FormRenderer } from "./form-renderer"; @@ -23,19 +24,14 @@ import { useMergedScheptaConfig } from "./hooks/use-merged-config"; import { useScheptaForm } from "./hooks/use-schepta-form"; import { useSchemaValidation } from "./hooks/use-schema-validation"; import { createDebugContext } from "./utils/debug"; -import { createFieldRenderer } from "./renderers/field-renderer"; import formSchemaDefinition from "../../src/schemas/form-schema.json"; import { ScheptaFormProvider } from "./context/schepta-form-context"; -import { - DefaultSubmitButton, - DefaultFieldWrapper, - type SubmitButtonComponentType, - type FieldWrapperType, -} from "./components"; import { defaultComponents } from "./defaults/register-default-components"; +import { defaultRenderers } from "./defaults/register-default-renderers"; // Register factory default components (called once on module load) setFactoryDefaultComponents(defaultComponents); +setFactoryDefaultRenderers(defaultRenderers); /** * Ref interface for external form control @@ -146,38 +142,12 @@ export const FormFactory = forwardRef( // Get root component key from schema const rootComponentKey = (schema as any)["x-component"] || "FormContainer"; - // Resolve SubmitButton component from registry (provider or local) or use default - const SubmitButtonComponent = useMemo((): SubmitButtonComponentType => { - const customComponent = mergedConfig.components.SubmitButton?.factory?.( - {}, - runtime - ); - return ( - (customComponent as SubmitButtonComponentType) || DefaultSubmitButton - ); - }, [mergedConfig.components.SubmitButton, runtime]); - - // Resolve FieldWrapper component from registry (provider or local) or use default - const FieldWrapperComponent = useMemo((): FieldWrapperType => { - const customComponent = mergedConfig.components.FieldWrapper?.factory?.( - {}, - runtime - ); - return (customComponent as FieldWrapperType) || DefaultFieldWrapper; - }, [mergedConfig.components.FieldWrapper, runtime]); - // Create renderer orchestrator const renderer = useMemo(() => { const getFactorySetup = (): FactorySetupResult => { // Create debug context const debugContext = createDebugContext(mergedConfig.debug); - // Create custom renderers with field renderer (passing resolved FieldWrapper) - const customRenderers = { - ...mergedConfig.renderers, - field: createFieldRenderer({ FieldWrapper: FieldWrapperComponent }), - }; - // Create template expression middleware with current form values (always first) const templateMiddleware = createTemplateExpressionMiddleware({ externalContext: mergedConfig.externalContext, @@ -194,7 +164,7 @@ export const FormFactory = forwardRef( return { components: mergedConfig.components, customComponents: mergedConfig.customComponents, - renderers: customRenderers, + renderers: mergedConfig.renderers, externalContext: { ...mergedConfig.externalContext, }, @@ -218,8 +188,6 @@ export const FormFactory = forwardRef( runtime, onSubmit, formValues, - SubmitButtonComponent, - FieldWrapperComponent, ]); return ( diff --git a/packages/factories/react/src/hooks/use-merged-config.ts b/packages/factories/react/src/hooks/use-merged-config.ts index 0b4da7e..4d8cfa8 100644 --- a/packages/factories/react/src/hooks/use-merged-config.ts +++ b/packages/factories/react/src/hooks/use-merged-config.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { defaultRenderers, getFactoryDefaultComponents, type ComponentSpec, type MiddlewareFn } from '@schepta/core'; +import { defaultRenderers, getFactoryDefaultComponents, getFactoryDefaultRenderers, type ComponentSpec, type MiddlewareFn } from '@schepta/core'; import { useScheptaContext } from '@schepta/adapter-react'; export interface MergedConfigInput { @@ -49,6 +49,7 @@ export function useMergedScheptaConfig(props: MergedConfigInput): MergedConfig { }, renderers: { ...defaultRenderers, + ...getFactoryDefaultRenderers(), ...(providerConfig?.renderers || {}), ...(props.renderers || {}), }, diff --git a/packages/factories/react/src/index.ts b/packages/factories/react/src/index.ts index 3513581..a0c0e5a 100644 --- a/packages/factories/react/src/index.ts +++ b/packages/factories/react/src/index.ts @@ -20,12 +20,9 @@ export { export { DefaultFormContainer, DefaultSubmitButton, - DefaultFieldWrapper, type FormContainerProps, type SubmitButtonProps, type SubmitButtonComponentType, - type FieldWrapperProps, - type FieldWrapperType, // Input components DefaultInputText, DefaultInputSelect, @@ -71,6 +68,11 @@ export { type FormSectionGroupContainerComponentType, } from './components'; +// Renderers (types and defaults) +export { + DefaultFieldRenderer, +} from './renderers'; + // Context and hooks for form state management export { ScheptaFormProvider, @@ -83,9 +85,6 @@ export { // Hooks (for advanced usage) export * from './hooks'; -// Renderers (for customization) -export * from './renderers'; - // Utilities export * from './utils'; diff --git a/packages/factories/react/src/renderers/DefaultFieldRenderer.tsx b/packages/factories/react/src/renderers/DefaultFieldRenderer.tsx new file mode 100644 index 0000000..bef5dd4 --- /dev/null +++ b/packages/factories/react/src/renderers/DefaultFieldRenderer.tsx @@ -0,0 +1,98 @@ +/** + * Default Field Renderer Component + * + * Renders field components with native Schepta form adapter binding. + * No external dependencies (react-hook-form, formik, etc.) + * + * Users can create custom field renderers for RHF, Formik, etc. + * by implementing the FieldRendererProps interface. + */ + +import React from 'react'; +import { useScheptaFormAdapter, useScheptaFieldValue } from '../context/schepta-form-context'; +import { FieldRendererProps } from '@schepta/core'; + +/** + * Type for custom field renderer components. + * Use this when creating a custom renderer for RHF, Formik, etc. + * + * @example Creating a custom RHF field renderer + * ```tsx + * import { ReactFieldRendererProps } from '@schepta/factory-react'; + * import { Controller, useFormContext } from 'react-hook-form'; + * + * export const RHFFieldRenderer: React.FC = ({ + * name, + * component: Component, + * componentProps = {}, + * children, + * }) => { + * const { control } = useFormContext(); + * return ( + * ( + * {children} + * )} + * /> + * ); + * }; + * ``` + */ +export type FieldRendererType = React.ComponentType; + +/** + * Default field renderer - uses native Schepta form adapter. + * No external dependencies (RHF, Formik, etc.) + * + * This is the built-in field renderer that uses the ScheptaFormContext + * to bind field values. For custom form libraries, create your own + * field renderer and register it via the renderers prop. + * + * @example Using default (automatic via FormFactory) + * ```tsx + * + * ``` + * + * @example Using custom field renderer + * ```tsx + * import { createRendererSpec } from '@schepta/core'; + * import { RHFFieldRenderer } from './rhf/RHFFieldRenderer'; + * + * const renderers = { + * field: createRendererSpec({ + * id: 'rhf-field-renderer', + * type: 'field', + * component: RHFFieldRenderer, + * }), + * }; + * + * + * ``` + */ +export const DefaultFieldRenderer: React.FC = ({ + name, + component: Component, + componentProps = {}, + children, +}) => { + const adapter = useScheptaFormAdapter(); + // Use the hook for reactivity - re-renders when this field's value changes + const value = useScheptaFieldValue(name); + + const handleChange = (newValue: any) => { + adapter.setValue(name, newValue); + }; + + return ( + + {children} + + ); +}; diff --git a/packages/factories/react/src/renderers/field-renderer.ts b/packages/factories/react/src/renderers/field-renderer.ts deleted file mode 100644 index 44c6956..0000000 --- a/packages/factories/react/src/renderers/field-renderer.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Field Renderer - * - * Custom renderer that wraps field components with FieldWrapper. - * Supports custom FieldWrapper components via registry (local > global > default). - */ - -import React from 'react'; -import type { ComponentSpec, RendererFn, RuntimeAdapter } from '@schepta/core'; -import { DefaultFieldWrapper, type FieldWrapperType } from '../components/DefaultFieldWrapper'; - -/** - * Options for creating the field renderer - */ -export interface FieldRendererOptions { - /** - * Custom FieldWrapper component. - * If not provided, uses DefaultFieldWrapper (native adapter). - * - * Users can provide their own FieldWrapper for RHF, Formik, etc. - */ - FieldWrapper?: FieldWrapperType; -} - -/** - * Create a field renderer that wraps fields with FieldWrapper - * - * The field renderer handles: - * - Wrapping field components with the configured FieldWrapper - * - Passing x-component-props to the underlying component - * - Sanitizing props before passing to DOM components - * - * @param options - Optional configuration including custom FieldWrapper - * @returns A renderer function for field components - * - * @example Using default FieldWrapper (native) - * ```tsx - * const fieldRenderer = createFieldRenderer(); - * ``` - * - */ -export function createFieldRenderer(options: FieldRendererOptions = {}): RendererFn { - const { FieldWrapper = DefaultFieldWrapper } = options; - - return ( - spec: ComponentSpec, - props: Record, - runtime: RuntimeAdapter, - children?: any[] - ) => { - // Extract name from props - const name = props.name || ''; - - // If this is a field component and we have a name, wrap it with FieldWrapper - if (name && spec.type === 'field') { - const Component = spec.factory(props, runtime); - - const xComponentProps = props['x-component-props'] || {}; - - const componentProps = { - ...xComponentProps, - name, // Ensure name is passed - ...(props.externalContext ? { externalContext: props.externalContext } : {}), - ...(props.schema ? { schema: props.schema } : {}), - ...(props.componentKey ? { componentKey: props.componentKey } : {}), - ...(props['data-test-id'] ? { 'data-test-id': props['data-test-id'] } : {}), - }; - - // Create FieldWrapper using React.createElement directly since we're in React context - return React.createElement(FieldWrapper, { - name, - component: Component as any, - componentProps, - children, - }); - } - - // For non-field components or fields without name, use default rendering - const xComponentProps = props['x-component-props'] || {}; - const mergedProps = { ...props, ...xComponentProps }; - - // Sanitize props to remove internal Schepta metadata before passing to DOM - - const propsWithChildren = - children && children.length > 0 - ? { ...mergedProps, children } - : mergedProps; - - return runtime.create(spec, propsWithChildren); - }; -} diff --git a/packages/factories/react/src/renderers/index.ts b/packages/factories/react/src/renderers/index.ts index d90b3d4..75a19bf 100644 --- a/packages/factories/react/src/renderers/index.ts +++ b/packages/factories/react/src/renderers/index.ts @@ -1,6 +1 @@ -/** - * Schepta React Factory Renderers - */ - -export * from './field-renderer'; - +export * from './DefaultFieldRenderer'; \ No newline at end of file From 9178892bb90a89662fdd96dc95c7a6a70367257a Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 13:14:12 +0000 Subject: [PATCH 5/8] refactor(adapters): wire providers and runtime adapters to renderer orchestrator Co-authored-by: Cursor --- packages/adapters/react/src/provider.tsx | 13 ++++--------- packages/adapters/react/src/runtime-adapter.tsx | 6 +++--- packages/adapters/vanilla/src/provider.ts | 10 ++++------ packages/adapters/vanilla/src/runtime-adapter.ts | 2 +- packages/adapters/vue/src/provider.ts | 8 +++----- packages/adapters/vue/src/runtime-adapter.ts | 2 +- 6 files changed, 16 insertions(+), 25 deletions(-) diff --git a/packages/adapters/react/src/provider.tsx b/packages/adapters/react/src/provider.tsx index 06ed48b..c0bc1ac 100644 --- a/packages/adapters/react/src/provider.tsx +++ b/packages/adapters/react/src/provider.tsx @@ -10,7 +10,6 @@ import type { FormSchema } from '@schepta/core'; import type { MiddlewareFn } from '@schepta/core'; import type { RendererFn } from '@schepta/core'; import { defaultDebugConfig } from '@schepta/core'; -import { getRendererRegistry } from '@schepta/core'; /** * Provider configuration type (matches SpectraProviderProps from old project) @@ -73,16 +72,14 @@ export function ScheptaProvider({ // Check if we're nested inside another ScheptaProvider (Material-UI pattern) const parentContext = useContext(ScheptaContext); - const contextValue = useMemo(() => { + const contextValue = useMemo(() => { // If we have a parent context, merge with it (hierarchical override) if (parentContext) { - // Use renderer registry for hierarchical merging - const mergedRenderers = getRendererRegistry(parentContext.renderers, renderers); return { components: { ...parentContext.components, ...components }, customComponents: { ...parentContext.customComponents, ...customComponents }, - renderers: mergedRenderers, + renderers: { ...parentContext.renderers, ...renderers }, middlewares: [...parentContext.middlewares, ...middlewares], debug: { ...parentContext.debug, ...debug }, schema: schema || parentContext.schema, @@ -90,14 +87,12 @@ export function ScheptaProvider({ }; } - // If we're the root provider, use renderer registry with defaults - const mergedRenderers = getRendererRegistry(undefined, renderers); const mergedDebug = { ...defaultDebugConfig, ...debug }; return { components, customComponents, - renderers: mergedRenderers, + renderers, middlewares, debug: mergedDebug, schema, @@ -106,7 +101,7 @@ export function ScheptaProvider({ }, [parentContext, components, customComponents, renderers, middlewares, debug, schema, externalContext]); return ( - + {children} ); diff --git a/packages/adapters/react/src/runtime-adapter.tsx b/packages/adapters/react/src/runtime-adapter.tsx index a8f6e41..8da0e50 100644 --- a/packages/adapters/react/src/runtime-adapter.tsx +++ b/packages/adapters/react/src/runtime-adapter.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import type { RuntimeAdapter, ComponentSpec, RenderResult } from '@schepta/core'; +import type { RuntimeAdapter, ComponentSpec, RenderResult, RendererSpec } from '@schepta/core'; /** * React runtime adapter implementation */ export class ReactRuntimeAdapter implements RuntimeAdapter { - create(spec: ComponentSpec, props: Record): RenderResult { - const component = spec.factory(props, this); + create(spec: ComponentSpec | RendererSpec, props: Record): RenderResult { + const component = spec.component(props, this); // If factory returns a React component type, create element if (typeof component === 'function' || typeof component === 'object') { return React.createElement(component as any, props); diff --git a/packages/adapters/vanilla/src/provider.ts b/packages/adapters/vanilla/src/provider.ts index 1ad255a..c19d8d3 100644 --- a/packages/adapters/vanilla/src/provider.ts +++ b/packages/adapters/vanilla/src/provider.ts @@ -9,7 +9,6 @@ import type { FormSchema } from '@schepta/core'; import type { MiddlewareFn } from '@schepta/core'; import type { RendererFn } from '@schepta/core'; import { defaultDebugConfig } from '@schepta/core'; -import { getRendererRegistry } from '@schepta/core'; /** * Provider configuration type @@ -56,10 +55,10 @@ export function createScheptaProvider( const parentContext = getParentProviderContext(container); // Compute merged context value - const contextValue: ScheptaContextType = (() => { + const contextValue = (() => { if (parentContext) { // Merge with parent (hierarchical override) - const mergedRenderers = getRendererRegistry(parentContext.renderers, props.renderers); + const mergedRenderers = { ...parentContext.renderers, ...props.renderers }; return { components: { ...parentContext.components, ...(props.components || {}) }, @@ -72,12 +71,11 @@ export function createScheptaProvider( } // Root provider - const mergedRenderers = getRendererRegistry(undefined, props.renderers); const mergedDebug = { ...defaultDebugConfig, ...props.debug }; return { components: props.components || {}, - renderers: mergedRenderers, + renderers: props.renderers, middlewares: props.middlewares || [], debug: mergedDebug, schema: props.schema, @@ -86,7 +84,7 @@ export function createScheptaProvider( })(); // Store provider config in map - providerMap.set(container, contextValue); + providerMap.set(container, contextValue as ScheptaContextType); return { destroy: () => { diff --git a/packages/adapters/vanilla/src/runtime-adapter.ts b/packages/adapters/vanilla/src/runtime-adapter.ts index 1d50fe6..41a1e51 100644 --- a/packages/adapters/vanilla/src/runtime-adapter.ts +++ b/packages/adapters/vanilla/src/runtime-adapter.ts @@ -13,7 +13,7 @@ import type { DOMElement } from './types'; */ export class VanillaRuntimeAdapter implements RuntimeAdapter { create(spec: ComponentSpec, props: Record): RenderResult { - const component = spec.factory(props, this); + const component = spec.component(props, this); // If component returns a DOM element directly if (component instanceof HTMLElement) { diff --git a/packages/adapters/vue/src/provider.ts b/packages/adapters/vue/src/provider.ts index 9e9ad42..e791e2f 100644 --- a/packages/adapters/vue/src/provider.ts +++ b/packages/adapters/vue/src/provider.ts @@ -4,13 +4,12 @@ * Vue implementation of schepta Provider */ -import { provide, inject, defineComponent, h, type Component } from 'vue'; +import { provide, inject, defineComponent, h } from 'vue'; import type { ComponentSpec, ComponentType, DebugConfig } from '@schepta/core'; import type { FormSchema } from '@schepta/core'; import type { MiddlewareFn } from '@schepta/core'; import type { RendererFn } from '@schepta/core'; import { defaultDebugConfig } from '@schepta/core'; -import { getRendererRegistry } from '@schepta/core'; /** * Provider configuration type @@ -84,7 +83,7 @@ export function createScheptaProvider(props: ScheptaProviderProps = {}) { if (parentContext) { // Merge with parent (hierarchical override) - const mergedRenderers = getRendererRegistry(parentContext.renderers, componentProps.renderers); + const mergedRenderers = { ...parentContext.renderers, ...componentProps.renderers }; return { components: { ...parentContext.components, ...componentProps.components }, @@ -97,12 +96,11 @@ export function createScheptaProvider(props: ScheptaProviderProps = {}) { } // Root provider - const mergedRenderers = getRendererRegistry(undefined, componentProps.renderers); const mergedDebug = { ...defaultDebugConfig, ...componentProps.debug }; return { components: componentProps.components || {}, - renderers: mergedRenderers, + renderers: componentProps.renderers, middlewares: localMiddlewares, debug: mergedDebug, schema: componentProps.schema as FormSchema | undefined, diff --git a/packages/adapters/vue/src/runtime-adapter.ts b/packages/adapters/vue/src/runtime-adapter.ts index a91775f..14766d3 100644 --- a/packages/adapters/vue/src/runtime-adapter.ts +++ b/packages/adapters/vue/src/runtime-adapter.ts @@ -12,7 +12,7 @@ import type { RuntimeAdapter, ComponentSpec, RenderResult } from '@schepta/core' */ export class VueRuntimeAdapter implements RuntimeAdapter { create(spec: ComponentSpec, props: Record): RenderResult { - const component = spec.factory(props, this) as any; + const component = spec.component(props, this) as any; // Extract children from props if present (Vue passes children as third argument to h()) const { children, ...restProps } = props; From 58f1eeee8fca2451458297ffd14c84004efe1013 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 13:14:18 +0000 Subject: [PATCH 6/8] refactor(factories): update vanilla and vue form-factory for renderer flow Co-authored-by: Cursor --- packages/factories/vanilla/src/form-factory.ts | 6 +++--- packages/factories/vue/src/form-factory.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/factories/vanilla/src/form-factory.ts b/packages/factories/vanilla/src/form-factory.ts index 23da3bd..b5fad39 100644 --- a/packages/factories/vanilla/src/form-factory.ts +++ b/packages/factories/vanilla/src/form-factory.ts @@ -23,12 +23,12 @@ setFactoryDefaultComponents({ FormContainer: createComponentSpec({ id: 'FormContainer', type: 'container', - factory: () => createDefaultFormContainer, + component: () => createDefaultFormContainer, }), SubmitButton: createComponentSpec({ id: 'SubmitButton', - type: 'content', - factory: () => createDefaultSubmitButton, + type: 'button', + component: () => createDefaultSubmitButton, }), }); diff --git a/packages/factories/vue/src/form-factory.ts b/packages/factories/vue/src/form-factory.ts index d0da6f8..dfdbea4 100644 --- a/packages/factories/vue/src/form-factory.ts +++ b/packages/factories/vue/src/form-factory.ts @@ -22,12 +22,12 @@ setFactoryDefaultComponents({ FormContainer: createComponentSpec({ id: 'FormContainer', type: 'container', - factory: () => DefaultFormContainer, + component: () => DefaultFormContainer, }), SubmitButton: createComponentSpec({ id: 'SubmitButton', type: 'content', - factory: () => DefaultSubmitButton, + component: () => DefaultSubmitButton, }), }); From 65ac160c503eb493b74b35e281b104d91a71a330 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 13:14:27 +0000 Subject: [PATCH 7/8] refactor(showcases): rename FieldWrapper to FieldRenderer and update registries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FormikFieldWrapper → FormikFieldRenderer, RHFFieldWrapper → RHFFieldRenderer - Update ComponentRegistry and form usages in basic-ui, chakra-ui, material-ui Co-authored-by: Cursor --- .../basic-ui/components/ComponentRegistry.tsx | 2 +- .../components/Forms/FormWithFormik.tsx | 29 +++++++++++-------- .../basic-ui/components/Forms/FormWithRHF.tsx | 29 ++++++++++--------- .../components/Forms/NativeComplexForm.tsx | 2 +- ...eldWrapper.tsx => FormikFieldRenderer.tsx} | 27 +++++++++-------- ...FFieldWrapper.tsx => RHFFieldRenderer.tsx} | 27 +++++++++-------- .../components/ComponentRegistry.tsx | 28 +++++++++--------- .../components/ComponentRegistry.tsx | 28 +++++++++--------- 8 files changed, 93 insertions(+), 79 deletions(-) rename showcases/src/basic-ui/components/formik/{FormikFieldWrapper.tsx => FormikFieldRenderer.tsx} (59%) rename showcases/src/basic-ui/components/rhf/{RHFFieldWrapper.tsx => RHFFieldRenderer.tsx} (58%) diff --git a/showcases/src/basic-ui/components/ComponentRegistry.tsx b/showcases/src/basic-ui/components/ComponentRegistry.tsx index d2bd3e0..67266bf 100644 --- a/showcases/src/basic-ui/components/ComponentRegistry.tsx +++ b/showcases/src/basic-ui/components/ComponentRegistry.tsx @@ -5,6 +5,6 @@ export const components = { InputText: createComponentSpec({ id: "InputText", type: "field", - factory: (props, runtime) => InputText, + component: (props, runtime) => InputText, }), }; \ No newline at end of file diff --git a/showcases/src/basic-ui/components/Forms/FormWithFormik.tsx b/showcases/src/basic-ui/components/Forms/FormWithFormik.tsx index 068a5ad..9c07e20 100644 --- a/showcases/src/basic-ui/components/Forms/FormWithFormik.tsx +++ b/showcases/src/basic-ui/components/Forms/FormWithFormik.tsx @@ -2,17 +2,18 @@ * Form with Formik * * Example component demonstrating how to use Schepta with Formik. - * This shows how to inject custom Formik components via the component registry + * This shows how to inject custom Formik field renderer via the renderer registry * with AJV validation using createFormikValidator. */ import React, { useState, useMemo } from 'react'; import { FormFactory } from '@schepta/factory-react'; import { - createComponentSpec, + createComponentSpec, + createRendererSpec, FormSchema, } from '@schepta/core'; -import { FormikFieldWrapper } from '../formik/FormikFieldWrapper'; +import { FormikFieldRenderer } from '../formik/FormikFieldRenderer'; import { FormikFormContainer } from '../formik/FormikFormContainer'; interface FormWithFormikProps { @@ -28,18 +29,21 @@ interface FormWithFormikProps { export const FormWithFormik: React.FC = ({ schema }) => { const [submittedValues, setSubmittedValues] = useState | null>(null); - const formikComponents = useMemo(() => ({ - // Register Formik FieldWrapper - this makes all fields use Formik's context - FieldWrapper: createComponentSpec({ - id: 'FieldWrapper', - type: 'field-wrapper', - factory: () => FormikFieldWrapper, + // Create Formik renderers - custom field renderer using Formik's context + const formikRenderers = useMemo(() => ({ + field: createRendererSpec({ + id: 'formik-field-renderer', + type: 'field', + component: () => FormikFieldRenderer, }), - // Register Formik FormContainer with validation - this provides the Formik context + }), []); + + // Create Formik components - custom FormContainer with Formik context + const formikComponents = useMemo(() => ({ FormContainer: createComponentSpec({ id: 'FormContainer', type: 'container', - factory: () => FormikFormContainer, + component: () => FormikFormContainer, }), }), []); @@ -65,10 +69,11 @@ export const FormWithFormik: React.FC = ({ schema }) => { fontSize: '14px', }}> This form uses Formik with AJV validation. - The FieldWrapper and FormContainer are custom Formik implementations. + The field renderer and FormContainer are custom Formik implementations. = ({ schema }) => { const [submittedValues, setSubmittedValues] = useState | null>(null); - // Create RHF components with validation config - const rhfComponents = useMemo(() => ({ - // Register RHF FieldWrapper - this makes all fields use RHF's Controller - FieldWrapper: createComponentSpec({ - id: 'FieldWrapper', - type: 'field-wrapper', - factory: () => RHFFieldWrapper, + // Create RHF renderers - custom field renderer using RHF's Controller + const rhfRenderers = useMemo(() => ({ + field: createRendererSpec({ + id: 'rhf-field-renderer', + type: 'field', + component: () => RHFFieldRenderer, }), - // Register RHF FormContainer with validation - this provides the FormProvider context + }), []); + + // Create RHF components - custom FormContainer with RHF FormProvider + const rhfComponents = useMemo(() => ({ FormContainer: createComponentSpec({ id: 'FormContainer', type: 'container', - factory: () => RHFFormContainer, + component: () => RHFFormContainer, }), }), []); @@ -63,10 +65,11 @@ export const FormWithRHF: React.FC = ({ schema }) => { fontSize: '14px', }}> This form uses react-hook-form with AJV validation. - The FieldWrapper and FormContainer are custom RHF implementations. + The field renderer and FormContainer are custom RHF implementations. { socialName: createComponentSpec({ id: 'socialName', // Same as the key name in the schema type: 'field', - factory: () => SocialNameInput, + component: () => SocialNameInput, }), }), []); diff --git a/showcases/src/basic-ui/components/formik/FormikFieldWrapper.tsx b/showcases/src/basic-ui/components/formik/FormikFieldRenderer.tsx similarity index 59% rename from showcases/src/basic-ui/components/formik/FormikFieldWrapper.tsx rename to showcases/src/basic-ui/components/formik/FormikFieldRenderer.tsx index f2d4a69..4e19b58 100644 --- a/showcases/src/basic-ui/components/formik/FormikFieldWrapper.tsx +++ b/showcases/src/basic-ui/components/formik/FormikFieldRenderer.tsx @@ -1,35 +1,38 @@ /** - * Formik Field Wrapper + * Formik Field Renderer * - * Custom FieldWrapper that uses Formik's context. + * Custom field renderer that uses Formik's context. * This demonstrates how to integrate Formik with Schepta forms. */ import React from 'react'; import { useFormikContext } from 'formik'; -import type { FieldWrapperProps } from '@schepta/factory-react'; +import type { FieldRendererProps } from '@schepta/core'; /** - * Formik-based FieldWrapper component. + * Formik-based field renderer component. * Uses Formik's context to bind fields to form state. * - * Register this component via the components prop to use Formik + * Register this renderer via the renderers prop to use Formik * for form state management. * * @example * ```tsx - * import { createComponentSpec } from '@schepta/core'; + * import { createRendererSpec } from '@schepta/core'; + * import { FormikFieldRenderer } from './formik/FormikFieldRenderer'; * - * const components = { - * FieldWrapper: createComponentSpec({ - * id: 'FieldWrapper', - * type: 'wrapper', - * factory: () => FormikFieldWrapper, + * const renderers = { + * field: createRendererSpec({ + * id: 'formik-field-renderer', + * type: 'field', + * component: FormikFieldRenderer, * }), * }; + * + * * ``` */ -export const FormikFieldWrapper: React.FC = ({ +export const FormikFieldRenderer: React.FC = ({ name, component: Component, componentProps = {}, diff --git a/showcases/src/basic-ui/components/rhf/RHFFieldWrapper.tsx b/showcases/src/basic-ui/components/rhf/RHFFieldRenderer.tsx similarity index 58% rename from showcases/src/basic-ui/components/rhf/RHFFieldWrapper.tsx rename to showcases/src/basic-ui/components/rhf/RHFFieldRenderer.tsx index a64f065..7b532e6 100644 --- a/showcases/src/basic-ui/components/rhf/RHFFieldWrapper.tsx +++ b/showcases/src/basic-ui/components/rhf/RHFFieldRenderer.tsx @@ -1,35 +1,38 @@ /** - * RHF Field Wrapper + * RHF Field Renderer * - * Custom FieldWrapper that uses react-hook-form's Controller. + * Custom field renderer that uses react-hook-form's Controller. * This demonstrates how to integrate RHF with Schepta forms. */ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import type { FieldWrapperProps } from '@schepta/factory-react'; +import type { FieldRendererProps } from '@schepta/core'; /** - * RHF-based FieldWrapper component. + * RHF-based field renderer component. * Uses react-hook-form's Controller to bind fields to form state. * - * Register this component via the components prop to use RHF + * Register this renderer via the renderers prop to use RHF * for form state management. * * @example * ```tsx - * import { createComponentSpec } from '@schepta/core'; + * import { createRendererSpec } from '@schepta/core'; + * import { RHFFieldRenderer } from './rhf/RHFFieldRenderer'; * - * const components = { - * FieldWrapper: createComponentSpec({ - * id: 'FieldWrapper', - * type: 'wrapper', - * factory: () => RHFFieldWrapper, + * const renderers = { + * field: createRendererSpec({ + * id: 'rhf-field-renderer', + * type: 'field', + * component: RHFFieldRenderer, * }), * }; + * + * * ``` */ -export const RHFFieldWrapper: React.FC = ({ +export const RHFFieldRenderer: React.FC = ({ name, component: Component, componentProps = {}, diff --git a/showcases/src/chakra-ui/components/ComponentRegistry.tsx b/showcases/src/chakra-ui/components/ComponentRegistry.tsx index 81faf46..8599d4f 100644 --- a/showcases/src/chakra-ui/components/ComponentRegistry.tsx +++ b/showcases/src/chakra-ui/components/ComponentRegistry.tsx @@ -17,72 +17,72 @@ export const components = { 'FormContainer': createComponentSpec({ id: "FormContainer", type: "container", - factory: (props, runtime) => FormContainer, + component: (props, runtime) => FormContainer, }), InputText: createComponentSpec({ id: "InputText", type: "field", - factory: (props, runtime) => InputText, + component: (props, runtime) => InputText, }), InputSelect: createComponentSpec({ id: "InputSelect", type: "field", - factory: (props, runtime) => InputSelect, + component: (props, runtime) => InputSelect, }), InputCheckbox: createComponentSpec({ id: "InputCheckbox", type: "field", - factory: (props, runtime) => InputCheckbox, + component: (props, runtime) => InputCheckbox, }), InputPhone: createComponentSpec({ id: "InputPhone", type: "field", - factory: (props, runtime) => InputText, + component: (props, runtime) => InputText, defaultProps: { type: "tel" }, }), InputTextarea: createComponentSpec({ id: "InputTextarea", type: "field", - factory: (props, runtime) => InputTextarea, + component: (props, runtime) => InputTextarea, }), InputNumber: createComponentSpec({ id: "InputNumber", type: "field", - factory: (props, runtime) => InputNumber, + component: (props, runtime) => InputNumber, }), InputDate: createComponentSpec({ id: "InputDate", type: "field", - factory: (props, runtime) => InputDate, + component: (props, runtime) => InputDate, }), SubmitButton: createComponentSpec({ id: "SubmitButton", type: 'content', - factory: (props, runtime) => SubmitButton, + component: (props, runtime) => SubmitButton, }), FormField: createComponentSpec({ id: "FormField", type: 'container', - factory: (props, runtime) => FormField, + component: (props, runtime) => FormField, }), FormSectionContainer: createComponentSpec({ id: "FormSectionContainer", type: "container", - factory: (props, runtime) => FormSectionContainer, + component: (props, runtime) => FormSectionContainer, }), FormSectionTitle: createComponentSpec({ id: "FormSectionTitle", type: 'content', - factory: (props, runtime) => FormSectionTitle, + component: (props, runtime) => FormSectionTitle, }), FormSectionGroupContainer: createComponentSpec({ id: "FormSectionGroupContainer", type: 'container', - factory: (props, runtime) => FormSectionGroupContainer, + component: (props, runtime) => FormSectionGroupContainer, }), FormSectionGroup: createComponentSpec({ id: "FormSectionGroup", type: 'container', - factory: (props, runtime) => FormSectionGroup, + component: (props, runtime) => FormSectionGroup, }), }; \ No newline at end of file diff --git a/showcases/src/material-ui/components/ComponentRegistry.tsx b/showcases/src/material-ui/components/ComponentRegistry.tsx index 81faf46..8599d4f 100644 --- a/showcases/src/material-ui/components/ComponentRegistry.tsx +++ b/showcases/src/material-ui/components/ComponentRegistry.tsx @@ -17,72 +17,72 @@ export const components = { 'FormContainer': createComponentSpec({ id: "FormContainer", type: "container", - factory: (props, runtime) => FormContainer, + component: (props, runtime) => FormContainer, }), InputText: createComponentSpec({ id: "InputText", type: "field", - factory: (props, runtime) => InputText, + component: (props, runtime) => InputText, }), InputSelect: createComponentSpec({ id: "InputSelect", type: "field", - factory: (props, runtime) => InputSelect, + component: (props, runtime) => InputSelect, }), InputCheckbox: createComponentSpec({ id: "InputCheckbox", type: "field", - factory: (props, runtime) => InputCheckbox, + component: (props, runtime) => InputCheckbox, }), InputPhone: createComponentSpec({ id: "InputPhone", type: "field", - factory: (props, runtime) => InputText, + component: (props, runtime) => InputText, defaultProps: { type: "tel" }, }), InputTextarea: createComponentSpec({ id: "InputTextarea", type: "field", - factory: (props, runtime) => InputTextarea, + component: (props, runtime) => InputTextarea, }), InputNumber: createComponentSpec({ id: "InputNumber", type: "field", - factory: (props, runtime) => InputNumber, + component: (props, runtime) => InputNumber, }), InputDate: createComponentSpec({ id: "InputDate", type: "field", - factory: (props, runtime) => InputDate, + component: (props, runtime) => InputDate, }), SubmitButton: createComponentSpec({ id: "SubmitButton", type: 'content', - factory: (props, runtime) => SubmitButton, + component: (props, runtime) => SubmitButton, }), FormField: createComponentSpec({ id: "FormField", type: 'container', - factory: (props, runtime) => FormField, + component: (props, runtime) => FormField, }), FormSectionContainer: createComponentSpec({ id: "FormSectionContainer", type: "container", - factory: (props, runtime) => FormSectionContainer, + component: (props, runtime) => FormSectionContainer, }), FormSectionTitle: createComponentSpec({ id: "FormSectionTitle", type: 'content', - factory: (props, runtime) => FormSectionTitle, + component: (props, runtime) => FormSectionTitle, }), FormSectionGroupContainer: createComponentSpec({ id: "FormSectionGroupContainer", type: 'container', - factory: (props, runtime) => FormSectionGroupContainer, + component: (props, runtime) => FormSectionGroupContainer, }), FormSectionGroup: createComponentSpec({ id: "FormSectionGroup", type: 'container', - factory: (props, runtime) => FormSectionGroup, + component: (props, runtime) => FormSectionGroup, }), }; \ No newline at end of file From f493f739477fd28c6bd982119931d02dffafbde3 Mon Sep 17 00:00:00 2001 From: guynikan Date: Mon, 2 Feb 2026 13:20:06 +0000 Subject: [PATCH 8/8] fix: changed param name in tests --- packages/adapters/vanilla/src/provider.test.ts | 12 ++++++------ packages/adapters/vue/src/provider.test.ts | 6 +++--- packages/core/src/provider/provider.test.ts | 12 ++++++------ .../react/src/form-factory.provider.test.tsx | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/adapters/vanilla/src/provider.test.ts b/packages/adapters/vanilla/src/provider.test.ts index 3a9d7b1..6f6a277 100644 --- a/packages/adapters/vanilla/src/provider.test.ts +++ b/packages/adapters/vanilla/src/provider.test.ts @@ -25,7 +25,7 @@ describe('Vanilla Provider', () => { it('should create provider and store config', () => { const componentSpec = createComponentSpec({ id: 'TestComponent', - factory: () => null, + component: () => null, type: 'field', }); @@ -43,7 +43,7 @@ describe('Vanilla Provider', () => { it('should get context from container', () => { const componentSpec = createComponentSpec({ id: 'TestComponent', - factory: () => null, + component: () => null, type: 'field', }); @@ -58,7 +58,7 @@ describe('Vanilla Provider', () => { it('should get context from child container (parent lookup)', () => { const componentSpec = createComponentSpec({ id: 'TestComponent', - factory: () => null, + component: () => null, type: 'field', }); @@ -73,13 +73,13 @@ describe('Vanilla Provider', () => { it('should support nested providers with hierarchical merge', () => { const ParentComponent = createComponentSpec({ id: 'ParentComponent', - factory: () => null, + component: () => null, type: 'field', }); const ChildComponent = createComponentSpec({ id: 'ChildComponent', - factory: () => null, + component: () => null, type: 'field', }); @@ -157,7 +157,7 @@ describe('Vanilla Provider', () => { it('should destroy provider and remove config', () => { const componentSpec = createComponentSpec({ id: 'TestComponent', - factory: () => null, + component: () => null, type: 'field', }); diff --git a/packages/adapters/vue/src/provider.test.ts b/packages/adapters/vue/src/provider.test.ts index ae235a6..b948ae1 100644 --- a/packages/adapters/vue/src/provider.test.ts +++ b/packages/adapters/vue/src/provider.test.ts @@ -31,7 +31,7 @@ describe('Vue Provider', () => { it('should provide components configuration', () => { const componentSpec = createComponentSpec({ id: 'TestComponent', - factory: () => null, + component: () => null, type: 'field', }); @@ -60,13 +60,13 @@ describe('Vue Provider', () => { it('should support nested providers with hierarchical merge', () => { const ParentComponent = createComponentSpec({ id: 'ParentComponent', - factory: () => null, + component: () => null, type: 'field', }); const ChildComponent = createComponentSpec({ id: 'ChildComponent', - factory: () => null, + component: () => null, type: 'field', }); diff --git a/packages/core/src/provider/provider.test.ts b/packages/core/src/provider/provider.test.ts index 5fab48c..6806611 100644 --- a/packages/core/src/provider/provider.test.ts +++ b/packages/core/src/provider/provider.test.ts @@ -15,7 +15,7 @@ describe('Provider Utilities', () => { components: { InputText: createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', }), }, @@ -25,7 +25,7 @@ describe('Provider Utilities', () => { components: { InputText: createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', displayName: 'CustomInputText', }), @@ -126,7 +126,7 @@ describe('Provider Utilities', () => { components: { InputText: createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', }), }, @@ -141,7 +141,7 @@ describe('Provider Utilities', () => { components: { InputText: createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', }), }, @@ -156,7 +156,7 @@ describe('Provider Utilities', () => { components: { InputText: createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', }), }, @@ -166,7 +166,7 @@ describe('Provider Utilities', () => { components: { Button: createComponentSpec({ id: 'Button', - factory: () => null, + component: () => null, type: 'field', }), }, diff --git a/packages/factories/react/src/form-factory.provider.test.tsx b/packages/factories/react/src/form-factory.provider.test.tsx index 2945c11..4a8d3aa 100644 --- a/packages/factories/react/src/form-factory.provider.test.tsx +++ b/packages/factories/react/src/form-factory.provider.test.tsx @@ -48,7 +48,7 @@ const simpleFormSchema = { // Mock components const InputTextSpec = createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', }); @@ -78,7 +78,7 @@ describe('FormFactory Provider Integration', () => { it('should prioritize local props over provider config', () => { const LocalInputTextSpec = createComponentSpec({ id: 'InputText', - factory: () => null, + component: () => null, type: 'field', displayName: 'LocalInputText', });