I'm having problems unsubscribing to a Subject when the Directive where the subscription is set is destroyed. Consider the following html:
<ng-container *ngFor="let item of items; let id = index">
<div [toggleCollapsible]="'target'+id">
{{ item.label }}
</div>
<div *toggleCollapsibleTarget="'target'+id">
<h1>Some nice content up in here</h1>
</div>
</ng-container>
The toggleCollapsible
directive receives and @Input()
with a unique ID that will be used to identify which content should collapse/uncollapse, which is done by the *toggleCollapsibleContent
structural directive. Communication between these 2 directives is handled with a service called toggleCollapsibleService
.
Here's some code for the toggleCollapsible
directive. I'm omitting some stuff for readability purpose:
@Directive({
selector: "[toggleCollapsible]",
host: {
"(click)": "_onClick($event)",
}
})
export class toggleCollapsibleDirective {
@Input('toggleCollapsible') target: string;
isOpen: boolean;
constructor(private _toggle: toggleCollapsibleService) {}
_onClick(e) {
this._toggle.toggleContent(this.target, this.isOpen);
this.isOpen = !this.isOpen;
}
}
Basically, when the host element is clicked, call the service method which receives 2 parameters, the target name and wether the collapsible is currently open or not. Now, my toggleCollapsibleService
:
@Injectable()
export class toggleCollapsibleService {
targetName: string;
private togglerState$: Subject<boolean> = new Subject();
toggleContent(target: string, currentState: boolean) {
this.targetName = target;
this.togglerState$.next(!currentState);
}
}
So, basically this is just saving the ID of the collapsible that's going to be open/closed and passing the corresponding value (again, wether it should open or close). Lets see *toggleCollapsibleContent
which is where things get tricky:
@Directive({
selector: "[toggleCollapsibleContent]"
})
export class toggleCollapsibleContentDirective {
private _name: string;
@Input("toggleCollapsibleContent")
set name(name: string) {
this._name = name;
this._toggle.togglerState$.subscribe(status => {
if (this._name == this._toggle.targetName && status) {
this.renderTarget();
} else if (this._name == this._toggle.targetName) {
this.unmountTarget();
}
});
}
constructor(
private _view: ViewContainerRef,
private _template: TemplateRef<any>,
private _toggle: toggleCollapsibleService
) {}
renderTarget() {
this._view.createEmbeddedView(this._template);
}
unmountTarget() {
if (this._view) this._view.clear();
}
}
The structural directive is working fine, so there's no problem with that side of the implementation. So the problem is, lets say I have the HTML snippet on my HomeComponent
and the items
collection is of length 2. That means I'm creating 2 instances of the *toggleCollapsibleContent
structural directive, each one subscribing to the togglerState$
Subject. If inspect via console.log
the togglerState$
object I get that my object has 2 observers which is the expected behavior, one for each instance of *toggleCollapsibleContent
.
However, if I go to another route and render another component and so on, the togglerState$
Subject still exists, and when I go back to my /home
route where the HomeComponent
is loaded, the togglerState$ adds 2 more observers and since the original ones are still there, now I have 4 observers, 2 for each instance of the *toggleCollapsibleContent
directive and thus my content gets duplicated.
Does anyone have any idea why this is happening?