I have a directive that needs to move the element on which it is applied to the body of the document:
@Directive({
selector: '[myDirective]'
})
export class MyDirective implements AfterViewInit {
constructor(private elem: ElementRef,
private renderer: Renderer2) {
}
ngAfterViewInit() {
// Move element to body
this.renderer.appendChild(document.body, this.elem.nativeElement);
}
}
Now I use this directive in an element with an ngIf
that I toggle based on some condition:
<div myDirective *ngIf="visible">Test</div>
<button (click)="visible = !visible">Toggle</button>
When it runs, the div will show after the button as expected, because it was appended to the body. The toggle button still works at this point, and I can show/hide the div.
The problem comes when I introduce routes:
<a routerLink="/route/1">Link</a>
<div myDirective *ngIf="visible">Test</div>
<button (click)="visible = !visible">Toggle</button>
Now, if I show the div, and navigate to another route, the div should get destroyed, but it's still visible! If I don't move the div to the body then it behaves as expected.
First, I tried removing the element in ngOnDestroy
but this doesn't work with animations, because it deletes the element without playing its :leave animation.
My current workaround is to inject the router into the directive and subscribe to the first event that comes through to restore the div to its original container:
this.routerSubscription = router.events.first().subscribe(() => {
this.renderer.appendChild(this.parentNode, this.elem.nativeElement);
});
While this works, it is not great, as now the directive has a dependency in the router when it should really know nothing about it.
How can improve on this hack to make sure the element gets destroyed properly?
DEMO: https://stackblitz.com/edit/angular-qximjw?file=app%2Fapp.component.ts
To reproduce the issue:
You can add code to remove the element at ngOnDestroy
:
ngOnDestroy() {
if (this.routerSubscription) {
this.routerSubscription.unsubscribe();
}
document.body.removeChild(this.elem.nativeElement);
}
But be aware that when you navigate to the same route, angular won't destroy it (this means ngOnDestroy won't trigger). You can try by add another route and navigate to it to confirm.
See demo.
Well i think it works as intended. If you use your browser inspect mode and click on the RouterLink button you will notice that the HomeComponent is getting re-rendered because you are serving the same Component on both of your Router Options.
const routes: Routes = [
{
path: '',
component: HomeComponent
},
{
path: 'route/:id',
component: HomeComponent
}
];
Therefore, if your Directive is appended and not removed from the DOM, when the HomeComponent gets rendered again, a new instance of that component and also your Directive will be created thus, the application will append an new <div>
on your body while clicking the (new) Button. To make it more clear i suggest you to replace the HomeComponent code with the one below and check out your JavaScript Console.
@Component({
selector: 'home-component',
template: `
<a routerLink="/route/1">Link</a>
<div myDirective *ngIf="visible" @transition>Test</div>
<button (click)="visible = !visible">Toggle</button>
`,
animations: [
trigger('transition', [
transition(':enter', [
style({ opacity: 0 }),
animate('.35s ease', style({ opacity: '*' }))
]),
transition(':leave', [
style({ opacity: '*' }),
animate('.35s ease', style({ opacity: 0 }))
])
])
]
})
export class HomeComponent {
visible = false;
constructor() {
console.log('New instance')
}
}
A workaround that i can think of is putting the Toggle button outside the router-outlet boundaries inside a Component.
@Component({
selector: 'app-toggle-button',
template: `
<div myDirective *ngIf="visible" @transition>Test</div>
<button (click)="visible = !visible">Toggle</button>
`,
animations: [
trigger('transition', [
transition(':enter', [
style({ opacity: 0 }),
animate('.35s ease', style({ opacity: '*' }))
]),
transition(':leave', [
style({ opacity: '*' }),
animate('.35s ease', style({ opacity: 0 }))
])
])
]
})
export class ToggleComponent {
visible = false;
}
And after that you can add it on your app.component.html file:
<app-toggle-button></app-toggle-button>
<router-outlet></router-outlet>
In that case you will always have A SINGLE instance of the ToggleComponent even if you navigate through multiple Routes.
Let me know if that helped you, cheers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With