refactor(core): remove unused embedded view instructions (#34715)

This commit performs a cleanup and removes unused embedded view instructions and corresponding tests.

PR Close #34715
This commit is contained in:
Andrew Kushnir 2020-01-09 17:50:29 -08:00 committed by Misko Hevery
parent ed1b4a8f19
commit 0c8adbc4ec
30 changed files with 447 additions and 3542 deletions

View File

@ -1601,16 +1601,6 @@
"packages/core/src/render3/component.ts",
"packages/core/src/render3/styling/static_styling.ts"
],
[
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts",
"packages/core/src/render3/component.ts",
"packages/core/src/render3/util/global_utils.ts",
"packages/core/src/render3/util/change_detection_utils.ts",
"packages/core/src/render3/instructions/all.ts",
"packages/core/src/render3/instructions/container.ts"
],
[
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
@ -1631,16 +1621,6 @@
"packages/core/src/render3/instructions/all.ts",
"packages/core/src/render3/instructions/element.ts"
],
[
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts",
"packages/core/src/render3/component.ts",
"packages/core/src/render3/util/global_utils.ts",
"packages/core/src/render3/util/change_detection_utils.ts",
"packages/core/src/render3/instructions/all.ts",
"packages/core/src/render3/instructions/embedded_view.ts"
],
[
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
@ -1662,6 +1642,16 @@
"packages/core/src/render3/instructions/styling.ts",
"packages/core/src/render3/styling/style_binding_list.ts"
],
[
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts",
"packages/core/src/render3/component.ts",
"packages/core/src/render3/util/global_utils.ts",
"packages/core/src/render3/util/change_detection_utils.ts",
"packages/core/src/render3/instructions/all.ts",
"packages/core/src/render3/instructions/template.ts"
],
[
"packages/core/src/render3/assert.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
@ -1715,15 +1705,6 @@
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts"
],
[
"packages/core/src/render3/component.ts",
"packages/core/src/render3/util/global_utils.ts",
"packages/core/src/render3/util/change_detection_utils.ts",
"packages/core/src/render3/instructions/all.ts",
"packages/core/src/render3/instructions/container.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts"
],
[
"packages/core/src/render3/component.ts",
"packages/core/src/render3/util/global_utils.ts",
@ -1751,6 +1732,15 @@
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts"
],
[
"packages/core/src/render3/component.ts",
"packages/core/src/render3/util/global_utils.ts",
"packages/core/src/render3/util/change_detection_utils.ts",
"packages/core/src/render3/instructions/all.ts",
"packages/core/src/render3/instructions/template.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
"packages/core/src/render3/index.ts"
],
[
"packages/core/src/render3/di_setup.ts",
"packages/core/src/render3/interfaces/type_checks.ts",

View File

@ -719,12 +719,6 @@ export declare type ɵɵComponentDefWithMeta<T, Selector extends String, ExportA
export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): typeof ɵɵcomponentHostSyntheticListener;
export declare function ɵɵcontainer(index: number): void;
export declare function ɵɵcontainerRefreshEnd(): void;
export declare function ɵɵcontainerRefreshStart(index: number): void;
export declare function ɵɵcontentQuery<T>(directiveIndex: number, predicate: Type<any> | string[], descend: boolean, read?: any): void;
export declare function ɵɵCopyDefinitionFeature(definition: ɵDirectiveDef<any> | ɵComponentDef<any>): void;
@ -828,10 +822,6 @@ export declare function ɵɵelementEnd(): void;
export declare function ɵɵelementStart(index: number, name: string, attrsIndex?: number | null, localRefsIndex?: number): void;
export declare function ɵɵembeddedViewEnd(): void;
export declare function ɵɵembeddedViewStart(viewBlockId: number, decls: number, vars: number): ɵRenderFlags;
export declare function ɵɵenableBindings(): void;
export declare type ɵɵFactoryDef<T, CtorDependencies extends CtorDependency[]> = () => T;

View File

@ -1,46 +0,0 @@
load("//tools:defaults.bzl", "ts_devserver", "ts_library")
load("//modules/benchmarks:benchmark_test.bzl", "benchmark_test")
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
ts_library(
name = "render3_function_lib",
srcs = glob(["**/*.ts"]),
deps = [
"//modules/benchmarks/src:util_lib",
"//modules/benchmarks/src/tree:util_lib",
"//modules/benchmarks/src/tree/render3:tree_lib",
"//packages/core",
],
)
ts_devserver(
name = "devserver",
entry_module = "angular/modules/benchmarks/src/tree/render3_function/index",
port = 4200,
scripts = [
"@npm//:node_modules/tslib/tslib.js",
"//tools/rxjs:rxjs_umd_modules",
],
static_files = ["index.html"],
deps = [":render3_function_lib"],
)
benchmark_test(
name = "perf",
server = ":devserver",
deps = [
"//modules/benchmarks/src/tree:detect_changes_perf_tests_lib",
"//modules/benchmarks/src/tree:perf_tests_lib",
],
)
e2e_test(
name = "e2e",
server = ":devserver",
deps = [
"//modules/benchmarks/src/tree:detect_changes_e2e_tests_lib",
"//modules/benchmarks/src/tree:e2e_tests_lib",
],
)

View File

@ -1,42 +0,0 @@
<!doctype html>
<html>
<head>
<!-- Prevent the browser from requesting any favicon. -->
<link rel="icon" href="data:,">
</head>
<body>
<h2>Params</h2>
<form>
Depth:
<input type="number" name="depth" placeholder="depth" value="10">
<br>
<button>Apply</button>
</form>
<h2>Render3 Tree Benchmark with functions</h2>
<p>
<button id="destroyDom">destroyDom</button>
<button id="createDom">createDom</button>
<button id="detectChanges">detectChanges</button>
<button id="updateDomProfile">profile updateDom</button>
<button id="createDomProfile">profile createDom</button>
<button id="detectChangesProfile">profile detectChanges</button>
</p>
<div>
Change detection runs:<span id="numberOfChecks"></span>
</div>
<div>
<tree id="root"></tree>
</div>
<script>
// TODO(mlaval): remove once we have a proper solution
ngDevMode = false;
</script>
<!--load location for ts_devserver-->
<script src="/app_bundle.js"></script>
</body>
</html>

View File

