diff --git a/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts b/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts index 9eb30407dc..d9657500d9 100644 --- a/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts +++ b/packages/dockview-angular/src/__tests__/angular-renderer.spec.ts @@ -4,9 +4,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - EnvironmentInjector, inject, Injector, + TemplateRef, + ViewChild, } from '@angular/core'; import { AngularRenderer } from '../lib/utils/angular-renderer'; @@ -36,18 +37,33 @@ class TestUpdateComponent { } } +@Component({ + selector: 'test-template-holder-component', + template: ` + + + + `, +}) +class TemplateHolderComponent { + @ViewChild('template', { static: true }) + public template?: TemplateRef; +} + describe('AngularRenderer', () => { let injector: Injector; - let environmentInjector: EnvironmentInjector; let application: ApplicationRef; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [TestComponent], + declarations: [ + TestComponent, + TestUpdateComponent, + TemplateHolderComponent, + ], }).compileComponents(); injector = TestBed.inject(Injector); - environmentInjector = TestBed.inject(EnvironmentInjector); application = TestBed.inject(ApplicationRef); }); @@ -63,7 +79,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: TestComponent, injector, - environmentInjector, }); renderer.init({ title: 'Updated Title', value: 'test-value' }); @@ -80,7 +95,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: TestComponent, injector, - environmentInjector, }); renderer.init({ title: 'Initial Title' }); @@ -97,7 +111,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: TestComponent, injector, - environmentInjector, }); renderer.init({ title: 'Test Title' }); @@ -118,7 +131,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: null as any, injector, - environmentInjector, }); expect(() => { @@ -130,7 +142,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: TestComponent, injector, - environmentInjector, }); renderer.init({ title: 'Test Title' }); @@ -145,7 +156,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: TestComponent, injector, - environmentInjector, }); renderer.init({ title: 'Test Title' }); @@ -161,7 +171,6 @@ describe('AngularRenderer', () => { const renderer = new AngularRenderer({ component: TestUpdateComponent, injector, - environmentInjector, }); renderer.init({}); @@ -172,4 +181,28 @@ describe('AngularRenderer', () => { application.tick(); expect(renderer.element.innerHTML).toContain('Counter: 1'); }); + + it('should render view from template', () => { + // Create component with template + const templateRenderer = new AngularRenderer({ + component: TemplateHolderComponent, + injector, + }); + templateRenderer.init({}); + const template = ( + templateRenderer.component.instance as TemplateHolderComponent + ).template; + + expect(template).toBeDefined(); + + // Create view from template + const renderer = new AngularRenderer({ + component: template, + injector: templateRenderer.component.injector, // use container injector to ensure we have a view + }); + renderer.init({}); + application.tick(); + + expect(renderer.element.innerHTML).toContain('Counter: 0'); + }); }); diff --git a/packages/dockview-angular/src/__tests__/component-factory.spec.ts b/packages/dockview-angular/src/__tests__/component-factory.spec.ts index c7bfb2f60f..b7f34acb7a 100644 --- a/packages/dockview-angular/src/__tests__/component-factory.spec.ts +++ b/packages/dockview-angular/src/__tests__/component-factory.spec.ts @@ -2,6 +2,7 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; import { Component, Injector, EnvironmentInjector } from '@angular/core'; import { AngularFrameworkComponentFactory } from '../lib/utils/component-factory'; import { CreateComponentOptions } from 'dockview-core'; +import { ComponentRegistryService } from '../lib/utils/component-registry.service'; @Component({ selector: 'test-dockview-component', @@ -53,6 +54,7 @@ describe('AngularFrameworkComponentFactory', () => { let injector: Injector; let environmentInjector: EnvironmentInjector; let factory: AngularFrameworkComponentFactory; + let resolver: ComponentRegistryService; const components = { 'dockview-test': TestDockviewComponent, @@ -84,9 +86,11 @@ describe('AngularFrameworkComponentFactory', () => { injector = TestBed.inject(Injector); environmentInjector = TestBed.inject(EnvironmentInjector); + resolver = TestBed.inject(ComponentRegistryService); + resolver.registerComponents(components); factory = new AngularFrameworkComponentFactory( - components, + resolver, injector, environmentInjector, tabComponents, @@ -237,7 +241,7 @@ describe('AngularFrameworkComponentFactory', () => { it('should return undefined when no component and no default', () => { const factoryWithoutDefault = new AngularFrameworkComponentFactory( - components, + resolver, injector, environmentInjector, {} @@ -266,7 +270,7 @@ describe('AngularFrameworkComponentFactory', () => { it('should throw error when no watermark component provided', () => { const factoryWithoutWatermark = new AngularFrameworkComponentFactory( - components, + resolver, injector, environmentInjector ); @@ -299,7 +303,7 @@ describe('AngularFrameworkComponentFactory', () => { it('should return undefined when no header actions components provided', () => { const factoryWithoutHeaderActions = new AngularFrameworkComponentFactory( - components, + resolver, injector, environmentInjector ); diff --git a/packages/dockview-angular/src/__tests__/component-registry.spec.ts b/packages/dockview-angular/src/__tests__/component-registry.spec.ts new file mode 100644 index 0000000000..e4f71f7610 --- /dev/null +++ b/packages/dockview-angular/src/__tests__/component-registry.spec.ts @@ -0,0 +1,121 @@ +import { TestBed } from '@angular/core/testing'; +import { Type } from '@angular/core'; +import { + ComponentRegistryService, + ComponentResolver, +} from '../lib/utils/component-registry.service'; + +describe('ComponentRegistryService', () => { + let service: ComponentRegistryService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ComponentRegistryService], + }); + service = TestBed.inject(ComponentRegistryService); + }); + + describe('registerComponent', () => { + it('should register a component with a valid name and reference', () => { + const mockComponent = {} as Type; + service.registerComponent('testComponent', mockComponent); + + expect(service.resolveComponent('testComponent')).toBe( + mockComponent + ); + }); + + it('should throw an error if component name or reference is not provided', () => { + expect(() => + service.registerComponent('', {} as Type) + ).toThrow('Component and reference must be provided'); + }); + }); + + describe('registerComponents', () => { + it('should register multiple components from a record', () => { + const components = { + componentA: {} as Type, + componentB: {} as Type, + }; + + service.registerComponents(components); + + expect(service.resolveComponent('componentA')).toBe( + components.componentA + ); + expect(service.resolveComponent('componentB')).toBe( + components.componentB + ); + }); + }); + + describe('resolveComponent', () => { + it('should return a registered component reference', () => { + const mockComponent = {} as Type; + service.registerComponent('testComponent', mockComponent); + + const resolved = service.resolveComponent('testComponent'); + expect(resolved).toBe(mockComponent); + }); + + it('should throw an error if component name is not provided', () => { + expect(() => service.resolveComponent('')).toThrow( + 'Component must be provided' + ); + }); + + it('should resolve a component dynamically through a resolver', () => { + const dynamicComponent = {} as Type; + const resolver: ComponentResolver = (component) => + component === 'dynamicComponent' ? dynamicComponent : undefined; + + service.registerResolver(resolver); + + expect(service.resolveComponent('dynamicComponent')).toBe( + dynamicComponent + ); + }); + + it('should fallback to static registration if no resolver matches', () => { + const staticComponent = {} as Type; + service.registerComponent('staticComponent', staticComponent); + + const resolver: ComponentResolver = () => undefined; + service.registerResolver(resolver); + + expect(service.resolveComponent('staticComponent')).toBe( + staticComponent + ); + }); + }); + + describe('registerResolver', () => { + it('should register a new resolver for dynamic component resolution', () => { + const dynamicComponent = {} as Type; + const resolver: ComponentResolver = (component) => + component === 'dynamicComponent' ? dynamicComponent : undefined; + + service.registerResolver(resolver); + + expect(service.resolveComponent('dynamicComponent')).toBe( + dynamicComponent + ); + }); + }); + + describe('unregisterResolver', () => { + it('should unregister a resolver', () => { + const dynamicComponent = {} as Type; + const resolver: ComponentResolver = (component) => + component === 'dynamicComponent' ? dynamicComponent : undefined; + + service.registerResolver(resolver); + service.unregisterResolver(resolver); + + expect( + service.resolveComponent('dynamicComponent') + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts b/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts index 093dc40193..374fb98f79 100644 --- a/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts +++ b/packages/dockview-angular/src/__tests__/dockview-angular.component.spec.ts @@ -33,14 +33,6 @@ describe('DockviewAngularComponent', () => { expect(component).toBeTruthy(); }); - it('should throw error if components input is not provided', () => { - component.components = undefined as any; - - expect(() => { - component.ngOnInit(); - }).toThrow('DockviewAngularComponent: components input is required'); - }); - it('should initialize dockview api on ngOnInit', () => { component.ngOnInit(); diff --git a/packages/dockview-angular/src/__tests__/gridview-angular.component.spec.ts b/packages/dockview-angular/src/__tests__/gridview-angular.component.spec.ts index 6923e35d36..3db0233df5 100644 --- a/packages/dockview-angular/src/__tests__/gridview-angular.component.spec.ts +++ b/packages/dockview-angular/src/__tests__/gridview-angular.component.spec.ts @@ -33,14 +33,6 @@ describe('GridviewAngularComponent', () => { expect(component).toBeTruthy(); }); - it('should throw error if components input is not provided', () => { - component.components = undefined as any; - - expect(() => { - component.ngOnInit(); - }).toThrow('GridviewAngularComponent: components input is required'); - }); - it('should initialize gridview api on ngOnInit', () => { component.ngOnInit(); diff --git a/packages/dockview-angular/src/__tests__/paneview-angular.component.spec.ts b/packages/dockview-angular/src/__tests__/paneview-angular.component.spec.ts index aa56b840aa..67e3618800 100644 --- a/packages/dockview-angular/src/__tests__/paneview-angular.component.spec.ts +++ b/packages/dockview-angular/src/__tests__/paneview-angular.component.spec.ts @@ -33,14 +33,6 @@ describe('PaneviewAngularComponent', () => { expect(component).toBeTruthy(); }); - it('should throw error if components input is not provided', () => { - component.components = undefined as any; - - expect(() => { - component.ngOnInit(); - }).toThrow('PaneviewAngularComponent: components input is required'); - }); - it('should initialize paneview api on ngOnInit', () => { component.ngOnInit(); diff --git a/packages/dockview-angular/src/__tests__/splitview-angular.component.spec.ts b/packages/dockview-angular/src/__tests__/splitview-angular.component.spec.ts index 3b566b0395..81b2d2a922 100644 --- a/packages/dockview-angular/src/__tests__/splitview-angular.component.spec.ts +++ b/packages/dockview-angular/src/__tests__/splitview-angular.component.spec.ts @@ -33,14 +33,6 @@ describe('SplitviewAngularComponent', () => { expect(component).toBeTruthy(); }); - it('should throw error if components input is not provided', () => { - component.components = undefined as any; - - expect(() => { - component.ngOnInit(); - }).toThrow('SplitviewAngularComponent: components input is required'); - }); - it('should initialize splitview api on ngOnInit', () => { component.ngOnInit(); diff --git a/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts b/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts index 3db53da583..c44fd1eb51 100644 --- a/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts +++ b/packages/dockview-angular/src/lib/dockview/dockview-angular.component.ts @@ -28,6 +28,8 @@ import { } from 'dockview-core'; import { AngularFrameworkComponentFactory } from '../utils/component-factory'; import { AngularLifecycleManager } from '../utils/lifecycle-utils'; +import { ComponentRegistryService } from '../utils/component-registry.service'; +import { ComponentReference } from '../types'; export interface DockviewAngularOptions extends DockviewOptions { components: Record>; @@ -60,10 +62,14 @@ export interface DockviewAngularOptions extends DockviewOptions { changeDetection: ChangeDetectionStrategy.OnPush, }) export class DockviewAngularComponent implements OnInit, OnDestroy, OnChanges { + private readonly componentRegistry: ComponentRegistryService = inject( + ComponentRegistryService + ); + @ViewChild('dockviewContainer', { static: true }) private containerRef!: ElementRef; - @Input() components!: Record>; + @Input() components?: Record; @Input() tabComponents?: Record>; @Input() watermarkComponent?: Type; @Input() defaultTabComponent?: Type; @@ -130,10 +136,8 @@ export class DockviewAngularComponent implements OnInit, OnDestroy, OnChanges { } private initializeDockview(): void { - if (!this.components) { - throw new Error( - 'DockviewAngularComponent: components input is required' - ); + if (this.components) { + this.componentRegistry.registerComponents(this.components); } const coreOptions = this.extractCoreOptions(); @@ -178,7 +182,7 @@ export class DockviewAngularComponent implements OnInit, OnDestroy, OnChanges { } const componentFactory = new AngularFrameworkComponentFactory( - this.components, + this.componentRegistry, this.injector, this.environmentInjector, this.tabComponents, diff --git a/packages/dockview-angular/src/lib/gridview/angular-gridview-panel.ts b/packages/dockview-angular/src/lib/gridview/angular-gridview-panel.ts index f3d5b37dcf..8ccd9bc073 100644 --- a/packages/dockview-angular/src/lib/gridview/angular-gridview-panel.ts +++ b/packages/dockview-angular/src/lib/gridview/angular-gridview-panel.ts @@ -1,12 +1,13 @@ -import { Type, Injector, EnvironmentInjector } from '@angular/core'; +import { Injector, EnvironmentInjector } from '@angular/core'; import { GridviewPanel, IFrameworkPart } from 'dockview-core'; import { AngularRenderer } from '../utils/angular-renderer'; +import { ComponentReference } from '../types'; export class AngularGridviewPanel extends GridviewPanel { constructor( id: string, component: string, - private readonly angularComponent: Type, + private readonly angularComponent: ComponentReference, private readonly injector: Injector, private readonly environmentInjector?: EnvironmentInjector ) { diff --git a/packages/dockview-angular/src/lib/gridview/gridview-angular.component.ts b/packages/dockview-angular/src/lib/gridview/gridview-angular.component.ts index 5ba3381740..e29f6498ef 100644 --- a/packages/dockview-angular/src/lib/gridview/gridview-angular.component.ts +++ b/packages/dockview-angular/src/lib/gridview/gridview-angular.component.ts @@ -26,6 +26,8 @@ import { import { AngularFrameworkComponentFactory } from '../utils/component-factory'; import { AngularLifecycleManager } from '../utils/lifecycle-utils'; import { GridviewAngularReadyEvent } from './types'; +import { ComponentRegistryService } from '../utils/component-registry.service'; +import { ComponentReference } from '../types'; export interface GridviewAngularOptions extends GridviewOptions { components: Record>; @@ -52,10 +54,14 @@ export interface GridviewAngularOptions extends GridviewOptions { changeDetection: ChangeDetectionStrategy.OnPush, }) export class GridviewAngularComponent implements OnInit, OnDestroy, OnChanges { + private readonly componentRegistry: ComponentRegistryService = inject( + ComponentRegistryService + ); + @ViewChild('gridviewContainer', { static: true }) private containerRef!: ElementRef; - @Input() components!: Record>; + @Input() components?: Record; // Core gridview options as inputs @Input() className?: string; @@ -107,10 +113,8 @@ export class GridviewAngularComponent implements OnInit, OnDestroy, OnChanges { } private initializeGridview(): void { - if (!this.components) { - throw new Error( - 'GridviewAngularComponent: components input is required' - ); + if (this.components) { + this.componentRegistry.registerComponents(this.components); } const coreOptions = this.extractCoreOptions(); @@ -140,7 +144,7 @@ export class GridviewAngularComponent implements OnInit, OnDestroy, OnChanges { private createFrameworkOptions(): GridviewFrameworkOptions { const componentFactory = new AngularFrameworkComponentFactory( - this.components, + this.componentRegistry, this.injector, this.environmentInjector ); diff --git a/packages/dockview-angular/src/lib/paneview/angular-pane-part.ts b/packages/dockview-angular/src/lib/paneview/angular-pane-part.ts index 535917a137..c85f211ef8 100644 --- a/packages/dockview-angular/src/lib/paneview/angular-pane-part.ts +++ b/packages/dockview-angular/src/lib/paneview/angular-pane-part.ts @@ -1,16 +1,17 @@ -import { Type, Injector, EnvironmentInjector } from '@angular/core'; +import { Injector, EnvironmentInjector } from '@angular/core'; import { IPanePart, PanelUpdateEvent, PanePanelComponentInitParameter, } from 'dockview-core'; import { AngularRenderer } from '../utils/angular-renderer'; +import { ComponentReference } from '../types'; export class AngularPanePart implements IPanePart { private renderer: AngularRenderer; constructor( - private readonly angularComponent: Type, + private readonly angularComponent: ComponentReference, private readonly injector: Injector, private readonly environmentInjector?: EnvironmentInjector ) { diff --git a/packages/dockview-angular/src/lib/paneview/paneview-angular.component.ts b/packages/dockview-angular/src/lib/paneview/paneview-angular.component.ts index b756ce5d36..ebbd1127e0 100644 --- a/packages/dockview-angular/src/lib/paneview/paneview-angular.component.ts +++ b/packages/dockview-angular/src/lib/paneview/paneview-angular.component.ts @@ -28,6 +28,8 @@ import { AngularFrameworkComponentFactory } from '../utils/component-factory'; import { AngularLifecycleManager } from '../utils/lifecycle-utils'; import { PaneviewAngularReadyEvent } from './types'; import { AngularPanePart } from './angular-pane-part'; +import { ComponentRegistryService } from '../utils/component-registry.service'; +import { ComponentReference } from '../types'; export interface PaneviewAngularOptions extends PaneviewOptions { components: Record>; @@ -55,10 +57,14 @@ export interface PaneviewAngularOptions extends PaneviewOptions { changeDetection: ChangeDetectionStrategy.OnPush, }) export class PaneviewAngularComponent implements OnInit, OnDestroy, OnChanges { + private readonly componentRegistry: ComponentRegistryService = inject( + ComponentRegistryService + ); + @ViewChild('paneviewContainer', { static: true }) private containerRef!: ElementRef; - @Input() components!: Record>; + @Input() components?: Record; @Input() headerComponents?: Record>; // Core paneview options as inputs @@ -111,10 +117,8 @@ export class PaneviewAngularComponent implements OnInit, OnDestroy, OnChanges { } private initializePaneview(): void { - if (!this.components) { - throw new Error( - 'PaneviewAngularComponent: components input is required' - ); + if (this.components) { + this.componentRegistry.registerComponents(this.components); } const coreOptions = this.extractCoreOptions(); @@ -147,7 +151,7 @@ export class PaneviewAngularComponent implements OnInit, OnDestroy, OnChanges { private createFrameworkOptions(): PaneviewFrameworkOptions { const componentFactory = new AngularFrameworkComponentFactory( - this.components, + this.componentRegistry, this.injector, this.environmentInjector, this.headerComponents diff --git a/packages/dockview-angular/src/lib/splitview/angular-splitview-panel.ts b/packages/dockview-angular/src/lib/splitview/angular-splitview-panel.ts index eedd63df69..dce1773c33 100644 --- a/packages/dockview-angular/src/lib/splitview/angular-splitview-panel.ts +++ b/packages/dockview-angular/src/lib/splitview/angular-splitview-panel.ts @@ -1,12 +1,13 @@ -import { Type, Injector, EnvironmentInjector } from '@angular/core'; +import { Injector, EnvironmentInjector } from '@angular/core'; import { SplitviewPanel, IFrameworkPart } from 'dockview-core'; import { AngularRenderer } from '../utils/angular-renderer'; +import { ComponentReference } from '../types'; export class AngularSplitviewPanel extends SplitviewPanel { constructor( id: string, component: string, - private readonly angularComponent: Type, + private readonly angularComponent: ComponentReference, private readonly injector: Injector, private readonly environmentInjector?: EnvironmentInjector ) { diff --git a/packages/dockview-angular/src/lib/splitview/splitview-angular.component.ts b/packages/dockview-angular/src/lib/splitview/splitview-angular.component.ts index e1577c0bec..24691b5535 100644 --- a/packages/dockview-angular/src/lib/splitview/splitview-angular.component.ts +++ b/packages/dockview-angular/src/lib/splitview/splitview-angular.component.ts @@ -26,6 +26,8 @@ import { import { AngularFrameworkComponentFactory } from '../utils/component-factory'; import { AngularLifecycleManager } from '../utils/lifecycle-utils'; import { SplitviewAngularReadyEvent } from './types'; +import { ComponentRegistryService } from '../utils/component-registry.service'; +import { ComponentReference } from '../types'; export interface SplitviewAngularOptions extends SplitviewOptions { components: Record>; @@ -52,10 +54,14 @@ export interface SplitviewAngularOptions extends SplitviewOptions { changeDetection: ChangeDetectionStrategy.OnPush, }) export class SplitviewAngularComponent implements OnInit, OnDestroy, OnChanges { + private readonly componentRegistry: ComponentRegistryService = inject( + ComponentRegistryService + ); + @ViewChild('splitviewContainer', { static: true }) private containerRef!: ElementRef; - @Input() components!: Record>; + @Input() components?: Record; // Core splitview options as inputs @Input() className?: string; @@ -107,10 +113,8 @@ export class SplitviewAngularComponent implements OnInit, OnDestroy, OnChanges { } private initializeSplitview(): void { - if (!this.components) { - throw new Error( - 'SplitviewAngularComponent: components input is required' - ); + if (this.components) { + this.componentRegistry.registerComponents(this.components); } const coreOptions = this.extractCoreOptions(); @@ -140,7 +144,7 @@ export class SplitviewAngularComponent implements OnInit, OnDestroy, OnChanges { private createFrameworkOptions(): SplitviewFrameworkOptions { const componentFactory = new AngularFrameworkComponentFactory( - this.components, + this.componentRegistry, this.injector, this.environmentInjector ); diff --git a/packages/dockview-angular/src/lib/types.ts b/packages/dockview-angular/src/lib/types.ts new file mode 100644 index 0000000000..a386c45546 --- /dev/null +++ b/packages/dockview-angular/src/lib/types.ts @@ -0,0 +1,3 @@ +import { TemplateRef, Type } from '@angular/core'; + +export type ComponentReference = Type | TemplateRef; diff --git a/packages/dockview-angular/src/lib/utils/angular-renderer.ts b/packages/dockview-angular/src/lib/utils/angular-renderer.ts index 979e8168a3..e722dade0c 100644 --- a/packages/dockview-angular/src/lib/utils/angular-renderer.ts +++ b/packages/dockview-angular/src/lib/utils/angular-renderer.ts @@ -6,17 +6,21 @@ import { createComponent, EnvironmentInjector, ApplicationRef, + TemplateRef, + ViewContainerRef, } from '@angular/core'; import { IContentRenderer, IFrameworkPart, Parameters } from 'dockview-core'; +import { ComponentReference } from '../types'; export interface AngularRendererOptions { - component: Type; + component: ComponentReference; injector: Injector; environmentInjector?: EnvironmentInjector; } export class AngularRenderer implements IContentRenderer, IFrameworkPart { private componentRef: ComponentRef | null = null; + private viewRef: EmbeddedViewRef | null = null; private _element: HTMLElement | null = null; private appRef: ApplicationRef; @@ -34,10 +38,13 @@ export class AngularRenderer implements IContentRenderer, IFrameworkPart { get component(): ComponentRef | null { return this.componentRef; } + get view(): EmbeddedViewRef | null { + return this.viewRef; + } init(parameters: Parameters): void { // If already initialized, just update the parameters - if (this.componentRef) { + if (this._element) { this.update(parameters); } else { this.render(parameters); @@ -45,6 +52,7 @@ export class AngularRenderer implements IContentRenderer, IFrameworkPart { } update(params: Parameters): void { + // Only component can have parameters if (!this.componentRef) { return; } @@ -56,43 +64,69 @@ export class AngularRenderer implements IContentRenderer, IFrameworkPart { } } - // trigger change detection + // Trigger change detection this.componentRef.changeDetectorRef.markForCheck(); } private render(parameters: Parameters): void { try { - // Create the component using modern Angular API - this.componentRef = createComponent(this.options.component, { - environmentInjector: - this.options.environmentInjector || - (this.options.injector as EnvironmentInjector), - elementInjector: this.options.injector, - }); - - // Set initial parameters - this.update(parameters); - - // Get the DOM element - const hostView = this.componentRef.hostView as EmbeddedViewRef; - this._element = hostView.rootNodes[0] as HTMLElement; - - // attach to change detection - this.appRef.attachView(hostView); - - // trigger change detection - this.componentRef.changeDetectorRef.markForCheck(); + if (this.options.component instanceof TemplateRef) { + this.setupView(this.options.component); + } else { + this.setupComponent(this.options.component, parameters); + } } catch (error) { console.error('Error creating Angular component:', error); throw error; } } + private setupComponent(component: Type, parameters: Parameters): void { + // Create the component using modern Angular API + this.componentRef = createComponent(component, { + environmentInjector: + this.options.environmentInjector || + (this.options.injector as EnvironmentInjector), + elementInjector: this.options.injector, + }); + + // Set initial parameters + this.update(parameters); + + // Get the DOM element + const hostView = this.componentRef.hostView as EmbeddedViewRef; + this._element = hostView.rootNodes[0] as HTMLElement; + + // Attach to change detection + this.appRef.attachView(hostView); + + // Trigger change detection + this.componentRef.changeDetectorRef.markForCheck(); + } + + private setupView(template: TemplateRef): void { + // Get factory for template instances + const vcr = this.options.injector.get(ViewContainerRef); + + // Create embedded view from template + this.viewRef = vcr.createEmbeddedView(template); + this._element = this.viewRef.rootNodes[0] as HTMLElement; + + // Already attached to change detection (of injector, usually dockview) + + // Trigger change detection + this.viewRef.markForCheck(); + } + dispose(): void { if (this.componentRef) { this.componentRef.destroy(); this.componentRef = null; } + if (this.viewRef) { + this.viewRef.destroy(); + this.viewRef = null; + } this._element = null; } } diff --git a/packages/dockview-angular/src/lib/utils/component-factory.ts b/packages/dockview-angular/src/lib/utils/component-factory.ts index c7c039d4b2..76791f31d0 100644 --- a/packages/dockview-angular/src/lib/utils/component-factory.ts +++ b/packages/dockview-angular/src/lib/utils/component-factory.ts @@ -13,10 +13,11 @@ import { AngularRenderer } from './angular-renderer'; import { AngularGridviewPanel } from '../gridview/angular-gridview-panel'; import { AngularSplitviewPanel } from '../splitview/angular-splitview-panel'; import { AngularPanePart } from '../paneview/angular-pane-part'; +import { ComponentRegistryService } from './component-registry.service'; export class AngularFrameworkComponentFactory { constructor( - private components: Record>, + private componentResolver: ComponentRegistryService, private injector: Injector, private environmentInjector?: EnvironmentInjector, private tabComponents?: Record>, @@ -27,7 +28,7 @@ export class AngularFrameworkComponentFactory { // For DockviewComponent createDockviewComponent(options: CreateComponentOptions): IContentRenderer { - const component = this.components[options.name]; + const component = this.componentResolver.resolveComponent(options.name); if (!component) { throw new Error( `Component '${options.name}' not found in component registry` @@ -46,7 +47,7 @@ export class AngularFrameworkComponentFactory { // For GridviewComponent createGridviewComponent(options: CreateComponentOptions): GridviewPanel { - const component = this.components[options.name]; + const component = this.componentResolver.resolveComponent(options.name); if (!component) { throw new Error( `Component '${options.name}' not found in component registry` @@ -64,7 +65,7 @@ export class AngularFrameworkComponentFactory { // For SplitviewComponent createSplitviewComponent(options: CreateComponentOptions): SplitviewPanel { - const component = this.components[options.name]; + const component = this.componentResolver.resolveComponent(options.name); if (!component) { throw new Error( `Component '${options.name}' not found in component registry` @@ -82,7 +83,7 @@ export class AngularFrameworkComponentFactory { // For PaneviewComponent createPaneviewComponent(options: CreateComponentOptions): IPanePart { - const component = this.components[options.name]; + const component = this.componentResolver.resolveComponent(options.name); if (!component) { throw new Error( `Component '${options.name}' not found in component registry` diff --git a/packages/dockview-angular/src/lib/utils/component-registry.service.ts b/packages/dockview-angular/src/lib/utils/component-registry.service.ts new file mode 100644 index 0000000000..fb640c91ba --- /dev/null +++ b/packages/dockview-angular/src/lib/utils/component-registry.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { ComponentReference } from '../types'; + +export type ComponentResolver = (component: string) => ComponentReference | undefined; + +@Injectable({ providedIn: 'root' }) +export class ComponentRegistryService { + private readonly components: Map = new Map(); + private readonly resolver: ComponentResolver[] = []; + + public registerResolver(resolver: ComponentResolver) { + this.resolver.push(resolver); + } + + public unregisterResolver(resolver: ComponentResolver) { + this.resolver.splice(this.resolver.indexOf(resolver), 1); + } + + public registerComponents(components: Record) { + for (const [component, reference] of Object.entries(components)) { + this.registerComponent(component, reference); + } + } + + public registerComponent(component: string, reference: ComponentReference) { + if (!component || !reference) { + throw new Error('Component and reference must be provided'); + } + + this.components.set(component, reference); + } + + public resolveComponent(component: string): ComponentReference | undefined { + if (!component) { + throw new Error('Component must be provided'); + } + + return this.getComponentReference(component); + } + + private getComponentReference(component: string): ComponentReference | undefined { + // first, try to get dynamic reference + for (const resolver of this.resolver) { + const reference = resolver(component); + if (reference) { + return reference; + } + } + + // last, try to get static reference + return this.components.get(component); + } +} diff --git a/packages/dockview-angular/src/public-api.ts b/packages/dockview-angular/src/public-api.ts index 9b2cd8f656..ca8c5f7701 100644 --- a/packages/dockview-angular/src/public-api.ts +++ b/packages/dockview-angular/src/public-api.ts @@ -40,7 +40,10 @@ export { SplitviewAngularReadyEvent, } from './lib/splitview/types'; +export * from './lib/types'; + // Utilities export * from './lib/utils/angular-renderer'; export * from './lib/utils/component-factory'; export * from './lib/utils/lifecycle-utils'; +export * from './lib/utils/component-registry.service';