6

So i have this Component of a from with an @Output event that trigger on submit, as follows:

@Component({
    selector: 'some-component',
    templateUrl: './SomeComponent.html'
})
export class SomeComponent{    
    @Input() data: any;
    @Output() onSubmit: EventEmitter<void> = new EventEmitter<void>();

    constructor(private someService: SomeService) {}

    submitForm(): void{
        this.someService.updateBackend(this.data, ()=>{
            this.onSubmit.emit();
        });
    }
}

I'm using an ngFor to create multiple elements of this Component :

<template let-data ngFor [ngForOf]="dataCollection">
    <some-component  [data]="data" (onSubmit)="doSomthing()"></some-component>
</template>

The last missing part is the service used on submitting:

@Injectable()
export class SomeService{

    constructor() {}

    updateBackend(data: any, callback: () => void): void{
        /*
         * updating the backend
         */.then((result) => {
            const { errors, data } = result;

            if (data) {
                callback();
            }
        })
    }
}

At the beginning of the submitForm() function, the this.onSubmit.observers is an Array containing one observer, like it should be.

As soon as it reaches the callback method, where the this.onSubmit.emit() is invoked, the this.onSubmit.observers is an Array containing ZERO observers.

I'm experiencing two very weird behaviors:

  • If i remove the actual calling to update the backend in SomeService.updateBackend it works perfectly fine, and the observers still is an Array containing one observer!
  • If i keep the actual calling to the backend BUT not using ngFor and displaying only one <some-element> it also works perfectly fine, keeping one observer in the this.onSubmit.observers within the callback scope!

Any idea what am i doing wrong?

Thanks in advance!

Update:

Thanks to @StevenLuke's comment about logging the ngOnDestroy of SomeComponent I found out that it is being destroyed before the emit.

Actually, the first thing it is doing when the SomeService.updateBackend finishes is Destroying all the instances of this component and recreate them!

This is what makes the observers change! Why would that happen?

