Skip to content

Commit 515e915

Browse files
authored
Refactor/components (#20)
* refactor: extract defaultComponents into a separate file * refactor: simplify resolveSpec function * refactor(react): simplify customComponent logic and how we get resolved component
1 parent 79b3cfe commit 515e915

6 files changed

Lines changed: 344 additions & 448 deletions

File tree

packages/core/src/orchestrator/renderer-orchestrator.ts

Lines changed: 38 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,16 @@
88
import type { RuntimeAdapter, ComponentSpec, DebugContextValue } from '../runtime/types';
99
import type { FormAdapter } from '../forms/types';
1010
import type { MiddlewareFn, MiddlewareContext } from '../middleware/types';
11-
import { getComponentSpec } from '../registries/component-registry';
1211
import { getRendererForType } from '../registries/renderer-registry';
1312
import { applyMiddlewares } from '../middleware/types';
1413
import { processValue } from '../expressions/template-processor';
1514
import { createDefaultResolver } from '../expressions/variable-resolver';
16-
import { FormSchema } from '../schema/schema-types';
1715

1816
/**
1917
* Resolution result - successful component resolution
2018
*/
2119
export interface ResolutionSuccess {
22-
renderSpec: ComponentSpec;
23-
componentToRender: ComponentSpec;
20+
componentSpec: ComponentSpec;
2421
rendererFn: ReturnType<typeof getRendererForType>;
2522
}
2623

@@ -48,26 +45,33 @@ export interface FactorySetupResult {
4845
* Resolve component spec from schema
4946
*/
5047
export function resolveSpec(
51-
schema: FormSchema,
48+
schema: any,
5249
componentKey: string,
5350
components: Record<string, ComponentSpec>,
51+
customComponents?: Record<string, ComponentSpec>,
5452
localRenderers?: Partial<Record<string, any>>,
5553
debugEnabled?: boolean
5654
): ResolutionResult {
5755
const componentName = schema['x-component'] || componentKey;
58-
// components already has provider components merged in the factory
59-
// Pass components as globalComponents (includes provider components) and undefined as localComponents
60-
const renderSpec = getComponentSpec(componentName, components, undefined, debugEnabled);
61-
62-
if (!renderSpec) {
56+
57+
const isCustomComponent = schema['x-custom'] === true;
58+
let componentSpec = null;
59+
60+
if (isCustomComponent && customComponents) {
61+
componentSpec = customComponents[componentKey];
62+
} else {
63+
componentSpec = components[componentName];
64+
}
65+
66+
if (!componentSpec) {
6367
if (debugEnabled) {
6468
console.warn(`Component not found: ${componentName}`);
6569
}
6670
// Return error element (framework adapter will handle)
6771
return null;
6872
}
6973

70-
const componentType = renderSpec.type || 'field';
74+
const componentType = componentSpec.type || 'field';
7175
const rendererFn = getRendererForType(
7276
componentType,
7377
undefined,
@@ -76,8 +80,7 @@ export function resolveSpec(
7680
);
7781

7882
return {
79-
renderSpec,
80-
componentToRender: renderSpec,
83+
componentSpec,
8184
rendererFn,
8285
};
8386
}
@@ -97,31 +100,31 @@ export function createRendererOrchestrator(
97100
namePath: string[] = [],
98101
isDirectRootProperty: boolean = false
99102
): any {
100-
const {
103+
const {
101104
components,
102105
customComponents,
103-
renderers: localRenderers,
104-
externalContext,
105-
state,
106-
middlewares,
106+
renderers: localRenderers,
107+
externalContext,
108+
state,
109+
middlewares,
107110
onSubmit,
108111
debug,
109112
formAdapter
110113
} = getFactorySetup();
111-
114+
112115
// Process schema with template expressions BEFORE extracting props
113116
// This ensures that $formValues.* and $externalContext.* are replaced
114117
// in any property of the schema (x-ui, x-content, x-component-props, etc.)
115118
const resolver = createDefaultResolver({
116119
externalContext,
117120
formValues: state,
118121
});
119-
122+
120123
const processedSchema = processValue(schema, resolver, {
121124
externalContext,
122125
formValues: state,
123126
}) as any;
124-
127+
125128
// Check visibility via x-ui.visible
126129
// If visible === false, don't render this component (and its children)
127130
// By default, visible is true
@@ -130,113 +133,36 @@ export function createRendererOrchestrator(
130133
return null;
131134
}
132135

133-
// Check for x-custom flag - if true, use custom component instead
134-
const isCustomComponent = processedSchema['x-custom'] === true;
135-
136-
if (isCustomComponent && customComponents) {
137-
// Look up custom component by the property key name
138-
const customSpec = customComponents[componentKey];
139-
140-
if (customSpec) {
141-
// Get renderer for the custom component type
142-
const customRendererFn = getRendererForType(
143-
customSpec.type || 'field',
144-
undefined,
145-
localRenderers as any,
146-
debug?.isEnabled
147-
);
148-
149-
// Build props for custom component
150-
const customProps = {
151-
...customSpec.defaultProps,
152-
// Injected for E2E: identifies component key in DOM
153-
'data-test-id': `${componentKey}`,
154-
// Pass the original schema so custom component can access x-component-props, etc.
155-
schema: processedSchema,
156-
// Pass the component key
157-
componentKey,
158-
// Pass the name path for form binding
159-
name: parentProps.name ? `${parentProps.name}.${componentKey}` : componentKey,
160-
// Pass external context
161-
externalContext,
162-
// Pass x-component-props if present
163-
...(processedSchema['x-component-props'] || {}),
164-
};
165-
166-
// Render children if schema has properties
167-
const customChildren: any[] = [];
168-
if (processedSchema.properties && typeof processedSchema.properties === 'object') {
169-
const childParentProps = {
170-
...customProps,
171-
name: customProps.name,
172-
};
173-
174-
const sortedEntries = Object.entries(processedSchema.properties).sort(
175-
([, a], [, b]) => {
176-
const orderA = (a as any)?.['x-ui']?.order ?? Infinity;
177-
const orderB = (b as any)?.['x-ui']?.order ?? Infinity;
178-
return orderA - orderB;
179-
}
180-
);
181-
182-
for (const [key, childSchema] of sortedEntries) {
183-
const childResult = render(key, childSchema as any, childParentProps, namePath, false);
184-
if (childResult !== null && childResult !== undefined) {
185-
customChildren.push(childResult);
186-
}
187-
}
188-
}
189-
190-
// Render the custom component
191-
return customRendererFn(customSpec, customProps, runtime, customChildren.length > 0 ? customChildren : undefined);
192-
} else {
193-
// Custom component not found - log warning and fall back to normal rendering
194-
if (debug?.isEnabled) {
195-
console.warn(`Custom component not found for key: ${componentKey}. Falling back to standard component.`);
196-
}
197-
}
198-
}
199-
200136
// Parse schema (now using processed schema)
201137
const { 'x-component-props': componentProps = {} } = processedSchema;
202138

203-
// const componentSpec = getComponentSpec(componentKey, components, undefined, debug?.isEnabled);
204-
205-
// if(!componentSpec) {
206-
// if (debug?.isEnabled) {
207-
// console.warn(`Component not found: ${componentKey}`);
208-
// }
209-
// // Return error element (framework adapter will handle)
210-
// return null;
211-
// }
212-
213139
// Resolve component and renderer
214140
// Use processedSchema for x-component resolution (in case x-component has templates)
215141
// components already has provider components merged in the factory
216142
const resolution = resolveSpec(
217-
processedSchema,
218-
componentKey,
219-
components,
220-
localRenderers,
143+
processedSchema,
144+
componentKey,
145+
components,
146+
customComponents,
147+
localRenderers,
221148
debug?.isEnabled
222149
);
223-
150+
224151
// Check if resolution failed
225152
if (!resolution || resolution === null) {
226153
// Return error component (framework adapter will provide)
227154
return null;
228155
}
229156

230157
// Extract successful resolution
231-
const { renderSpec, componentToRender, rendererFn } = resolution as ResolutionSuccess;
158+
const { componentSpec, rendererFn } = resolution as ResolutionSuccess;
232159

233160
// Construct name path for nested fields
234161
// ONLY components with type: 'field' are included in the name path
235162
// EXCEPTION: componentKeys that are direct properties of root schema (isDirectRootProperty)
236163
// All other components (containers, FormContainer, content, etc.) are ignored
237-
const componentType = renderSpec.type || 'field';
238-
const isFieldComponent = componentType === 'field';
239-
164+
const isFieldComponent = componentSpec.type === 'field';
165+
240166
// If field OR direct root property: add componentKey to name path
241167
// If not field and not root property: keep parentProps.name (don't add this component's key)
242168
const shouldIncludeInPath = isFieldComponent || isDirectRootProperty;
@@ -246,10 +172,11 @@ export function createRendererOrchestrator(
246172

247173
// Props Processing (using processedSchema)
248174
const baseProps = {
249-
...renderSpec.defaultProps,
175+
...componentSpec.defaultProps,
250176
...parentProps,
251177
// Injected for E2E: identifies component key in DOM
252178
'data-test-id': `${componentKey}`,
179+
schema,
253180
// Add name prop ONLY for field components
254181
...(isFieldComponent && currentName ? { name: currentName } : {}),
255182
...(Object.keys(componentProps).length > 0 ? { 'x-component-props': componentProps } : {}),
@@ -271,7 +198,7 @@ export function createRendererOrchestrator(
271198
debug,
272199
formAdapter,
273200
};
274-
201+
275202
const mergedProps = applyMiddlewares(baseProps, processedSchema, middlewares, middlewareContext);
276203

277204
// Render children if schema has properties (use processedSchema)
@@ -292,7 +219,7 @@ export function createRendererOrchestrator(
292219
return orderA - orderB;
293220
}
294221
);
295-
222+
296223
for (const [key, childSchema] of sortedEntries) {
297224
// If this component is the root (FormContainer) and has no name,
298225
// then its direct children are root properties and should be included in name path
@@ -305,7 +232,7 @@ export function createRendererOrchestrator(
305232
}
306233

307234
// Final Rendering using renderer function with children
308-
return rendererFn(componentToRender, mergedProps, runtime, children.length > 0 ? children : undefined);
235+
return rendererFn(componentSpec, mergedProps, runtime, children.length > 0 ? children : undefined);
309236
};
310237
}
311238

packages/core/src/registries/component-registry.ts

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -46,89 +46,17 @@ export function setFactoryDefaultComponents(
4646
factoryDefaultComponents = components;
4747
}
4848

49-
/**
50-
* Get factory default components
51-
*/
5249
export function getFactoryDefaultComponents(): Record<string, ComponentSpec> {
5350
return factoryDefaultComponents;
54-
}
55-
56-
/**
57-
* Get unified component registry
58-
*
59-
* Priority order: local > global > registry overrides > factory defaults
60-
*/
61-
export function getComponentRegistry(
62-
globalComponents?: Record<string, ComponentSpec>,
63-
localComponents?: Record<string, ComponentSpec>
64-
): Record<string, ComponentSpec> {
65-
// Start with factory default components
66-
const merged: Record<string, ComponentSpec> = { ...factoryDefaultComponents };
67-
68-
// Apply global components (from provider)
69-
if (globalComponents) {
70-
Object.keys(globalComponents).forEach(componentName => {
71-
const component = globalComponents[componentName];
72-
if (component) {
73-
merged[componentName] = component;
74-
}
75-
});
76-
}
77-
78-
// Apply local components (maximum priority)
79-
if (localComponents) {
80-
Object.keys(localComponents).forEach(componentName => {
81-
const component = localComponents[componentName];
82-
if (component) {
83-
merged[componentName] = component;
84-
}
85-
});
86-
}
87-
88-
return merged;
89-
}
90-
91-
/**
92-
* Get effective component configuration
93-
* Includes fallback logic and debug logging
94-
*/
95-
export function getComponentSpec(
96-
componentName: string,
97-
globalComponents?: Record<string, ComponentSpec>,
98-
localComponents?: Record<string, ComponentSpec>,
99-
debugEnabled?: boolean
100-
): ComponentSpec | null {
101-
// Local components have maximum priority
102-
if (localComponents?.[componentName]) {
103-
if (debugEnabled) {
104-
console.log(`Component resolved from local: ${componentName}`);
105-
}
106-
return localComponents[componentName];
107-
}
108-
109-
const globalRegistry = getComponentRegistry(globalComponents, localComponents);
110-
111-
if (globalRegistry[componentName]) {
112-
if (debugEnabled) {
113-
console.log(`Component resolved from registry: ${componentName}`);
114-
}
115-
return globalRegistry[componentName];
116-
}
117-
118-
if (debugEnabled) {
119-
console.warn(`Component not found: ${componentName}`);
120-
}
121-
122-
return null;
123-
}
51+
};
12452

12553
/**
12654
* Create a component spec from a factory function
12755
*/
12856
export function createComponentSpec(config: {
12957
id: string;
13058
factory: ComponentSpec['factory'];
131-
type?: ComponentType;
59+
type: ComponentType;
13260
displayName?: string;
13361
defaultProps?: Record<string, any>;
13462
}): ComponentSpec {

packages/core/src/runtime/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface ComponentSpec {
1717
/** Component identifier */
1818
id: string;
1919
/** Component type (field, container, etc.) */
20-
type?: ComponentType;
20+
type: ComponentType;
2121
/** Display name for debugging */
2222
displayName?: string;
2323
/** Default props to apply */

0 commit comments

Comments
 (0)