From 36766b93d9ecabb643087c685da59e22b5518064 Mon Sep 17 00:00:00 2001 From: Edson Batista Date: Sun, 25 Jan 2026 20:56:48 +0000 Subject: [PATCH 1/3] feat: replace RHF adapter with native form adapter - Add NativeReactFormAdapter (useState-based) - Remove form-adapter.ts (ReactHookFormAdapter) and react-hook-form dependency - Export createNativeReactFormAdapter from index --- packages/adapters/react/package.json | 4 +- packages/adapters/react/src/form-adapter.ts | 95 ------- packages/adapters/react/src/index.ts | 4 +- .../adapters/react/src/native-form-adapter.ts | 243 ++++++++++++++++++ 4 files changed, 246 insertions(+), 100 deletions(-) delete mode 100644 packages/adapters/react/src/form-adapter.ts create mode 100644 packages/adapters/react/src/native-form-adapter.ts diff --git a/packages/adapters/react/package.json b/packages/adapters/react/package.json index 5dfb7e0..03acaa3 100644 --- a/packages/adapters/react/package.json +++ b/packages/adapters/react/package.json @@ -38,9 +38,7 @@ "clean": "rm -rf dist" }, "dependencies": { - "@schepta/core": "workspace:*", - "react": "^18.2.0", - "react-hook-form": "^7.52.2" + "@schepta/core": "workspace:*" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/adapters/react/src/form-adapter.ts b/packages/adapters/react/src/form-adapter.ts deleted file mode 100644 index d2ad559..0000000 --- a/packages/adapters/react/src/form-adapter.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * React Hook Form Adapter - * - * Implements FormAdapter using react-hook-form - */ - -import type { UseFormReturn } from 'react-hook-form'; -import type { FormAdapter, FieldOptions, ReactiveState } from '@schepta/core'; -import { ReactReactiveState } from './reactive-state'; - -/** - * React Hook Form adapter implementation - */ -export class ReactHookFormAdapter implements FormAdapter { - private form: UseFormReturn; - - constructor(form: UseFormReturn) { - this.form = form; - } - - getValues(): Record { - return this.form.getValues(); - } - - getValue(field: string): any { - return this.form.getValues(field); - } - - setValue(field: string, value: any): void { - this.form.setValue(field, value); - } - - watch(field?: string): ReactiveState { - const watchedValue = field - ? this.form.watch(field) - : this.form.watch(); - - return new ReactReactiveState(watchedValue, (newValue) => { - if (field) { - this.setValue(field, newValue); - } - }); - } - - reset(values?: Record): void { - this.form.reset(values); - } - - register(field: string, options?: FieldOptions): void { - this.form.register(field, { - required: options?.required, - validate: options?.validate, - }); - } - - unregister(field: string): void { - this.form.unregister(field); - } - - getErrors(): Record { - return this.form.formState.errors; - } - - getError(field: string): any { - return this.form.formState.errors[field]; - } - - setError(field: string, error: any): void { - this.form.setError(field, error); - } - - clearErrors(field?: string): void { - if (field) { - this.form.clearErrors(field); - } else { - this.form.clearErrors(); - } - } - - isValid(): boolean { - return this.form.formState.isValid; - } - - handleSubmit(onSubmit: (values: Record) => void | Promise): () => void { - return this.form.handleSubmit(onSubmit); - } -} - -/** - * Create a React Hook Form adapter - */ -export function createReactHookFormAdapter(form: UseFormReturn): ReactHookFormAdapter { - return new ReactHookFormAdapter(form); -} - diff --git a/packages/adapters/react/src/index.ts b/packages/adapters/react/src/index.ts index 68816fd..03be526 100644 --- a/packages/adapters/react/src/index.ts +++ b/packages/adapters/react/src/index.ts @@ -1,11 +1,11 @@ /** * @schepta/adapter-react * - * React adapter for schepta rendering engine + * React adapter for schepta rendering engine. */ export * from './runtime-adapter'; -export * from './form-adapter'; +export * from './native-form-adapter'; export * from './reactive-state'; export * from './context'; export * from './provider'; diff --git a/packages/adapters/react/src/native-form-adapter.ts b/packages/adapters/react/src/native-form-adapter.ts new file mode 100644 index 0000000..cb822d9 --- /dev/null +++ b/packages/adapters/react/src/native-form-adapter.ts @@ -0,0 +1,243 @@ +/** + * Native React Form Adapter + * + * Implements FormAdapter using React state (useState). + */ + +import type { FormAdapter, FieldOptions, ReactiveState } from '@schepta/core'; +import { ReactReactiveState } from './reactive-state'; + +/** + * Native React form adapter implementation. + * Uses React state for form value management. + */ +export class NativeReactFormAdapter implements FormAdapter { + private state: Record; + private setState: React.Dispatch>>; + private errors: Record; + private setErrors: React.Dispatch>>; + private validators: Map boolean | string>; + private listeners: Set<(field: string, value: any) => void>; + + constructor( + state: Record, + setState: React.Dispatch>>, + errors: Record = {}, + setErrors: React.Dispatch>> = () => {} + ) { + this.state = state; + this.setState = setState; + this.errors = errors; + this.setErrors = setErrors; + this.validators = new Map(); + this.listeners = new Set(); + } + + /** + * Update internal state reference (called when state changes) + */ + updateState(newState: Record): void { + this.state = newState; + } + + /** + * Update internal errors reference (called when errors change) + */ + updateErrors(newErrors: Record): void { + this.errors = newErrors; + } + + getValues(): Record { + return { ...this.state }; + } + + getValue(field: string): any { + // Support nested fields like "user.name" + const parts = field.split('.'); + let value: any = this.state; + for (const part of parts) { + if (value === undefined || value === null) return undefined; + value = value[part]; + } + return value; + } + + setValue(field: string, value: any): void { + // Support nested fields like "user.name" + const parts = field.split('.'); + + this.setState((prevState) => { + const newState = { ...prevState }; + + if (parts.length === 1) { + newState[field] = value; + } else { + // Handle nested path + let current: any = newState; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (current[part] === undefined || current[part] === null) { + current[part] = {}; + } else { + current[part] = { ...current[part] }; + } + current = current[part]; + } + current[parts[parts.length - 1]] = value; + } + + return newState; + }); + + // Validate field if validator exists + this.validateField(field, value); + + // Notify listeners + this.listeners.forEach(listener => listener(field, value)); + } + + watch(field?: string): ReactiveState { + if (field) { + const value = this.getValue(field); + const state = new ReactReactiveState(value, (newValue) => { + this.setValue(field, newValue); + }); + return state; + } else { + const state = new ReactReactiveState(this.getValues(), (newValues) => { + this.setState(newValues); + }); + return state; + } + } + + reset(values?: Record): void { + this.setState(values || {}); + this.setErrors({}); + } + + register(field: string, options?: FieldOptions): void { + if (options?.validate) { + this.validators.set(field, options.validate); + } + if (options?.defaultValue !== undefined && this.getValue(field) === undefined) { + this.setValue(field, options.defaultValue); + } + } + + unregister(field: string): void { + this.validators.delete(field); + // Optionally remove the field value + this.setState((prevState) => { + const newState = { ...prevState }; + delete newState[field]; + return newState; + }); + } + + getErrors(): Record { + return { ...this.errors }; + } + + getError(field: string): any { + return this.errors[field]; + } + + setError(field: string, error: any): void { + this.setErrors((prevErrors) => ({ + ...prevErrors, + [field]: error, + })); + } + + clearErrors(field?: string): void { + if (field) { + this.setErrors((prevErrors) => { + const newErrors = { ...prevErrors }; + delete newErrors[field]; + return newErrors; + }); + } else { + this.setErrors({}); + } + } + + isValid(): boolean { + return Object.keys(this.errors).length === 0; + } + + handleSubmit(onSubmit: (values: Record) => void | Promise): () => void { + return () => { + // Run all validators first + let hasErrors = false; + const newErrors: Record = {}; + + this.validators.forEach((validator, field) => { + const value = this.getValue(field); + const result = validator(value); + if (result !== true) { + hasErrors = true; + newErrors[field] = typeof result === 'string' ? result : 'Validation failed'; + } + }); + + if (hasErrors) { + this.setErrors(newErrors); + return; + } + + // If valid, submit + onSubmit(this.getValues()); + }; + } + + private validateField(field: string, value: any): void { + const validator = this.validators.get(field); + if (validator) { + const result = validator(value); + if (result === true) { + // Clear error for this field + this.setErrors((prevErrors) => { + const newErrors = { ...prevErrors }; + delete newErrors[field]; + return newErrors; + }); + } else { + this.setErrors((prevErrors) => ({ + ...prevErrors, + [field]: typeof result === 'string' ? result : 'Validation failed', + })); + } + } + } + + /** + * Subscribe to field changes + */ + subscribe(callback: (field: string, value: any) => void): () => void { + this.listeners.add(callback); + return () => { + this.listeners.delete(callback); + }; + } +} + +/** + * Create a native React form adapter. + * Use this with useState hooks in your component. + * + * @example + * ```tsx + * const [formState, setFormState] = useState({}); + * const [errors, setErrors] = useState({}); + * const adapter = createNativeReactFormAdapter(formState, setFormState, errors, setErrors); + * ``` + */ +export function createNativeReactFormAdapter( + state: Record, + setState: React.Dispatch>>, + errors?: Record, + setErrors?: React.Dispatch>> +): NativeReactFormAdapter { + return new NativeReactFormAdapter(state, setState, errors, setErrors); +} From ebad6cde29b487c709eafe94758ec8173e1cf770 Mon Sep 17 00:00:00 2001 From: Edson Batista Date: Sun, 25 Jan 2026 20:56:56 +0000 Subject: [PATCH 2/3] feat(factory-react): native form state and injectable FieldWrapper - Add ScheptaFormContext, ScheptaFormProvider, useScheptaFormAdapter, useScheptaFieldValue - Add DefaultFieldWrapper; remove field-wrapper.tsx; FieldWrapper injectable via registry - use-schepta-form: native state (useState), createNativeReactFormAdapter - form-factory: ScheptaFormProvider, pass values to context, resolve FieldWrapper from registry - DefaultFormContainer: use useScheptaFormAdapter for submit - Drop react-hook-form from peerDependencies --- packages/factories/react/package.json | 4 +- .../src/components/DefaultFieldWrapper.tsx | 124 ++++++++++++ .../src/components/DefaultFormContainer.tsx | 14 +- .../factories/react/src/components/index.ts | 1 + packages/factories/react/src/context/index.ts | 12 ++ .../src/context/schepta-form-context.tsx | 184 ++++++++++++++++++ .../factories/react/src/field-wrapper.tsx | 37 ---- packages/factories/react/src/form-factory.tsx | 59 +++--- .../react/src/hooks/use-schepta-form.ts | 123 ++++++++---- packages/factories/react/src/index.ts | 20 +- .../react/src/renderers/field-renderer.ts | 32 ++- 11 files changed, 501 insertions(+), 109 deletions(-) create mode 100644 packages/factories/react/src/components/DefaultFieldWrapper.tsx create mode 100644 packages/factories/react/src/context/index.ts create mode 100644 packages/factories/react/src/context/schepta-form-context.tsx delete mode 100644 packages/factories/react/src/field-wrapper.tsx diff --git a/packages/factories/react/package.json b/packages/factories/react/package.json index 63854e2..68a3af7 100644 --- a/packages/factories/react/package.json +++ b/packages/factories/react/package.json @@ -28,7 +28,6 @@ "rendering", "schema", "forms", - "react-hook-form", "server-driven-ui" ], "scripts": { @@ -45,8 +44,7 @@ }, "peerDependencies": { "react": ">=18.0.0", - "react-dom": ">=18.0.0", - "react-hook-form": "^7.52.2" + "react-dom": ">=18.0.0" }, "devDependencies": { "@testing-library/react": "^14.0.0", diff --git a/packages/factories/react/src/components/DefaultFieldWrapper.tsx b/packages/factories/react/src/components/DefaultFieldWrapper.tsx new file mode 100644 index 0000000..4225d88 --- /dev/null +++ b/packages/factories/react/src/components/DefaultFieldWrapper.tsx @@ -0,0 +1,124 @@ +/** + * 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/DefaultFormContainer.tsx b/packages/factories/react/src/components/DefaultFormContainer.tsx index 6276b58..504e64a 100644 --- a/packages/factories/react/src/components/DefaultFormContainer.tsx +++ b/packages/factories/react/src/components/DefaultFormContainer.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { useScheptaFormAdapter } from '../context/schepta-form-context'; import { DefaultSubmitButton, SubmitButtonComponentType } from './DefaultSubmitButton'; /** @@ -50,10 +51,19 @@ export const DefaultFormContainer: React.FC = ({ children, onSubmit, }) => { + const adapter = useScheptaFormAdapter(); + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (onSubmit) { + adapter.handleSubmit(onSubmit)(); + } + }; + return ( -
+ {children} - {onSubmit && } + {onSubmit && } ); }; diff --git a/packages/factories/react/src/components/index.ts b/packages/factories/react/src/components/index.ts index 09bd3ee..cae7259 100644 --- a/packages/factories/react/src/components/index.ts +++ b/packages/factories/react/src/components/index.ts @@ -6,3 +6,4 @@ export * from './DefaultFormContainer'; export * from './DefaultSubmitButton'; +export * from './DefaultFieldWrapper'; diff --git a/packages/factories/react/src/context/index.ts b/packages/factories/react/src/context/index.ts new file mode 100644 index 0000000..2f01260 --- /dev/null +++ b/packages/factories/react/src/context/index.ts @@ -0,0 +1,12 @@ +/** + * Schepta Form Context exports + */ + +export { + ScheptaFormProvider, + useScheptaFormAdapter, + useScheptaFormValues, + useScheptaFieldValue, + ScheptaFormContext, + type ScheptaFormProviderProps, +} from './schepta-form-context'; diff --git a/packages/factories/react/src/context/schepta-form-context.tsx b/packages/factories/react/src/context/schepta-form-context.tsx new file mode 100644 index 0000000..3b730c0 --- /dev/null +++ b/packages/factories/react/src/context/schepta-form-context.tsx @@ -0,0 +1,184 @@ +/** + * Schepta Form Context + * + * Provides form adapter to child components without external dependencies. + * This is the native form state management system for Schepta. + */ + +import React, { createContext, useContext, useMemo, useState, useRef, useEffect } from 'react'; +import type { FormAdapter } from '@schepta/core'; +import { NativeReactFormAdapter, createNativeReactFormAdapter } from '@schepta/adapter-react'; + +/** + * Context type for Schepta form adapter + */ +interface ScheptaFormContextType { + /** The form adapter instance */ + adapter: FormAdapter; + /** Current form values (for reactivity) */ + values: Record; +} + +/** + * React context for the form adapter + */ +const ScheptaFormContext = createContext(null); + +/** + * Props for ScheptaFormProvider + */ +export interface ScheptaFormProviderProps { + /** Child components */ + children: React.ReactNode; + /** Initial form values */ + initialValues?: Record; + /** External adapter (optional - for custom implementations) */ + adapter?: FormAdapter; + /** Current form values (for reactivity - passed from FormFactory) */ + values?: Record; +} + +/** + * Provider component that wraps form content and provides the form adapter. + * + * @example Using with default native adapter + * ```tsx + * + * + * + * ``` + * + * @example Using with custom adapter and values + * ```tsx + * const myAdapter = createCustomAdapter(); + * + * + * + * ``` + */ +export function ScheptaFormProvider({ + children, + initialValues = {}, + adapter: externalAdapter, + values: externalValues, +}: ScheptaFormProviderProps) { + // If external values are provided (from FormFactory), use them + // Otherwise, create our own state management + const [internalValues, setInternalValues] = useState>(initialValues); + const [errors, setErrors] = useState>({}); + + // Use external values if provided, otherwise use internal state + const values = externalValues ?? internalValues; + + // Create or use provided adapter + const adapterRef = useRef(null); + + if (!adapterRef.current) { + if (externalAdapter) { + adapterRef.current = externalAdapter; + } else { + adapterRef.current = createNativeReactFormAdapter(internalValues, setInternalValues, errors, setErrors); + } + } + + // Update native adapter's internal state reference when internal values change + // (only if we're using internal state management) + useEffect(() => { + if (!externalValues && adapterRef.current instanceof NativeReactFormAdapter) { + adapterRef.current.updateState(internalValues); + } + }, [internalValues, externalValues]); + + // Update native adapter's internal errors reference when errors change + useEffect(() => { + if (adapterRef.current instanceof NativeReactFormAdapter) { + adapterRef.current.updateErrors(errors); + } + }, [errors]); + + const contextValue = useMemo(() => ({ + adapter: adapterRef.current!, + values, + }), [values]); + + return ( + + {children} + + ); +} + +/** + * Hook to access the form adapter from context. + * + * @returns The form adapter instance + * @throws Error if used outside of ScheptaFormProvider + * + * @example + * ```tsx + * function MyField() { + * const adapter = useScheptaFormAdapter(); + * const value = adapter.getValue('fieldName'); + * return adapter.setValue('fieldName', e.target.value)} />; + * } + * ``` + */ +export function useScheptaFormAdapter(): FormAdapter { + const context = useContext(ScheptaFormContext); + + if (!context) { + throw new Error( + 'useScheptaFormAdapter must be used within a ScheptaFormProvider. ' + + 'Make sure your component is wrapped with ScheptaFormProvider or FormFactory.' + ); + } + + return context.adapter; +} + +/** + * Hook to access form values reactively. + * Triggers re-render when form values change. + * + * @returns Current form values + */ +export function useScheptaFormValues(): Record { + const context = useContext(ScheptaFormContext); + + if (!context) { + throw new Error( + 'useScheptaFormValues must be used within a ScheptaFormProvider.' + ); + } + + return context.values; +} + +/** + * Hook to get a specific field value reactively. + * + * @param field - The field name (supports dot notation for nested fields) + * @returns The field value + */ +export function useScheptaFieldValue(field: string): any { + const context = useContext(ScheptaFormContext); + + if (!context) { + throw new Error( + 'useScheptaFieldValue must be used within a ScheptaFormProvider.' + ); + } + + // Support nested fields + const parts = field.split('.'); + let value: any = context.values; + for (const part of parts) { + if (value === undefined || value === null) return undefined; + value = value[part]; + } + + return value; +} + +// Export context for advanced usage +export { ScheptaFormContext }; diff --git a/packages/factories/react/src/field-wrapper.tsx b/packages/factories/react/src/field-wrapper.tsx deleted file mode 100644 index 108622d..0000000 --- a/packages/factories/react/src/field-wrapper.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Field Wrapper - * - * Wraps field components with react-hook-form Controller - */ - -import React from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; - -export interface FieldWrapperProps { - name: string; - component: React.ComponentType; - componentProps?: Record; - children?: React.ReactNode; -} - -export function FieldWrapper({ name, component: Component, componentProps = {}, children }: FieldWrapperProps) { - const { control } = useFormContext(); - - return ( - ( - field.onChange(value)} - > - {children} - - )} - /> - ); -} - diff --git a/packages/factories/react/src/form-factory.tsx b/packages/factories/react/src/form-factory.tsx index dee0e3d..7b9d14e 100644 --- a/packages/factories/react/src/form-factory.tsx +++ b/packages/factories/react/src/form-factory.tsx @@ -1,12 +1,11 @@ /** * React Form Factory * - * Factory component for rendering forms from JSON schemas + * Factory component for rendering forms from JSON schemas. */ import React, { useMemo, forwardRef, useImperativeHandle } from 'react'; -import { FormProvider, UseFormReturn } from 'react-hook-form'; -import type { FormSchema, ComponentSpec, MiddlewareFn } from '@schepta/core'; +import type { FormSchema, ComponentSpec, MiddlewareFn, FormAdapter } from '@schepta/core'; import { createReactRuntimeAdapter } from '@schepta/adapter-react'; import { createRendererOrchestrator, @@ -22,10 +21,13 @@ 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 { DefaultFormContainer, DefaultSubmitButton, - type SubmitButtonComponentType + DefaultFieldWrapper, + type SubmitButtonComponentType, + type FieldWrapperType, } from './components'; // Register factory default components (called once on module load) @@ -40,6 +42,11 @@ setFactoryDefaultComponents({ type: 'content', factory: () => DefaultSubmitButton, }), + FieldWrapper: createComponentSpec({ + id: 'FieldWrapper', + type: 'field-wrapper', + factory: () => DefaultFieldWrapper, + }), }); /** @@ -60,7 +67,7 @@ export interface FormFactoryProps { renderers?: Partial>; externalContext?: Record; middlewares?: MiddlewareFn[]; - formContext?: UseFormReturn; + adapter?: FormAdapter; initialValues?: Record; onSubmit?: (values: Record) => void | Promise; debug?: boolean; @@ -72,7 +79,7 @@ export const FormFactory = forwardRef(function renderers, externalContext, middlewares, - formContext: providedFormContext, + adapter: providedAdapter, initialValues, onSubmit, debug = false, @@ -91,18 +98,17 @@ export const FormFactory = forwardRef(function debug, }); - // Setup react-hook-form - const { formContext, formAdapter, formState } = useScheptaForm(schema, { - formContext: providedFormContext, + const { formAdapter, formState, reset } = useScheptaForm(schema, { initialValues, + adapter: providedAdapter, }); // Expose form control methods via ref for external submit scenarios useImperativeHandle(ref, () => ({ - submit: (submitFn) => formContext.handleSubmit(submitFn)(), - reset: (values) => formContext.reset(values), - getValues: () => formContext.getValues(), - }), [formContext]); + submit: (submitFn) => formAdapter.handleSubmit(submitFn)(), + reset: (values) => reset(values), + getValues: () => formAdapter.getValues(), + }), [formAdapter, reset]); // Create runtime adapter const runtime = useMemo(() => createReactRuntimeAdapter(), []); @@ -141,25 +147,28 @@ export const FormFactory = forwardRef(function 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 => { - // Get current form state - const currentFormState = formContext.watch(); - // Create debug context const debugContext = createDebugContext(mergedConfig.debug); - // Create custom renderers with field renderer + // Create custom renderers with field renderer (passing resolved FieldWrapper) const customRenderers = { ...mergedConfig.renderers, - field: createFieldRenderer(), + field: createFieldRenderer({ FieldWrapper: FieldWrapperComponent }), }; // Create template expression middleware with current form state (always first) const templateMiddleware = createTemplateExpressionMiddleware({ externalContext: mergedConfig.externalContext, - formState: currentFormState, + formState, debug: debugContext, }); @@ -175,7 +184,7 @@ export const FormFactory = forwardRef(function externalContext: { ...mergedConfig.externalContext, }, - state: currentFormState, + state: formState, middlewares: updatedMiddlewares, onSubmit, debug: debugContext, @@ -190,21 +199,21 @@ export const FormFactory = forwardRef(function mergedConfig.externalContext, mergedConfig.baseMiddlewares, mergedConfig.debug, - formContext, formAdapter, runtime, onSubmit, - formState, // Include formState to trigger re-renders on form changes - SubmitButtonComponent, // Include resolved SubmitButton for FormContainer + formState, + SubmitButtonComponent, + FieldWrapperComponent, ]); return ( - + - + ); }); diff --git a/packages/factories/react/src/hooks/use-schepta-form.ts b/packages/factories/react/src/hooks/use-schepta-form.ts index 6e224b3..57c789d 100644 --- a/packages/factories/react/src/hooks/use-schepta-form.ts +++ b/packages/factories/react/src/hooks/use-schepta-form.ts @@ -1,79 +1,132 @@ /** * useScheptaForm Hook * - * Encapsulates react-hook-form setup and state management for Schepta forms. + * Encapsulates native form state management for Schepta forms. */ -import { useEffect, useMemo } from 'react'; -import { useForm, UseFormReturn, useWatch } from 'react-hook-form'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { FormSchema, FormAdapter } from '@schepta/core'; -import { createReactHookFormAdapter } from '@schepta/adapter-react'; +import { createNativeReactFormAdapter, NativeReactFormAdapter } from '@schepta/adapter-react'; import { buildInitialValuesFromSchema } from '@schepta/core'; export interface ScheptaFormOptions { - /** External form context (if managing form state externally) */ - formContext?: UseFormReturn; /** Initial values for the form */ initialValues?: Record; + /** External adapter (optional - for custom implementations) */ + adapter?: FormAdapter; } export interface ScheptaFormResult { - /** The react-hook-form context */ - formContext: UseFormReturn; /** The Schepta form adapter */ formAdapter: FormAdapter; - /** Current form state (watched for reactivity) */ + /** Current form state (for reactivity) */ formState: Record; + /** Form errors */ + formErrors: Record; + /** Set form state directly */ + setFormState: React.Dispatch>>; + /** Set form errors directly */ + setFormErrors: React.Dispatch>>; + /** Reset form to initial values */ + reset: (values?: Record) => void; } /** - * Hook to setup and manage form state for Schepta forms + * Hook to setup and manage form state for Schepta forms. + * Uses native React state. * * @param schema - The form schema - * @param options - Form options including external context and initial values - * @returns Form context, adapter, and reactive state + * @param options - Form options including initial values and optional external adapter + * @returns Form adapter, reactive state, and control functions + * + * @example Basic usage + * ```tsx + * const { formAdapter, formState } = useScheptaForm(schema, { + * initialValues: { name: 'John' }, + * }); + * ``` + * + * @example With custom adapter + * ```tsx + * const myAdapter = createCustomAdapter(); + * const { formAdapter, formState } = useScheptaForm(schema, { + * adapter: myAdapter, + * }); + * ``` */ export function useScheptaForm( schema: FormSchema, options: ScheptaFormOptions = {} ): ScheptaFormResult { - const { formContext: providedFormContext, initialValues } = options; + const { initialValues, adapter: externalAdapter } = options; + + // Build default values from schema if no initial values provided + const defaultValues = useMemo(() => { + const schemaDefaults = buildInitialValuesFromSchema(schema); + return { ...schemaDefaults, ...initialValues }; + }, [schema, initialValues]); + + const [formState, setFormState] = useState>(defaultValues); + const [formErrors, setFormErrors] = useState>({}); + + // Create adapter (ref to maintain identity) + const adapterRef = useRef(null); - // Create default form context if not provided - const defaultFormContext = useForm({ - defaultValues: initialValues || buildInitialValuesFromSchema(schema), - }); + if (!adapterRef.current) { + if (externalAdapter) { + adapterRef.current = externalAdapter; + } else { + adapterRef.current = createNativeReactFormAdapter( + formState, + setFormState, + formErrors, + setFormErrors + ); + } + } - // Use provided context or default - const formContext = providedFormContext || defaultFormContext; + // Update native adapter's internal state reference when state changes + useEffect(() => { + if (adapterRef.current instanceof NativeReactFormAdapter) { + adapterRef.current.updateState(formState); + } + }, [formState]); - // Create form adapter (memoized) - const formAdapter = useMemo( - () => createReactHookFormAdapter(formContext), - [formContext] - ); + // Update native adapter's internal errors reference when errors change + useEffect(() => { + if (adapterRef.current instanceof NativeReactFormAdapter) { + adapterRef.current.updateErrors(formErrors); + } + }, [formErrors]); // Reset form when initialValues change useEffect(() => { if (initialValues !== undefined) { - const defaultValues = { + const newDefaults = { ...buildInitialValuesFromSchema(schema), ...initialValues, }; - formContext.reset(defaultValues); + setFormState(newDefaults); + setFormErrors({}); } - }, [formContext, initialValues, schema]); + }, [initialValues, schema]); - // Watch form state to trigger re-renders when values change - // This ensures template expressions with $formValues are updated - const formState = useWatch({ - control: formContext.control, - }); + // Reset function + const reset = useMemo(() => { + return (values?: Record) => { + const resetValues = values || defaultValues; + setFormState(resetValues); + setFormErrors({}); + }; + }, [defaultValues]); return { - formContext, - formAdapter, - formState: formState as Record, + formAdapter: adapterRef.current!, + formState, + formErrors, + setFormState, + setFormErrors, + reset, }; } diff --git a/packages/factories/react/src/index.ts b/packages/factories/react/src/index.ts index d915edb..4d8ed30 100644 --- a/packages/factories/react/src/index.ts +++ b/packages/factories/react/src/index.ts @@ -1,7 +1,12 @@ /** * @schepta/factory-react * - * React factories for schepta rendering engine + * React factories for schepta rendering engine. + * No external form library dependencies (react-hook-form, formik, etc.) + * + * Users can integrate form libraries by creating custom components. + * See examples/react/src/basic-ui/components/rhf/ for RHF integration. + * See examples/react/src/basic-ui/components/formik/ for Formik integration. */ // Main factory @@ -15,11 +20,23 @@ export { export { DefaultFormContainer, DefaultSubmitButton, + DefaultFieldWrapper, type FormContainerProps, type SubmitButtonProps, type SubmitButtonComponentType, + type FieldWrapperProps, + type FieldWrapperType, } from './components'; +// Context and hooks for form state management +export { + ScheptaFormProvider, + useScheptaFormAdapter, + useScheptaFormValues, + useScheptaFieldValue, + type ScheptaFormProviderProps, +} from './context'; + // Hooks (for advanced usage) export * from './hooks'; @@ -31,4 +48,3 @@ export * from './utils'; // Re-export internal components for advanced usage export { FormRenderer } from './form-renderer'; -export { FieldWrapper } from './field-wrapper'; diff --git a/packages/factories/react/src/renderers/field-renderer.ts b/packages/factories/react/src/renderers/field-renderer.ts index 5f0e23e..839f201 100644 --- a/packages/factories/react/src/renderers/field-renderer.ts +++ b/packages/factories/react/src/renderers/field-renderer.ts @@ -1,26 +1,48 @@ /** * Field Renderer * - * Custom renderer that wraps field components with FieldWrapper - * for react-hook-form integration. + * 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 { sanitizePropsForDOM } from '@schepta/core'; -import { FieldWrapper } from '../field-wrapper'; +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 react-hook-form Controller + * - 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(): RendererFn { +export function createFieldRenderer(options: FieldRendererOptions = {}): RendererFn { + const { FieldWrapper = DefaultFieldWrapper } = options; + return ( spec: ComponentSpec, props: Record, From 3d3d3dd0e5b326594a8ebdce213a608711b2de47 Mon Sep 17 00:00:00 2001 From: Edson Batista Date: Sun, 25 Jan 2026 20:57:06 +0000 Subject: [PATCH 3/3] feat(examples): reorganize forms, add RHF/Formik demos, tsconfig fix - basic-ui: Form -> Forms/NativeForm, FormModal -> Forms/ModalForm - basic-ui: remove custom FormContainer and SubmitButton (use factory defaults) - Add FormWithRHF and FormWithFormik with rhf/ and formik/ components - BasicFormPage: NativeForm, ModalForm, FormWithRHF, FormWithFormik - tsconfig: paths {} to fix rootDir with @schepta/* resolution - chakra/material: FormContainer props alignment - Add formik dependency; update pnpm-lock.yaml --- examples/react/package.json | 1 + .../basic-ui/components/ComponentRegistry.tsx | 12 -- .../components/Containers/FormContainer.tsx | 23 --- .../components/Forms/FormWithFormik.tsx | 145 ++++++++++++++++++ .../basic-ui/components/Forms/FormWithRHF.tsx | 145 ++++++++++++++++++ .../{FormModal.tsx => Forms/ModalForm.tsx} | 2 +- .../{Form.tsx => Forms/NativeForm.tsx} | 4 +- .../src/basic-ui/components/SubmitButton.tsx | 30 ---- .../components/formik/FormikFieldWrapper.tsx | 59 +++++++ .../components/formik/FormikFormContainer.tsx | 77 ++++++++++ .../components/rhf/RHFFieldWrapper.tsx | 57 +++++++ .../components/rhf/RHFFormContainer.tsx | 73 +++++++++ .../src/basic-ui/pages/BasicFormPage.tsx | 30 ++-- .../components/Containers/FormContainer.tsx | 8 + .../components/Containers/FormContainer.tsx | 8 + examples/react/tsconfig.json | 19 +-- pnpm-lock.yaml | 59 ++++++- 17 files changed, 658 insertions(+), 94 deletions(-) delete mode 100644 examples/react/src/basic-ui/components/Containers/FormContainer.tsx create mode 100644 examples/react/src/basic-ui/components/Forms/FormWithFormik.tsx create mode 100644 examples/react/src/basic-ui/components/Forms/FormWithRHF.tsx rename examples/react/src/basic-ui/components/{FormModal.tsx => Forms/ModalForm.tsx} (98%) rename examples/react/src/basic-ui/components/{Form.tsx => Forms/NativeForm.tsx} (91%) delete mode 100644 examples/react/src/basic-ui/components/SubmitButton.tsx create mode 100644 examples/react/src/basic-ui/components/formik/FormikFieldWrapper.tsx create mode 100644 examples/react/src/basic-ui/components/formik/FormikFormContainer.tsx create mode 100644 examples/react/src/basic-ui/components/rhf/RHFFieldWrapper.tsx create mode 100644 examples/react/src/basic-ui/components/rhf/RHFFormContainer.tsx diff --git a/examples/react/package.json b/examples/react/package.json index 7640570..28546ce 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -15,6 +15,7 @@ "@schepta/adapter-react": "workspace:*", "@schepta/core": "workspace:*", "@schepta/factory-react": "workspace:*", + "formik": "^2.4.6", "framer-motion": "^10.16.16", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/examples/react/src/basic-ui/components/ComponentRegistry.tsx b/examples/react/src/basic-ui/components/ComponentRegistry.tsx index 1e4f845..4268111 100644 --- a/examples/react/src/basic-ui/components/ComponentRegistry.tsx +++ b/examples/react/src/basic-ui/components/ComponentRegistry.tsx @@ -5,8 +5,6 @@ import { InputCheckbox } from "./Inputs/InputCheckbox"; import { InputTextarea } from "./Inputs/InputTextarea"; import { InputNumber } from "./Inputs/InputNumber"; import { InputDate } from "./Inputs/InputDate"; -import { SubmitButton } from "./SubmitButton"; -import { FormContainer } from "./Containers/FormContainer"; import { FormField } from "./Containers/FormField"; import { FormSectionContainer } from "./Containers/FormSectionContainer"; import { FormSectionTitle } from "./Containers/FormSectionTitle"; @@ -14,11 +12,6 @@ import { FormSectionGroupContainer } from "./Containers/FormSectionGroupContaine import { FormSectionGroup } from "./Containers/FormSectionGroup"; export const components = { - 'FormContainer': createComponentSpec({ - id: "FormContainer", - type: "FormContainer", - factory: (props, runtime) => FormContainer, - }), InputText: createComponentSpec({ id: "InputText", type: "field", @@ -55,11 +48,6 @@ export const components = { type: "field", factory: (props, runtime) => InputDate, }), - SubmitButton: createComponentSpec({ - id: "SubmitButton", - type: 'content', - factory: (props, runtime) => SubmitButton, - }), FormField: createComponentSpec({ id: "FormField", type: 'container', diff --git a/examples/react/src/basic-ui/components/Containers/FormContainer.tsx b/examples/react/src/basic-ui/components/Containers/FormContainer.tsx deleted file mode 100644 index 922bc6a..0000000 --- a/examples/react/src/basic-ui/components/Containers/FormContainer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { useFormContext } from 'react-hook-form'; -import type { FormContainerProps } from '@schepta/factory-react'; -import { SubmitButton } from '../SubmitButton'; - -export const FormContainer: React.FC = ({ - children, - onSubmit, -}) => { - const formContext = useFormContext(); - - const handleFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (onSubmit) formContext.handleSubmit(onSubmit)(); - }; - - return ( -
- {children} - {onSubmit && } - - ); -}; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Forms/FormWithFormik.tsx b/examples/react/src/basic-ui/components/Forms/FormWithFormik.tsx new file mode 100644 index 0000000..9df3140 --- /dev/null +++ b/examples/react/src/basic-ui/components/Forms/FormWithFormik.tsx @@ -0,0 +1,145 @@ +/** + * Form with Formik + * + * Example component demonstrating how to use Schepta with Formik. + * This shows how to inject custom Formik components via the component registry. + */ + +import React, { useState } from 'react'; +import { FormFactory } from '@schepta/factory-react'; +import { createComponentSpec, FormSchema } from '@schepta/core'; +import { FormikFieldWrapper } from '../formik/FormikFieldWrapper'; +import { FormikFormContainer } from '../formik/FormikFormContainer'; + +// Import the same input components from basic-ui +import { InputText } from '../Inputs/InputText'; +import { InputSelect } from '../Inputs/InputSelect'; +import { InputCheckbox } from '../Inputs/InputCheckbox'; +import { InputTextarea } from '../Inputs/InputTextarea'; +import { InputNumber } from '../Inputs/InputNumber'; +import { InputDate } from '../Inputs/InputDate'; + +/** + * Formik-specific component registry. + * Registers the Formik FieldWrapper and FormContainer to use + * Formik for form state management. + */ +const formikComponents = { + // Register Formik FieldWrapper - this makes all fields use Formik's context + FieldWrapper: createComponentSpec({ + id: 'FieldWrapper', + type: 'field-wrapper', + factory: () => FormikFieldWrapper, + }), + // Register Formik FormContainer - this provides the Formik context + FormContainer: createComponentSpec({ + id: 'FormContainer', + type: 'FormContainer', + factory: () => FormikFormContainer, + }), + // Standard input components (same as basic-ui) + InputText: createComponentSpec({ + id: 'InputText', + type: 'field', + factory: () => InputText, + }), + InputSelect: createComponentSpec({ + id: 'InputSelect', + type: 'field', + factory: () => InputSelect, + }), + InputCheckbox: createComponentSpec({ + id: 'InputCheckbox', + type: 'field', + factory: () => InputCheckbox, + }), + InputTextarea: createComponentSpec({ + id: 'InputTextarea', + type: 'field', + factory: () => InputTextarea, + }), + InputNumber: createComponentSpec({ + id: 'InputNumber', + type: 'field', + factory: () => InputNumber, + }), + InputDate: createComponentSpec({ + id: 'InputDate', + type: 'field', + factory: () => InputDate, + }), + InputPhone: createComponentSpec({ + id: 'InputPhone', + type: 'field', + factory: () => InputText, + defaultProps: { type: 'tel' }, + }), +}; + +interface FormWithFormikProps { + schema: FormSchema; +} + +/** + * FormWithFormik Component + * + * Renders a form using Formik for state management. + * Demonstrates how to integrate external form libraries with Schepta. + */ +export const FormWithFormik: React.FC = ({ schema }) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + + const handleSubmit = (values: Record) => { + console.log('Form submitted (Formik):', values); + setSubmittedValues(values); + }; + + return ( + <> +
+
+ This form uses Formik for state management. + The FieldWrapper and FormContainer are custom Formik implementations. +
+ +
+ {submittedValues && ( +
+

Submitted Values (Formik):

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/examples/react/src/basic-ui/components/Forms/FormWithRHF.tsx b/examples/react/src/basic-ui/components/Forms/FormWithRHF.tsx new file mode 100644 index 0000000..5936610 --- /dev/null +++ b/examples/react/src/basic-ui/components/Forms/FormWithRHF.tsx @@ -0,0 +1,145 @@ +/** + * Form with React Hook Form + * + * Example component demonstrating how to use Schepta with react-hook-form. + * This shows how to inject custom RHF components via the component registry. + */ + +import React, { useState } from 'react'; +import { FormFactory } from '@schepta/factory-react'; +import { createComponentSpec, FormSchema } from '@schepta/core'; +import { RHFFieldWrapper } from '../rhf/RHFFieldWrapper'; +import { RHFFormContainer } from '../rhf/RHFFormContainer'; + +// Import the same input components from basic-ui +import { InputText } from '../Inputs/InputText'; +import { InputSelect } from '../Inputs/InputSelect'; +import { InputCheckbox } from '../Inputs/InputCheckbox'; +import { InputTextarea } from '../Inputs/InputTextarea'; +import { InputNumber } from '../Inputs/InputNumber'; +import { InputDate } from '../Inputs/InputDate'; + +/** + * RHF-specific component registry. + * Registers the RHF FieldWrapper and FormContainer to use + * react-hook-form for form state management. + */ +const rhfComponents = { + // Register RHF FieldWrapper - this makes all fields use RHF's Controller + FieldWrapper: createComponentSpec({ + id: 'FieldWrapper', + type: 'field-wrapper', + factory: () => RHFFieldWrapper, + }), + // Register RHF FormContainer - this provides the FormProvider context + FormContainer: createComponentSpec({ + id: 'FormContainer', + type: 'FormContainer', + factory: () => RHFFormContainer, + }), + // Standard input components (same as basic-ui) + InputText: createComponentSpec({ + id: 'InputText', + type: 'field', + factory: () => InputText, + }), + InputSelect: createComponentSpec({ + id: 'InputSelect', + type: 'field', + factory: () => InputSelect, + }), + InputCheckbox: createComponentSpec({ + id: 'InputCheckbox', + type: 'field', + factory: () => InputCheckbox, + }), + InputTextarea: createComponentSpec({ + id: 'InputTextarea', + type: 'field', + factory: () => InputTextarea, + }), + InputNumber: createComponentSpec({ + id: 'InputNumber', + type: 'field', + factory: () => InputNumber, + }), + InputDate: createComponentSpec({ + id: 'InputDate', + type: 'field', + factory: () => InputDate, + }), + InputPhone: createComponentSpec({ + id: 'InputPhone', + type: 'field', + factory: () => InputText, + defaultProps: { type: 'tel' }, + }), +}; + +interface FormWithRHFProps { + schema: FormSchema; +} + +/** + * FormWithRHF Component + * + * Renders a form using react-hook-form for state management. + * Demonstrates how to integrate external form libraries with Schepta. + */ +export const FormWithRHF: React.FC = ({ schema }) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + + const handleSubmit = (values: Record) => { + console.log('Form submitted (RHF):', values); + setSubmittedValues(values); + }; + + return ( + <> +
+
+ This form uses react-hook-form for state management. + The FieldWrapper and FormContainer are custom RHF implementations. +
+ +
+ {submittedValues && ( +
+

Submitted Values (RHF):

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/examples/react/src/basic-ui/components/FormModal.tsx b/examples/react/src/basic-ui/components/Forms/ModalForm.tsx similarity index 98% rename from examples/react/src/basic-ui/components/FormModal.tsx rename to examples/react/src/basic-ui/components/Forms/ModalForm.tsx index 6c97413..a9bdf5f 100644 --- a/examples/react/src/basic-ui/components/FormModal.tsx +++ b/examples/react/src/basic-ui/components/Forms/ModalForm.tsx @@ -13,7 +13,7 @@ interface FormModalProps { * The form is rendered in a modal-like structure where the submit button * is outside the FormFactory component (in the footer). */ -export const FormModal = ({ schema, onSubmit }: FormModalProps) => { +export const ModalForm = ({ schema, onSubmit }: FormModalProps) => { const formRef = useRef(null); const [submittedValues, setSubmittedValues] = useState | null>(null); const [isOpen, setIsOpen] = useState(false); diff --git a/examples/react/src/basic-ui/components/Form.tsx b/examples/react/src/basic-ui/components/Forms/NativeForm.tsx similarity index 91% rename from examples/react/src/basic-ui/components/Form.tsx rename to examples/react/src/basic-ui/components/Forms/NativeForm.tsx index ed4e4e7..1f11dfc 100644 --- a/examples/react/src/basic-ui/components/Form.tsx +++ b/examples/react/src/basic-ui/components/Forms/NativeForm.tsx @@ -1,12 +1,12 @@ import React, { useState } from "react"; -import { FormFactory } from "@schepta/factory-react"; +import { FormFactory } from '@schepta/factory-react'; import { FormSchema } from "@schepta/core"; interface FormProps { schema: FormSchema; } -export const Form = ({ schema }: FormProps) => { +export const NativeForm = ({ schema }: FormProps) => { const [submittedValues, setSubmittedValues] = useState | null>(null); const handleSubmit = (values: Record) => { diff --git a/examples/react/src/basic-ui/components/SubmitButton.tsx b/examples/react/src/basic-ui/components/SubmitButton.tsx deleted file mode 100644 index afad264..0000000 --- a/examples/react/src/basic-ui/components/SubmitButton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import type { SubmitButtonProps } from '@schepta/factory-react'; - -export const SubmitButton: React.FC = ({ - children, - 'x-content': content, - ...props -}) => { - return ( -
- -
- ); -}; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/formik/FormikFieldWrapper.tsx b/examples/react/src/basic-ui/components/formik/FormikFieldWrapper.tsx new file mode 100644 index 0000000..f2d4a69 --- /dev/null +++ b/examples/react/src/basic-ui/components/formik/FormikFieldWrapper.tsx @@ -0,0 +1,59 @@ +/** + * Formik Field Wrapper + * + * Custom FieldWrapper 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'; + +/** + * Formik-based FieldWrapper component. + * Uses Formik's context to bind fields to form state. + * + * Register this component via the components prop to use Formik + * for form state management. + * + * @example + * ```tsx + * import { createComponentSpec } from '@schepta/core'; + * + * const components = { + * FieldWrapper: createComponentSpec({ + * id: 'FieldWrapper', + * type: 'wrapper', + * factory: () => FormikFieldWrapper, + * }), + * }; + * ``` + */ +export const FormikFieldWrapper: React.FC = ({ + name, + component: Component, + componentProps = {}, + children, +}) => { + const formik = useFormikContext>(); + + // Get value from Formik state (support nested paths) + const value = name.split('.').reduce((obj: any, key) => { + return obj?.[key]; + }, formik.values); + + const handleChange = (newValue: any) => { + formik.setFieldValue(name, newValue); + }; + + return ( + + {children} + + ); +}; diff --git a/examples/react/src/basic-ui/components/formik/FormikFormContainer.tsx b/examples/react/src/basic-ui/components/formik/FormikFormContainer.tsx new file mode 100644 index 0000000..5c4f157 --- /dev/null +++ b/examples/react/src/basic-ui/components/formik/FormikFormContainer.tsx @@ -0,0 +1,77 @@ +/** + * Formik Form Container + * + * Custom FormContainer that uses Formik for state management. + * This demonstrates how to integrate Formik with Schepta forms. + */ + +import React from 'react'; +import { Formik, Form } from 'formik'; +import type { FormContainerProps } from '@schepta/factory-react'; + +/** + * Formik-based FormContainer component. + * Wraps children with Formik context for form state management. + * + * This component: + * - Creates a Formik form context + * - Wraps children with Formik's Form component + * - Handles form submission with Formik's onSubmit + * + * @example + * ```tsx + * import { createComponentSpec } from '@schepta/core'; + * + * const components = { + * FormContainer: createComponentSpec({ + * id: 'FormContainer', + * type: 'FormContainer', + * factory: () => FormikFormContainer, + * }), + * }; + * ``` + */ +export const FormikFormContainer: React.FC = ({ + children, + onSubmit, +}) => { + const handleSubmit = (values: Record) => { + if (onSubmit) { + onSubmit(values); + } + }; + + return ( + + {({ isSubmitting }) => ( +
+ {children} + {onSubmit && ( +
+ +
+ )} +
+ )} +
+ ); +}; diff --git a/examples/react/src/basic-ui/components/rhf/RHFFieldWrapper.tsx b/examples/react/src/basic-ui/components/rhf/RHFFieldWrapper.tsx new file mode 100644 index 0000000..a64f065 --- /dev/null +++ b/examples/react/src/basic-ui/components/rhf/RHFFieldWrapper.tsx @@ -0,0 +1,57 @@ +/** + * RHF Field Wrapper + * + * Custom FieldWrapper 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'; + +/** + * RHF-based FieldWrapper component. + * Uses react-hook-form's Controller to bind fields to form state. + * + * Register this component via the components prop to use RHF + * for form state management. + * + * @example + * ```tsx + * import { createComponentSpec } from '@schepta/core'; + * + * const components = { + * FieldWrapper: createComponentSpec({ + * id: 'FieldWrapper', + * type: 'wrapper', + * factory: () => RHFFieldWrapper, + * }), + * }; + * ``` + */ +export const RHFFieldWrapper: React.FC = ({ + name, + component: Component, + componentProps = {}, + children, +}) => { + const { control } = useFormContext(); + + return ( + ( + field.onChange(value)} + > + {children} + + )} + /> + ); +}; diff --git a/examples/react/src/basic-ui/components/rhf/RHFFormContainer.tsx b/examples/react/src/basic-ui/components/rhf/RHFFormContainer.tsx new file mode 100644 index 0000000..8e08b07 --- /dev/null +++ b/examples/react/src/basic-ui/components/rhf/RHFFormContainer.tsx @@ -0,0 +1,73 @@ +/** + * RHF Form Container + * + * Custom FormContainer that uses react-hook-form for state management. + * This demonstrates how to integrate RHF with Schepta forms. + */ + +import React from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; +import type { FormContainerProps } from '@schepta/factory-react'; + +/** + * RHF-based FormContainer component. + * Creates its own useForm context and wraps children with FormProvider. + * + * This component: + * - Creates a RHF form context with useForm() + * - Wraps children with FormProvider for nested components to access + * - Handles form submission with RHF's handleSubmit + * + * @example + * ```tsx + * import { createComponentSpec } from '@schepta/core'; + * + * const components = { + * FormContainer: createComponentSpec({ + * id: 'FormContainer', + * type: 'FormContainer', + * factory: () => RHFFormContainer, + * }), + * }; + * ``` + */ +export const RHFFormContainer: React.FC = ({ + children, + onSubmit, +}) => { + const methods = useForm(); + + const handleFormSubmit = methods.handleSubmit((values) => { + if (onSubmit) { + onSubmit(values); + } + }); + + return ( + +
+ {children} + {onSubmit && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/examples/react/src/basic-ui/pages/BasicFormPage.tsx b/examples/react/src/basic-ui/pages/BasicFormPage.tsx index a3ec9bf..9152a21 100644 --- a/examples/react/src/basic-ui/pages/BasicFormPage.tsx +++ b/examples/react/src/basic-ui/pages/BasicFormPage.tsx @@ -1,11 +1,13 @@ import React, { useState } from "react"; -import { Tab, Tabs, Box, Paper } from "@mui/material"; +import { Tab, Tabs, Paper } from "@mui/material"; import simpleFormSchema from "../../../../../instances/form/simple-form.json"; import complexFormSchema from "../../../../../instances/form/complex-form.json"; -import { FormSchema } from "@schepta/core"; -import { Form } from "../components/Form"; +import { NativeForm } from "../components/Forms/NativeForm"; import { TabPanel } from "../../material-ui/pages/MaterialFormPage"; -import { FormModal } from "../components/FormModal"; +import { ModalForm } from "../components/Forms/ModalForm"; +import { FormWithRHF } from "../components/Forms/FormWithRHF"; +import { FormWithFormik } from "../components/Forms/FormWithFormik"; +import { FormSchema } from "@schepta/core"; export function BasicFormPage() { const [tabValue, setTabValue] = useState(0); @@ -20,22 +22,30 @@ export function BasicFormPage() { <> - - - + + + + + -
+ - + - + + + + + + +

Expressions Example

diff --git a/examples/react/src/chakra-ui/components/Containers/FormContainer.tsx b/examples/react/src/chakra-ui/components/Containers/FormContainer.tsx index 0504b0d..69d2b77 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormContainer.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormContainer.tsx @@ -2,16 +2,24 @@ import React from "react"; import { Box } from "@chakra-ui/react"; import type { FormContainerProps } from "@schepta/factory-react"; import { SubmitButton } from "../SubmitButton"; +import { useScheptaFormAdapter } from "@schepta/factory-react"; export const FormContainer: React.FC = ({ children, onSubmit, }) => { + const adapter = useScheptaFormAdapter(); + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (onSubmit) adapter.handleSubmit(onSubmit)(); + }; return ( {children} {onSubmit && } diff --git a/examples/react/src/material-ui/components/Containers/FormContainer.tsx b/examples/react/src/material-ui/components/Containers/FormContainer.tsx index ff1f786..97a9760 100644 --- a/examples/react/src/material-ui/components/Containers/FormContainer.tsx +++ b/examples/react/src/material-ui/components/Containers/FormContainer.tsx @@ -2,16 +2,24 @@ import React from "react"; import { Box } from "@mui/material"; import type { FormContainerProps } from "@schepta/factory-react"; import { SubmitButton } from "../SubmitButton"; +import { useScheptaFormAdapter } from "@schepta/factory-react"; export const FormContainer: React.FC = ({ children, onSubmit, }) => { + const adapter = useScheptaFormAdapter(); + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (onSubmit) adapter.handleSubmit(onSubmit)(); + }; return ( {children} {onSubmit && } diff --git a/examples/react/tsconfig.json b/examples/react/tsconfig.json index a50a5cd..15a0a7f 100644 --- a/examples/react/tsconfig.json +++ b/examples/react/tsconfig.json @@ -1,10 +1,11 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "jsx": "preserve" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] - } \ No newline at end of file + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "jsx": "preserve", + "paths": {} + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9501d0..b942882 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: '@schepta/factory-react': specifier: workspace:* version: link:../../packages/factories/react + formik: + specifier: ^2.4.6 + version: 2.4.9(@types/react@18.3.27)(react@18.3.1) framer-motion: specifier: ^10.16.16 version: 10.18.0(react-dom@18.3.1)(react@18.3.1) @@ -223,14 +226,11 @@ importers: specifier: workspace:* version: link:../../core react: - specifier: ^18.2.0 + specifier: '>=18.0.0' version: 18.3.1 react-dom: specifier: '>=18.0.0' version: 18.3.1(react@18.3.1) - react-hook-form: - specifier: ^7.52.2 - version: 7.68.0(react@18.3.1) devDependencies: '@testing-library/jest-dom': specifier: ^6.1.0 @@ -349,9 +349,6 @@ importers: react-dom: specifier: '>=18.0.0' version: 18.3.1(react@18.3.1) - react-hook-form: - specifier: ^7.52.2 - version: 7.68.0(react@18.3.1) devDependencies: '@testing-library/jest-dom': specifier: ^6.1.0 @@ -2099,6 +2096,15 @@ packages: '@types/unist': 3.0.3 dev: true + /@types/hoist-non-react-statics@3.3.7(@types/react@18.3.27): + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + dependencies: + '@types/react': 18.3.27 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/jexl@2.3.4: resolution: {integrity: sha512-3BU5DbkPvwqOeW8kZcB9Bvn5bspTFY3Lo0yBS6ephuI8kVSpCjzbGtRPbWnvdbr67tMvXDxiJRcL3fwbz/6+PQ==} dev: true @@ -3024,6 +3030,11 @@ packages: which-typed-array: 1.1.19 dev: true + /deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3374,6 +3385,24 @@ packages: mime-types: 2.1.35 dev: true + /formik@2.4.9(@types/react@18.3.27)(react@18.3.1): + resolution: {integrity: sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==} + peerDependencies: + react: '>=16.8.0' + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.27) + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.23 + lodash-es: 4.17.23 + react: 18.3.1 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/react' + dev: false + /framer-motion@10.18.0(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==} peerDependencies: @@ -3938,10 +3967,18 @@ packages: pkg-types: 1.3.1 dev: true + /lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + dev: false + /lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} dev: false + /lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + dev: false + /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4457,6 +4494,10 @@ packages: react: 18.3.1 scheduler: 0.23.2 + /react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + dev: false + /react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} dev: false @@ -5060,6 +5101,10 @@ packages: any-promise: 1.3.0 dev: true + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + /tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} dev: true