From d24dde159e73cd8499c193df6576fcd3c6fdff4b Mon Sep 17 00:00:00 2001 From: elid Date: Mon, 6 Dec 2021 13:58:47 -0500 Subject: [PATCH] fix(component): fixes recursive rendering (#3255) Closes #3246 --- .../component/spec/let/let.directive.spec.ts | 58 +++++++++++++++++++ modules/component/src/let/let.directive.ts | 21 +++---- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/modules/component/spec/let/let.directive.spec.ts b/modules/component/spec/let/let.directive.spec.ts index 67f89e9d9c..2c918fa981 100644 --- a/modules/component/spec/let/let.directive.spec.ts +++ b/modules/component/spec/let/let.directive.spec.ts @@ -1,6 +1,7 @@ import { ChangeDetectorRef, Component, + Directive, TemplateRef, ViewContainerRef, } from '@angular/core'; @@ -13,6 +14,7 @@ import { waitForAsync, } from '@angular/core/testing'; import { + BehaviorSubject, EMPTY, interval, NEVER, @@ -60,6 +62,29 @@ class LetDirectiveTestCompleteComponent { value$: Observable = of(42); } +@Directive({ + selector: '[recursiveDirective]', +}) +export class RecursiveDirective { + constructor(private subject: BehaviorSubject) { + this.subject.next(1); + } +} + +@Component({ + template: ` + {{ + value + }} + `, +}) +class LetDirectiveTestRecursionComponent { + constructor(public subject: BehaviorSubject) {} + get value$() { + return this.subject; + } +} + let fixtureLetDirectiveTestComponent: ComponentFixture; let letDirectiveTestComponent: { value$: ObservableInput | undefined | null; @@ -117,6 +142,29 @@ const setupLetDirectiveTestComponentComplete = (): void => { componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement; }; +const setupLetDirectiveTestRecursionComponent = (): void => { + const subject = new BehaviorSubject(0); + TestBed.configureTestingModule({ + declarations: [ + LetDirectiveTestRecursionComponent, + RecursiveDirective, + LetDirective, + ], + providers: [ + { provide: ChangeDetectorRef, useClass: MockChangeDetectorRef }, + TemplateRef, + ViewContainerRef, + { provide: BehaviorSubject, useValue: subject }, + ], + }); + fixtureLetDirectiveTestComponent = TestBed.createComponent( + LetDirectiveTestRecursionComponent + ); + letDirectiveTestComponent = + fixtureLetDirectiveTestComponent.componentInstance; + componentNativeElement = fixtureLetDirectiveTestComponent.nativeElement; +}; + describe('LetDirective', () => { describe('when nexting values', () => { beforeEach(waitForAsync(setupLetDirectiveTestComponent)); @@ -281,4 +329,14 @@ describe('LetDirective', () => { expect(componentNativeElement.textContent).toBe('true'); }); }); + + describe('when rendering recursively', () => { + beforeEach(waitForAsync(setupLetDirectiveTestRecursionComponent)); + + it('should render 2nd emitted value if the observable emits while the view is being rendered', fakeAsync(() => { + fixtureLetDirectiveTestComponent.detectChanges(); + expect(letDirectiveTestComponent).toBeDefined(); + expect(componentNativeElement.textContent).toBe('1'); + })); + }); }); diff --git a/modules/component/src/let/let.directive.ts b/modules/component/src/let/let.directive.ts index 5ae3458916..5aad4a9da2 100644 --- a/modules/component/src/let/let.directive.ts +++ b/modules/component/src/let/let.directive.ts @@ -94,7 +94,7 @@ export class LetDirective implements OnDestroy { // eslint-disable-next-line @typescript-eslint/naming-convention static ngTemplateGuard_ngrxLet: 'binding'; - private embeddedView: any; + private isEmbeddedViewCreated = false; private readonly viewContext: LetViewContext = { $implicit: undefined, ngrxLet: undefined, @@ -107,7 +107,7 @@ export class LetDirective implements OnDestroy { private readonly resetContextObserver: NextObserver = { next: () => { // if not initialized no need to set undefined - if (this.embeddedView) { + if (this.isEmbeddedViewCreated) { this.viewContext.$implicit = undefined; this.viewContext.ngrxLet = undefined; this.viewContext.$error = false; @@ -117,26 +117,26 @@ export class LetDirective implements OnDestroy { }; private readonly updateViewContextObserver: Observer = { next: (value: U | null | undefined) => { + this.viewContext.$implicit = value; + this.viewContext.ngrxLet = value; // to have init lazy - if (!this.embeddedView) { + if (!this.isEmbeddedViewCreated) { this.createEmbeddedView(); } - this.viewContext.$implicit = value; - this.viewContext.ngrxLet = value; }, error: (error: Error) => { + this.viewContext.$error = true; // to have init lazy - if (!this.embeddedView) { + if (!this.isEmbeddedViewCreated) { this.createEmbeddedView(); } - this.viewContext.$error = true; }, complete: () => { + this.viewContext.$complete = true; // to have init lazy - if (!this.embeddedView) { + if (!this.isEmbeddedViewCreated) { this.createEmbeddedView(); } - this.viewContext.$complete = true; }, }; @@ -169,7 +169,8 @@ export class LetDirective implements OnDestroy { } createEmbeddedView() { - this.embeddedView = this.viewContainerRef.createEmbeddedView( + this.isEmbeddedViewCreated = true; + this.viewContainerRef.createEmbeddedView( this.templateRef, this.viewContext );