fix(ivy): should mark OnPush ancestor of dynamically created views as dirty (#28687)

While marking a given views tree as dirty we should go all the way to the
root of the views tree and cross boundaries of dynamically inserted views.
In other words the markForCheck functionality should consider parents of
dynamically inserted views.

PR Close #28687
This commit is contained in:
Pawel Kozlowski 2019-02-13 12:10:20 +01:00 committed by Miško Hevery
parent 2f27a8051b
commit 6d057cc05d
3 changed files with 70 additions and 10 deletions

View File

@ -30,7 +30,7 @@ import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, Pro
import {PlayerFactory} from './interfaces/player'; import {PlayerFactory} from './interfaces/player';
import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection';
import {LQueries} from './interfaces/query'; import {LQueries} from './interfaces/query';
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization'; import {SanitizerFn} from './interfaces/sanitization';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView, T_HOST} from './interfaces/view'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView, T_HOST} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
@ -41,7 +41,7 @@ import {getInitialClassNameValue, getInitialStyleStringValue, initializeStaticCo
import {BoundPlayerFactory} from './styling/player_factory'; import {BoundPlayerFactory} from './styling/player_factory';
import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util'; import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingContext, forceClassesAsString, forceStylesAsString, getStylingContext, hasClassInput, hasStyleInput, hasStyling, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens'; import {NO_CHANGE} from './tokens';
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util';
@ -2729,15 +2729,16 @@ function wrapListener(
* @returns the root LView * @returns the root LView
*/ */
export function markViewDirty(lView: LView): LView|null { export function markViewDirty(lView: LView): LView|null {
while (lView && !(lView[FLAGS] & LViewFlags.IsRoot)) { while (lView) {
lView[FLAGS] |= LViewFlags.Dirty; lView[FLAGS] |= LViewFlags.Dirty;
// Stop traversing up as soon as you find a root view that wasn't attached to any container
if (isRootView(lView) && lView[CONTAINER_INDEX] === -1) {
return lView;
}
// continue otherwise
lView = lView[PARENT] !; lView = lView[PARENT] !;
} }
// Detached views do not have a PARENT and also aren't root views return null;
if (lView) {
lView[FLAGS] |= LViewFlags.Dirty;
}
return lView;
} }
/** /**

View File

@ -16,7 +16,7 @@ import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFl
import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
import {RComment, RElement, RText} from './interfaces/renderer'; import {RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling'; import {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView, T_HOST} from './interfaces/view'; import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, T_HOST} from './interfaces/view';

View File

@ -7,7 +7,7 @@
*/ */
import {ApplicationRef, Component, Directive, EmbeddedViewRef, TemplateRef, ViewContainerRef} from '@angular/core'; import {ApplicationRef, ChangeDetectionStrategy, Component, ComponentFactoryResolver, ComponentRef, Directive, EmbeddedViewRef, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -68,4 +68,63 @@ describe('change detection', () => {
}); });
describe('markForCheck', () => {
it('should mark OnPush ancestor of dynamically created component views as dirty', () => {
@Component({
selector: `test-cmpt`,
template: `{{counter}}|<ng-template #vc></ng-template>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class TestCmpt {
counter = 0;
@ViewChild('vc', {read: ViewContainerRef}) vcRef !: ViewContainerRef;
constructor(private _cfr: ComponentFactoryResolver) {}
createComponentView<T>(cmptType: Type<T>): ComponentRef<T> {
const cf = this._cfr.resolveComponentFactory(cmptType);
return this.vcRef.createComponent(cf);
}
}
@Component({
selector: 'dynamic-cmpt',
template: `dynamic`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class DynamicCmpt {
}
@NgModule({declarations: [DynamicCmpt], entryComponents: [DynamicCmpt]})
class DynamicModule {
}
TestBed.configureTestingModule({imports: [DynamicModule], declarations: [TestCmpt]});
const fixture = TestBed.createComponent(TestCmpt);
// initial CD to have query results
// NOTE: we call change detection without checkNoChanges to have clearer picture
fixture.detectChanges(false);
expect(fixture.nativeElement).toHaveText('0|');
// insert a dynamic component
const dynamicCmptRef = fixture.componentInstance.createComponentView(DynamicCmpt);
fixture.detectChanges(false);
expect(fixture.nativeElement).toHaveText('0|dynamic');
// update model in the OnPush component - should not update UI
fixture.componentInstance.counter = 1;
fixture.detectChanges(false);
expect(fixture.nativeElement).toHaveText('0|dynamic');
// now mark the dynamically inserted component as dirty
dynamicCmptRef.changeDetectorRef.markForCheck();
fixture.detectChanges(false);
expect(fixture.nativeElement).toHaveText('1|dynamic');
});
});
}); });