@ -1,90 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵrenderComponent as renderComponent, ɵRenderFlags, ɵɵadvance, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵstyleProp, ɵɵtext, ɵɵtextInterpolate1} from '@angular/core';
import {bindAction, profile} from '../../util';
import {createDom, destroyDom, detectChanges} from '../render3/tree';
import {emptyTree, TreeNode} from '../util';
function noop() {}
export class TreeFunction {
data: TreeNode = emptyTree;
/** @nocollapse */
static ɵfac = () => new TreeFunction;
/** @nocollapse */
static ɵcmp = ɵɵdefineComponent({
type: TreeFunction,
selectors: [['tree']],
decls: 5,
vars: 2,
template:
function(rf: ɵRenderFlags, ctx: TreeFunction) {
// bit of a hack
TreeTpl(rf, ctx.data);
},
inputs: {data: 'data'}
});
}
const TreeFunctionCmpDef = TreeFunction.ɵcmp as {decls: number, vars: number};
export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) {
if (rf & ɵRenderFlags.Create) {
ɵɵelementStart(0, 'tree');
{
ɵɵelementStart(1, 'span');
{ ɵɵtext(2); }
ɵɵelementEnd();
ɵɵcontainer(3);
ɵɵcontainer(4);
}
ɵɵelementEnd();
}
if (rf & ɵRenderFlags.Update) {
ɵɵadvance(1);
ɵɵstyleProp('background-color', ctx.depth % 2 ? '' : 'grey');
ɵɵadvance(1);
ɵɵtextInterpolate1(' ', ctx.value, ' ');
ɵɵcontainerRefreshStart(3);
{
if (ctx.left != null) {
let rf0 = ɵɵembeddedViewStart(0, 5, 2);
{ TreeTpl(rf0, ctx.left); }
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(4);
{
if (ctx.right != null) {
let rf0 = ɵɵembeddedViewStart(0, TreeFunctionCmpDef.decls, TreeFunctionCmpDef.vars);
{ TreeTpl(rf0, ctx.right); }
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
let component: TreeFunction;
if (typeof window !== 'undefined') {
component = renderComponent(TreeFunction);
bindAction('#createDom', () => createDom(component as any));
bindAction('#destroyDom', () => destroyDom(component as any));
bindAction('#detectChanges', () => detectChanges(component as any));
bindAction(
'#detectChangesProfile',
profile(() => detectChanges(component as any), noop, 'detectChanges'));
bindAction('#updateDomProfile', profile(() => createDom(component as any), noop, 'update'));
bindAction(
'#createDomProfile',
profile(() => createDom(component as any), () => destroyDom(component as any), 'create'));
}

View File

@ -132,8 +132,6 @@ export class Identifiers {
static stylePropInterpolateV:
o.ExternalReference = {name: 'ɵɵstylePropInterpolateV', moduleName: CORE};
static containerCreate: o.ExternalReference = {name: 'ɵɵcontainer', moduleName: CORE};
static nextContext: o.ExternalReference = {name: 'ɵɵnextContext', moduleName: CORE};
static templateCreate: o.ExternalReference = {name: 'ɵɵtemplate', moduleName: CORE};

View File

@ -125,9 +125,6 @@ export {
ɵɵclassProp,
ɵɵComponentDefWithMeta,
ɵɵcomponentHostSyntheticListener,
ɵɵcontainer,
ɵɵcontainerRefreshEnd,
ɵɵcontainerRefreshStart,
ɵɵcontentQuery,
ɵɵCopyDefinitionFeature,
ɵɵdefineComponent,
@ -143,8 +140,6 @@ export {
ɵɵelementContainerStart,
ɵɵelementEnd,
ɵɵelementStart,
ɵɵembeddedViewEnd,
ɵɵembeddedViewStart,
ɵɵenableBindings,
ɵɵFactoryDef,
ɵɵgetCurrentView,

View File

@ -57,11 +57,6 @@ export function assertDataNext(lView: LView, index: number, arr?: any[]) {
arr.length, index, `index ${index} expected to be at the end of arr (length ${arr.length})`);
}
export function assertLContainerOrUndefined(value: any): asserts value is LContainer|undefined|
null {
value && assertEqual(isLContainer(value), true, 'Expecting LContainer or undefined or null');
}
export function assertLContainer(value: any): asserts value is LContainer {
assertDefined(value, 'LContainer must be defined');
assertEqual(isLContainer(value), true, 'Expecting LContainer');

View File

@ -50,23 +50,15 @@ export {
ɵɵclassProp,
ɵɵcomponentHostSyntheticListener,
ɵɵcontainer,
ɵɵcontainerRefreshEnd,
ɵɵcontainerRefreshStart,
ɵɵdirectiveInject,
ɵɵelement,
ɵɵelementContainer,
ɵɵelementContainerEnd,
ɵɵelementContainerStart,
ɵɵelementEnd,
ɵɵelementStart,
ɵɵembeddedViewEnd,
ɵɵembeddedViewStart,
ɵɵgetCurrentView,
ɵɵhostProperty,

View File

@ -33,7 +33,6 @@ export * from './storage';
export * from './di';
export * from './element';
export * from './element_container';
export * from './embedded_view';
export * from './get_current_view';
export * from './listener';
export * from './namespace';

View File

@ -5,46 +5,20 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertDataInRange, assertEqual} from '../../util/assert';
import {assertFirstCreatePass, assertHasParent} from '../assert';
import {assertFirstCreatePass} from '../assert';
import {attachPatchData} from '../context_discovery';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags, registerPostOrderHooks} from '../hooks';
import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {registerPostOrderHooks} from '../hooks';
import {ComponentTemplate} from '../interfaces/definition';
import {LocalRefExtractor, TAttributes, TContainerNode, TNode, TNodeType, TViewNode} from '../interfaces/node';
import {LocalRefExtractor, TAttributes, TContainerNode, TNodeType, TViewNode} from '../interfaces/node';
import {isDirectiveHost} from '../interfaces/type_checks';
import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, RENDERER, T_HOST, TView, TViewType} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {appendChild, removeView} from '../node_manipulation';
import {getBindingIndex, getCheckNoChangesMode, getIsParent, getLView, getPreviousOrParentTNode, getTView, setIsNotParent, setPreviousOrParentTNode} from '../state';
import {getConstant, getLContainerActiveIndex, load} from '../util/view_utils';
import {HEADER_OFFSET, LView, RENDERER, T_HOST, TView, TViewType} from '../interfaces/view';
import {appendChild} from '../node_manipulation';
import {getLView, getTView, setPreviousOrParentTNode} from '../state';
import {getConstant} from '../util/view_utils';
import {addToViewTree, createDirectivesInstances, createLContainer, createTNode, createTView, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared';
/**
* Creates an LContainer for inline views, e.g.
*
* % if (showing) {
* <div></div>
* % }
*
* @param index The index of the container in the data array
*
* @codeGenApi
*/
export function ɵɵcontainer(index: number): void {
const lView = getLView();
const tView = getTView();
const tNode = containerInternal(tView, lView, index, null, null);
if (tView.firstCreatePass) {
tNode.tViews = [];
}
setIsNotParent();
}
function templateFirstCreatePass(
index: number, tView: TView, lView: LView, templateFn: ComponentTemplate<any>|null,
decls: number, vars: number, tagName?: string|null, attrsIndex?: number|null,
@ -122,96 +96,3 @@ export function ɵɵtemplate(
saveResolvedLocalsInData(lView, tNode, localRefExtractor);
}
}
/**
* Sets a container up to receive views.
*
* @param index The index of the container in the data array
*
* @codeGenApi
*/
export function ɵɵcontainerRefreshStart(index: number): void {
const lView = getLView();
const tView = getTView();
let previousOrParentTNode = load(tView.data, index) as TNode;
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Container);
setPreviousOrParentTNode(previousOrParentTNode, true);
lView[index + HEADER_OFFSET][ACTIVE_INDEX] = 0;
// 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).
if (!getCheckNoChangesMode()) {
const hooksInitPhaseCompleted =
(lView[FLAGS] & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted;
if (hooksInitPhaseCompleted) {
const preOrderCheckHooks = tView.preOrderCheckHooks;
if (preOrderCheckHooks !== null) {
executeCheckHooks(lView, preOrderCheckHooks, null);
}
} else {
const preOrderHooks = tView.preOrderHooks;
if (preOrderHooks !== null) {
executeInitAndCheckHooks(lView, preOrderHooks, InitPhaseState.OnInitHooksToBeRun, null);
}
incrementInitPhaseFlags(lView, InitPhaseState.OnInitHooksToBeRun);
}
}
}
/**
* Marks the end of the LContainer.
*
* Marking the end of LContainer is the time when to child views get inserted or removed.
*
* @codeGenApi
*/
export function ɵɵcontainerRefreshEnd(): void {
let previousOrParentTNode = getPreviousOrParentTNode();
if (getIsParent()) {
setIsNotParent();
} else {
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.View);
ngDevMode && assertHasParent(previousOrParentTNode);
previousOrParentTNode = previousOrParentTNode.parent!;
setPreviousOrParentTNode(previousOrParentTNode, false);
}
ngDevMode && assertNodeType(previousOrParentTNode, TNodeType.Container);
const lContainer: LContainer = getLView()[previousOrParentTNode.index];
const nextIndex = getLContainerActiveIndex(lContainer);
// remove extra views at the end of the container
while (nextIndex < lContainer.length - CONTAINER_HEADER_OFFSET) {
removeView(lContainer, nextIndex);
}
}
function containerInternal(
tView: TView, lView: LView, nodeIndex: number, tagName: string|null,
attrs: TAttributes|null): TContainerNode {
ngDevMode &&
assertEqual(
getBindingIndex(), tView.bindingStartIndex,
'container nodes should be created before any bindings');
const adjustedIndex = nodeIndex + HEADER_OFFSET;
ngDevMode && assertDataInRange(lView, nodeIndex + HEADER_OFFSET);
ngDevMode && ngDevMode.rendererCreateComment++;
const comment = lView[adjustedIndex] =
lView[RENDERER].createComment(ngDevMode ? 'container' : '');
const tNode =
getOrCreateTNode(tView, lView[T_HOST], nodeIndex, TNodeType.Container, tagName, attrs);
const lContainer = lView[adjustedIndex] = createLContainer(comment, lView, comment, tNode);
appendChild(tView, lView, comment, tNode);
attachPatchData(comment, lView);
// Containers are added to the current view tree instead of their embedded views
// because views can be removed and re-inserted.
addToViewTree(lView, lContainer);
ngDevMode && assertNodeType(getPreviousOrParentTNode(), TNodeType.Container);
return tNode;
}

View File

@ -1,145 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {assertDefined, assertEqual} from '../../util/assert';
import {assertLContainerOrUndefined} from '../assert';
import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {RenderFlags} from '../interfaces/definition';
import {TContainerNode, TNodeType} from '../interfaces/node';
import {CONTEXT, LView, LViewFlags, PARENT, T_HOST, TVIEW, TView, TViewType} from '../interfaces/view';
import {assertNodeType} from '../node_assert';
import {insertView, removeView} from '../node_manipulation';
import {enterView, getIsParent, getLView, getPreviousOrParentTNode, getTView, leaveView, setIsParent, setPreviousOrParentTNode} from '../state';
import {getLContainerActiveIndex, isCreationMode} from '../util/view_utils';
import {assignTViewNodeToLView, createLView, createTView, refreshView, renderView} from './shared';
/**
* Marks the start of an embedded view.
*
* @param viewBlockId The ID of this view
* @return boolean Whether or not this view is in creation mode
*
* @codeGenApi
*/
export function ɵɵembeddedViewStart(viewBlockId: number, decls: number, vars: number): RenderFlags {
const lView = getLView();
const previousOrParentTNode = getPreviousOrParentTNode();
// The previous node can be a view node if we are processing an inline for loop
const containerTNode = previousOrParentTNode.type === TNodeType.View ?
previousOrParentTNode.parent! :
previousOrParentTNode;
const lContainer = lView[containerTNode.index] as LContainer;
ngDevMode && assertNodeType(containerTNode, TNodeType.Container);
let viewToRender = scanForView(lContainer, getLContainerActiveIndex(lContainer), viewBlockId);
if (viewToRender) {
setIsParent();
enterView(viewToRender, viewToRender[TVIEW].node);
} else {
// When we create a new LView, we always reset the state of the instructions.
viewToRender = createLView(
lView, getOrCreateEmbeddedTView(viewBlockId, decls, vars, containerTNode as TContainerNode),
null, LViewFlags.CheckAlways, null, null);
const tParentNode = getIsParent() ? previousOrParentTNode :
previousOrParentTNode && previousOrParentTNode.parent;
assignTViewNodeToLView(viewToRender[TVIEW], tParentNode, viewBlockId, viewToRender);
enterView(viewToRender, viewToRender[TVIEW].node);
}
if (lContainer) {
if (isCreationMode(viewToRender)) {
// it is a new view, insert it into collection of views for a given container
insertView(
viewToRender[TVIEW], viewToRender, lContainer, getLContainerActiveIndex(lContainer));
}
lContainer[ACTIVE_INDEX] += ActiveIndexFlag.INCREMENT;
}
return isCreationMode(viewToRender) ? RenderFlags.Create | RenderFlags.Update :
RenderFlags.Update;
}
/**
* Initialize the TView (e.g. static data) for the active embedded view.
*
* Each embedded view block must create or retrieve its own TView. Otherwise, the embedded view's
* static data for a particular node would overwrite the static data for a node in the view above
* it with the same index (since it's in the same template).
*
* @param viewIndex The index of the TView in TNode.tViews
* @param decls The number of nodes, local refs, and pipes in this template
* @param vars The number of bindings and pure function bindings in this template
* @param container The parent container in which to look for the view's static data
* @returns TView
*/
function getOrCreateEmbeddedTView(
viewIndex: number, decls: number, vars: number, parent: TContainerNode): TView {
const tView = getLView()[TVIEW];
ngDevMode && assertNodeType(parent, TNodeType.Container);
const containerTViews = parent.tViews as TView[];
ngDevMode && assertDefined(containerTViews, 'TView expected');
ngDevMode && assertEqual(Array.isArray(containerTViews), true, 'TViews should be in an array');
if (viewIndex >= containerTViews.length || containerTViews[viewIndex] == null) {
containerTViews[viewIndex] = createTView(
TViewType.Embedded, viewIndex, null, decls, vars, tView.directiveRegistry,
tView.pipeRegistry, null, null, tView.consts);
}
return containerTViews[viewIndex];
}
/**
* Looks for a view with a given view block id inside a provided LContainer.
* Removes views that need to be deleted in the process.
*
* @param lContainer to search for views
* @param startIdx starting index in the views array to search from
* @param viewBlockId exact view block id to look for
*/
function scanForView(lContainer: LContainer, startIdx: number, viewBlockId: number): LView|null {
for (let i = startIdx + CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
const viewAtPositionId = lContainer[i][TVIEW].id;
if (viewAtPositionId === viewBlockId) {
return lContainer[i];
} else if (viewAtPositionId < viewBlockId) {
// found a view that should not be at this position - remove
removeView(lContainer, i - CONTAINER_HEADER_OFFSET);
} else {
// found a view with id greater than the one we are searching for
// which means that required view doesn't exist and can't be found at
// later positions in the views array - stop the searchdef.cont here
break;
}
}
return null;
}
/**
* Marks the end of an embedded view.
*
* @codeGenApi
*/
export function ɵɵembeddedViewEnd(): void {
const lView = getLView();
const tView = getTView();
const viewHost = lView[T_HOST];
const context = lView[CONTEXT];
if (isCreationMode(lView)) {
renderView(tView, lView, context); // creation mode pass
}
refreshView(tView, lView, tView.template, context); // update mode pass
const lContainer = lView[PARENT] as LContainer;
ngDevMode && assertLContainerOrUndefined(lContainer);
leaveView();
setPreviousOrParentTNode(viewHost!, false);
}

View File

@ -20,6 +20,7 @@ import {HOST, LView, NEXT, PARENT, T_HOST, TRANSPLANTED_VIEWS_TO_REFRESH} from '
* `LContainer`.
*/
export const TYPE = 1;
/**
* Below are constants for LContainer indices to help us look up LContainer members
* without having to remember the specific indices.
@ -48,9 +49,7 @@ export const CONTAINER_HEADER_OFFSET = 10;
/**
* Used to track:
* - Inline embedded views (see: `ɵɵembeddedViewStart`)
* - Transplanted `LView`s (see: `LView[DECLARATION_COMPONENT_VIEW])`
* Used to track Transplanted `LView`s (see: `LView[DECLARATION_COMPONENT_VIEW])`
*/
export const enum ActiveIndexFlag {
/**
@ -72,13 +71,6 @@ export const enum ActiveIndexFlag {
* Number of bits to shift inline embedded views counter to make space for other flags.
*/
SHIFT = 1,
/**
* When incrementing the active index for inline embedded views, the amount to increment to leave
* space for other flags.
*/
INCREMENT = 1 << SHIFT,
}
/**
@ -111,10 +103,6 @@ export interface LContainer extends Array<any> {
* it is set to null to identify this scenario, as indices are "absolute" in that case,
* i.e. provided directly by the user of the ViewContainerRef API.
*
* This is used by `ɵɵembeddedViewStart` to track which `LView` is currently active.
* Because `ɵɵembeddedViewStart` is not generated by the compiler this feature is essentially
* unused.
*
* The lowest bit signals that this `LContainer` has transplanted views which need to be change
* detected as part of the declaration CD. (See `LView[DECLARATION_COMPONENT_VIEW]`)
*/

View File

@ -49,10 +49,7 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵProvidersFeature': r3.ɵɵProvidersFeature,
'ɵɵCopyDefinitionFeature': r3.ɵɵCopyDefinitionFeature,
'ɵɵInheritDefinitionFeature': r3.ɵɵInheritDefinitionFeature,
'ɵɵcontainer': r3.ɵɵcontainer,
'ɵɵnextContext': r3.ɵɵnextContext,
'ɵɵcontainerRefreshStart': r3.ɵɵcontainerRefreshStart,
'ɵɵcontainerRefreshEnd': r3.ɵɵcontainerRefreshEnd,
'ɵɵnamespaceHTML': r3.ɵɵnamespaceHTML,
'ɵɵnamespaceMathML': r3.ɵɵnamespaceMathML,
'ɵɵnamespaceSVG': r3.ɵɵnamespaceSVG,
@ -152,8 +149,6 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵtextInterpolate7': r3.ɵɵtextInterpolate7,
'ɵɵtextInterpolate8': r3.ɵɵtextInterpolate8,
'ɵɵtextInterpolateV': r3.ɵɵtextInterpolateV,
'ɵɵembeddedViewStart': r3.ɵɵembeddedViewStart,
'ɵɵembeddedViewEnd': r3.ɵɵembeddedViewEnd,
'ɵɵi18n': r3.ɵɵi18n,
'ɵɵi18nAttributes': r3.ɵɵi18nAttributes,
'ɵɵi18nExp': r3.ɵɵi18nExp,

View File

@ -289,4 +289,111 @@ describe('event listeners', () => {
expect(log).toEqual(['dirA.click', 'dirB.click', 'component.click']);
});
});
it('should destroy listeners when view is removed', () => {
@Component({
selector: 'my-comp',
template: `
<button *ngIf="visible" (click)="count()">Click me!</button>
`,
})
class MyComp {
visible = true;
counter = 0;
count() {
this.counter++;
}
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(comp.counter).toBe(1);
comp.visible = false;
fixture.detectChanges();
button.click();
expect(comp.counter).toBe(1);
});
it('should destroy listeners when views generated using *ngFor are removed', () => {
let counter = 0;
@Component({
selector: 'my-comp',
template: `
<button *ngFor="let button of buttons" (click)="count()">Click me!</button>
`,
})
class MyComp {
buttons = [1, 2];
count() {
counter++;
}
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
const buttons = fixture.nativeElement.querySelectorAll('button');
buttons[0].click();
buttons[1].click();
expect(counter).toBe(2);
comp.buttons = [];
fixture.detectChanges();
buttons[0].click();
buttons[1].click();
expect(counter).toBe(2);
});
it('should destroy listeners when nested view is removed', () => {
@Component({
selector: 'my-comp',
template: `
<ng-container *ngIf="isSectionVisible">
Click to submit a form:
<button *ngIf="isButtonVisible" (click)="count()">Click me!</button>
</ng-container>
`,
})
class MyComp {
isSectionVisible = true;
isButtonVisible = true;
counter = 0;
count() {
this.counter++;
}
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(comp.counter).toBe(1);
comp.isButtonVisible = false;
fixture.detectChanges();
button.click();
expect(comp.counter).toBe(1);
comp.isSectionVisible = false;
fixture.detectChanges();
button.click();
expect(comp.counter).toBe(1);
});
});

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule} from '@angular/common';
import {Component, Directive, forwardRef, Inject, Injectable, InjectionToken, Injector, NgModule, Optional} from '@angular/core';
import {async, inject, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
@ -506,6 +507,47 @@ describe('providers', () => {
expect(destroyCalls).toBe(0);
});
it('should call ngOnDestroy if host component is destroyed', () => {
const logs: string[] = [];
@Injectable()
class InjectableWithDestroyHookToken {
ngOnDestroy() {
logs.push('OnDestroy Token');
}
}
@Component({
selector: 'comp-with-provider',
template: '',
providers: [InjectableWithDestroyHookToken],
})
class CompWithProvider {
constructor(token: InjectableWithDestroyHookToken) {}
}
@Component({
selector: 'app',
template: '<comp-with-provider *ngIf="condition"></comp-with-provider>',
})
class App {
condition = true;
}
TestBed.configureTestingModule({
declarations: [App, CompWithProvider],
imports: [CommonModule],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
fixture.componentInstance.condition = false;
fixture.detectChanges();
expect(logs).toEqual(['OnDestroy Token']);
});
});
describe('components and directives', () => {
@ -669,4 +711,69 @@ describe('providers', () => {
expect(injector.get(MyService).value).toBe(null);
});
});
describe('view providers', () => {
it('should have access to viewProviders within the same component', () => {
@Component({
selector: 'comp',
template: '{{s}}-{{n}}',
providers: [
{provide: Number, useValue: 1, multi: true},
],
viewProviders: [
{provide: String, useValue: 'bar'},
{provide: Number, useValue: 2, multi: true},
]
})
class Comp {
constructor(private s: String, private n: Number) {}
}
TestBed.configureTestingModule({declarations: [Comp]});
const fixture = TestBed.createComponent(Comp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('bar-1,2');
});
it('should have access to viewProviders of the host component', () => {
@Component({
selector: 'repeated',
template: '[{{s}}-{{n}}]',
})
class Repeated {
constructor(private s: String, private n: Number) {}
}
@Component({
template: `
<div>
<ng-container *ngFor="let item of items">
<repeated></repeated>
</ng-container>
</div>
`,
providers: [
{provide: Number, useValue: 1, multi: true},
],
viewProviders: [
{provide: String, useValue: 'foo'},
{provide: Number, useValue: 2, multi: true},
],
})
class ComponentWithProviders {
items = [1, 2, 3];
}
TestBed.configureTestingModule({
declarations: [ComponentWithProviders, Repeated],
imports: [CommonModule],
});
const fixture = TestBed.createComponent(ComponentWithProviders);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('[foo-1,2][foo-1,2][foo-1,2]');
});
});
});

View File

@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, forwardRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
import {AfterViewInit, Component, ContentChild, ContentChildren, Directive, ElementRef, EventEmitter, forwardRef, Input, QueryList, TemplateRef, Type, ViewChild, ViewChildren, ViewContainerRef, ViewRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -300,6 +300,51 @@ describe('query logic', () => {
fixture.detectChanges();
expect(fixture.componentInstance.viewChildAvailableInAfterViewInit).toBe(true);
});
it('should destroy QueryList when the containing view is destroyed', () => {
let queryInstance: QueryList<any>;
@Component({
selector: 'comp-with-view-query',
template: '<div #foo>Content</div>',
})
class ComponentWithViewQuery {
@ViewChildren('foo')
set foo(value: any) {
queryInstance = value;
}
get foo() {
return queryInstance;
}
}
@Component({
selector: 'root',
template: `
<ng-container *ngIf="condition">
<comp-with-view-query></comp-with-view-query>
</ng-container>
`
})
class Root {
condition = true;
}
TestBed.configureTestingModule({
declarations: [Root, ComponentWithViewQuery],
imports: [CommonModule],
});
const fixture = TestBed.createComponent(Root);
fixture.detectChanges();
expect((queryInstance!.changes as EventEmitter<any>).closed).toBeFalsy();
fixture.componentInstance.condition = false;
fixture.detectChanges();
expect((queryInstance!.changes as EventEmitter<any>).closed).toBeTruthy();
});
});
describe('content queries', () => {
@ -676,6 +721,68 @@ describe('query logic', () => {
fixture.detectChanges();
expect(queryList.length).toBe(0);
});
it('should support content queries for directives within repeated embedded views', () => {
const withContentInstances: DirWithContentQuery[] = [];
@Directive({
selector: '[with-content]',
})
class DirWithContentQuery {
constructor() {
withContentInstances.push(this);
}
@ContentChildren('foo', {descendants: false}) foos!: QueryList<ElementRef>;
contentInitQuerySnapshot = 0;
contentCheckedQuerySnapshot = 0;
ngAfterContentInit() {
this.contentInitQuerySnapshot = this.foos ? this.foos.length : 0;
}
ngAfterContentChecked() {
this.contentCheckedQuerySnapshot = this.foos ? this.foos.length : 0;
}
}
@Component({
selector: 'comp',
template: `
<ng-container *ngFor="let item of items">
<div with-content>
<span #foo></span>
</div>
</ng-container>
`,
})
class Root {
items = [1, 2, 3];
}
TestBed.configureTestingModule({
declarations: [Root, DirWithContentQuery],
imports: [CommonModule],
});
const fixture = TestBed.createComponent(Root);
fixture.detectChanges();
for (let i = 0; i < 3; i++) {
expect(withContentInstances[i].foos.length)
.toBe(1, `Expected content query to match <span #foo>.`);
expect(withContentInstances[i].contentInitQuerySnapshot)
.toBe(
1,
`Expected content query results to be available when ngAfterContentInit was called.`);
expect(withContentInstances[i].contentCheckedQuerySnapshot)
.toBe(
1,
`Expected content query results to be available when ngAfterContentChecked was called.`);
}
});
});
// Some root components may have ContentChildren queries if they are also

View File

@ -11,6 +11,7 @@ import {ɵAnimationEngine, ɵNoopAnimationStyleNormalizer} from '@angular/animat
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {DOCUMENT} from '@angular/common';
import {Component, DoCheck, NgZone, RendererFactory2, RendererType2} from '@angular/core';
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
import {NoopNgZone} from '@angular/core/src/zone/ng_zone';
import {TestBed} from '@angular/core/testing';
import {EventManager, ɵDomSharedStylesHost} from '@angular/platform-browser';
@ -253,3 +254,79 @@ describe('custom renderer', () => {
}).not.toThrow();
});
});
onlyInIvy('access global ngDevMode').describe('Renderer2 destruction hooks', () => {
@Component({
selector: 'some-component',
template: `
<span *ngIf="isContentVisible">A</span>
<span *ngIf="isContentVisible">B</span>
<span *ngIf="isContentVisible">C</span>
`,
})
class SimpleApp {
isContentVisible = true;
}
@Component({
selector: 'basic-comp',
template: 'comp(<ng-content></ng-content>)',
})
class BasicComponent {
}
@Component({
selector: 'some-component',
template: `
<basic-comp *ngIf="isContentVisible">A</basic-comp>
<basic-comp *ngIf="isContentVisible">B</basic-comp>
<basic-comp *ngIf="isContentVisible">C</basic-comp>
`,
})
class AppWithComponents {
isContentVisible = true;
}
beforeEach(() => {
// Tests below depend on perf counters when running with Ivy. In order to have
// clean perf counters at the beginning of a test, we reset those here.
ngDevModeResetPerfCounters();
TestBed.configureTestingModule({
declarations: [SimpleApp, AppWithComponents, BasicComponent],
providers: [{
provide: RendererFactory2,
useFactory: (document: any) => getRendererFactory2(document),
deps: [DOCUMENT]
}]
});
});
it('should call renderer.destroyNode for each node destroyed', () => {
const fixture = TestBed.createComponent(SimpleApp);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('ABC');
fixture.componentInstance.isContentVisible = false;
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('');
expect(ngDevMode!.rendererDestroy).toBe(0);
expect(ngDevMode!.rendererDestroyNode).toBe(3);
});
it('should call renderer.destroy for each component destroyed', () => {
const fixture = TestBed.createComponent(AppWithComponents);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('comp(A)comp(B)comp(C)');
fixture.componentInstance.isContentVisible = false;
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('');
expect(ngDevMode!.rendererDestroy).toBe(3);
expect(ngDevMode!.rendererDestroyNode).toBe(3);
});
});

