Support dynamic views
ribizli opened this issue Β· comments
I'm submitting a...
[ ] Regression (a behavior that used to work and stopped working in a new release)
[X] Bug report
[ ] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request
[ ] Other... Please describe:
Current behavior
@ViewChild
's setter is called several times, if the element is toggled in the view. If the element is removed (e.g. through *ngIf=show
), the setter is called with undefined
ElementRef
. This case is not handled (NPE).
If we swap the element with an other (e.g. ngIf
+ else
with same #templateVar
), the setter is called again with the other ElementRef
. (1) The former fromEvent
is not unsubscribed (might not cause memory leak), and (2) the chaining also breaks, since fromEvent
is not usable as event target (aka in subscribe
).
Expected behavior
To keep the original Subject
for the users and swap out it's source with the latest ElementRef
's events.
Also verify if unsubscribe from the previous ElementRef
's events is needed.
Minimal reproduction of the problem with instructions
https://ng-run.com/edit/VqWigxZwytu3Sj26PDnK
- click on 'test' and see the alert
- click on 'toggle' to swap out the button first
- click on 'test alternative' and see that alert is not triggered anymore
- click on 'toggle' to remove the button, see the NPE
What is the motivation / use case for changing the behavior?
Free this lib from bugs ;)
Environment
Angular version: 9.1.1
Browser:
- [X] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
@tonivj5 could you take it, please?
@tonivj5 if you want I could take over, but you must feel more comfortable with your change.
Good catch @ribizli
I'll fix it
We have an issue:
error TS1169: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
At build?
Yes, I added the build to the CI process.
Ok, I will check it
@tonivj5 I'm not sure, about the code in finalize()
, where the subject gets completed (event$.complete()
). Can you please explain it for me? Doesn't it cause late subscribers won't get notified anymore?
@tonivj5 also (IMHO) using a Subject
as source is not really safe after operations piped on it... (I've got TS complaining about)
See my plain test for my .complete()
concern: https://stackblitz.com/edit/rxjs-dlybi3?file=index.ts
Can you please explain it for me?
Sure!
Doesn't it cause late subscribers won't get notified anymore?
No, it will create a new Subject
and subscription to the event with the new subscription. It works because we return a defer
observable, on subscribe, it will create the Subject
(if it's needed) and subscribe to the event (if it's possible). Defer docs.
Defer
allow us to run initIfNeeded
and subscribeToEventIfPossible
on every subscription. And finalize
release the memory (complete the the subject and unsubscribe from fromEvent
) when no one is subscribed to it.
also (IMHO) using a Subject as source is not really safe after operations piped on it... (I've got TS complaining about)
However, type says that pipe
returns an Observable
, it's returning itself (a Subject
), that's the reason because we cast it.
Yeah, we could have an source
symbol (subject piped) and our subject
symbol (without piped), or in our defer
returns this[tokens.subject].asObservable()
to limit how it can be used. But I think it doesn't improve it too much.
See my plain test for my .complete() concern: stackblitz.com/edit/rxjs-dlybi3?file=index.ts
This example doesn't show the same behavior that we are really doing. Here I have modified your example to reflect more properly how FromEvent
is working: https://stackblitz.com/edit/rxjs-eacmif?file=index.ts
Surely it can be improved! You can play with the code and check if tests pass (I think it covers a lot of use-cases
A few notes if we remove defer
or share
(it could lead to more problems...):
- Without
defer
, the subject or subscription wouldn't be created on demand, for example, this POC would fail
constructor() {
const logSomething$ = this.button$/* getter */.pipe(tap(console.log));
// called finalize (subject removed and unsubscribe from event)
logSomething$.subscribe().unsubscribe();
// point to a completed/dead subject, it completes automatically
logSomething/* no getter */.subscribe()
}
- Without
share
, if one subject is unsubscribed,finally
is called and clean up is done and this would affect to other subscriptions to the same subject, for example, this POC would fail
constructor() {
this.button$.subscribe();
// it calls finalize and this subscription and the above one complete
this.button$.subscribe().unsubscribe();
}
@tonivj5 You did a great job!