diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index a4ad46a243..3c97ca0087 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 14861, + "main-es2015": 15039, "polyfills-es2015": 36808 } } diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index a51bcb9bef..a6863b55db 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -86,6 +86,7 @@ export const TViewConstructor = class TView implements ITView { public expandoStartIndex: number, // public expandoInstructions: ExpandoInstructions|null, // public firstTemplatePass: boolean, // + public firstUpdatePass: boolean, // public staticViewQueries: boolean, // public staticContentQueries: boolean, // public preOrderHooks: HookData|null, // diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 052915456d..39ceddc989 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -463,6 +463,9 @@ export function refreshView( } } finally { + if (tView.firstUpdatePass === true) { + tView.firstUpdatePass = false; + } lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); leaveViewProcessExit(); } @@ -609,6 +612,7 @@ export function createTView( initialViewLength, // expandoStartIndex: number, null, // expandoInstructions: ExpandoInstructions|null, true, // firstTemplatePass: boolean, + true, // firstUpdatePass: boolean, false, // staticViewQueries: boolean, false, // staticContentQueries: boolean, null, // preOrderHooks: HookData|null, @@ -640,6 +644,7 @@ export function createTView( expandoStartIndex: initialViewLength, expandoInstructions: null, firstTemplatePass: true, + firstUpdatePass: true, staticViewQueries: false, staticContentQueries: false, preOrderHooks: null, diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 93ba5fabda..ff50bd1805 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -363,6 +363,9 @@ export interface TView { /** Whether or not this template has been processed. */ firstTemplatePass: boolean; + /** Whether or not the first update for this element has been processed. */ + firstUpdatePass: boolean; + /** Static data equivalent of LView.data[]. Contains TNodes, PipeDefInternal or TI18n. */ data: TData; diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index 1ff23d0780..1a8762a838 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -7,6 +7,9 @@ */ import {CommonModule} from '@angular/common'; import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgModule, OnInit, Output, Pipe, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; +import {TVIEW} from '@angular/core/src/render3/interfaces/view'; +import {getLView} from '@angular/core/src/render3/state'; +import {loadLContext} from '@angular/core/src/render3/util/discovery_utils'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -1877,4 +1880,97 @@ describe('acceptance integration tests', () => { const fixture = TestBed.createComponent(Cmp); expect(() => fixture.detectChanges()).toThrowError('this error is expected'); }); + + describe('tView.firstUpdatePass', () => { + function isFirstUpdatePass() { + const lView = getLView(); + const tView = lView[TVIEW]; + return tView.firstUpdatePass; + } + + function assertAttrValues(element: Element, value: string) { + expect(element.getAttribute('data-comp')).toEqual(value); + expect(element.getAttribute('data-dir')).toEqual(value); + } + + onlyInIvy('tView instances are ivy-specific') + .it('should be marked with `firstUpdatePass` up until the template and host bindings are evaluated', + () => { + @Directive({ + selector: '[dir]', + }) + class Dir { + @HostBinding('attr.data-dir') + get text() { + return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass'; + } + } + + @Component({ + template: '
', + }) + class Cmp { + get text() { + return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass'; + } + } + + TestBed.configureTestingModule({ + declarations: [Cmp, Dir], + }); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(false); + const element = fixture.nativeElement.querySelector('div') !; + + assertAttrValues(element, 'first-update-pass'); + + fixture.detectChanges(false); + + assertAttrValues(element, 'post-update-pass'); + }); + + onlyInIvy('tView instances are ivy-specific') + .it('tView.firstUpdatePass should be applied immediately after the first embedded view is processed', + () => { + @Directive({ + selector: '[dir]', + }) + class Dir { + @HostBinding('attr.data-dir') + get text() { + return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass'; + } + } + + @Component({ + template: ` +
+ ... +
+ ` + }) + class Cmp { + items = [1, 2, 3]; + get text() { + return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass'; + } + } + + TestBed.configureTestingModule({ + declarations: [Cmp, Dir], + }); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(false); + + const elements = fixture.nativeElement.querySelectorAll('div'); + assertAttrValues(elements[0], 'first-update-pass'); + assertAttrValues(elements[1], 'post-update-pass'); + assertAttrValues(elements[2], 'post-update-pass'); + + fixture.detectChanges(false); + assertAttrValues(elements[0], 'post-update-pass'); + assertAttrValues(elements[1], 'post-update-pass'); + assertAttrValues(elements[2], 'post-update-pass'); + }); + }); });