View File

@ -1,79 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵdefineComponent} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {document, renderComponent} from './render_util';
describe('iv perf test', () => {
const count = 100000;
const noOfIterations = 10;
describe('render', () => {
for (let iteration = 0; iteration < noOfIterations; iteration++) {
it(`${iteration}. create ${count} divs in DOM`, () => {
const start = new Date().getTime();
const container = document.createElement('div');
for (let i = 0; i < count; i++) {
const div = document.createElement('div');
div.appendChild(document.createTextNode('-'));
container.appendChild(div);
}
const end = new Date().getTime();
log(`${count} DIVs in DOM`, (end - start) / count);
});
it(`${iteration}. create ${count} divs in Render3`, () => {
class Component {
static ɵfac = () => new Component;
static ɵcmp = ɵɵdefineComponent({
type: Component,
selectors: [['div']],
decls: 1,
vars: 0,
template:
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
for (let i = 0; i < count; i++) {
let rf0 = ɵɵembeddedViewStart(0, 2, 0);
{
if (rf0 & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
ɵɵtext(1, '-');
ɵɵelementEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
});
}
const start = new Date().getTime();
renderComponent(Component);
const end = new Date().getTime();
log(`${count} DIVs in Render3`, (end - start) / count);
});
}
});
});
function log(text: string, duration: number) {
// tslint:disable-next-line:no-console
console.log(text, duration * 1000, 'ns');
}

View File

@ -8,12 +8,12 @@
import {ViewEncapsulation, ɵɵdefineInjectable, ɵɵdefineInjector} from '../../src/core';
import {createInjector} from '../../src/di/r3_injector';
import {AttributeMarker, ComponentFactory, getRenderedText, LifecycleHooksFeature, markDirty, ɵɵadvance, ɵɵdefineComponent, ɵɵdirectiveInject, ɵɵproperty, ɵɵselect, ɵɵtemplate} from '../../src/render3/index';
import {tick, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵnextContext, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {ComponentDef, RenderFlags} from '../../src/render3/interfaces/definition';
import {AttributeMarker, markDirty, ɵɵadvance, ɵɵdefineComponent, ɵɵdirectiveInject, ɵɵproperty, ɵɵtemplate} from '../../src/render3/index';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def';
import {ComponentFixture, containerEl, createComponent, MockRendererFactory, renderComponent, renderToHtml, requestAnimationFrame, toHtml} from './render_util';
import {ComponentFixture, containerEl, createComponent, MockRendererFactory, renderComponent, requestAnimationFrame, toHtml} from './render_util';
describe('component', () => {
class CounterComponent {
@ -220,348 +220,3 @@ it('should not invoke renderer destroy method for embedded views', () => {
// in case child views are created/removed
expect(destroySpy.calls.count()).toBe(0);
});
describe('component with a container', () => {
function showItems(rf: RenderFlags, ctx: {items: string[]}) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
for (const item of ctx.items) {
const rf0 = ɵɵembeddedViewStart(0, 1, 1);
{
if (rf0 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf0 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵtextInterpolate(item);
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
class WrapperComponent {
// TODO(issue/24571): remove '!'.
items!: string[];
static ɵfac = () => new WrapperComponent;
static ɵcmp = ɵɵdefineComponent({
type: WrapperComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['wrapper']],
decls: 1,
vars: 0,
template:
function ChildComponentTemplate(rf: RenderFlags, ctx: {items: string[]}) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
{ showItems(rf0, {items: ctx.items}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
},
inputs: {items: 'items'}
});
}
function template(rf: RenderFlags, ctx: {items: string[]}) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'wrapper');
}
if (rf & RenderFlags.Update) {
ɵɵproperty('items', ctx.items);
}
}
const defs = [WrapperComponent];
it('should re-render on input change', () => {
const ctx: {items: string[]} = {items: ['a']};
expect(renderToHtml(template, ctx, 1, 1, defs)).toEqual('<wrapper>a</wrapper>');
ctx.items = [...ctx.items, 'b'];
expect(renderToHtml(template, ctx, 1, 1, defs)).toEqual('<wrapper>ab</wrapper>');
});
});
describe('recursive components', () => {
let events: string[];
let count: number;
beforeEach(() => {
events = [];
count = 0;
});
class TreeNode {
constructor(
public value: number, public depth: number, public left: TreeNode|null,
public right: TreeNode|null) {}
}
/**
* {{ data.value }}
*
* % if (data.left != null) {
* <tree-comp [data]="data.left"></tree-comp>
* % }
* % if (data.right != null) {
* <tree-comp [data]="data.right"></tree-comp>
* % }
*/
class TreeComponent {
data: TreeNode = _buildTree(0);
ngDoCheck() {
events.push('check' + this.data.value);
}
ngOnDestroy() {
events.push('destroy' + this.data.value);
}
static ɵfac = () => new TreeComponent();
static ɵcmp = ɵɵdefineComponent({
type: TreeComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['tree-comp']],
decls: 3,
vars: 1,
template:
(rf: RenderFlags, ctx: TreeComponent) => {
if (rf & RenderFlags.Create) {
ɵɵtext(0);
ɵɵcontainer(1);
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵtextInterpolate(ctx.data.value);
ɵɵcontainerRefreshStart(1);
{
if (ctx.data.left != null) {
let rf0 = ɵɵembeddedViewStart(0, 1, 1);
if (rf0 & RenderFlags.Create) {
ɵɵelement(0, 'tree-comp');
}
if (rf0 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵproperty('data', ctx.data.left);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(2);
{
if (ctx.data.right != null) {
let rf0 = ɵɵembeddedViewStart(0, 1, 1);
if (rf0 & RenderFlags.Create) {
ɵɵelement(0, 'tree-comp');
}
if (rf0 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵproperty('data', ctx.data.right);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
inputs: {data: 'data'}
});
}
(TreeComponent.ɵcmp as ComponentDef<TreeComponent>).directiveDefs = () => [TreeComponent.ɵcmp];
/**
* {{ data.value }}
* <ng-if-tree [data]="data.left" *ngIf="data.left"></ng-if-tree>
* <ng-if-tree [data]="data.right" *ngIf="data.right"></ng-if-tree>
*/
class NgIfTree {
data: TreeNode = _buildTree(0);
ngDoCheck() {
events.push('check' + this.data.value);
}
ngOnDestroy() {
events.push('destroy' + this.data.value);
}
static ɵfac = () => new NgIfTree();
static ɵcmp = ɵɵdefineComponent({
type: NgIfTree,
encapsulation: ViewEncapsulation.None,
selectors: [['ng-if-tree']],
decls: 3,
vars: 3,
consts: [[AttributeMarker.Bindings, 'data', AttributeMarker.Template, 'ngIf']],
template:
(rf: RenderFlags, ctx: NgIfTree) => {
if (rf & RenderFlags.Create) {
ɵɵtext(0);
ɵɵtemplate(1, IfTemplate, 1, 1, 'ng-if-tree', 0);
ɵɵtemplate(2, IfTemplate2, 1, 1, 'ng-if-tree', 0);
}
if (rf & RenderFlags.Update) {
ɵɵtextInterpolate(ctx.data.value);
ɵɵadvance(1);
ɵɵproperty('ngIf', ctx.data.left);
ɵɵadvance(1);
ɵɵproperty('ngIf', ctx.data.right);
}
},
inputs: {data: 'data'},
});
}
function IfTemplate(rf: RenderFlags, left: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'ng-if-tree');
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
const parent = ɵɵnextContext();
ɵɵproperty('data', parent.data.left);
}
}
function IfTemplate2(rf: RenderFlags, right: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'ng-if-tree');
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
const parent = ɵɵnextContext();
ɵɵproperty('data', parent.data.right);
}
}
(NgIfTree.ɵcmp as ComponentDef<NgIfTree>).directiveDefs = () => [NgIfTree.ɵcmp, NgIf.ɵdir];
function _buildTree(currDepth: number): TreeNode {
const children = currDepth < 2 ? _buildTree(currDepth + 1) : null;
const children2 = currDepth < 2 ? _buildTree(currDepth + 1) : null;
return new TreeNode(count++, currDepth, children, children2);
}
it('should check each component just once', () => {
const comp = renderComponent(TreeComponent, {hostFeatures: [LifecycleHooksFeature]});
expect(getRenderedText(comp)).toEqual('6201534');
expect(events).toEqual(['check6', 'check2', 'check0', 'check1', 'check5', 'check3', 'check4']);
events = [];
tick(comp);
expect(events).toEqual(['check6', 'check2', 'check0', 'check1', 'check5', 'check3', 'check4']);
});
// This tests that the view tree is set up properly for recursive components
it('should call onDestroys properly', () => {
/**
* % if (!skipContent) {
* <tree-comp></tree-comp>
* % }
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
if (!ctx.skipContent) {
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
if (rf0 & RenderFlags.Create) {
ɵɵelementStart(0, 'tree-comp');
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
}, 1, 0, [TreeComponent]);
const fixture = new ComponentFixture(App);
expect(getRenderedText(fixture.component)).toEqual('6201534');
events = [];
fixture.component.skipContent = true;
fixture.update();
expect(events).toEqual(
['destroy0', 'destroy1', 'destroy2', 'destroy3', 'destroy4', 'destroy5', 'destroy6']);
});
it('should call onDestroys properly with ngIf', () => {
/**
* % if (!skipContent) {
* <ng-if-tree></ng-if-tree>
* % }
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
if (!ctx.skipContent) {
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
if (rf0 & RenderFlags.Create) {
ɵɵelementStart(0, 'ng-if-tree');
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
}, 1, 0, [NgIfTree]);
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;
fixture.update();
expect(events).toEqual(
['destroy0', 'destroy1', 'destroy2', 'destroy3', 'destroy4', 'destroy5', 'destroy6']);
});
it('should map inputs minified & unminified names', async () => {
class TestInputsComponent {
// TODO(issue/24571): remove '!'.
minifiedName!: string;
static ɵfac = () => new TestInputsComponent();
static ɵcmp = ɵɵdefineComponent({
type: TestInputsComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['test-inputs']],
inputs: {minifiedName: 'unminifiedName'},
decls: 0,
vars: 0,
template: function(rf: RenderFlags, ctx: TestInputsComponent):
void {
// Template not needed for this test
}
});
}
const testInputsComponentFactory = new ComponentFactory(TestInputsComponent.ɵcmp);
expect([
{propName: 'minifiedName', templateName: 'unminifiedName'}
]).toEqual(testInputsComponentFactory.inputs);
});
});

View File

@ -1,961 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵdefineComponent} from '../../src/render3/definition';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵselect, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {ComponentFixture, createComponent, TemplateFixture} from './render_util';
describe('JS control flow', () => {
it('should work with if block', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(1, 2, 1);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
{ ɵɵtext(1); }
ɵɵelementEnd();
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(1);
ɵɵtextInterpolate(ctx.message);
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 2);
const fixture = new ComponentFixture(App);
fixture.component.condition = true;
fixture.component.message = 'Hello';
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
fixture.component.condition = false;
fixture.component.message = 'Hi!';
fixture.update();
expect(fixture.html).toEqual('<div></div>');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hi!</span></div>');
});
it('should work with nested if blocks', () => {
/**
* <div>
* % if(ctx.condition) {
* <span>
* % if(ctx.condition2) {
* Hello
* % }
* </span>
* % }
* </div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(1, 2, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf1 & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition2) {
let rf2 = ɵɵembeddedViewStart(2, 1, 0);
{
if (rf2 & RenderFlags.Create) {
ɵɵtext(0, 'Hello');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 2);
const fixture = new ComponentFixture(App);
fixture.component.condition = true;
fixture.component.condition2 = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
fixture.component.condition = false;
fixture.update();
expect(fixture.html).toEqual('<div></div>');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
fixture.component.condition2 = false;
fixture.update();
expect(fixture.html).toEqual('<div><span></span></div>');
fixture.component.condition2 = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
fixture.component.condition2 = false;
fixture.update();
expect(fixture.html).toEqual('<div><span></span></div>');
fixture.component.condition = false;
fixture.update();
expect(fixture.html).toEqual('<div></div>');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('<div><span></span></div>');
fixture.component.condition2 = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
});
it('should work with nested adjacent if blocks', () => {
const ctx:
{condition: boolean,
condition2: boolean,
condition3: boolean} = {condition: true, condition2: false, condition3: true};
/**
* % if(ctx.condition) {
* % if(ctx.condition2) {
* Hello
* % }
* % if(ctx.condition3) {
* World
* % }
* % }
*/
function createTemplate() {
ɵɵcontainer(0);
}
function updateTemplate() {
ɵɵcontainerRefreshStart(0);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(1, 2, 0);
{
if (rf1 & RenderFlags.Create) {
{
ɵɵcontainer(0);
}
{ ɵɵcontainer(1); }
}
if (rf1 & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.condition2) {
let rf2 = ɵɵembeddedViewStart(2, 1, 0);
{
if (rf2 & RenderFlags.Create) {
ɵɵtext(0, 'Hello');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition3) {
let rf2 = ɵɵembeddedViewStart(2, 1, 0);
{
if (rf2 & RenderFlags.Create) {
ɵɵtext(0, 'World');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
const fixture = new TemplateFixture(createTemplate, updateTemplate, 1);
expect(fixture.html).toEqual('World');
ctx.condition2 = true;
fixture.update();
expect(fixture.html).toEqual('HelloWorld');
});
it('should work with adjacent if blocks managing views in the same container', () => {
/**
* % if(ctx.condition1) {
* 1
* % }; if(ctx.condition2) {
* 2
* % }; if(ctx.condition3) {
* 3
* % }
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
if (ctx.condition1) {
const rf1 = ɵɵembeddedViewStart(1, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵtext(0, '1');
}
ɵɵembeddedViewEnd();
} // can't have ; here due linting rules
if (ctx.condition2) {
const rf2 = ɵɵembeddedViewStart(2, 1, 0);
if (rf2 & RenderFlags.Create) {
ɵɵtext(0, '2');
}
ɵɵembeddedViewEnd();
} // can't have ; here due linting rules
if (ctx.condition3) {
const rf3 = ɵɵembeddedViewStart(3, 1, 0);
if (rf3 & RenderFlags.Create) {
ɵɵtext(0, '3');
}
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
}, 1);
const fixture = new ComponentFixture(App);
fixture.component.condition1 = true;
fixture.component.condition2 = true;
fixture.component.condition3 = true;
fixture.update();
expect(fixture.html).toEqual('123');
fixture.component.condition2 = false;
fixture.update();
expect(fixture.html).toEqual('13');
});
it('should work with containers with views as parents', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵtext(1, 'hello'); }
ɵɵelementEnd();
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(2);
{
if (ctx.condition1) {
let rf0 = ɵɵembeddedViewStart(0, 1, 0);
{
if (rf0 & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf0 & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.condition2) {
let rf0 = ɵɵembeddedViewStart(0, 1, 0);
{
if (rf0 & RenderFlags.Create) {
ɵɵtext(0, 'world');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 3);
const fixture = new ComponentFixture(App);
fixture.component.condition1 = true;
fixture.component.condition2 = true;
fixture.update();
expect(fixture.html).toEqual('<div>hello</div>world');
fixture.component.condition1 = false;
fixture.component.condition2 = false;
fixture.update();
expect(fixture.html).toEqual('<div>hello</div>');
});
it('should work with loop block', () => {
let data: string[] = ['a', 'b', 'c'];
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'ul');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
for (let i = 0; i < data.length; i++) {
let rf1 = ɵɵembeddedViewStart(1, 2, 1);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'li');
{ ɵɵtext(1); }
ɵɵelementEnd();
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(1);
ɵɵtextInterpolate(data[i]);
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 2);
const fixture = new ComponentFixture(App);
fixture.update();
expect(fixture.html).toEqual('<ul><li>a</li><li>b</li><li>c</li></ul>');
data = ['e', 'f'];
fixture.update();
expect(fixture.html).toEqual('<ul><li>e</li><li>f</li></ul>');
data = [];
fixture.update();
expect(fixture.html).toEqual('<ul></ul>');
data = ['a', 'b', 'c'];
fixture.update();
expect(fixture.html).toEqual('<ul><li>a</li><li>b</li><li>c</li></ul>');
data.push('d');
fixture.update();
expect(fixture.html).toEqual('<ul><li>a</li><li>b</li><li>c</li><li>d</li></ul>');
data = ['e'];
fixture.update();
expect(fixture.html).toEqual('<ul><li>e</li></ul>');
});
it('should work with nested loop blocks', () => {
let data: string[][] = [['a', 'b', 'c'], ['m', 'n']];
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'ul');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
for (let i = 0; i < data[0].length; i++) {
let rf1 = ɵɵembeddedViewStart(1, 2, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'li');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf1 & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
data[1].forEach((value: string, ind: number) => {
let rf2 = ɵɵembeddedViewStart(2, 1, 1);
if (rf2 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf2 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵtextInterpolate(data[0][i] + value);
}
ɵɵembeddedViewEnd();
});
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 2);
const fixture = new ComponentFixture(App);
fixture.update();
expect(fixture.html).toEqual('<ul><li>aman</li><li>bmbn</li><li>cmcn</li></ul>');
data = [[], []];
fixture.update();
expect(fixture.html).toEqual('<ul></ul>');
});
it('should work with nested loop blocks where nested container is a root node', () => {
let cafes = [
{name: '1', entrees: ['a', 'b', 'c']}, {name: '2', entrees: ['d', 'e', 'f']},
{name: '3', entrees: ['g', 'h', 'i']}
];
/**
* <div>
* Before
* % for (let i = 0; i < cafes.length; i++) {
* <h2> {{ cafes[i].name }} </h2>
* % for (let j = 0; j < cafes[i].entrees; j++) {
* {{ cafes[i].entrees[j] }}
* % }
* -
* % }
* After
* <div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{
ɵɵtext(1, 'Before');
ɵɵcontainer(2);
ɵɵtext(3, 'After');
}
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(2);
{
for (let i = 0; i < cafes.length; i++) {
let rf1 = ɵɵembeddedViewStart(1, 4, 1);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'h2');
{ ɵɵtext(1); }
ɵɵelementEnd();
ɵɵcontainer(2);
ɵɵtext(3, '-');
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(1);
ɵɵtextInterpolate(cafes[i].name);
ɵɵcontainerRefreshStart(2);
{
for (let j = 0; j < cafes[i].entrees.length; j++) {
let rf2 = ɵɵembeddedViewStart(2, 1, 1);
if (rf2 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf2 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵtextInterpolate(cafes[i].entrees[j]);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 4);
const fixture = new ComponentFixture(App);
fixture.update();
expect(fixture.html)
.toEqual('<div>Before<h2>1</h2>abc-<h2>2</h2>def-<h2>3</h2>ghi-After</div>');
cafes = [];
fixture.update();
expect(fixture.html).toEqual('<div>BeforeAfter</div>');
cafes = [
{name: '1', entrees: ['a', 'c']},
{name: '2', entrees: ['d', 'e']},
];
fixture.update();
expect(fixture.html).toEqual('<div>Before<h2>1</h2>ac-<h2>2</h2>de-After</div>');
});
it('should work with loop blocks nested three deep', () => {
let cafes = [
{
name: '1',
entrees:
[{name: 'a', foods: [1, 2]}, {name: 'b', foods: [3, 4]}, {name: 'c', foods: [5, 6]}]
},
{
name: '2',
entrees:
[{name: 'd', foods: [1, 2]}, {name: 'e', foods: [3, 4]}, {name: 'f', foods: [5, 6]}]
}
];
/**
* <div>
* Before
* % for (let i = 0; i < cafes.length; i++) {
* <h2> {{ cafes[i].name }} </h2>
* % for (let j = 0; j < cafes[i].entrees.length; j++) {
* <h3> {{ cafes[i].entrees[j].name }} </h3>
* % for (let k = 0; k < cafes[i].entrees[j].foods.length; k++) {
* {{ cafes[i].entrees[j].foods[k] }}
* % }
* % }
* -
* % }
* After
* <div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{
ɵɵtext(1, 'Before');
ɵɵcontainer(2);
ɵɵtext(3, 'After');
}
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(2);
{
for (let i = 0; i < cafes.length; i++) {
let rf1 = ɵɵembeddedViewStart(1, 4, 1);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'h2');
{ ɵɵtext(1); }
ɵɵelementEnd();
ɵɵcontainer(2);
ɵɵtext(3, '-');
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(1);
ɵɵtextInterpolate(cafes[i].name);
ɵɵcontainerRefreshStart(2);
{
for (let j = 0; j < cafes[i].entrees.length; j++) {
let rf1 = ɵɵembeddedViewStart(1, 3, 1);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'h3');
{ ɵɵtext(1); }
ɵɵelementEnd();
ɵɵcontainer(2);
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(1);
ɵɵtextInterpolate(cafes[i].entrees[j].name);
ɵɵcontainerRefreshStart(2);
{
for (let k = 0; k < cafes[i].entrees[j].foods.length; k++) {
let rf2 = ɵɵembeddedViewStart(1, 1, 1);
if (rf2 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf2 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵtextInterpolate(cafes[i].entrees[j].foods[k]);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 4);
const fixture = new ComponentFixture(App);
fixture.update();
expect(fixture.html)
.toEqual(
'<div>' +
'Before' +
'<h2>1</h2><h3>a</h3>12<h3>b</h3>34<h3>c</h3>56-' +
'<h2>2</h2><h3>d</h3>12<h3>e</h3>34<h3>f</h3>56-' +
'After' +
'</div>');
cafes = [];
fixture.update();
expect(fixture.html).toEqual('<div>BeforeAfter</div>');
});
it('should work with if/else blocks', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(1, 2, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
{ ɵɵtext(1, 'Hello'); }
ɵɵelementEnd();
}
}
ɵɵembeddedViewEnd();
} else {
let rf2 = ɵɵembeddedViewStart(2, 2, 0);
{
if (rf2) {
ɵɵelementStart(0, 'div');
{ ɵɵtext(1, 'Goodbye'); }
ɵɵelementEnd();
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 2);
const fixture = new ComponentFixture(App);
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
fixture.component.condition = false;
fixture.update();
expect(fixture.html).toEqual('<div><div>Goodbye</div></div>');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('<div><span>Hello</span></div>');
});
it('should work with sibling if blocks with children', () => {
let log: string[] = [];
// Intentionally duplicating the templates in test below so we are
// testing the behavior on firstCreatePass for each of these tests
class Comp {
static ɵfac =
() => {
log.push('comp!');
return new Comp();
}
static ɵcmp = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
decls: 0,
vars: 0,
template: function(rf: RenderFlags, ctx: Comp) {}
});
}
class App {
condition = true;
condition2 = true;
static ɵfac = () => new App();
static ɵcmp = ɵɵdefineComponent({
type: App,
selectors: [['app']],
decls: 3,
vars: 0,
template:
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div');
ɵɵcontainer(1);
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(2);
{
if (ctx.condition2) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
directives: () => [Comp]
});
}
const fixture = new ComponentFixture(App);
expect(log).toEqual(['comp!', 'comp!']);
});
it('should work with a sibling if block that starts closed', () => {
let log: string[] = [];
// Intentionally duplicating the templates from above so we are
// testing the behavior on firstCreatePass for each of these tests
class Comp {
static ɵfac =
() => {
log.push('comp!');
return new Comp();
}
static ɵcmp = ɵɵdefineComponent({
type: Comp,
selectors: [['comp']],
decls: 0,
vars: 0,
template: function(rf: RenderFlags, ctx: Comp) {}
});
}
class App {
condition = false;
condition2 = true;
static ɵfac = () => new App();
static ɵcmp = ɵɵdefineComponent({
type: App,
selectors: [['app']],
decls: 3,
vars: 0,
template:
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div');
ɵɵcontainer(1);
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(2);
{
if (ctx.condition2) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
directives: () => [Comp]
});
}
const fixture = new ComponentFixture(App);
expect(log).toEqual(['comp!']);
fixture.component.condition = true;
fixture.update();
expect(log).toEqual(['comp!', 'comp!']);
});
});
describe('JS for loop', () => {
it('should work with sibling for blocks', () => {
const config: {data1: string[], data2: number[]} = {data1: ['a', 'b', 'c'], data2: [1, 2]};
/**
* <div>
* % for (let i = 0; i < ctx.data1.length; i++) {
* {{data1[i]}}
* % } for (let j = 0; j < ctx.data2.length; j++) {
* {{data1[j]}}
* % }
* </div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
for (let i = 0; i < config.data1.length; i++) {
let rf2 = ɵɵembeddedViewStart(1, 1, 1);
if (rf2 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf2 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵtextInterpolate(config.data1[i]);
}
ɵɵembeddedViewEnd();
}
for (let j = 0; j < config.data2.length; j++) {
let rf2 = ɵɵembeddedViewStart(1, 1, 1);
if (rf2 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf2 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵtextInterpolate(config.data2[j]);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 2);
const fixture = new ComponentFixture(App);
expect(fixture.html).toEqual('<div>abc12</div>');
config.data1 = ['e', 'f'];
fixture.update();
expect(fixture.html).toEqual('<div>ef12</div>');
config.data2 = [8];
fixture.update();
expect(fixture.html).toEqual('<div>ef8</div>');
config.data1 = ['x', 'y'];
fixture.update();
expect(fixture.html).toEqual('<div>xy8</div>');
});
});
describe('function calls', () => {
it('should work', () => {
let data: string[] = ['foo', 'bar'];
function spanify(rf: RenderFlags, ctx: {message: string|null}) {
const message = ctx.message;
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'span');
{ ɵɵtext(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵselect(1);
ɵɵtextInterpolate(message);
}
}
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{
ɵɵtext(1, 'Before');
ɵɵcontainer(2);
ɵɵcontainer(3);
ɵɵtext(4, 'After');
}
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(2);
{
let rf0 = ɵɵembeddedViewStart(0, 2, 1);
{ spanify(rf0, {message: data[0]}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(3);
{
let rf0 = ɵɵembeddedViewStart(0, 2, 1);
{ spanify(rf0, {message: data[1]}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
}, 5);
const fixture = new ComponentFixture(App);
expect(fixture.html).toEqual('<div>Before<span>foo</span><span>bar</span>After</div>');
data = [];
fixture.update();
expect(fixture.html).toEqual('<div>Before<span></span><span></span>After</div>');
});
});

View File

@ -9,7 +9,7 @@
import {RendererType2} from '../../src/render/api';
import {getLContext} from '../../src/render3/context_discovery';
import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵhostProperty, ɵɵproperty} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all';
import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {domRendererFactory3, RElement, Renderer3, RendererFactory3} from '../../src/render3/interfaces/renderer';
@ -41,156 +41,6 @@ describe('render3 integration test', () => {
});
});
});
describe('tree', () => {
interface Tree {
beforeLabel?: string;
subTrees?: Tree[];
afterLabel?: string;
}
interface ParentCtx {
beforeTree: Tree;
projectedTree: Tree;
afterTree: Tree;
}
function showLabel(rf: RenderFlags, ctx: {label: string|undefined}) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.label != null) {
let rf1 = ɵɵembeddedViewStart(0, 1, 1);
if (rf1 & RenderFlags.Create) {
ɵɵtext(0);
}
if (rf1 & RenderFlags.Update) {
ɵɵtextInterpolate(ctx.label);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
function showTree(rf: RenderFlags, ctx: {tree: Tree}) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
ɵɵcontainer(1);
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
{ showLabel(rf0, {label: ctx.tree.beforeLabel}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(1);
{
for (let subTree of ctx.tree.subTrees || []) {
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
{ showTree(rf0, {tree: subTree}); }
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(2);
{
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
{ showLabel(rf0, {label: ctx.tree.afterLabel}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
}
class ChildComponent {
// TODO(issue/24571): remove '!'.
beforeTree!: Tree;
// TODO(issue/24571): remove '!'.
afterTree!: Tree;
static ɵfac = () => new ChildComponent;
static ɵcmp = ɵɵdefineComponent({
selectors: [['child']],
type: ChildComponent,
decls: 3,
vars: 0,
template:
function ChildComponentTemplate(
rf: RenderFlags, ctx: {beforeTree: Tree, afterTree: Tree}) {
if (rf & RenderFlags.Create) {
ɵɵprojectionDef();
ɵɵcontainer(0);
ɵɵprojection(1);
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
{ showTree(rf0, {tree: ctx.beforeTree}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(2);
{
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
{ showTree(rf0, {tree: ctx.afterTree}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
},
inputs: {beforeTree: 'beforeTree', afterTree: 'afterTree'}
});
}
function parentTemplate(rf: RenderFlags, ctx: ParentCtx) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'child');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵproperty('beforeTree', ctx.beforeTree);
ɵɵproperty('afterTree', ctx.afterTree);
ɵɵcontainerRefreshStart(1);
{
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
{ showTree(rf0, {tree: ctx.projectedTree}); }
ɵɵembeddedViewEnd();
}
ɵɵcontainerRefreshEnd();
}
}
it('should work with a tree', () => {
const ctx: ParentCtx = {
beforeTree: {subTrees: [{beforeLabel: 'a'}]},
projectedTree: {beforeLabel: 'p'},
afterTree: {afterLabel: 'z'}
};
const defs = [ChildComponent];
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>apz</child>');
ctx.projectedTree = {subTrees: [{}, {}, {subTrees: [{}, {}]}, {}]};
ctx.beforeTree.subTrees!.push({afterLabel: 'b'});
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abz</child>');
ctx.projectedTree.subTrees![1].afterLabel = 'h';
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abhz</child>');
ctx.beforeTree.subTrees!.push({beforeLabel: 'c'});
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abchz</child>');
// To check the context easily:
// console.log(JSON.stringify(ctx));
});
});
});
describe('component styles', () => {

View File

@ -1,120 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentTemplate, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵproperty} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def';
import {ComponentFixture, createComponent} from './render_util';
describe('lifecycles', () => {
function getParentTemplate(name: string) {
return (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, name);
}
if (rf & RenderFlags.Update) {
ɵɵproperty('val', ctx.val);
}
};
}
describe('onInit', () => {
let events: string[];
beforeEach(() => {
events = [];
});
let Comp = createOnInitComponent('comp', (rf: RenderFlags) => {
if (rf & RenderFlags.Create) {
ɵɵprojectionDef();
ɵɵelementStart(0, 'div');
{ ɵɵprojection(1); }
ɵɵelementEnd();
}
}, 2);
let Parent = createOnInitComponent('parent', getParentTemplate('comp'), 1, 1, [Comp]);
let ProjectedComp = createOnInitComponent('projected', (rf: RenderFlags) => {
if (rf & RenderFlags.Create) {
ɵɵtext(0, 'content');
}
}, 1);
function createOnInitComponent(
name: string, template: ComponentTemplate<any>, decls: number, vars: number = 0,
directives: any[] = []) {
return class Component {
val: string = '';
ngOnInit() {
if (!this.val) this.val = '';
events.push(`${name}${this.val}`);
}
static ɵfac = () => new Component();
static ɵcmp = ɵɵdefineComponent({
type: Component,
selectors: [[name]],
decls: decls,
vars: vars,
inputs: {val: 'val'},
template,
directives: directives
});
};
}
class Directive {
ngOnInit() {
events.push('dir');
}
static ɵfac = () => new Directive();
static ɵdir = ɵɵdefineDirective({type: Directive, selectors: [['', 'dir', '']]});
}
const directives = [Comp, Parent, ProjectedComp, Directive, NgIf];
it('should call onInit every time a new view is created (if block)', () => {
/**
* % if (!skip) {
* <comp></comp>
* % }
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (!ctx.skip) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}, 1, 0, directives);
const fixture = new ComponentFixture(App);
expect(events).toEqual(['comp']);
fixture.component.skip = true;
fixture.update();
expect(events).toEqual(['comp']);
fixture.component.skip = false;
fixture.update();
expect(events).toEqual(['comp', 'comp']);
});
});
});

View File

@ -8,14 +8,14 @@
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
import {markDirty, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵreference, ɵɵresolveBody, ɵɵresolveDocument, ɵɵselect, ɵɵtextInterpolate} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵgetCurrentView, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all';
import {ɵɵdefineComponent, ɵɵdefineDirective, ɵɵreference, ɵɵresolveBody, ɵɵresolveDocument} from '../../src/render3/index';
import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵgetCurrentView, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {GlobalTargetResolver} from '../../src/render3/interfaces/renderer';
import {ɵɵrestoreView} from '../../src/render3/state';
import {getRendererFactory2} from './imported_renderer2';
import {ComponentFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, requestAnimationFrame, TemplateFixture} from './render_util';
import {ComponentFixture, containerEl, createComponent, getDirectiveOnNode, renderToHtml, TemplateFixture} from './render_util';
describe('event listeners', () => {
@ -287,277 +287,6 @@ describe('event listeners', () => {
expect(ctx.showing).toBe(false);
});
it('should support listeners in views', () => {
/**
* % if (ctx.showing) {
* <button (click)="onClick()"> Click me </button>
* % }
*/
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.showing) {
if (ɵɵembeddedViewStart(1, 2, 0)) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.onClick();
});
ɵɵtext(1, 'Click me');
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
let comp = new MyComp();
renderToHtml(Template, comp, 1);
const button = containerEl.querySelector('button')!;
button.click();
expect(comp.counter).toEqual(1);
button.click();
expect(comp.counter).toEqual(2);
// the listener should be removed when the view is removed
comp.showing = false;
renderToHtml(Template, comp, 1);
button.click();
expect(comp.counter).toEqual(2);
});
it('should destroy listeners in views with renderer2', () => {
/**
* % if (ctx.showing) {
* <button (click)="onClick()"> Click me </button>
* % }
*/
class AppComp {
counter = 0;
showing = true;
onClick() {
this.counter++;
}
static ɵfac = () => new AppComp();
static ɵcmp = ɵɵdefineComponent({
type: AppComp,
selectors: [['app-comp']],
decls: 1,
vars: 0,
template:
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.showing) {
if (ɵɵembeddedViewStart(0, 2, 0)) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.onClick();
});
ɵɵtext(1, 'Click me');
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
});
}
const fixture = new ComponentFixture(AppComp, {rendererFactory: getRendererFactory2(document)});
const comp = fixture.component;
const button = fixture.hostElement.querySelector('button')!;
button.click();
expect(comp.counter).toEqual(1);
button.click();
expect(comp.counter).toEqual(2);
// the listener should be removed when the view is removed
comp.showing = false;
fixture.update();
button.click();
expect(comp.counter).toEqual(2);
});
it('should destroy listeners in for loops', () => {
/**
* % for (let i = 0; i < ctx.buttons; i++) {
* <button (click)="onClick(i)"> Click me </button>
* % }
*/
class AppComp {
buttons = 2;
counters = [0, 0];
onClick(index: number) {
this.counters[index]++;
}
static ɵfac = () => new AppComp();
static ɵcmp = ɵɵdefineComponent({
type: AppComp,
selectors: [['app-comp']],
decls: 1,
vars: 0,
template:
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
for (let i = 0; i < ctx.buttons; i++) {
if (ɵɵembeddedViewStart(0, 2, 0)) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.onClick(i);
});
ɵɵtext(1, 'Click me');
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
});
}
const fixture = new ComponentFixture(AppComp);
const comp = fixture.component;
const buttons = fixture.hostElement.querySelectorAll('button')!;
buttons[0].click();
expect(comp.counters).toEqual([1, 0]);
buttons[1].click();
expect(comp.counters).toEqual([1, 1]);
// the listener should be removed when the view is removed
comp.buttons = 0;
fixture.update();
buttons[0].click();
buttons[1].click();
expect(comp.counters).toEqual([1, 1]);
});
it('should destroy listeners in for loops with renderer2', () => {
/**
* % for (let i = 0; i < ctx.buttons; i++) {
* <button (click)="onClick(i)"> Click me </button>
* {{ counters[i] }}
* % }
*/
class AppComp {
buttons = 2;
counters = [0, 0];
onClick(index: number) {
this.counters[index]++;
}
static ɵfac = () => new AppComp();
static ɵcmp = ɵɵdefineComponent({
type: AppComp,
selectors: [['app-comp']],
decls: 1,
vars: 0,
template:
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
for (let i = 0; i < ctx.buttons; i++) {
const rf1 = ɵɵembeddedViewStart(1, 4, 1);
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.onClick(i);
});
ɵɵtext(1, 'Click me');
}
ɵɵelementEnd();
ɵɵelementStart(2, 'div');
{ ɵɵtext(3); }
ɵɵelementEnd();
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(3);
ɵɵtextInterpolate(ctx.counters[i]);
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
});
}
const fixture = new ComponentFixture(AppComp, {rendererFactory: getRendererFactory2(document)});
const comp = fixture.component;
const buttons = fixture.hostElement.querySelectorAll('button')!;
const divs = fixture.hostElement.querySelectorAll('div');
buttons[0].click();
expect(comp.counters).toEqual([1, 0]);
expect(divs[0].textContent).toEqual('0');
expect(divs[1].textContent).toEqual('0');
markDirty(comp);
requestAnimationFrame.flush();
expect(divs[0].textContent).toEqual('1');
expect(divs[1].textContent).toEqual('0');
buttons[1].click();
expect(comp.counters).toEqual([1, 1]);
expect(divs[0].textContent).toEqual('1');
expect(divs[1].textContent).toEqual('0');
markDirty(comp);
requestAnimationFrame.flush();
expect(divs[0].textContent).toEqual('1');
expect(divs[1].textContent).toEqual('1');
// the listener should be removed when the view is removed
comp.buttons = 0;
fixture.update();
buttons[0].click();
buttons[1].click();
expect(comp.counters).toEqual([1, 1]);
});
it('should support host listeners on components', () => {
let events: string[] = [];
class MyComp {
@ -718,254 +447,6 @@ describe('event listeners', () => {
expect(comp.counter).toEqual(6);
});
it('should destroy listeners in nested views', () => {
/**
* % if (showing) {
* Hello
* % if (button) {
* <button (click)="onClick()"> Click </button>
* % }
* % }
*/
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.showing) {
let rf1 = ɵɵembeddedViewStart(0, 2, 0);
if (rf1 & RenderFlags.Create) {
ɵɵtext(0, 'Hello');
ɵɵcontainer(1);
}
if (rf1 & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.button) {
let rf1 = ɵɵembeddedViewStart(0, 2, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.onClick();
});
ɵɵtext(1, 'Click');
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
const comp = {
showing: true,
counter: 0,
button: true,
onClick: function() {
this.counter++;
}
};
renderToHtml(Template, comp, 1);
const button = containerEl.querySelector('button')!;
button.click();
expect(comp.counter).toEqual(1);
// the child view listener should be removed when the parent view is removed
comp.showing = false;
renderToHtml(Template, comp, 1);
button.click();
expect(comp.counter).toEqual(1);
});
it('should destroy listeners in component views', () => {
/**
* % if (showing) {
* Hello
* <comp></comp>
* <comp></comp>
* % }
*
* comp:
* <button (click)="onClick()"> Click </button>
*/
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.showing) {
let rf1 = ɵɵembeddedViewStart(0, 3, 0);
if (rf1 & RenderFlags.Create) {
ɵɵtext(0, 'Hello');
ɵɵelement(1, 'comp');
ɵɵelement(2, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
const ctx = {showing: true};
renderToHtml(Template, ctx, 1, 0, [MyComp]);
const buttons = containerEl.querySelectorAll('button')!;
buttons[0].click();
expect(comps[0]!.counter).toEqual(1);
buttons[1].click();
expect(comps[1]!.counter).toEqual(1);
// the child view listener should be removed when the parent view is removed
ctx.showing = false;
renderToHtml(Template, ctx, 1, 0, [MyComp]);
buttons[0].click();
buttons[1].click();
expect(comps[0]!.counter).toEqual(1);
expect(comps[1]!.counter).toEqual(1);
});
it('should destroy global listeners in component views', () => {
const ctx = {showing: true};
const fixture = new TemplateFixture(
() => {
ɵɵcontainer(0);
},
() => {
ɵɵcontainerRefreshStart(0);
{
if (ctx.showing) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'comp');
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
},
1, 0, [MyCompWithGlobalListeners]);
const body = fixture.hostElement.ownerDocument!.body;
body.click();
expect(events).toEqual(['component - body:click']);
// the child view listener should be removed when the parent view is removed
ctx.showing = false;
fixture.update();
body.click();
// expecting no changes in events array
expect(events).toEqual(['component - body:click']);
});
it('should support listeners with sibling nested containers', () => {
/**
* % if (condition) {
* Hello
* % if (sub1) {
* <button (click)="counter1++">there</button>
* % }
*
* % if (sub2) {
* <button (click)="counter2++">world</button>
* % }
* % }
*/
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(0, 3, 0);
if (rf1 & RenderFlags.Create) {
ɵɵtext(0, 'Hello');
ɵɵcontainer(1);
ɵɵcontainer(2);
}
if (rf1 & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.sub1) {
let rf1 = ɵɵembeddedViewStart(0, 2, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.counter1++;
});
ɵɵtext(1, 'Click');
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
ɵɵcontainerRefreshStart(2);
{
if (ctx.sub2) {
let rf1 = ɵɵembeddedViewStart(0, 2, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.counter2++;
});
ɵɵtext(1, 'Click');
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
const ctx = {condition: true, counter1: 0, counter2: 0, sub1: true, sub2: true};
renderToHtml(Template, ctx, 1);
const buttons = containerEl.querySelectorAll('button')!;
buttons[0].click();
expect(ctx.counter1).toEqual(1);
buttons[1].click();
expect(ctx.counter2).toEqual(1);
// the child view listeners should be removed when the parent view is removed
ctx.condition = false;
renderToHtml(Template, ctx, 1);
buttons[0].click();
buttons[1].click();
expect(ctx.counter1).toEqual(1);
expect(ctx.counter2).toEqual(1);
});
it('should support local refs in listeners', () => {
let compInstance: any;

View File

@ -1,117 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {EventEmitter} from '@angular/core';
import {ɵɵdefineComponent, ɵɵdefineDirective} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵlistener, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {renderToHtml} from './render_util';
describe('outputs', () => {
let buttonToggle: ButtonToggle;
class ButtonToggle {
change = new EventEmitter();
resetStream = new EventEmitter();
static ɵfac = () => buttonToggle = new ButtonToggle();
static ɵcmp = ɵɵdefineComponent({
type: ButtonToggle,
selectors: [['button-toggle']],
template: function(rf: RenderFlags, ctx: any) {},
decls: 0,
vars: 0,
outputs: {change: 'change', resetStream: 'reset'}
});
}
let otherDir: OtherDir;
class OtherDir {
changeStream = new EventEmitter();
static ɵfac = () => otherDir = new OtherDir;
static ɵdir = ɵɵdefineDirective(
{type: OtherDir, selectors: [['', 'otherDir', '']], outputs: {changeStream: 'change'}});
}
const deps = [ButtonToggle, OtherDir];
it('should work with outputs at same index in if block', () => {
/**
* <button (click)="onClick()">Click me</button> // outputs: null
* % if (condition) {
* <button-toggle (change)="onChange()"></button-toggle> // outputs: {change: [0, 'change']}
* % } else {
* <div otherDir (change)="onChange()"></div> // outputs: {change: [0,
* 'changeStream']}
* % }
*/
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'button');
{
ɵɵlistener('click', function() {
return ctx.onClick();
});
ɵɵtext(1, 'Click me');
}
ɵɵelementEnd();
ɵɵcontainer(2);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(2);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'button-toggle');
{
ɵɵlistener('change', function() {
return ctx.onChange();
});
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
} else {
if (ɵɵembeddedViewStart(1, 1, 0)) {
ɵɵelementStart(0, 'div', 0);
{
ɵɵlistener('change', function() {
return ctx.onChange();
});
}
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
let counter = 0;
const ctx = {condition: true, onChange: () => counter++, onClick: () => {}};
const attrs = [['otherDir', '']];
renderToHtml(Template, ctx, 3, 0, deps, null, null, false, attrs);
buttonToggle!.change.next();
expect(counter).toEqual(1);
ctx.condition = false;
renderToHtml(Template, ctx, 3, 0, deps, null, null, false, attrs);
expect(counter).toEqual(1);
otherDir!.changeStream.next();
expect(counter).toEqual(2);
});
});

View File

@ -9,8 +9,7 @@
import {Component as _Component, ComponentFactoryResolver, ElementRef, Injectable as _Injectable, InjectFlags, InjectionToken, InjectorType, Provider, RendererFactory2, ViewContainerRef, ɵNgModuleDef as NgModuleDef, ɵɵdefineInjectable, ɵɵdefineInjector, ɵɵinject} from '../../src/core';
import {forwardRef} from '../../src/di/forward_ref';
import {createInjector} from '../../src/di/r3_injector';
import {injectComponentFactoryResolver, ɵɵadvance, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵProvidersFeature, ɵɵtextInterpolate1} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵProvidersFeature, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgModuleFactory} from '../../src/render3/ng_module_ref';
import {getInjector} from '../../src/render3/util/discovery_utils';
@ -983,192 +982,6 @@ describe('providers', () => {
});
});
describe('- embedded views', () => {
it('should have access to viewProviders of the host component', () => {
@Component({
template: '{{s}}{{n}}',
})
class Repeated {
constructor(private s: String, private n: Number) {}
static ɵfac =
() => {
return new Repeated(ɵɵdirectiveInject(String), ɵɵdirectiveInject(Number));
}
static ɵcmp = ɵɵdefineComponent({
type: Repeated,
selectors: [['repeated']],
decls: 2,
vars: 2,
template:
function(fs: RenderFlags, ctx: Repeated) {
if (fs & RenderFlags.Create) {
ɵɵtext(0);
ɵɵtext(1);
}
if (fs & RenderFlags.Update) {
ɵɵtextInterpolate(ctx.s);
ɵɵadvance(1);
ɵɵtextInterpolate(ctx.n);
}
}
});
}
@Component({
template: `<div>
% for (let i = 0; i < 3; i++) {
<repeated></repeated>
% }
</div>`,
providers: [{provide: Number, useValue: 1, multi: true}],
viewProviders:
[{provide: String, useValue: 'foo'}, {provide: Number, useValue: 2, multi: true}],
})
class ComponentWithProviders {
static ɵfac = () => new ComponentWithProviders();
static ɵcmp = ɵɵdefineComponent({
type: ComponentWithProviders,
selectors: [['component-with-providers']],
decls: 2,
vars: 0,
template:
function(fs: RenderFlags, ctx: ComponentWithProviders) {
if (fs & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (fs & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
for (let i = 0; i < 3; i++) {
let rf1 = ɵɵembeddedViewStart(1, 1, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'repeated');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
features:
[
ɵɵProvidersFeature(
[{provide: Number, useValue: 1, multi: true}],
[
{provide: String, useValue: 'foo'},
{provide: Number, useValue: 2, multi: true}
]),
],
directives: [Repeated]
});
}
const fixture = new ComponentFixture(ComponentWithProviders);
expect(fixture.html)
.toEqual(
'<div><repeated>foo1,2</repeated><repeated>foo1,2</repeated><repeated>foo1,2</repeated></div>');
});
it('should have access to viewProviders of the repeated component', () => {
@Component({
template: '{{s}}{{n}}',
providers: [{provide: Number, useValue: 1, multi: true}],
viewProviders:
[{provide: String, useValue: 'bar'}, {provide: Number, useValue: 2, multi: true}]
})
class Repeated {
constructor(private s: String, private n: Number) {}
static ɵfac =
() => {
return new Repeated(ɵɵdirectiveInject(String), ɵɵdirectiveInject(Number));
}
static ɵcmp = ɵɵdefineComponent({
type: Repeated,
selectors: [['repeated']],
decls: 2,
vars: 2,
template:
function(fs: RenderFlags, ctx: Repeated) {
if (fs & RenderFlags.Create) {
ɵɵtext(0);
ɵɵtext(1);
}
if (fs & RenderFlags.Update) {
ɵɵtextInterpolate(ctx.s);
ɵɵadvance(1);
ɵɵtextInterpolate(ctx.n);
}
},
features:
[
ɵɵProvidersFeature(
[{provide: Number, useValue: 1, multi: true}],
[
{provide: String, useValue: 'bar'},
{provide: Number, useValue: 2, multi: true}
]),
],
});
}
@Component({
template: `<div>
% for (let i = 0; i < 3; i++) {
<repeated></repeated>
% }
</div>`,
viewProviders: [{provide: toString, useValue: 'foo'}],
})
class ComponentWithProviders {
static ɵfac = () => new ComponentWithProviders();
static ɵcmp = ɵɵdefineComponent({
type: ComponentWithProviders,
selectors: [['component-with-providers']],
decls: 2,
vars: 0,
template:
function(fs: RenderFlags, ctx: ComponentWithProviders) {
if (fs & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (fs & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
for (let i = 0; i < 3; i++) {
let rf1 = ɵɵembeddedViewStart(1, 1, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'repeated');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
features: [ɵɵProvidersFeature([], [{provide: String, useValue: 'foo'}])],
directives: [Repeated]
});
}
const fixture = new ComponentFixture(ComponentWithProviders);
expect(fixture.html)
.toEqual(
'<div><repeated>bar1,2</repeated><repeated>bar1,2</repeated><repeated>bar1,2</repeated></div>');
});
});
describe('- dynamic components dependency resolution', () => {
let hostComponent: HostComponent|null = null;
@ -1469,118 +1282,6 @@ describe('providers', () => {
expect(injector.get(Some).location).toEqual('From app component');
});
});
describe('lifecycles', () => {
it('should execute ngOnDestroy hooks on providers (and only this one)', () => {
const logs: string[] = [];
@Injectable()
class InjectableWithLifeCycleHooks {
ngOnChanges() {
logs.push('Injectable OnChanges');
}
ngOnInit() {
logs.push('Injectable OnInit');
}
ngDoCheck() {
logs.push('Injectable DoCheck');
}
ngAfterContentInit() {
logs.push('Injectable AfterContentInit');
}
ngAfterContentChecked() {
logs.push('Injectable AfterContentChecked');
}
ngAfterViewInit() {
logs.push('Injectable AfterViewInit');
}
ngAfterViewChecked() {
logs.push('Injectable gAfterViewChecked');
}
ngOnDestroy() {
logs.push('Injectable OnDestroy');
}
}
@Component({template: `<span></span>`, providers: [InjectableWithLifeCycleHooks]})
class MyComponent {
constructor(foo: InjectableWithLifeCycleHooks) {}
static ɵfac =
() => {
return new MyComponent(ɵɵdirectiveInject(InjectableWithLifeCycleHooks));
}
static ɵcmp = ɵɵdefineComponent({
type: MyComponent,
selectors: [['my-comp']],
decls: 1,
vars: 0,
template:
(rf: RenderFlags, ctx: MyComponent) => {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'span');
}
},
features: [ɵɵProvidersFeature([InjectableWithLifeCycleHooks])]
});
}
@Component({
template: `
<div>
% if (ctx.condition) {
<my-comp></my-comp>
% }
</div>
`,
})
class App {
public condition = true;
static ɵfac = () => new App();
static ɵcmp = ɵɵdefineComponent({
type: App,
selectors: [['app-cmp']],
decls: 2,
vars: 0,
template:
(rf: RenderFlags, ctx: App) => {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(1);
{
if (ctx.condition) {
let rf1 = ɵɵembeddedViewStart(1, 2, 1);
{
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'my-comp');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
directives: [MyComponent]
});
}
const fixture = new ComponentFixture(App);
fixture.update();
expect(fixture.html).toEqual('<div><my-comp><span></span></my-comp></div>');
fixture.component.condition = false;
fixture.update();
expect(fixture.html).toEqual('<div></div>');
expect(logs).toEqual(['Injectable OnDestroy']);
});
});
});
interface ComponentTest {
providers?: Provider[];

View File

@ -1,89 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ɵɵdefineComponent, ɵɵproperty, ɵɵselect} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {ɵɵpureFunction2} from '../../src/render3/pure_function';
import {getDirectiveOnNode, renderToHtml} from '../../test/render3/render_util';
describe('object literals', () => {
let objectComp: ObjectComp;
class ObjectComp {
// TODO(issue/24571): remove '!'.
config!: {[key: string]: any};
static ɵfac = function ObjectComp_Factory() {
return objectComp = new ObjectComp();
};
static ɵcmp = ɵɵdefineComponent({
type: ObjectComp,
selectors: [['object-comp']],
decls: 0,
vars: 1,
template: function ObjectComp_Template() {},
inputs: {config: 'config'}
});
}
const defs = [ObjectComp];
// NOTE: This test cannot be ported to acceptance tests with TestBed because
// the syntax is still unsupported.
it('should support multiple view instances with multiple bindings', () => {
let objectComps: ObjectComp[] = [];
/**
* % for(let i = 0; i < 2; i++) {
* <object-comp [config]="{opacity: configs[i].opacity, duration: configs[i].duration}">
* </object-comp>
* % }
*/
function Template(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
for (let i = 0; i < 2; i++) {
let rf1 = ɵɵembeddedViewStart(0, 1, 4);
if (rf1 & RenderFlags.Create) {
ɵɵelementStart(0, 'object-comp');
objectComps.push(getDirectiveOnNode(0));
ɵɵelementEnd();
}
if (rf1 & RenderFlags.Update) {
ɵɵselect(0);
ɵɵproperty(
'config',
ɵɵpureFunction2(1, e0_ff, ctx.configs[i].opacity, ctx.configs[i].duration));
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
}
const e0_ff = (v1: any, v2: any) => {
return {opacity: v1, duration: v2};
};
const configs = [{opacity: 0, duration: 500}, {opacity: 1, duration: 600}];
renderToHtml(Template, {configs}, 1, 0, defs);
expect(objectComps[0].config).toEqual({opacity: 0, duration: 500});
expect(objectComps[1].config).toEqual({opacity: 1, duration: 600});
configs[0].duration = 1000;
renderToHtml(Template, {configs}, 1, 0, defs);
expect(objectComps[0].config).toEqual({opacity: 0, duration: 1000});
expect(objectComps[1].config).toEqual({opacity: 1, duration: 600});
});
});

View File

@ -8,9 +8,8 @@
import {ElementRef, QueryList, TemplateRef, ViewContainerRef} from '@angular/core';
import {EventEmitter} from '../..';
import {AttributeMarker, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵProvidersFeature} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all';
import {ɵɵdirectiveInject, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementStart, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {ɵɵcontentQuery, ɵɵloadQuery, ɵɵqueryRefresh, ɵɵviewQuery} from '../../src/render3/query';
import {getLView} from '../../src/render3/state';
@ -1298,68 +1297,6 @@ describe('query', () => {
});
});
describe('queryList', () => {
it('should be destroyed when the containing view is destroyed', () => {
let queryInstance: QueryList<any>;
const SimpleComponentWithQuery = createComponent(
'some-component-with-query',
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'div', null, 0);
}
},
2, 0, [], [],
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵviewQuery(['foo'], false);
}
if (rf & RenderFlags.Update) {
let tmp: any;
ɵɵqueryRefresh(tmp = ɵɵloadQuery<QueryList<any>>()) &&
(ctx.query = queryInstance = tmp as QueryList<any>);
}
},
[], [], undefined, [['foo', '']]);
function createTemplate() {
ɵɵcontainer(0);
}
function updateTemplate() {
ɵɵcontainerRefreshStart(0);
{
if (condition) {
let rf1 = ɵɵembeddedViewStart(1, 1, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'some-component-with-query');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
/**
* % if (condition) {
* <some-component-with-query></some-component-with-query>
* %}
*/
let condition = true;
const t =
new TemplateFixture(createTemplate, updateTemplate, 1, 0, [SimpleComponentWithQuery]);
expect(t.html).toEqual('<some-component-with-query><div></div></some-component-with-query>');
expect((queryInstance!.changes as EventEmitter<any>).closed).toBeFalsy();
condition = false;
t.update();
expect(t.html).toEqual('');
expect((queryInstance!.changes as EventEmitter<any>).closed).toBeTruthy();
});
});
it('should restore queries if view changes', () => {
class SomeDir {
constructor(public vcr: ViewContainerRef, public temp: TemplateRef<any>) {
@ -1481,55 +1418,6 @@ describe('query', () => {
`Expected content query results to be available when ngAfterContentChecked was called.`);
});
it('should support content queries for directives within repeated embedded views', () => {
/**
* % for (let i = 0; i < 3; i++) {
* <div with-content>
* <span #foo></span>
* </div>
* % }
*/
const AppComponent = createComponent(
'app-component',
function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵcontainer(0);
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(0);
{
for (let i = 0; i < 3; i++) {
let rf = ɵɵembeddedViewStart(1, 3, 0);
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'div', 0);
{ ɵɵelement(1, 'span', null, 1); }
ɵɵelementEnd();
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
1, 0, [WithContentDirective], [], null, [], [], undefined,
[[AttributeMarker.Bindings, 'with-content'], ['foo', '']]);
const fixture = new ComponentFixture(AppComponent);
expect(withContentInstance!.foos.length)
.toBe(1, `Expected content query to match <span #foo>.`);
expect(withContentInstance!.contentInitQuerySnapshot)
.toBe(
1,
`Expected content query results to be available when ngAfterContentInit was called.`);
expect(withContentInstance!.contentCheckedQuerySnapshot)
.toBe(
1,
`Expected content query results to be available when ngAfterContentChecked was called.`);
});
it('should not match directive host with content queries', () => {
/**
* <div with-content #foo>

View File

@ -8,11 +8,11 @@
import {RendererType2, ViewEncapsulation} from '../../src/core';
import {ɵɵdefineComponent} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtext} from '../../src/render3/instructions/all';
import {ɵɵelement, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {getRendererFactory2} from './imported_renderer2';
import {document, renderToHtml, TemplateFixture} from './render_util';
import {document, renderToHtml} from './render_util';
describe('renderer factory lifecycle', () => {
let logs: string[] = [];
@ -109,101 +109,3 @@ describe('renderer factory lifecycle', () => {
expect(logs).toEqual(['begin', 'function_with_component update', 'component update', 'end']);
});
});
describe('Renderer2 destruction hooks', () => {
const rendererFactory = getRendererFactory2(document);
it('should call renderer.destroyNode for each node destroyed', () => {
let condition = true;
function createTemplate() {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
function updateTemplate() {
ɵɵcontainerRefreshStart(1);
{
if (condition) {
let rf1 = ɵɵembeddedViewStart(1, 3, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'span');
ɵɵelement(1, 'span');
ɵɵelement(2, 'span');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
const t = new TemplateFixture(
createTemplate, updateTemplate, 2, 0, null, null, null, rendererFactory);
expect(t.html).toEqual('<div><span></span><span></span><span></span></div>');
condition = false;
t.update();
expect(t.html).toEqual('<div></div>');
expect(ngDevMode).toHaveProperties({rendererDestroy: 0, rendererDestroyNode: 3});
});
it('should call renderer.destroy for each component destroyed', () => {
class SimpleComponent {
static ɵfac = () => new SimpleComponent;
static ɵcmp = ɵɵdefineComponent({
type: SimpleComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['simple']],
decls: 1,
vars: 0,
template:
function(rf: RenderFlags, ctx: SimpleComponent) {
if (rf & RenderFlags.Create) {
ɵɵelement(0, 'span');
}
},
});
}
let condition = true;
function createTemplate() {
ɵɵelementStart(0, 'div');
{ ɵɵcontainer(1); }
ɵɵelementEnd();
}
function updateTemplate() {
ɵɵcontainerRefreshStart(1);
{
if (condition) {
let rf1 = ɵɵembeddedViewStart(1, 3, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵelement(0, 'simple');
ɵɵelement(1, 'span');
ɵɵelement(2, 'simple');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
const t = new TemplateFixture(
createTemplate, updateTemplate, 2, 0, [SimpleComponent], null, null, rendererFactory);
expect(t.html).toEqual(
'<div><simple><span></span></simple><span></span><simple><span></span></simple></div>');
condition = false;
t.update();
expect(t.html).toEqual('<div></div>');
expect(ngDevMode).toHaveProperties({rendererDestroy: 2, rendererDestroyNode: 3});
});
});

View File

@ -6,16 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ComponentRef, ElementRef, QueryList, TemplateRef, ViewContainerRef, ViewRef,} from '../../src/core';
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, QueryList, TemplateRef, ViewContainerRef, ViewRef} from '../../src/core';
import {ViewEncapsulation} from '../../src/metadata';
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵlistener, ɵɵloadQuery, ɵɵqueryRefresh, ɵɵviewQuery,} from '../../src/render3/index';
import {ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵtemplate, ɵɵtext,} from '../../src/render3/instructions/all';
import {injectComponentFactoryResolver, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵlistener, ɵɵloadQuery, ɵɵqueryRefresh, ɵɵviewQuery} from '../../src/render3/index';
import {ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {RElement} from '../../src/render3/interfaces/renderer';
import {getLView} from '../../src/render3/state';
import {getNativeByIndex} from '../../src/render3/util/view_utils';
import {ComponentFixture, createComponent, TemplateFixture,} from './render_util';
import {ComponentFixture, createComponent, TemplateFixture} from './render_util';
const Component: typeof _Component = function(...args: any[]): any {
// In test we use @Component for documentation only so it's safe to mock out the implementation.
@ -133,110 +133,6 @@ describe('ViewContainerRef', () => {
directiveInstances![0].insertTpl({});
expect(fixture.html).toEqual('before|AB|after');
});
it('should add embedded views at the right position in the DOM tree (ng-template next to a JS block)',
() => {
let directiveInstance: TestDirective;
class TestDirective {
static ɵfac = () => directiveInstance = new TestDirective(
ɵɵdirectiveInject(ViewContainerRef as any), ɵɵdirectiveInject(TemplateRef as any))
static ɵdir =
ɵɵdefineDirective({type: TestDirective, selectors: [['', 'testdir', '']]});
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
insertTpl(ctx: {}) {
this._vcRef.createEmbeddedView(this._tplRef, ctx);
}
insertTpl2(ctx: {}) {
const viewRef = this._tplRef.createEmbeddedView(ctx);
this._vcRef.insert(viewRef);
}
remove(index?: number) {
this._vcRef.remove(index);
}
}
function EmbeddedTemplateA(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
ɵɵtext(0, 'A');
}
}
/**
* before|
* <ng-template testDir>A<ng-template>
* % if (condition) {
* B
* % }
* |after
*/
class TestComponent {
condition = false;
// TODO(issue/24571): remove '!'.
testDir!: TestDirective;
static ɵfac = () => new TestComponent();
static ɵcmp = ɵɵdefineComponent({
type: TestComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['test-cmp']],
decls: 4,
vars: 0,
consts: [['testdir', '']],
template:
(rf: RenderFlags, cmp: TestComponent) => {
if (rf & RenderFlags.Create) {
ɵɵtext(0, 'before|');
ɵɵtemplate(1, EmbeddedTemplateA, 1, 0, 'ng-template', 0);
ɵɵcontainer(2);
ɵɵtext(3, '|after');
}
if (rf & RenderFlags.Update) {
ɵɵcontainerRefreshStart(2);
{
if (cmp.condition) {
let rf1 = ɵɵembeddedViewStart(0, 1, 0);
{
if (rf1 & RenderFlags.Create) {
ɵɵtext(0, 'B');
}
}
ɵɵembeddedViewEnd();
}
}
ɵɵcontainerRefreshEnd();
}
},
directives: [TestDirective]
});
}
const fixture = new ComponentFixture(TestComponent);
expect(fixture.html).toEqual('before||after');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('before|B|after');
directiveInstance!.insertTpl({});
expect(fixture.html).toEqual('before|AB|after');
fixture.component.condition = false;
fixture.update();
expect(fixture.html).toEqual('before|A|after');
directiveInstance!.insertTpl2({});
expect(fixture.html).toEqual('before|AA|after');
fixture.component.condition = true;
fixture.update();
expect(fixture.html).toEqual('before|AAB|after');
});
});
describe('createComponent', () => {