diff --git a/packages/common/test/BUILD.bazel b/packages/common/test/BUILD.bazel index f7c3831581..3a7c7cf38f 100644 --- a/packages/common/test/BUILD.bazel +++ b/packages/common/test/BUILD.bazel @@ -15,6 +15,7 @@ ts_library( "//packages/platform-browser", "//packages/platform-browser-dynamic", "//packages/platform-browser/testing", + "//packages/private/testing", ], ) diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index d3552c2cca..bc8ae923df 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -11,6 +11,7 @@ import {NgComponentOutlet} from '@angular/common/src/directives/ng_component_out import {Compiler, Component, ComponentRef, Inject, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, Optional, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {TestBed, async} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; +import {modifiedInIvy} from '@angular/private/testing'; describe('insert/remove', () => { @@ -106,17 +107,20 @@ describe('insert/remove', () => { })); - it('should resolve a with injector', async(() => { - let fixture = TestBed.createComponent(TestComponent); - fixture.componentInstance.cmpRef = null; - fixture.componentInstance.currentComponent = InjectedComponent; - fixture.detectChanges(); - let cmpRef: ComponentRef = fixture.componentInstance.cmpRef !; - expect(cmpRef).toBeAnInstanceOf(ComponentRef); - expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent); - expect(cmpRef.instance.testToken).toBeNull(); - })); + modifiedInIvy('Static ViewChild and ContentChild queries are resolved in update mode') + .it('should resolve with an injector', async(() => { + let fixture = TestBed.createComponent(TestComponent); + + // We are accessing a ViewChild (ngComponentOutlet) before change detection has run + fixture.componentInstance.cmpRef = null; + fixture.componentInstance.currentComponent = InjectedComponent; + fixture.detectChanges(); + let cmpRef: ComponentRef = fixture.componentInstance.cmpRef !; + expect(cmpRef).toBeAnInstanceOf(ComponentRef); + expect(cmpRef.instance).toBeAnInstanceOf(InjectedComponent); + expect(cmpRef.instance.testToken).toBeNull(); + })); it('should render projectable nodes, if supplied', async(() => { const template = `projected foo${TEST_CMP_TEMPLATE}`; diff --git a/packages/core/src/render3/bindings.ts b/packages/core/src/render3/bindings.ts index 84a70a6eb4..88f63a9bd2 100644 --- a/packages/core/src/render3/bindings.ts +++ b/packages/core/src/render3/bindings.ts @@ -11,7 +11,7 @@ import {devModeEqual} from '../change_detection/change_detection_util'; import {assertDataInRange, assertLessThan, assertNotEqual} from './assert'; import {throwErrorIfNoChangesMode} from './errors'; import {BINDING_INDEX, LView} from './interfaces/view'; -import {getCheckNoChangesMode, getCreationMode} from './state'; +import {getCheckNoChangesMode, isCreationMode} from './state'; import {NO_CHANGE} from './tokens'; import {isDifferent} from './util'; @@ -44,7 +44,7 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any): } else if (isDifferent(lView[bindingIndex], value)) { if (ngDevMode && getCheckNoChangesMode()) { if (!devModeEqual(lView[bindingIndex], value)) { - throwErrorIfNoChangesMode(getCreationMode(), lView[bindingIndex], value); + throwErrorIfNoChangesMode(isCreationMode(lView), lView[bindingIndex], value); } } lView[bindingIndex] = value; diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 1a50c67d7e..30daba0a46 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -22,7 +22,7 @@ import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition' import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {PlayerHandler} from './interfaces/player'; import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; -import {CONTEXT, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; +import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state'; import {defaultScheduler, getRootView, readPatchedLView, stringify} from './util'; @@ -133,7 +133,9 @@ export function renderComponent( component = createRootComponent( componentView, componentDef, rootView, rootContext, opts.hostFeatures || null); - refreshDescendantViews(rootView, null); + refreshDescendantViews(rootView); // creation mode pass + rootView[FLAGS] &= ~LViewFlags.CreationMode; + refreshDescendantViews(rootView); // update mode pass } finally { leaveView(oldView); if (rendererFactory.end) rendererFactory.end(); diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 91e125c411..fc293f5d2e 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -208,10 +208,9 @@ export class ComponentFactory extends viewEngine_ComponentFactory { componentView, this.componentDef, rootLView, rootContext, [LifecycleHooksFeature]); addToViewTree(rootLView, HEADER_OFFSET, componentView); - - refreshDescendantViews(rootLView, RenderFlags.Create); + refreshDescendantViews(rootLView); } finally { - leaveView(oldLView, true); + leaveView(oldLView); if (rendererFactory.end) rendererFactory.end(); } diff --git a/packages/core/src/render3/hooks.ts b/packages/core/src/render3/hooks.ts index 77a10e98c4..c00146b3e9 100644 --- a/packages/core/src/render3/hooks.ts +++ b/packages/core/src/render3/hooks.ts @@ -93,9 +93,10 @@ function queueDestroyHooks(def: DirectiveDef, tView: TView, i: number): voi * * @param currentView The current view */ -export function executeInitHooks(currentView: LView, tView: TView, creationMode: boolean): void { - if (currentView[FLAGS] & LViewFlags.RunInit) { - executeHooks(currentView, tView.initHooks, tView.checkHooks, creationMode); +export function executeInitHooks( + currentView: LView, tView: TView, checkNoChangesMode: boolean): void { + if (!checkNoChangesMode && currentView[FLAGS] & LViewFlags.RunInit) { + executeHooks(currentView, tView.initHooks, tView.checkHooks, checkNoChangesMode); currentView[FLAGS] &= ~LViewFlags.RunInit; } } @@ -106,17 +107,19 @@ export function executeInitHooks(currentView: LView, tView: TView, creationMode: * @param currentView The current view */ export function executeHooks( - data: LView, allHooks: HookData | null, checkHooks: HookData | null, - creationMode: boolean): void { - const hooksToCall = creationMode ? allHooks : checkHooks; + currentView: LView, allHooks: HookData | null, checkHooks: HookData | null, + checkNoChangesMode: boolean): void { + if (checkNoChangesMode) return; + + const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? allHooks : checkHooks; if (hooksToCall) { - callHooks(data, hooksToCall); + callHooks(currentView, hooksToCall); } } /** * Calls lifecycle hooks with their contexts, skipping init hooks if it's not - * creation mode. + * the first LView pass. * * @param currentView The current view * @param arr The array in which the hooks are found diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 80d11dcf31..2f18c17bc4 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -35,7 +35,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLA import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, findComponentView, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCreationMode, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; +import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; import {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; import {getStylingContext, isAnimationProp} from './styling/util'; @@ -60,36 +60,30 @@ const enum BindingDirection { * bindings, refreshes child components. * Note: view hooks are triggered later when leaving the view. */ -export function refreshDescendantViews(lView: LView, rf: RenderFlags | null) { +export function refreshDescendantViews(lView: LView) { const tView = lView[TVIEW]; // This needs to be set before children are processed to support recursive components tView.firstTemplatePass = false; setFirstTemplatePass(false); - // Dynamically created views must run first only in creation mode. If this is a - // creation-only pass, we should not call lifecycle hooks or evaluate bindings. - // This will be done in the update-only pass. - if (rf !== RenderFlags.Create) { - const creationMode = getCreationMode(); + // If this is a creation pass, we should not call lifecycle hooks or evaluate bindings. + // This will be done in the update pass. + if (!isCreationMode(lView)) { const checkNoChangesMode = getCheckNoChangesMode(); - if (!checkNoChangesMode) { - executeInitHooks(lView, tView, creationMode); - } + executeInitHooks(lView, tView, checkNoChangesMode); refreshDynamicEmbeddedViews(lView); // Content query results must be refreshed before content hooks are called. refreshContentQueries(tView); - if (!checkNoChangesMode) { - executeHooks(lView, tView.contentHooks, tView.contentCheckHooks, creationMode); - } + executeHooks(lView, tView.contentHooks, tView.contentCheckHooks, checkNoChangesMode); setHostBindings(tView, lView); } - refreshChildComponents(tView.components, rf); + refreshChildComponents(tView.components); } @@ -147,10 +141,10 @@ function refreshContentQueries(tView: TView): void { } /** Refreshes child components in the current view. */ -function refreshChildComponents(components: number[] | null, rf: RenderFlags | null): void { +function refreshChildComponents(components: number[] | null): void { if (components != null) { for (let i = 0; i < components.length; i++) { - componentRefresh(components[i], rf); + componentRefresh(components[i]); } } } @@ -160,7 +154,8 @@ export function createLView( rendererFactory?: RendererFactory3 | null, renderer?: Renderer3 | null, sanitizer?: Sanitizer | null, injector?: Injector | null): LView { const lView = tView.blueprint.slice() as LView; - lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit; + lView[FLAGS] = flags | LViewFlags.CreationMode | LViewFlags.Attached | LViewFlags.RunInit | + LViewFlags.FirstLViewPass; lView[PARENT] = lView[DECLARATION_VIEW] = parentLView; lView[CONTEXT] = context; lView[RENDERER_FACTORY] = (rendererFactory || parentLView && parentLView[RENDERER_FACTORY]) !; @@ -303,8 +298,7 @@ export function renderTemplate( renderer, sanitizer); hostView[HOST_NODE] = createNodeAtIndex(0, TNodeType.Element, hostNode, null, null); } - renderComponentOrTemplate(hostView, context, null, templateFn); - + renderComponentOrTemplate(hostView, context, templateFn); return hostView; } @@ -348,8 +342,7 @@ export function createEmbeddedViewAndNode( * can't store TViews in the template function itself (as we do for comps). Instead, we store the * TView for dynamically created views on their host TNode, which only has one instance. */ -export function renderEmbeddedTemplate( - viewToRender: LView, tView: TView, context: T, rf: RenderFlags) { +export function renderEmbeddedTemplate(viewToRender: LView, tView: TView, context: T) { const _isParent = getIsParent(); const _previousOrParentTNode = getPreviousOrParentTNode(); setIsParent(true); @@ -365,22 +358,17 @@ export function renderEmbeddedTemplate( oldView = enterView(viewToRender, viewToRender[HOST_NODE]); namespaceHTML(); - tView.template !(rf, context); - if (rf & RenderFlags.Update) { - refreshDescendantViews(viewToRender, null); - } else { - // This must be set to false immediately after the first creation run because in an - // ngFor loop, all the views will be created together before update mode runs and turns - // off firstTemplatePass. If we don't set it here, instances will perform directive - // matching, etc again and again. - viewToRender[TVIEW].firstTemplatePass = false; - setFirstTemplatePass(false); - } + tView.template !(getRenderFlags(viewToRender), context); + // This must be set to false immediately after the first creation run because in an + // ngFor loop, all the views will be created together before update mode runs and turns + // off firstTemplatePass. If we don't set it here, instances will perform directive + // matching, etc again and again. + viewToRender[TVIEW].firstTemplatePass = false; + setFirstTemplatePass(false); + + refreshDescendantViews(viewToRender); } finally { - // renderEmbeddedTemplate() is called twice, once for creation only and then once for - // update. When for creation only, leaveView() must not trigger view hooks, nor clean flags. - const isCreationOnly = (rf & RenderFlags.Create) === RenderFlags.Create; - leaveView(oldView !, isCreationOnly); + leaveView(oldView !); setIsParent(_isParent); setPreviousOrParentTNode(_previousOrParentTNode); } @@ -402,19 +390,28 @@ export function nextContext(level: number = 1): T { } function renderComponentOrTemplate( - hostView: LView, componentOrContext: T, rf: RenderFlags | null, - templateFn?: ComponentTemplate) { + hostView: LView, context: T, templateFn?: ComponentTemplate) { const rendererFactory = hostView[RENDERER_FACTORY]; const oldView = enterView(hostView, hostView[HOST_NODE]); try { if (rendererFactory.begin) { rendererFactory.begin(); } - if (templateFn) { - namespaceHTML(); - templateFn(rf || getRenderFlags(hostView), componentOrContext !); + + if (isCreationMode(hostView)) { + // creation mode pass + if (templateFn) { + namespaceHTML(); + templateFn(RenderFlags.Create, context !); + } + + refreshDescendantViews(hostView); + hostView[FLAGS] &= ~LViewFlags.CreationMode; } - refreshDescendantViews(hostView, rf); + + // update mode pass + templateFn && templateFn(RenderFlags.Update, context !); + refreshDescendantViews(hostView); } finally { if (rendererFactory.end) { rendererFactory.end(); @@ -425,16 +422,11 @@ function renderComponentOrTemplate( /** * This function returns the default configuration of rendering flags depending on when the - * template is in creation mode or update mode. By default, the update block is run with the - * creation block when the view is in creation mode. Otherwise, the update block is run - * alone. - * - * Dynamically created views do NOT use this configuration (update block and create block are - * always run separately). + * template is in creation mode or update mode. Update block and create block are + * always run separately. */ function getRenderFlags(view: LView): RenderFlags { - return view[FLAGS] & LViewFlags.CreationMode ? RenderFlags.Create | RenderFlags.Update : - RenderFlags.Update; + return isCreationMode(view) ? RenderFlags.Create : RenderFlags.Update; } ////////////////////////// @@ -1160,7 +1152,7 @@ export function elementStyling( styleDeclarations?: (string | boolean | InitialStylingFlags)[] | null, styleSanitizer?: StyleSanitizeFn | null, directive?: {}): void { if (directive != undefined) { - getCreationMode() && + isCreationMode() && hackImplementationOfElementStyling( classDeclarations || null, styleDeclarations || null, styleSanitizer || null, directive); // supported in next PR @@ -1214,7 +1206,7 @@ export function elementStylingApply(index: number, directive?: {}): void { return hackImplementationOfElementStylingApply(index, directive); // supported in next PR } const lView = getLView(); - const isFirstRender = (lView[FLAGS] & LViewFlags.CreationMode) !== 0; + const isFirstRender = (lView[FLAGS] & LViewFlags.FirstLViewPass) !== 0; const totalPlayersQueued = renderStyleAndClassBindings( getStylingContext(index + HEADER_OFFSET, lView), lView[RENDERER], lView, isFirstRender); if (totalPlayersQueued > 0) { @@ -1992,11 +1984,9 @@ export function containerRefreshStart(index: number): void { lView[index + HEADER_OFFSET][ACTIVE_INDEX] = 0; - if (!getCheckNoChangesMode()) { - // We need to execute init hooks here so ngOnInit hooks are called in top level views - // before they are called in embedded views (for backwards compatibility). - executeInitHooks(lView, tView, getCreationMode()); - } + // We need to execute init hooks here so ngOnInit hooks are called in top level views + // before they are called in embedded views (for backwards compatibility). + executeInitHooks(lView, tView, getCheckNoChangesMode()); } /** @@ -2041,9 +2031,7 @@ function refreshDynamicEmbeddedViews(lView: LView) { const dynamicViewData = container[VIEWS][i]; // The directives and pipes are not needed here as an existing view is only being refreshed. ngDevMode && assertDefined(dynamicViewData[TVIEW], 'TView must be allocated'); - renderEmbeddedTemplate( - dynamicViewData, dynamicViewData[TVIEW], dynamicViewData[CONTEXT] !, - RenderFlags.Update); + renderEmbeddedTemplate(dynamicViewData, dynamicViewData[TVIEW], dynamicViewData[CONTEXT] !); } } } @@ -2118,13 +2106,14 @@ export function embeddedViewStart(viewBlockId: number, consts: number, vars: num enterView(viewToRender, viewToRender[TVIEW].node); } if (lContainer) { - if (getCreationMode()) { + if (isCreationMode(viewToRender)) { // it is a new view, insert it into collection of views for a given container insertView(viewToRender, lContainer, lView, lContainer[ACTIVE_INDEX] !, -1); } lContainer[ACTIVE_INDEX] !++; } - return getRenderFlags(viewToRender); + return isCreationMode(viewToRender) ? RenderFlags.Create | RenderFlags.Update : + RenderFlags.Update; } /** @@ -2158,7 +2147,12 @@ function getOrCreateEmbeddedTView( export function embeddedViewEnd(): void { const lView = getLView(); const viewHost = lView[HOST_NODE]; - refreshDescendantViews(lView, null); + + if (isCreationMode(lView)) { + refreshDescendantViews(lView); // creation mode pass + lView[FLAGS] &= ~LViewFlags.CreationMode; + } + refreshDescendantViews(lView); // update mode pass leaveView(lView[PARENT] !); setPreviousOrParentTNode(viewHost !); setIsParent(false); @@ -2170,9 +2164,8 @@ export function embeddedViewEnd(): void { * Refreshes components by entering the component view and processing its bindings, queries, etc. * * @param adjustedElementIndex Element index in LView[] (adjusted for HEADER_OFFSET) - * @param rf The render flags that should be used to process this template */ -export function componentRefresh(adjustedElementIndex: number, rf: RenderFlags | null): void { +export function componentRefresh(adjustedElementIndex: number): void { const lView = getLView(); ngDevMode && assertDataInRange(lView, adjustedElementIndex); const hostView = getComponentViewByIndex(adjustedElementIndex, lView); @@ -2181,7 +2174,7 @@ export function componentRefresh(adjustedElementIndex: number, rf: RenderFlag // Only attached CheckAlways components or attached, dirty OnPush components should be checked if (viewAttached(hostView) && hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) { syncViewWithBlueprint(hostView); - detectChangesInternal(hostView, hostView[CONTEXT], rf); + checkView(hostView, hostView[CONTEXT]); } } @@ -2461,7 +2454,7 @@ export function tick(component: T): void { function tickRootContext(rootContext: RootContext) { for (let i = 0; i < rootContext.components.length; i++) { const rootComponent = rootContext.components[i]; - renderComponentOrTemplate(readPatchedLView(rootComponent) !, rootComponent, RenderFlags.Update); + renderComponentOrTemplate(readPatchedLView(rootComponent) !, rootComponent); } } @@ -2479,7 +2472,21 @@ function tickRootContext(rootContext: RootContext) { * @param component The component which the change detection should be performed on. */ export function detectChanges(component: T): void { - detectChangesInternal(getComponentViewByInstance(component) !, component, null); + const view = getComponentViewByInstance(component) !; + detectChangesInternal(view, component); +} + +export function detectChangesInternal(view: LView, context: T) { + const rendererFactory = view[RENDERER_FACTORY]; + + if (rendererFactory.begin) rendererFactory.begin(); + + if (isCreationMode(view)) { + checkView(view, context); // creation mode pass + } + checkView(view, context); // update mode pass + + if (rendererFactory.end) rendererFactory.end(); } /** @@ -2526,7 +2533,7 @@ export function checkNoChangesInRootView(lView: LView): void { } /** Checks the view of the component provided. Does not gate on dirty checks or execute doCheck. */ -export function detectChangesInternal(hostView: LView, component: T, rf: RenderFlags | null) { +export function checkView(hostView: LView, component: T) { const hostTView = hostView[TVIEW]; const oldView = enterView(hostView, hostView[HOST_NODE]); const templateFn = hostTView.template !; @@ -2534,27 +2541,23 @@ export function detectChangesInternal(hostView: LView, component: T, rf: Rend try { namespaceHTML(); - createViewQuery(viewQuery, rf, hostView[FLAGS], component); - templateFn(rf || getRenderFlags(hostView), component); - refreshDescendantViews(hostView, rf); - updateViewQuery(viewQuery, hostView[FLAGS], component); + createViewQuery(viewQuery, hostView, component); + templateFn(getRenderFlags(hostView), component); + refreshDescendantViews(hostView); + updateViewQuery(viewQuery, hostView, component); } finally { - leaveView(oldView, rf === RenderFlags.Create); + leaveView(oldView); } } -function createViewQuery( - viewQuery: ComponentQuery<{}>| null, renderFlags: RenderFlags | null, viewFlags: LViewFlags, - component: T): void { - if (viewQuery && (renderFlags === RenderFlags.Create || - (renderFlags === null && (viewFlags & LViewFlags.CreationMode)))) { +function createViewQuery(viewQuery: ComponentQuery<{}>| null, view: LView, component: T): void { + if (viewQuery && isCreationMode(view)) { viewQuery(RenderFlags.Create, component); } } -function updateViewQuery( - viewQuery: ComponentQuery<{}>| null, flags: LViewFlags, component: T): void { - if (viewQuery && flags & RenderFlags.Update) { +function updateViewQuery(viewQuery: ComponentQuery<{}>| null, view: LView, component: T): void { + if (viewQuery && !isCreationMode(view)) { viewQuery(RenderFlags.Update, component); } } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 1b28ee54df..996415a284 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -221,16 +221,25 @@ export const enum LViewFlags { * back into the parent view, `data` will be defined and `creationMode` will be * improperly reported as false. */ - CreationMode = 0b0000001, + CreationMode = 0b000000001, + + /** + * Whether or not this LView instance is on its first processing pass. + * + * An LView instance is considered to be on its "first pass" until it + * has completed one creation mode run and one update mode run. At this + * time, the flag is turned off. + */ + FirstLViewPass = 0b000000010, /** Whether this view has default change detection strategy (checks always) or onPush */ - CheckAlways = 0b0000010, + CheckAlways = 0b000000100, /** Whether or not this view is currently dirty (needing check) */ - Dirty = 0b0000100, + Dirty = 0b000001000, /** Whether or not this view is currently attached to change detection tree. */ - Attached = 0b0001000, + Attached = 0b000010000, /** * Whether or not the init hooks have run. @@ -239,13 +248,13 @@ export const enum LViewFlags { * runs OR the first cR() instruction that runs (so inits are run for the top level view before * any embedded views). */ - RunInit = 0b0010000, + RunInit = 0b000100000, /** Whether or not this view is destroyed. */ - Destroyed = 0b0100000, + Destroyed = 0b001000000, /** Whether or not this view is the root view */ - IsRoot = 0b1000000, + IsRoot = 0b010000000, } /** diff --git a/packages/core/src/render3/pure_function.ts b/packages/core/src/render3/pure_function.ts index 562b6b0d1e..6f78bf7ae7 100644 --- a/packages/core/src/render3/pure_function.ts +++ b/packages/core/src/render3/pure_function.ts @@ -7,7 +7,7 @@ */ import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, getBinding, updateBinding} from './bindings'; -import {getBindingRoot, getCreationMode, getLView} from './state'; +import {getBindingRoot, getLView, isCreationMode} from './state'; @@ -42,7 +42,7 @@ export function pureFunction0(slotOffset: number, pureFn: () => T, thisArg?: // TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings const bindingIndex = getBindingRoot() + slotOffset; const lView = getLView(); - return getCreationMode() ? + return isCreationMode() ? updateBinding(lView, bindingIndex, thisArg ? pureFn.call(thisArg) : pureFn()) : getBinding(lView, bindingIndex); } diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 76b5ab90e3..69185a2340 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -186,14 +186,9 @@ export function getOrCreateCurrentQueries( return currentQueries || (lView[QUERIES] = new QueryType(null, null, null)); } -/** - * This property gets set before entering a template. - */ -let creationMode: boolean; - -export function getCreationMode(): boolean { - // top level variables should not be exported for performance reasons (PERF_NOTES.md) - return creationMode; +/** Checks whether a given view is in creation mode */ +export function isCreationMode(view: LView = lView): boolean { + return (view[FLAGS] & LViewFlags.CreationMode) === LViewFlags.CreationMode; } /** @@ -276,8 +271,6 @@ export function enterView(newView: LView, hostTNode: TElementNode | TViewNode | const oldView = lView; if (newView) { const tView = newView[TVIEW]; - - creationMode = (newView[FLAGS] & LViewFlags.CreationMode) === LViewFlags.CreationMode; firstTemplatePass = tView.firstTemplatePass; bindingRootIndex = tView.bindingStartIndex; } @@ -320,19 +313,17 @@ export function resetComponentState() { * the direction of traversal (up or down the view tree) a bit clearer. * * @param newView New state to become active - * @param creationOnly An optional boolean to indicate that the view was processed in creation mode - * only, i.e. the first update will be done later. Only possible for dynamically created views. */ -export function leaveView(newView: LView, creationOnly?: boolean): void { +export function leaveView(newView: LView): void { const tView = lView[TVIEW]; - if (!creationOnly) { - if (!checkNoChangesMode) { - executeHooks(lView, tView.viewHooks, tView.viewCheckHooks, creationMode); - } + if (isCreationMode(lView)) { + lView[FLAGS] &= ~LViewFlags.CreationMode; + } else { + executeHooks(lView, tView.viewHooks, tView.viewCheckHooks, checkNoChangesMode); // Views are clean and in update mode after being checked, so these bits are cleared - lView[FLAGS] &= ~(LViewFlags.CreationMode | LViewFlags.Dirty); + lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); + lView[FLAGS] |= LViewFlags.RunInit; + lView[BINDING_INDEX] = tView.bindingStartIndex; } - lView[FLAGS] |= LViewFlags.RunInit; - lView[BINDING_INDEX] = tView.bindingStartIndex; enterView(newView, null); } diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index 437abd47f3..d38319b354 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -112,7 +112,7 @@ export function createTemplateRef( if (container) { insertView(lView, container, hostView !, index !, hostTNode !.index); } - renderEmbeddedTemplate(lView, this._tView, context, RenderFlags.Create); + renderEmbeddedTemplate(lView, this._tView, context); const viewRef = new ViewRef(lView, context, -1); viewRef._tViewNode = lView[HOST_NODE] as TViewNode; return viewRef; diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index b42a754297..b88ec8cb94 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; -import {checkNoChanges, checkNoChangesInRootView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; +import {checkNoChanges, checkNoChangesInRootView, checkView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; import {TNode, TNodeType, TViewNode} from './interfaces/node'; import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT, RENDERER_FACTORY} from './interfaces/view'; import {destroyLView} from './node_manipulation'; @@ -244,16 +244,7 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int * * See {@link ChangeDetectorRef#detach detach} for more information. */ - detectChanges(): void { - const rendererFactory = this._lView[RENDERER_FACTORY]; - if (rendererFactory.begin) { - rendererFactory.begin(); - } - detectChangesInternal(this._lView, this.context, null); - if (rendererFactory.end) { - rendererFactory.end(); - } - } + detectChanges(): void { detectChangesInternal(this._lView, this.context); } /** * Checks the change detector and its children, and throws if any changes are detected. diff --git a/packages/core/test/animation/animation_integration_spec.ts b/packages/core/test/animation/animation_integration_spec.ts index fd1a961a46..7c0734fb82 100644 --- a/packages/core/test/animation/animation_integration_spec.ts +++ b/packages/core/test/animation/animation_integration_spec.ts @@ -13,7 +13,7 @@ import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {ɵDomRendererFactory2} from '@angular/platform-browser'; import {ANIMATION_MODULE_TYPE, BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {fixmeIvy} from '@angular/private/testing'; +import {fixmeIvy, ivyEnabled} from '@angular/private/testing'; const DEFAULT_NAMESPACE_ID = 'id'; const DEFAULT_COMPONENT_ID = '1'; @@ -3110,6 +3110,10 @@ const DEFAULT_COMPONENT_ID = '1'; expect(element.style['height']).toEqual(height); } + // In Ivy, change detection needs to run before the ViewQuery for cmp.element will + // resolve. Keeping this test enabled since we still want to test the animation logic. + if (ivyEnabled) fixture.detectChanges(); + const cmp = fixture.componentInstance; const element = cmp.element.nativeElement; fixture.detectChanges(); diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index a350739d13..f30cf1d597 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -14,7 +14,7 @@ import {CommonModule} from '@angular/common'; import {Component, HostBinding, ViewChild} from '@angular/core'; import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {fixmeIvy} from '@angular/private/testing'; +import {fixmeIvy, ivyEnabled} from '@angular/private/testing'; import {HostListener} from '../../src/metadata/directives'; @@ -1706,6 +1706,11 @@ import {HostListener} from '../../src/metadata/directives'; TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); const fixture = TestBed.createComponent(ParentCmp); const cmp = fixture.componentInstance; + + // In Ivy, change detection needs to run before the ViewQuery for cmp.child will resolve. + // Keeping this test enabled since we still want to test the animation logic in Ivy. + if (ivyEnabled) fixture.detectChanges(); + cmp.child.items = [4, 5, 6]; fixture.detectChanges(); diff --git a/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts b/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts index 22c1c498c5..b502ae5af2 100644 --- a/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts +++ b/packages/core/test/animation/animations_with_css_keyframes_animations_integration_spec.ts @@ -11,6 +11,7 @@ import {AnimationGroupPlayer} from '@angular/animations/src/players/animation_gr import {Component, ViewChild} from '@angular/core'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {browserDetection} from '@angular/platform-browser/testing/src/browser_util'; +import {ivyEnabled} from '@angular/private/testing'; import {TestBed} from '../../testing'; @@ -290,6 +291,10 @@ import {TestBed} from '../../testing'; const fixture = TestBed.createComponent(Cmp); const cmp = fixture.componentInstance; + // In Ivy, change detection needs to run before the ViewQuery for cmp.element will resolve. + // Keeping this test enabled since we still want to test the animation logic in Ivy. + if (ivyEnabled) fixture.detectChanges(); + const elm = cmp.element.nativeElement; const foo = elm.querySelector('.foo') as HTMLElement; diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index 0b194e7fea..5a5f9e38c8 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -15,7 +15,7 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {fixmeIvy} from '@angular/private/testing'; +import {fixmeIvy, ivyEnabled, modifiedInIvy} from '@angular/private/testing'; import {NoopNgZone} from '../src/zone/ng_zone'; import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing'; @@ -421,6 +421,11 @@ class SomeComponent { it('should detach attached embedded views if they are destroyed', () => { const comp = TestBed.createComponent(EmbeddedViewComp); const appRef: ApplicationRef = TestBed.get(ApplicationRef); + + // In Ivy, change detection needs to run before the ViewQuery for tplRef will resolve. + // Keeping this test enabled since we still want to test this destroy logic in Ivy. + if (ivyEnabled) comp.detectChanges(); + const embeddedViewRef = comp.componentInstance.tplRef.createEmbeddedView({}); appRef.attachView(embeddedViewRef); diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 2cb59a4e45..c63e3016e5 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -170,6 +170,9 @@ { "name": "checkNoChangesMode" }, + { + "name": "checkView" + }, { "name": "componentRefresh" }, @@ -209,9 +212,6 @@ { "name": "defineComponent" }, - { - "name": "detectChangesInternal" - }, { "name": "diPublicInInjector" }, @@ -254,9 +254,6 @@ { "name": "getContainerRenderParent" }, - { - "name": "getCreationMode" - }, { "name": "getDirectiveDef" }, @@ -353,6 +350,9 @@ { "name": "isComponentDef" }, + { + "name": "isCreationMode" + }, { "name": "isFactory" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index edefdd0c71..4a0a88740c 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -437,6 +437,9 @@ { "name": "checkNoChangesMode" }, + { + "name": "checkView" + }, { "name": "cleanUpView" }, @@ -659,9 +662,6 @@ { "name": "getContextLView" }, - { - "name": "getCreationMode" - }, { "name": "getCurrentView" }, @@ -896,6 +896,9 @@ { "name": "isContextDirty" }, + { + "name": "isCreationMode" + }, { "name": "isCssClassMatching" }, diff --git a/packages/core/test/linker/query_integration_spec.ts b/packages/core/test/linker/query_integration_spec.ts index 6e16295415..41aa635ccb 100644 --- a/packages/core/test/linker/query_integration_spec.ts +++ b/packages/core/test/linker/query_integration_spec.ts @@ -112,23 +112,21 @@ describe('Query API', () => { expect(directive.child.text).toEqual('foo'); }); - fixmeIvy('FW-782 - View queries are executed twice in some cases') - .it('should contain the first view child', () => { - const template = ''; - const view = createTestCmpAndDetectChanges(MyComp0, template); + it('should contain the first view child', () => { + const template = ''; + const view = createTestCmpAndDetectChanges(MyComp0, template); - const q: NeedsViewChild = view.debugElement.children[0].references !['q']; - expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]); + const q: NeedsViewChild = view.debugElement.children[0].references !['q']; + expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]); - q.shouldShow = false; - view.detectChanges(); - expect(q.logs).toEqual([ - ['setter', 'foo'], ['init', 'foo'], ['check', 'foo'], ['setter', null], - ['check', null] - ]); - }); + q.shouldShow = false; + view.detectChanges(); + expect(q.logs).toEqual([ + ['setter', 'foo'], ['init', 'foo'], ['check', 'foo'], ['setter', null], ['check', null] + ]); + }); - fixmeIvy('FW-782 - View queries are executed twice in some cases') + modifiedInIvy('Static ViewChild and ContentChild queries are resolved in update mode') .it('should set static view and content children already after the constructor call', () => { const template = '
'; @@ -142,34 +140,33 @@ describe('Query API', () => { expect(q.viewChild.text).toEqual('viewFoo'); }); - fixmeIvy('FW-782 - View queries are executed twice in some cases') - .it('should contain the first view child across embedded views', () => { - TestBed.overrideComponent( - MyComp0, {set: {template: ''}}); - TestBed.overrideComponent(NeedsViewChild, { - set: { - template: - '
' - } - }); - const view = TestBed.createComponent(MyComp0); + it('should contain the first view child across embedded views', () => { + TestBed.overrideComponent( + MyComp0, {set: {template: ''}}); + TestBed.overrideComponent(NeedsViewChild, { + set: { + template: + '
' + } + }); + const view = TestBed.createComponent(MyComp0); - view.detectChanges(); - const q: NeedsViewChild = view.debugElement.children[0].references !['q']; - expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]); + view.detectChanges(); + const q: NeedsViewChild = view.debugElement.children[0].references !['q']; + expect(q.logs).toEqual([['setter', 'foo'], ['init', 'foo'], ['check', 'foo']]); - q.shouldShow = false; - q.shouldShow2 = true; - q.logs = []; - view.detectChanges(); - expect(q.logs).toEqual([['setter', 'bar'], ['check', 'bar']]); + q.shouldShow = false; + q.shouldShow2 = true; + q.logs = []; + view.detectChanges(); + expect(q.logs).toEqual([['setter', 'bar'], ['check', 'bar']]); - q.shouldShow = false; - q.shouldShow2 = false; - q.logs = []; - view.detectChanges(); - expect(q.logs).toEqual([['setter', null], ['check', null]]); - }); + q.shouldShow = false; + q.shouldShow2 = false; + q.logs = []; + view.detectChanges(); + expect(q.logs).toEqual([['setter', null], ['check', null]]); + }); fixmeIvy( 'FW-781 - Directives invocation sequence on root and nested elements is different in Ivy') @@ -589,31 +586,35 @@ describe('Query API', () => { }); // Note: this test is just document our current behavior, which we do for performance reasons. - fixmeIvy('FW-782 - View queries are executed twice in some cases') - .it('should not affected queries for projected templates if views are detached or moved', () => { - const template = - '
'; - const view = createTestCmpAndDetectChanges(MyComp0, template); - const q = view.debugElement.children[0].references !['q'] as ManualProjecting; - expect(q.query.length).toBe(0); + fixmeIvy('FW-853: Query results are cleared if embedded views are detached / moved') + .it('should not affect queries for projected templates if views are detached or moved', + () => { + const template = ` + +
+
+
`; + const view = createTestCmpAndDetectChanges(MyComp0, template); + const q = view.debugElement.children[0].references !['q'] as ManualProjecting; + expect(q.query.length).toBe(0); - const view1 = q.vc.createEmbeddedView(q.template, {'x': '1'}); - const view2 = q.vc.createEmbeddedView(q.template, {'x': '2'}); - view.detectChanges(); - expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); + const view1 = q.vc.createEmbeddedView(q.template, {'x': '1'}); + const view2 = q.vc.createEmbeddedView(q.template, {'x': '2'}); + view.detectChanges(); + expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); - q.vc.detach(1); - q.vc.detach(0); + q.vc.detach(1); + q.vc.detach(0); - view.detectChanges(); - expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); + view.detectChanges(); + expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); - q.vc.insert(view2); - q.vc.insert(view1); + q.vc.insert(view2); + q.vc.insert(view1); - view.detectChanges(); - expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); - }); + view.detectChanges(); + expect(q.query.map((d: TextDirective) => d.text)).toEqual(['1', '2']); + }); fixmeIvy('unknown').it( 'should remove manually projected templates if their parent view is destroyed', () => { diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index 0d924733b3..c5fb5dd039 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -87,73 +87,6 @@ describe('change detection', () => { await whenRendered(myComp); expect(getRenderedText(myComp)).toEqual('updated'); })); - - it('should support detectChanges on components that have LContainers', () => { - let structuralComp !: StructuralComp; - - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - text(0); - } - if (rf & RenderFlags.Update) { - textBinding(0, bind(ctx.value)); - } - } - - class StructuralComp { - tmp !: TemplateRef; - value = 'one'; - - constructor(public vcr: ViewContainerRef) {} - - create() { return this.vcr.createEmbeddedView(this.tmp, this); } - - static ngComponentDef = defineComponent({ - type: StructuralComp, - selectors: [['structural-comp']], - factory: () => structuralComp = - new StructuralComp(directiveInject(ViewContainerRef as any)), - inputs: {tmp: 'tmp'}, - consts: 1, - vars: 1, - template: FooTemplate - }); - } - - /** - * {{ value }} - * - */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - template(0, FooTemplate, 2, 1, 'ng-template', null, ['foo', ''], templateRefExtractor); - element(2, 'structural-comp'); - } - if (rf & RenderFlags.Update) { - const foo = reference(1) as any; - elementProperty(2, 'tmp', bind(foo)); - } - }, 3, 1, [StructuralComp]); - - const fixture = new ComponentFixture(App); - fixture.update(); - expect(fixture.html).toEqual('one'); - - const viewRef: EmbeddedViewRef = structuralComp.create(); - fixture.update(); - expect(fixture.html).toEqual('oneone'); - - // check embedded view update - structuralComp.value = 'two'; - viewRef.detectChanges(); - expect(fixture.html).toEqual('onetwo'); - - // check root view update - structuralComp.value = 'three'; - fixture.update(); - expect(fixture.html).toEqual('threethree'); - }); - }); describe('onPush', () => { @@ -662,6 +595,117 @@ describe('change detection', () => { expect(getRenderedText(comp)).toEqual('1'); }); + describe('dynamic views', () => { + let structuralComp: StructuralComp|null = null; + + beforeEach(() => structuralComp = null); + + class StructuralComp { + tmp !: TemplateRef; + value = 'one'; + + constructor(public vcr: ViewContainerRef) {} + + create() { return this.vcr.createEmbeddedView(this.tmp, this); } + + static ngComponentDef = defineComponent({ + type: StructuralComp, + selectors: [['structural-comp']], + factory: () => structuralComp = + new StructuralComp(directiveInject(ViewContainerRef as any)), + inputs: {tmp: 'tmp'}, + consts: 1, + vars: 1, + template: function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + text(0); + } + if (rf & RenderFlags.Update) { + textBinding(0, bind(ctx.value)); + } + } + }); + } + + it('should support ViewRef.detectChanges()', () => { + function FooTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + text(0); + } + if (rf & RenderFlags.Update) { + textBinding(0, bind(ctx.value)); + } + } + + /** + * {{ value }} + * + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template( + 0, FooTemplate, 1, 1, 'ng-template', null, ['foo', ''], templateRefExtractor); + element(2, 'structural-comp'); + } + if (rf & RenderFlags.Update) { + const foo = reference(1) as any; + elementProperty(2, 'tmp', bind(foo)); + } + }, 3, 1, [StructuralComp]); + + const fixture = new ComponentFixture(App); + fixture.update(); + expect(fixture.html).toEqual('one'); + + const viewRef: EmbeddedViewRef = structuralComp !.create(); + fixture.update(); + expect(fixture.html).toEqual('oneone'); + + // check embedded view update + structuralComp !.value = 'two'; + viewRef.detectChanges(); + expect(fixture.html).toEqual('onetwo'); + + // check root view update + structuralComp !.value = 'three'; + fixture.update(); + expect(fixture.html).toEqual('threethree'); + }); + + it('should support ViewRef.detectChanges() directly after creation', () => { + function FooTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + text(0, 'Template text'); + } + } + + /** + * Template text + * + */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + template( + 0, FooTemplate, 1, 0, 'ng-template', null, ['foo', ''], templateRefExtractor); + element(2, 'structural-comp'); + } + if (rf & RenderFlags.Update) { + const foo = reference(1) as any; + elementProperty(2, 'tmp', bind(foo)); + } + }, 3, 1, [StructuralComp]); + + const fixture = new ComponentFixture(App); + fixture.update(); + expect(fixture.html).toEqual('one'); + + const viewRef: EmbeddedViewRef = structuralComp !.create(); + viewRef.detectChanges(); + expect(fixture.html).toEqual('oneTemplate text'); + }); + + }); + }); describe('attach/detach', () => { diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts index 817e2594ef..743590a276 100644 --- a/packages/core/test/render3/component_spec.ts +++ b/packages/core/test/render3/component_spec.ts @@ -498,6 +498,8 @@ describe('recursive components', () => { class NgIfTree { data: TreeNode = _buildTree(0); + ngDoCheck() { events.push('check' + this.data.value); } + ngOnDestroy() { events.push('destroy' + this.data.value); } static ngComponentDef = defineComponent({ @@ -628,6 +630,7 @@ describe('recursive components', () => { const fixture = new ComponentFixture(App); expect(getRenderedText(fixture.component)).toEqual('6201534'); + expect(events).toEqual(['check6', 'check2', 'check0', 'check1', 'check5', 'check3', 'check4']); events = []; fixture.component.skipContent = true; diff --git a/packages/core/test/render3/control_flow_spec.ts b/packages/core/test/render3/control_flow_spec.ts index e333402a20..e7129336da 100644 --- a/packages/core/test/render3/control_flow_spec.ts +++ b/packages/core/test/render3/control_flow_spec.ts @@ -38,8 +38,8 @@ describe('JS control flow', () => { embeddedViewEnd(); } } + containerRefreshEnd(); } - containerRefreshEnd(); }, 2); const fixture = new ComponentFixture(App); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 89468d775f..3c981a96c4 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -115,9 +115,7 @@ describe('render3 integration test', () => { function Template(rf: RenderFlags, value: string) { if (rf & RenderFlags.Create) { text(0); - } - if (rf & RenderFlags.Update) { - textBinding(0, rf & RenderFlags.Create ? value : NO_CHANGE); + textBinding(0, value); } } expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once'); diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts index 40851f0046..8a13ec371a 100644 --- a/packages/core/test/render3/properties_spec.ts +++ b/packages/core/test/render3/properties_spec.ts @@ -49,9 +49,7 @@ describe('elementProperty', () => { function Template(rf: RenderFlags, ctx: string) { if (rf & RenderFlags.Create) { element(0, 'span'); - } - if (rf & RenderFlags.Update) { - elementProperty(0, 'id', rf & RenderFlags.Create ? expensive(ctx) : NO_CHANGE); + elementProperty(0, 'id', expensive(ctx)); } } diff --git a/packages/core/test/render3/renderer_factory_spec.ts b/packages/core/test/render3/renderer_factory_spec.ts index 3e1459c1a5..3fdd05170e 100644 --- a/packages/core/test/render3/renderer_factory_spec.ts +++ b/packages/core/test/render3/renderer_factory_spec.ts @@ -37,10 +37,13 @@ describe('renderer factory lifecycle', () => { consts: 1, vars: 0, template: function(rf: RenderFlags, ctx: SomeComponent) { - logs.push('component'); if (rf & RenderFlags.Create) { + logs.push('component create'); text(0, 'foo'); } + if (rf & RenderFlags.Update) { + logs.push('component update'); + } }, factory: () => new SomeComponent }); @@ -61,31 +64,38 @@ describe('renderer factory lifecycle', () => { } function Template(rf: RenderFlags, ctx: any) { - logs.push('function'); if (rf & RenderFlags.Create) { + logs.push('function create'); text(0, 'bar'); } + if (rf & RenderFlags.Update) { + logs.push('function update'); + } } const directives = [SomeComponent, SomeComponentWhichThrows]; function TemplateWithComponent(rf: RenderFlags, ctx: any) { - logs.push('function_with_component'); if (rf & RenderFlags.Create) { + logs.push('function_with_component create'); text(0, 'bar'); element(1, 'some-component'); } + if (rf & RenderFlags.Update) { + logs.push('function_with_component update'); + } } beforeEach(() => { logs = []; }); it('should work with a component', () => { const component = renderComponent(SomeComponent, {rendererFactory}); - expect(logs).toEqual(['create', 'create', 'begin', 'component', 'end']); + expect(logs).toEqual( + ['create', 'create', 'begin', 'component create', 'component update', 'end']); logs = []; tick(component); - expect(logs).toEqual(['begin', 'component', 'end']); + expect(logs).toEqual(['begin', 'component update', 'end']); }); it('should work with a component which throws', () => { @@ -95,21 +105,23 @@ describe('renderer factory lifecycle', () => { it('should work with a template', () => { renderToHtml(Template, {}, 1, 0, null, null, rendererFactory); - expect(logs).toEqual(['create', 'begin', 'function', 'end']); + expect(logs).toEqual(['create', 'begin', 'function create', 'function update', 'end']); logs = []; renderToHtml(Template, {}); - expect(logs).toEqual(['begin', 'function', 'end']); + expect(logs).toEqual(['begin', 'function update', 'end']); }); it('should work with a template which contains a component', () => { renderToHtml(TemplateWithComponent, {}, 2, 0, directives, null, rendererFactory); - expect(logs).toEqual( - ['create', 'begin', 'function_with_component', 'create', 'component', 'end']); + expect(logs).toEqual([ + 'create', 'begin', 'function_with_component create', 'create', 'component create', + 'function_with_component update', 'component update', 'end' + ]); logs = []; renderToHtml(TemplateWithComponent, {}, 2, 0, directives); - expect(logs).toEqual(['begin', 'function_with_component', 'component', 'end']); + expect(logs).toEqual(['begin', 'function_with_component update', 'component update', 'end']); }); });