1

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?

Osman Cea
  • 1,467
  • 9
  • 18

1 Answers1

0

You need to unsubscribe explicitely:

    this.subscription = this._toggle.togglerState$.subscribe(status => { ...

...

ngOnDestroy() {
  this.subscription.unsubscribe();
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • The observers are still registered, so when I go back to `/home` I'm still getting the duplication bug. I've also tried [this approach](https://stackoverflow.com/a/41177163/3949174) and though the completion handler executes, it doesn't solve my issue. – Osman Cea Jul 20 '17 at 18:00
  • If you only subscribe to within the component like shown in your question, then nothing else should be necessary. – Günter Zöchbauer Jul 20 '17 at 18:04
  • Yeah, that's what I initially thought. I'm gonna try to reproduce this in a plunker cause perhaps it has something to do with Dependecy Injection or the current setup of my project. – Osman Cea Jul 20 '17 at 18:19