diff --git a/package.json b/package.json index 234702c..27c99cd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "build": "turbo build", - "dev": "turbo dev", + "showcases:dev": "turbo --filter showcases dev", "test": "turbo test --filter='./packages/*'", "test:e2e": "playwright test --config=tests/playwright.config.ts", "test:e2e:ui": "playwright test --ui --config=tests/playwright.config.ts", diff --git a/packages/core/src/defaults/register-default-renderers.ts b/packages/core/src/defaults/register-default-renderers.ts new file mode 100644 index 0000000..567d82e --- /dev/null +++ b/packages/core/src/defaults/register-default-renderers.ts @@ -0,0 +1,54 @@ +import { ComponentSpec, ComponentType, RuntimeAdapter } from "../runtime/types"; + +/** + * Renderer function - wraps component rendering with additional logic + */ +export type RendererFn = ( + componentSpec: ComponentSpec, + props: Record, + runtime: RuntimeAdapter, + children?: any[] +) => any; + +/** + * Default renderers - pass props as is + */ +export const defaultRenderers: Record = { + field: (spec, props, runtime, children) => { + const propsWithChildren = children && children.length > 0 + ? { ...props, children } + : props; + return runtime.create(spec, propsWithChildren); + }, + 'field-wrapper': (spec, props, runtime, children) => { + const propsWithChildren = children && children.length > 0 + ? { ...props, children } + : props; + return runtime.create(spec, propsWithChildren); + }, + 'container': (spec, props, runtime, children) => { + return runtime.create(spec, { ...props, children }); + }, + content: (spec, props, runtime, children) => { + const propsWithChildren = children && children.length > 0 + ? { ...props, children } + : props; + return runtime.create(spec, propsWithChildren); + }, + addon: (spec, props, runtime) => { + return runtime.create(spec, props); + }, + 'menu-item': (spec, props, runtime, children) => { + const propsWithChildren = children && children.length > 0 + ? { ...props, children } + : props; + return runtime.create(spec, propsWithChildren); + }, + 'menu-container': (spec, props, runtime, children) => { + const propsWithChildren = children && children.length > 0 + ? { ...props, children } + : props; + return runtime.create(spec, propsWithChildren); + }, + +} \ No newline at end of file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2d5fbed..b0147f3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,8 +11,10 @@ export * from './runtime/types'; export * from './forms/types'; // Registry system -export * from './registry/component-registry'; -export * from './registry/renderer-registry'; +export * from './registries/component-registry'; +export * from './registries/renderer-registry'; + +export * from './defaults/register-default-renderers' // Orchestrator export * from './orchestrator/renderer-orchestrator'; @@ -34,7 +36,6 @@ export * from './provider'; // Utils export * from './utils/jexl-config'; -export * from './utils/sanitize-props'; // Validation export * from './validation'; diff --git a/packages/core/src/orchestrator/renderer-orchestrator.ts b/packages/core/src/orchestrator/renderer-orchestrator.ts index c3ae2a8..2b92954 100644 --- a/packages/core/src/orchestrator/renderer-orchestrator.ts +++ b/packages/core/src/orchestrator/renderer-orchestrator.ts @@ -8,8 +8,8 @@ import type { RuntimeAdapter, ComponentSpec, DebugContextValue } from '../runtime/types'; import type { FormAdapter } from '../forms/types'; import type { MiddlewareFn, MiddlewareContext } from '../middleware/types'; -import { getComponentSpec } from '../registry/component-registry'; -import { getRendererForType } from '../registry/renderer-registry'; +import { getComponentSpec } from '../registries/component-registry'; +import { getRendererForType } from '../registries/renderer-registry'; import { applyMiddlewares } from '../middleware/types'; import { processValue } from '../expressions/template-processor'; import { createDefaultResolver } from '../expressions/variable-resolver'; @@ -200,6 +200,16 @@ export function createRendererOrchestrator( // Parse schema (now using processed schema) const { 'x-component-props': componentProps = {} } = processedSchema; + // const componentSpec = getComponentSpec(componentKey, components, undefined, debug?.isEnabled); + + // if(!componentSpec) { + // if (debug?.isEnabled) { + // console.warn(`Component not found: ${componentKey}`); + // } + // // Return error element (framework adapter will handle) + // return null; + // } + // Resolve component and renderer // Use processedSchema for x-component resolution (in case x-component has templates) // components already has provider components merged in the factory diff --git a/packages/core/src/provider/provider.test.ts b/packages/core/src/provider/provider.test.ts index 6f13e1b..5fab48c 100644 --- a/packages/core/src/provider/provider.test.ts +++ b/packages/core/src/provider/provider.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest'; import { mergeProviderConfigs, resolveProviderConfig } from './provider'; import type { ProviderConfig } from './types'; import type { FormSchema } from '../schema/schema-types'; -import { createComponentSpec } from '../registry/component-registry'; +import { createComponentSpec } from '../registries/component-registry'; describe('Provider Utilities', () => { describe('mergeProviderConfigs', () => { diff --git a/packages/core/src/provider/provider.ts b/packages/core/src/provider/provider.ts index 87f737c..9364a39 100644 --- a/packages/core/src/provider/provider.ts +++ b/packages/core/src/provider/provider.ts @@ -6,7 +6,7 @@ import type { ProviderConfig } from './types'; import { defaultDebugConfig } from './types'; -import { getRendererRegistry } from '../registry/renderer-registry'; +import { getRendererRegistry } from '../registries/renderer-registry'; /** * Merge two provider configurations hierarchically diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index 17b46d7..e622b5d 100644 --- a/packages/core/src/provider/types.ts +++ b/packages/core/src/provider/types.ts @@ -7,7 +7,7 @@ import type { ComponentSpec, ComponentType, DebugConfig } from '../runtime/types'; import type { FormSchema } from '../schema/schema-types'; import type { MiddlewareFn } from '../middleware/types'; -import type { RendererFn } from '../registry/renderer-registry'; +import { RendererFn } from '../defaults/register-default-renderers'; /** * Provider configuration diff --git a/packages/core/src/registry/component-registry.ts b/packages/core/src/registries/component-registry.ts similarity index 84% rename from packages/core/src/registry/component-registry.ts rename to packages/core/src/registries/component-registry.ts index 01aae8b..79e1501 100644 --- a/packages/core/src/registry/component-registry.ts +++ b/packages/core/src/registries/component-registry.ts @@ -14,7 +14,6 @@ export const defaultTypeProps: Record> = { field: { fullWidth: true }, 'field-wrapper': {}, 'container': {}, - 'FormContainer': {}, content: {}, addon: {}, 'menu-item': {}, @@ -54,21 +53,6 @@ export function getFactoryDefaultComponents(): Record { return factoryDefaultComponents; } -/** - * Registry overrides (global registrations) - */ -const registryOverrides = new Map>(); - -/** - * Register a component override globally - */ -export function registerComponent(name: string, config: Partial): void { - registryOverrides.set(name, { - ...registryOverrides.get(name), - ...config, - }); -} - /** * Get unified component registry * @@ -101,16 +85,6 @@ export function getComponentRegistry( }); } - // Apply registry overrides - for (const [name, override] of Array.from(registryOverrides.entries())) { - const existing = merged[name]; - merged[name] = { - ...existing, - ...override, - id: override.id || existing?.id || name, - } as ComponentSpec; - } - return merged; } diff --git a/packages/core/src/registries/renderer-registry.ts b/packages/core/src/registries/renderer-registry.ts new file mode 100644 index 0000000..aaf254d --- /dev/null +++ b/packages/core/src/registries/renderer-registry.ts @@ -0,0 +1,79 @@ +/** + * Renderer Registry + * + * Framework-agnostic renderer registration system. + * Renderers are functions that wrap components with additional rendering logic. + */ + +import { defaultRenderers, RendererFn } from '../defaults/register-default-renderers'; +import type { ComponentType } from '../runtime/types'; + + +/** + * Get unified renderer registry with hierarchical merging + * + * Priority order: local > global > default + */ +export function getRendererRegistry( + globalRenderers?: Partial>, + localRenderers?: Partial> +): Record { + // Start with built-in renderers + let merged = { ...defaultRenderers }; + + // 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; + } + }); + } + + // 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; + } + }); + } + + return merged; +} + +/** + * Get effective renderer for a component type + * Includes debug logging + */ +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]; +} + diff --git a/packages/core/src/registry/renderer-registry.ts b/packages/core/src/registry/renderer-registry.ts deleted file mode 100644 index 5526302..0000000 --- a/packages/core/src/registry/renderer-registry.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Renderer Registry - * - * Framework-agnostic renderer registration system. - * Renderers are functions that wrap components with additional rendering logic. - */ - -import type { ComponentType, ComponentSpec, RuntimeAdapter } from '../runtime/types'; -import { sanitizePropsForDOM } from '../utils/sanitize-props'; - -/** - * Renderer function - wraps component rendering with additional logic - */ -export type RendererFn = ( - componentSpec: ComponentSpec, - props: Record, - runtime: RuntimeAdapter, - children?: any[] -) => any; - -/** - * Default renderers - sanitize props before passing to components - */ -export const defaultTypeRenderers: Record = { - field: (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); - const propsWithChildren = children && children.length > 0 - ? { ...sanitized, children } - : sanitized; - return runtime.create(spec, propsWithChildren); - }, - 'field-wrapper': (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); - const propsWithChildren = children && children.length > 0 - ? { ...sanitized, children } - : sanitized; - return runtime.create(spec, propsWithChildren); - }, - 'container': (spec, props, runtime, children) => { - const xComponentProps = props['x-component-props'] || {}; - const mergedProps = { ...props, ...xComponentProps }; - const sanitized = sanitizePropsForDOM(mergedProps); - const propsWithChildren = children && children.length > 0 - ? { ...sanitized, children } - : sanitized; - return runtime.create(spec, propsWithChildren); - }, - 'FormContainer': (spec, props, runtime, children) => { - const { onSubmit, externalContext } = props; - const sanitized = sanitizePropsForDOM(props); - const propsWithChildren = { - ...sanitized, - onSubmit, - externalContext, - ...(children && children.length > 0 ? { children } : {}), - }; - return runtime.create(spec, propsWithChildren); - }, - content: (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); - const propsWithChildren = children && children.length > 0 - ? { ...sanitized, children } - : sanitized; - return runtime.create(spec, propsWithChildren); - }, - addon: (spec, props, runtime) => { - const sanitized = sanitizePropsForDOM(props); - return runtime.create(spec, sanitized); - }, - 'menu-item': (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); - const propsWithChildren = children && children.length > 0 - ? { ...sanitized, children } - : sanitized; - return runtime.create(spec, propsWithChildren); - }, - 'menu-container': (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); - const propsWithChildren = children && children.length > 0 - ? { ...sanitized, children } - : sanitized; - return runtime.create(spec, propsWithChildren); - }, -}; - -/** - * Renderer registry overrides - */ -const rendererRegistryOverrides = new Map(); - -/** - * Register a renderer override globally - */ -export function registerRenderer(type: ComponentType, renderer: RendererFn): void { - rendererRegistryOverrides.set(type, renderer); -} - -/** - * Get renderer by type with registry overrides - */ -export function getRendererByType(type: ComponentType): RendererFn { - return rendererRegistryOverrides.get(type) || defaultTypeRenderers[type]; -} - -/** - * Get unified renderer registry with hierarchical merging - * - * Priority order: local > global > registry overrides > default - */ -export function getRendererRegistry( - globalRenderers?: Partial>, - localRenderers?: Partial> -): Record { - // Start with built-in renderers - let merged = { ...defaultTypeRenderers }; - - // Apply registry overrides (global registrations) - rendererRegistryOverrides.forEach((renderer, type) => { - merged[type] = renderer; - }); - - // 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; - } - }); - } - - // 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; - } - }); - } - - return merged; -} - -/** - * Get effective renderer for a component type - * Includes debug logging - */ -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]!; - } - - // Registry overrides third priority - if (rendererRegistryOverrides.has(type)) { - if (debugEnabled) { - console.log(`Renderer resolved from registry override: ${type}`); - } - return rendererRegistryOverrides.get(type)!; - } - - // Default renderer last - if (debugEnabled) { - console.log(`Renderer resolved from default: ${type}`); - } - return defaultTypeRenderers[type]; -} - diff --git a/packages/core/src/runtime/types.ts b/packages/core/src/runtime/types.ts index f0f9b35..19b8a46 100644 --- a/packages/core/src/runtime/types.ts +++ b/packages/core/src/runtime/types.ts @@ -104,7 +104,6 @@ export type ComponentType = | 'field' | 'field-wrapper' | 'container' - | 'FormContainer' | 'content' | 'addon' | 'menu-item' diff --git a/packages/core/src/utils/sanitize-props.ts b/packages/core/src/utils/sanitize-props.ts deleted file mode 100644 index 75d49af..0000000 --- a/packages/core/src/utils/sanitize-props.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Props Sanitization - * - * Removes internal Schepta props before passing to DOM components. - */ - -/** - * List of internal props that should NOT be passed to the DOM - * These are Schepta-specific metadata props - */ -const INTERNAL_PROPS = [ - 'name', - 'x-ui', - 'x-component-props', - 'x-rules', - 'x-reactions', - 'x-slots', - 'externalContext', - 'onSubmit', - 'formAdapter', - 'debug', -] as const; - -/** - * Remove internal Schepta props before passing to DOM components - * - * This prevents props like x-ui, externalContext from appearing - * as [object Object] attributes in the rendered HTML. - * - * @param props - Props to sanitize - * @returns Props with internal Schepta props removed - * - * @example - * ```typescript - * const props = { name: 'field', 'x-ui': { order: 1 }, externalContext: {...} }; - * const sanitized = sanitizePropsForDOM(props); - * // sanitized = { name: 'field' } - * ``` - */ -export function sanitizePropsForDOM(props: Record): Record { - const sanitized = { ...props }; - - for (const key of INTERNAL_PROPS) { - delete sanitized[key]; - } - - return sanitized; -} - -/** - * List of internal prop keys (exported for testing/extension) - */ -export const SCHEPTA_INTERNAL_PROPS = INTERNAL_PROPS; - diff --git a/packages/factories/react/src/components/DefaultFormField.tsx b/packages/factories/react/src/components/DefaultFormField.tsx index 8e44d38..0a56577 100644 --- a/packages/factories/react/src/components/DefaultFormField.tsx +++ b/packages/factories/react/src/components/DefaultFormField.tsx @@ -4,7 +4,7 @@ * Wrapper (grid cell) for a single form field. Can be overridden via createComponentSpec. */ -import React from 'react'; +import React from "react"; /** * Props passed to the FormField component. @@ -14,8 +14,10 @@ export interface FormFieldProps extends React.HTMLAttributes { /** Form field children (typically the rendered input) */ children?: React.ReactNode; /** Test ID for the form field */ - 'data-test-id'?: string; + "data-test-id"?: string; externalContext?: Record; + "x-component-props"?: Record; + "x-ui"?: Record; } /** @@ -30,10 +32,15 @@ export type FormFieldComponentType = React.ComponentType; export const DefaultFormField: React.FC = ({ children, externalContext, + "x-component-props": xComponentProps, + "x-ui": xUi, ...props }) => { return ( -
+
{children}
); diff --git a/packages/factories/react/src/components/DefaultFormSectionContainer.tsx b/packages/factories/react/src/components/DefaultFormSectionContainer.tsx index 579148d..a4330d9 100644 --- a/packages/factories/react/src/components/DefaultFormSectionContainer.tsx +++ b/packages/factories/react/src/components/DefaultFormSectionContainer.tsx @@ -17,6 +17,8 @@ export interface FormSectionContainerProps /** Test ID for the form section container */ 'data-test-id'?: string; externalContext?: Record; + "x-component-props"?: Record; + 'x-ui'?: Record; } /** @@ -32,6 +34,8 @@ export type FormSectionContainerComponentType = export const DefaultFormSectionContainer: React.FC = ({ children, externalContext, + 'x-ui': xUi, + "x-component-props": xComponentProps, ...props }) => { return ( diff --git a/packages/factories/react/src/components/DefaultFormSectionGroup.tsx b/packages/factories/react/src/components/DefaultFormSectionGroup.tsx index c35dcef..0ebc8bb 100644 --- a/packages/factories/react/src/components/DefaultFormSectionGroup.tsx +++ b/packages/factories/react/src/components/DefaultFormSectionGroup.tsx @@ -16,6 +16,7 @@ export interface FormSectionGroupProps children?: React.ReactNode; /** Test ID for the form section group */ 'data-test-id'?: string; + 'x-component-props'?: Record; externalContext?: Record; [key: string]: any; } @@ -33,6 +34,7 @@ export type FormSectionGroupComponentType = export const DefaultFormSectionGroup: React.FC = ({ children, externalContext, + 'x-component-props': xComponentProps, ...props }) => { return ( diff --git a/packages/factories/react/src/components/DefaultFormSectionGroupContainer.tsx b/packages/factories/react/src/components/DefaultFormSectionGroupContainer.tsx index 30f6f88..8944ce1 100644 --- a/packages/factories/react/src/components/DefaultFormSectionGroupContainer.tsx +++ b/packages/factories/react/src/components/DefaultFormSectionGroupContainer.tsx @@ -17,6 +17,8 @@ export interface FormSectionGroupContainerProps /** Test ID for the form section group container */ 'data-test-id'?: string; externalContext?: Record; + "x-component-props"?: Record; + "x-ui"?: Record; } /** diff --git a/packages/factories/react/src/components/DefaultFormSectionTitle.tsx b/packages/factories/react/src/components/DefaultFormSectionTitle.tsx index 7d3f9e8..8386263 100644 --- a/packages/factories/react/src/components/DefaultFormSectionTitle.tsx +++ b/packages/factories/react/src/components/DefaultFormSectionTitle.tsx @@ -19,6 +19,8 @@ export interface FormSectionTitleProps /** Test ID for the form section title */ 'data-test-id'?: string; externalContext?: Record; + "x-component-props"?: Record; + "x-ui"?: Record; } /** @@ -35,10 +37,12 @@ export const DefaultFormSectionTitle: React.FC = ({ 'x-content': content, children, externalContext, + "x-component-props": xComponentProps, + "x-ui": xUi, ...props }) => { return ( -

+

{content ?? children}

); diff --git a/packages/factories/react/src/components/DefaultInputAutocomplete.tsx b/packages/factories/react/src/components/DefaultInputAutocomplete.tsx index 6a1f24a..4b5b0ab 100644 --- a/packages/factories/react/src/components/DefaultInputAutocomplete.tsx +++ b/packages/factories/react/src/components/DefaultInputAutocomplete.tsx @@ -30,6 +30,8 @@ export interface InputAutocompleteProps /** List of options for autocomplete (value used for both value and label if label omitted) */ options?: InputAutocompleteOption[] | string[]; externalContext?: Record; + "x-component-props"?: Record; + "x-ui"?: Record; } /** @@ -67,7 +69,7 @@ function normalizeOptions( export const DefaultInputAutocomplete = React.forwardRef< HTMLInputElement, InputAutocompleteProps ->(({ label, name, value, onChange, placeholder, options = [], externalContext, ...rest }, ref) => { +>(({ label, name, value, onChange, placeholder, options = [], externalContext, "x-component-props": xComponentProps, "x-ui": xUi, ...rest }, ref) => { const listId = `${name}-datalist`; const normalizedOptions = normalizeOptions(options); @@ -87,6 +89,7 @@ export const DefaultInputAutocomplete = React.forwardRef< placeholder={placeholder} onChange={(e) => onChange?.(e.target.value)} style={inputStyle} + {...xComponentProps} {...rest} /> diff --git a/packages/factories/react/src/components/DefaultInputCheckbox.tsx b/packages/factories/react/src/components/DefaultInputCheckbox.tsx index b902aff..2b726a8 100644 --- a/packages/factories/react/src/components/DefaultInputCheckbox.tsx +++ b/packages/factories/react/src/components/DefaultInputCheckbox.tsx @@ -22,6 +22,9 @@ export interface InputCheckboxProps onChange?: (value: boolean) => void; label?: string; children?: React.ReactNode; + externalContext?: Record; + "x-component-props"?: Record; + "x-ui"?: Record; } /** @@ -42,7 +45,7 @@ const labelStyle: React.CSSProperties = { * Default checkbox input component. */ export const DefaultInputCheckbox = React.forwardRef( - ({ label, name, value, onChange, children, ...rest }, ref) => { + ({ label, name, value, onChange, children, externalContext, "x-component-props": xComponentProps, "x-ui": xUi, ...rest }, ref) => { return (