7.9 KiB
Automatic Component Dependency Registration
Overview
The framework now automatically registers all component dependencies before rendering. When you bootstrap or create a component, all components listed in its imports array are recursively registered as native Web Components.
How It Works
Registration Flow
@Component({
selector: 'app-root',
template: '<dashboard></dashboard>',
imports: [DashboardComponent],
})
export class AppComponent {}
// When bootstrapping:
Core.bootstrap(AppComponent);
Internal Process:
Core.bootstrap(AppComponent)is calledWebComponentFactory.createFromElement()is invokedregisterWithDependencies(appComponentInstance)is called- Framework checks
imports: [DashboardComponent] - Creates
DashboardComponentinstance via Injector - Recursively calls
registerWithDependencies(dashboardComponentInstance) - Registers
DashboardComponentas<dashboard>custom element - Registers
AppComponentas<app-root>custom element - Renders the template with all dependencies ready
Code Implementation
// In WebComponentFactory
static registerWithDependencies(component: IComponent): boolean {
const imports = component._quarcComponent[0].imports || [];
const injector = Injector.get();
// Register all imported components first
for (const importItem of imports) {
if (this.isComponentType(importItem)) {
let componentInstance = injector.createInstance<IComponent>(
importItem as Type<IComponent>
);
if (!componentInstance._quarcComponent) {
componentInstance = importItem as unknown as IComponent;
}
if (componentInstance._quarcComponent) {
// Recursive call for nested dependencies
this.registerWithDependencies(componentInstance);
}
}
}
// Finally register the parent component
return this.tryRegister(component);
}
Example: Multi-Level Dependencies
// Level 3: Icon Component (no dependencies)
@Component({
selector: 'app-icon',
template: '<svg><path d="..."/></svg>',
})
export class IconComponent {}
// Level 2: Button Component (depends on Icon)
@Component({
selector: 'app-button',
template: `
<button>
<app-icon></app-icon>
<span>Click Me</span>
</button>
`,
imports: [IconComponent],
})
export class ButtonComponent {}
// Level 1: Dashboard Component (depends on Button)
@Component({
selector: 'dashboard',
template: `
<div class="dashboard">
<h1>Dashboard</h1>
<app-button></app-button>
</div>
`,
imports: [ButtonComponent],
})
export class DashboardComponent {}
// Level 0: Root Component (depends on Dashboard)
@Component({
selector: 'app-root',
template: '<dashboard></dashboard>',
imports: [DashboardComponent],
})
export class AppComponent {}
Registration Order:
IconComponent→<app-icon>ButtonComponent→<app-button>DashboardComponent→<dashboard>AppComponent→<app-root>
Benefits
✅ No Manual Registration
Before:
// Manual registration required
WebComponentFactory.tryRegister(iconComponent);
WebComponentFactory.tryRegister(buttonComponent);
WebComponentFactory.tryRegister(dashboardComponent);
Core.bootstrap(AppComponent);
After:
// Automatic registration
Core.bootstrap(AppComponent); // All dependencies registered automatically
✅ Correct Order Guaranteed
The framework ensures components are registered in the correct dependency order, preventing errors where a parent tries to use an unregistered child component.
✅ Prevents Duplicate Registration
The tryRegister() method checks if a component is already registered and returns false if it is, preventing duplicate registration errors.
✅ Supports Circular Dependencies
If two components import each other (not recommended but possible), the registration system handles it gracefully by checking if a component is already registered before attempting to register it again.
✅ Works with Lazy Loading
Components are only registered when they're actually needed, supporting lazy loading patterns:
// Only registers when loaded
const lazyComponent = await import('./lazy.component');
WebComponentFactory.create(lazyComponent.LazyComponent);
Type Checking
The isComponentType() helper ensures only valid components are processed:
private static isComponentType(item: any): boolean {
// Check if it's a class constructor
if (typeof item === 'function') {
return true;
}
// Check if it's already an instance with metadata
if (item && typeof item === 'object' && item._quarcComponent) {
return true;
}
return false;
}
Integration with Injector
The framework uses the Injector to create component instances, which:
- Resolves constructor dependencies
- Caches instances for reuse
- Supports dependency injection patterns
const injector = Injector.get();
let componentInstance = injector.createInstance<IComponent>(
importItem as Type<IComponent>
);
Error Handling
Registration errors are caught and logged without breaking the application:
try {
customElements.define(tagName, WebComponentClass);
this.registeredComponents.set(tagName, WebComponentClass);
this.componentInstances.set(tagName, component);
return true;
} catch (error) {
console.warn(`Failed to register component ${tagName}:`, error);
return false;
}
Common errors:
- Duplicate registration: Component already registered (handled gracefully)
- Invalid tag name: Tag name doesn't contain a hyphen (auto-converted)
- Constructor errors: Component constructor throws an error (logged)
Best Practices
1. Always Declare Imports
@Component({
selector: 'parent',
template: '<child></child>',
imports: [ChildComponent], // ✅ Declared
})
export class ParentComponent {}
2. Avoid Circular Dependencies
// ❌ Avoid this
@Component({
selector: 'comp-a',
imports: [ComponentB],
})
export class ComponentA {}
@Component({
selector: 'comp-b',
imports: [ComponentA], // Circular!
})
export class ComponentB {}
3. Use Lazy Loading for Large Dependencies
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>',
// Don't import heavy components here
})
export class AppComponent {
async loadHeavyComponent() {
const { HeavyComponent } = await import('./heavy.component');
WebComponentFactory.create(HeavyComponent);
}
}
Debugging
Enable console logging to see the registration flow:
// In WebComponentFactory.registerWithDependencies
console.log(`Registering dependencies for: ${component._quarcComponent[0].selector}`);
// In WebComponentFactory.tryRegister
console.log(`Registering component: ${tagName}`);
Migration from Manual Registration
If you have existing code with manual registration:
Before:
WebComponentFactory.tryRegister(childComponent);
WebComponentFactory.tryRegister(parentComponent);
After:
// Just use the parent, children are registered automatically
WebComponentFactory.create(parentComponent);
Or simply:
Core.bootstrap(ParentComponent);
Summary
The automatic dependency registration system:
- ✅ Registers components in correct order
- ✅ Handles nested dependencies recursively
- ✅ Prevents duplicate registrations
- ✅ Integrates with dependency injection
- ✅ Provides error handling and logging
- ✅ Supports lazy loading patterns
- ✅ Simplifies component usage
No manual registration needed - just declare your imports and bootstrap your app!