Kesem David
  • 2,135
  • 3
  • 27
  • 46
  • I would suggest to change title to something that reflects the question. `this` doesn't change at all. It is the same object. Output event listeners in ngFor do. – Estus Flask Sep 13 '16 at 14:02
  • I will update the title as you suggested, and yes, `dataCollection` is fetched by this service right at the beginning. the `doSomthing` function actually just refetches the data – Kesem David Sep 13 '16 at 14:05
  • @JohnSiu using RC6 – Kesem David Sep 13 '16 at 14:41
  • RC6, there should be no "providers" in '@Component', that is moved to '@NgModule' – John Siu Sep 13 '16 at 14:43
  • @JohnSiu Edited my question, still the same results, any ideas? – Kesem David Sep 13 '16 at 14:46
  • Any chance the subscriber to the event is unsubscribing by the time the response from the backend update is called? For example, does the submit redirect to another component, so this component is unloaded? – Steven Luke Sep 13 '16 at 15:10
  • @StevenLuke I'm not sure why would an unsubscribing happen. This specific component is actually a Modal with a form in it, and when clicking the submit **AFTER** the emit the modal closes, does that count as redirecting components? – Kesem David Sep 13 '16 at 15:23
  • @StevenLuke also note I mentioned it works fine if it is calling the backend but not in `ngFor` – Kesem David Sep 13 '16 at 15:29
  • I saw that it works with a single child. The difference would be the amount of work that has to be done during the submit, so the response could come back faster. You are saying that you click submit, and the event you emit causes the modal to disappear, but if the emit doesn't occur the modal stays in place, right? If so, then it probably isn't my suggestion. Would be worth adding some logging to ngOnDestroy() to be sure. – Steven Luke Sep 13 '16 at 15:50
  • @StevenLuke Clarification: the closing of the modal is statement that comes right after the emit. and it will execute them both, one after the other. Thanks to your advise about logging in the `ngOnDestroy` I found out that the modal is being destroyed REGARDLESS m closing statement. Actually, the first thing it is doing when the `SomeService.updateBackend` finishes is Destroying all the instances of this component and recreate them! This is what makes the `observers` change! Why would that happen? – Kesem David Sep 13 '16 at 17:30
  • @GünterZöchbauer (Please note my last comment to StevenLuke) Googling a bit i found this http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 , it might be related but it seems like im unable to understand how to do it, it looks like you've been a part of the conversation, i figured you might be able to help me – Kesem David Sep 13 '16 at 17:38
  • If `dataCollection` is replaced by a different instance, `*ngFor` recreates the whole list of components. You might want to move data to a shared service that outlives recreation by `*ngFor`. – Günter Zöchbauer Sep 13 '16 at 17:55
  • @GünterZöchbauer What you mean is that because `dataCollection` has changed, the `*ngFor` rerenders and therefore the child components destroy? i'll dive into it and let you know if that's it – Kesem David Sep 13 '16 at 17:58
  • If `dataCollection` has changed, especially when it's a new instance, not only added or removed entries, then I'm pretty sure this is the case. – Günter Zöchbauer Sep 13 '16 at 18:03
  • @GünterZöchbauer I see, what is the proper way to place the data in a shared service to "outlive recreation by `*ngFor`"? – Kesem David Sep 13 '16 at 18:04
  • To add it to a service that is provided at a component that doesn't get recreated. Where you add the provider defines the scope where the instance is shared. If you want a singleton for the whole application, provide it in the `AppModule` otherwise on a common parent that doesn't get recreated during the lifetime where you want to keep that instance. – Günter Zöchbauer Sep 13 '16 at 18:10
  • 1
    @GünterZöchbauer Alright, ill try implementing your suggestion and update as soon as I'm done – Kesem David Sep 13 '16 at 18:12
  • @GünterZöchbauer I wanted to thank you for getting my in the right direction. it did occur because the `dataCollection` changed and triggered the rerendering of the `ngFor`. In my particular case, the data was already on a service but it was connected to a server so doing as you suggested was not the solution, I did solve it in a way that fits to my application. I would really appreciate if you could find a few minutes to write an answer to this post so I could upvote and accept it for the next lookers :) – Kesem David Sep 14 '16 at 16:11
  • Umm, if you just write an answer yourself, I guess this would fit better to your question because you know exactly what needed to be fixed. I guess there are enough other possiblilities where you can upvote an answers of mine ;-) – Günter Zöchbauer Sep 14 '16 at 16:17
  • 1
    When the observable replaces the array `*ngFor` iterates over, then `*ngFor` has to update the view to reflect the change in the model. If you only add or remove items then `*ngFor` also just adds removes the related elements in the DOM. – Günter Zöchbauer Sep 15 '16 at 14:31
  • 1
    @GünterZöchbauer And since my `doSomething()` was actually refetching the data from the backend, it was replacing the bound `dataCollection` and caused complete reinitializing of the `ngFor` – Kesem David Sep 15 '16 at 14:35
  • 1
    Sorry, I just saw what you changed in your question. Your answer contains that already :D – Günter Zöchbauer Sep 15 '16 at 14:37
  • You are so active lol, well 118K reputation doesn't fall from the skies :) – Kesem David Sep 15 '16 at 14:39

2 Answers2

10

If you provide a trackBy function in your *ngFor to identify items in your dataCollection, it will not destroy and init. Your template would be:

<some-component *ngFor="let data of dataCollection;trackBy:trackByFunction"
  [data]="data" (onSubmit)="doSomthing()"></some-component>

And the trackByFunction would look like:

trackByFunction(index, item) {
    return item ? item.id : undefined;
}

So even though an item in your dataCollection is a fresh object, if its id matches an id in the previous collection, *ngFor will update [data] but not destroy and init the component.

ksach
  • 103
  • 1
  • 5
2

Thanks to @GünterZöchbauer comments I found out the case was that the data the ngFor is bound to was being replaced by a new instance as I updated the backend, hence, it rerendered it's child Components causing reinitializing (destory + init) of them, which made the instance of the Component to be overwritten.

In order to solve this issue i had to place the dataCollection in a separate service, getting it for the parent component ngOnInit, saving it from causing a rerender of the ngFor, and fetch its data again only after the execution of the Child Components ended

Hope it'll be helpful to somebody!

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Kesem David
  • 2,135
  • 3
  • 27
  • 46