I have a situation in which Angular thinks it needs to recreate components, instead of just using the old component, which leads to components 'refreshing' after dragging them to another place in my interface. I have a tree, from which I generate components. The following is a simplified version of my tree:
{
"left": {
"type": "FirstComponent"
},
"right": {
"left": {
"type": "SecondComponent"
},
"right": {
"type": "ThirdComponent"
}
}
}
After dragging of the components to another position:
{
"left": {
"left": {
"type": "FirstComponent"
},
"right": {
"type": "ThirdComponent"
}
},
"right": {
"type": "SecondComponent"
}
}
I dynamically insert the component in my DockComponent like this:
private panelRef: ComponentRef<any>;
ngOnChanges() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.panel.type);
this.dynamicInsert.clear();
this.panelRef = this.dynamicInsert.createComponent(componentFactory);
}
Whenever I change the tree, the tree gets regenerated, so the components get built from the ground up. I tried to save this.panelRef.instance within my dockTree, but this.panelRef.instance is readonly, so I can't do anything with it. I tried saving this.panelRef.instance and set all properties of this.panelRef.instance to the properties of the saved instance. This got everything all messed up.
Is there a way to dynamically insert an existing component in Angular?
[edit]
Based on the answer of Ilia, I tried to save the ViewRefs in my tree.
export class DockTreeContainer extends DockTreeNode {
private orientation: Orientation;
private left: DockTreeNode;
private right: DockTreeNode;
}
export class DockTreePanel extends DockTreeNode {
type: Type<DockablePanel>;
ref?: ViewRef;
}
The tree exists of DockTreeNodes, that can be either a DockTreeContainer, holding two DockTreeNodes, or a DockTreePanel, holding the type of the panel. I added the ViewRef, setting it in my DockComponent:
ngOnInit() {
if (this.panel.ref) {
this.insertExistingComponent();
} else {
this.createNewComponent();
}
}
private createNewComponent() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.panel.type);
this.panelRef = this.dynamicInsert.createComponent(componentFactory);
this.initializePanel(this.panelRef.instance);
this.panelRef.instance['panel'] = this.panel;
this.panel.ref = this.dynamicInsert.get(0);
}
private insertExistingComponent() {
this.dynamicInsert.clear();
this.dynamicInsert.insert(this.panel.ref);
}
Unfortunately this didn't work, because my components are recursively created using the tree like this:
<div class="dock-orientation {{getOrientation()}}">
<div *ngIf="isPanel(dockTree.getLeft()); then leftDock else leftContainer"></div>
<div *ngIf="isPanel(dockTree.getRight()); then rightDock else rightContainer"></div>
</div>
<ng-template #leftDock>
<ct-dock class="dock-node dock" [panel]="leftAsPanel()"></ct-dock>
</ng-template>
<ng-template #rightDock>
<ct-dock class="dock-node dock" [panel]="rightAsPanel()"></ct-dock>
</ng-template>
<ng-template #leftContainer>
<ct-dock-container class="dock-node dock-container" [dockTree]="leftAsContainer()"></ct-dock-container>
</ng-template>
<ng-template #rightContainer>
<ct-dock-container class="dock-node dock-container" [dockTree]="rightAsContainer()"></ct-dock-container>
</ng-template>
Moving one of the panels can put it in a completely different place in the tree, which causes Angular's change detection to be triggered. By the time my ngOnInit in the DockComponent is called, the saved reference I am trying to restore is already destroyed.
Is there still a way to solve this, or should I try to add all my recursion into one component while hoping my refs will not yet be destroyed?
[edit2]
So, I got to the part where I can actually detach my components within time, so I can re-attach them later. However, all panels are being detached, but not all panels are initializing, resulting in some panels dissapearing when moving other panels around.
// getTreeChange is an Observable that gets triggered after each change in the dockTree, which detaches all the panels in the application.
ngOnInit(): void {
// Detach our panel on changes in the dockTree
this.dockTreeSub = this.dockerService.getTreeChange().subscribe((value) => {
if (value !== '') {
if (this.dynamicInsert.length > 0) {
this.detachPanel();
}
}
});
}
ngAfterViewInit(): void {
if (this.panel.ref) {
this.insertExistingComponent();
} else {
this.createNewComponent();
}
}
/**
* Creates a new component and inserts it into the Dock
*/
private createNewComponent() {
console.log('No panel ref found, creating new component');
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.panel.type);
this.panelRef = this.dynamicInsert.createComponent(componentFactory);
this.initializePanel(this.panelRef.instance);
this.panelRef.instance['panel'] = this.panel;
}
/**
* Takes the old ViewRef from memory and inserts that into the Dock
*/
private insertExistingComponent() {
if (this.panel.ref.destroyed) {
throw new Error('Found destroyed panel');
}
console.log('Found intact panel, inserting');
this.dynamicInsert.clear();
this.dynamicInsert.insert(this.panel.ref);
}
/**
* Detach the panel from this Dock and save it in the DockTree
*/
private detachPanel() {
console.log('Detaching!');
this.panel.ref = this.dynamicInsert.get(0);
this.dynamicInsert.detach(0);
}
[edit3]
Finally got this to work. I am using the AfterViewChecked hook to see if we already initialized a panel. If not, we re-use the old one. AfterViewChecked gave problems with the change detection, so you need to call detectChanges()! Accepting Ilia's answer as it helped me figure it out.
ngOnInit(): void {
// Detach our panel on changes in the dockTree
this.dockTreeSub = this.dockerService.getTreeChange().subscribe((value) => {
if (value !== '') {
if (this.dynamicInsert.length > 0) {
this.detachPanel();
}
}
});
if (!this.panel.ref && this.dynamicInsert.length <= 0) {
this.createNewComponent();
}
}
/**
* Check if the panel needs to be re-inserted after moving panels
*/
ngAfterViewChecked(): void {
if (this.panel.ref && this.dynamicInsert.length <= 0) {
this.insertExistingComponent();
}
this.changeDetector.detectChanges();
}
ViewReffrom currentViewContainerRefbefore it's component will be destroyed (inngOnDestroy)? In this way,ViewRefshould not be destroyed. And destroy them inngOnDestroyof parent component to prevent memory leak.