fix(core): Ensure OnPush ancestors are marked dirty when events occur (#39833)

We currently only wrap the event listener in the function which ensures
ancestors are marked for check when the listener is placed on an element
that has a native method for listening to an event. We actually need to do
this wrapping in all cases so that events that are attached to non-rendered
template items (`ng-template` and `ng-container`) also mark ancestors for check
when they receive the event.

fixes #39832

PR Close #39833
This commit is contained in:
Andrew Scott 2020-11-24 17:18:07 -08:00 committed by Jessica Janiuk
parent e148382bd0
commit 68d4a74411
2 changed files with 41 additions and 2 deletions

View File

@ -193,6 +193,10 @@ function listenerInternal(
lCleanup.push(listenerFn); lCleanup.push(listenerFn);
tCleanup && tCleanup.push(eventName, idxOrTargetGetter, lCleanupIndex, useCapture); tCleanup && tCleanup.push(eventName, idxOrTargetGetter, lCleanupIndex, useCapture);
} }
} else {
// Even if there is no native listener to add, we still need to wrap the listener so that OnPush
// ancestors are marked dirty when an event occurs.
listenerFn = wrapListener(tNode, lView, listenerFn, false /** preventDefault */);
} }
// subscribe to directive outputs // subscribe to directive outputs

View File

@ -8,8 +8,8 @@
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, Input, NgModule, OnInit, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {ApplicationRef, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, DoCheck, EmbeddedViewRef, ErrorHandler, EventEmitter, Input, NgModule, OnInit, Output, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled} from '@angular/private/testing'; import {ivyEnabled} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject} from 'rxjs';
@ -461,6 +461,41 @@ describe('change detection', () => {
expect(comp.doCheckCount).toEqual(2); expect(comp.doCheckCount).toEqual(2);
expect(fixture.nativeElement.textContent.trim()).toEqual('3 - 2 - Nancy'); expect(fixture.nativeElement.textContent.trim()).toEqual('3 - 2 - Nancy');
}); });
it('should check parent OnPush components when child directive on a template emits event',
fakeAsync(() => {
@Directive({
selector: '[emitter]',
})
class Emitter {
@Output() event = new EventEmitter<string>();
ngOnInit() {
setTimeout(() => {
this.event.emit('new message');
});
}
}
@Component({
selector: 'my-app',
template: '{{message}} <ng-template emitter (event)="message = $event"></ng-template>',
changeDetection: ChangeDetectionStrategy.OnPush
})
class MyApp {
message = 'initial message';
}
const fixture = TestBed.configureTestingModule({declarations: [MyApp, Emitter]})
.createComponent(MyApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toEqual('initial message');
tick();
fixture.detectChanges();
expect(fixture.nativeElement.textContent.trim()).toEqual('new message');
}));
}); });
describe('ChangeDetectorRef', () => { describe('ChangeDetectorRef', () => {