Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 38 additions & 111 deletions packages/core/src/orchestrator/renderer-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,16 @@
import type { RuntimeAdapter, ComponentSpec, DebugContextValue } from '../runtime/types';
import type { FormAdapter } from '../forms/types';
import type { MiddlewareFn, MiddlewareContext } from '../middleware/types';
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';
import { FormSchema } from '../schema/schema-types';

/**
* Resolution result - successful component resolution
*/
export interface ResolutionSuccess {
renderSpec: ComponentSpec;
componentToRender: ComponentSpec;
componentSpec: ComponentSpec;
rendererFn: ReturnType<typeof getRendererForType>;
}

Expand Down Expand Up @@ -48,26 +45,33 @@ export interface FactorySetupResult {
* Resolve component spec from schema
*/
export function resolveSpec(
schema: FormSchema,
schema: any,
componentKey: string,
components: Record<string, ComponentSpec>,
customComponents?: Record<string, ComponentSpec>,
localRenderers?: Partial<Record<string, any>>,
debugEnabled?: boolean
): ResolutionResult {
const componentName = schema['x-component'] || componentKey;
// components already has provider components merged in the factory
// Pass components as globalComponents (includes provider components) and undefined as localComponents
const renderSpec = getComponentSpec(componentName, components, undefined, debugEnabled);

if (!renderSpec) {

const isCustomComponent = schema['x-custom'] === true;
let componentSpec = null;

if (isCustomComponent && customComponents) {
componentSpec = customComponents[componentKey];
} else {
componentSpec = components[componentName];
}

if (!componentSpec) {
if (debugEnabled) {
console.warn(`Component not found: ${componentName}`);
}
// Return error element (framework adapter will handle)
return null;
}

const componentType = renderSpec.type || 'field';
const componentType = componentSpec.type || 'field';
const rendererFn = getRendererForType(
componentType,
undefined,
Expand All @@ -76,8 +80,7 @@ export function resolveSpec(
);

return {
renderSpec,
componentToRender: renderSpec,
componentSpec,
rendererFn,
};
}
Expand All @@ -97,31 +100,31 @@ export function createRendererOrchestrator(
namePath: string[] = [],
isDirectRootProperty: boolean = false
): any {
const {
const {
components,
customComponents,
renderers: localRenderers,
externalContext,
state,
middlewares,
renderers: localRenderers,
externalContext,
state,
middlewares,
onSubmit,
debug,
formAdapter
} = getFactorySetup();

// Process schema with template expressions BEFORE extracting props
// This ensures that $formValues.* and $externalContext.* are replaced
// in any property of the schema (x-ui, x-content, x-component-props, etc.)
const resolver = createDefaultResolver({
externalContext,
formValues: state,
});

const processedSchema = processValue(schema, resolver, {
externalContext,
formValues: state,
}) as any;

// Check visibility via x-ui.visible
// If visible === false, don't render this component (and its children)
// By default, visible is true
Expand All @@ -130,113 +133,36 @@ export function createRendererOrchestrator(
return null;
}

// Check for x-custom flag - if true, use custom component instead
const isCustomComponent = processedSchema['x-custom'] === true;

if (isCustomComponent && customComponents) {
// Look up custom component by the property key name
const customSpec = customComponents[componentKey];

if (customSpec) {
// Get renderer for the custom component type
const customRendererFn = getRendererForType(
customSpec.type || 'field',
undefined,
localRenderers as any,
debug?.isEnabled
);

// Build props for custom component
const customProps = {
...customSpec.defaultProps,
// Injected for E2E: identifies component key in DOM
'data-test-id': `${componentKey}`,
// Pass the original schema so custom component can access x-component-props, etc.
schema: processedSchema,
// Pass the component key
componentKey,
// Pass the name path for form binding
name: parentProps.name ? `${parentProps.name}.${componentKey}` : componentKey,
// Pass external context
externalContext,
// Pass x-component-props if present
...(processedSchema['x-component-props'] || {}),
};

// Render children if schema has properties
const customChildren: any[] = [];
if (processedSchema.properties && typeof processedSchema.properties === 'object') {
const childParentProps = {
...customProps,
name: customProps.name,
};

const sortedEntries = Object.entries(processedSchema.properties).sort(
([, a], [, b]) => {
const orderA = (a as any)?.['x-ui']?.order ?? Infinity;
const orderB = (b as any)?.['x-ui']?.order ?? Infinity;
return orderA - orderB;
}
);

for (const [key, childSchema] of sortedEntries) {
const childResult = render(key, childSchema as any, childParentProps, namePath, false);
if (childResult !== null && childResult !== undefined) {
customChildren.push(childResult);
}
}
}

// Render the custom component
return customRendererFn(customSpec, customProps, runtime, customChildren.length > 0 ? customChildren : undefined);
} else {
// Custom component not found - log warning and fall back to normal rendering
if (debug?.isEnabled) {
console.warn(`Custom component not found for key: ${componentKey}. Falling back to standard component.`);
}
}
}

// 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
const resolution = resolveSpec(
processedSchema,
componentKey,
components,
localRenderers,
processedSchema,
componentKey,
components,
customComponents,
localRenderers,
debug?.isEnabled
);

// Check if resolution failed
if (!resolution || resolution === null) {
// Return error component (framework adapter will provide)
return null;
}

// Extract successful resolution
const { renderSpec, componentToRender, rendererFn } = resolution as ResolutionSuccess;
const { componentSpec, rendererFn } = resolution as ResolutionSuccess;

// Construct name path for nested fields
// ONLY components with type: 'field' are included in the name path
// EXCEPTION: componentKeys that are direct properties of root schema (isDirectRootProperty)
// All other components (containers, FormContainer, content, etc.) are ignored
const componentType = renderSpec.type || 'field';
const isFieldComponent = componentType === 'field';

const isFieldComponent = componentSpec.type === 'field';

// If field OR direct root property: add componentKey to name path
// If not field and not root property: keep parentProps.name (don't add this component's key)
const shouldIncludeInPath = isFieldComponent || isDirectRootProperty;
Expand All @@ -246,10 +172,11 @@ export function createRendererOrchestrator(

// Props Processing (using processedSchema)
const baseProps = {
...renderSpec.defaultProps,
...componentSpec.defaultProps,
...parentProps,
// Injected for E2E: identifies component key in DOM
'data-test-id': `${componentKey}`,
schema,
// Add name prop ONLY for field components
...(isFieldComponent && currentName ? { name: currentName } : {}),
...(Object.keys(componentProps).length > 0 ? { 'x-component-props': componentProps } : {}),
Expand All @@ -271,7 +198,7 @@ export function createRendererOrchestrator(
debug,
formAdapter,
};

const mergedProps = applyMiddlewares(baseProps, processedSchema, middlewares, middlewareContext);

// Render children if schema has properties (use processedSchema)
Expand All @@ -292,7 +219,7 @@ export function createRendererOrchestrator(
return orderA - orderB;
}
);

for (const [key, childSchema] of sortedEntries) {
// If this component is the root (FormContainer) and has no name,
// then its direct children are root properties and should be included in name path
Expand All @@ -305,7 +232,7 @@ export function createRendererOrchestrator(
}

// Final Rendering using renderer function with children
return rendererFn(componentToRender, mergedProps, runtime, children.length > 0 ? children : undefined);
return rendererFn(componentSpec, mergedProps, runtime, children.length > 0 ? children : undefined);
};
}

76 changes: 2 additions & 74 deletions packages/core/src/registries/component-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,89 +46,17 @@ export function setFactoryDefaultComponents(
factoryDefaultComponents = components;
}

/**
* Get factory default components
*/
export function getFactoryDefaultComponents(): Record<string, ComponentSpec> {
return factoryDefaultComponents;
}

/**
* Get unified component registry
*
* Priority order: local > global > registry overrides > factory defaults
*/
export function getComponentRegistry(
globalComponents?: Record<string, ComponentSpec>,
localComponents?: Record<string, ComponentSpec>
): Record<string, ComponentSpec> {
// Start with factory default components
const merged: Record<string, ComponentSpec> = { ...factoryDefaultComponents };

// Apply global components (from provider)
if (globalComponents) {
Object.keys(globalComponents).forEach(componentName => {
const component = globalComponents[componentName];
if (component) {
merged[componentName] = component;
}
});
}

// Apply local components (maximum priority)
if (localComponents) {
Object.keys(localComponents).forEach(componentName => {
const component = localComponents[componentName];
if (component) {
merged[componentName] = component;
}
});
}

return merged;
}

/**
* Get effective component configuration
* Includes fallback logic and debug logging
*/
export function getComponentSpec(
componentName: string,
globalComponents?: Record<string, ComponentSpec>,
localComponents?: Record<string, ComponentSpec>,
debugEnabled?: boolean
): ComponentSpec | null {
// Local components have maximum priority
if (localComponents?.[componentName]) {
if (debugEnabled) {
console.log(`Component resolved from local: ${componentName}`);
}
return localComponents[componentName];
}

const globalRegistry = getComponentRegistry(globalComponents, localComponents);

if (globalRegistry[componentName]) {
if (debugEnabled) {
console.log(`Component resolved from registry: ${componentName}`);
}
return globalRegistry[componentName];
}

if (debugEnabled) {
console.warn(`Component not found: ${componentName}`);
}

return null;
}
};

/**
* Create a component spec from a factory function
*/
export function createComponentSpec(config: {
id: string;
factory: ComponentSpec['factory'];
type?: ComponentType;
type: ComponentType;
displayName?: string;
defaultProps?: Record<string, any>;
}): ComponentSpec {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ComponentSpec {
/** Component identifier */
id: string;
/** Component type (field, container, etc.) */
type?: ComponentType;
type: ComponentType;
/** Display name for debugging */
displayName?: string;
/** Default props to apply */
Expand Down
Loading