fix(ivy): ngOnChanges only runs for binding updates (#27965)

PR Close #27965
This commit is contained in:
Ben Lesh 2018-12-20 17:23:25 -08:00 committed by Andrew Kushnir
parent b0caf02d4f
commit 8ebdb437dc
47 changed files with 1468 additions and 1397 deletions

View File

@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"runtime": 1497,
"main": 187134,
"main": 187437,
"polyfills": 59608
}
}

View File

@ -34,14 +34,20 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange
*/
@Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges {
// TODO(issue/24571): remove '!'.
private _viewRef !: EmbeddedViewRef<any>;
private _viewRef: EmbeddedViewRef<any>|null = null;
// TODO(issue/24571): remove '!'.
@Input() public ngTemplateOutletContext !: Object;
/**
* A context object to attach to the {@link EmbeddedViewRef}. This should be an
* object, the object's keys will be available for binding by the local template `let`
* declarations.
* Using the key `$implicit` in the context object will set its value as default.
*/
@Input() public ngTemplateOutletContext: Object|null = null;
// TODO(issue/24571): remove '!'.
@Input() public ngTemplateOutlet !: TemplateRef<any>;
/**
* A string defining the template reference and optionally the context object for the template.
*/
@Input() public ngTemplateOutlet: TemplateRef<any>|null = null;
constructor(private _viewContainerRef: ViewContainerRef) {}
@ -97,7 +103,7 @@ export class NgTemplateOutlet implements OnChanges {
private _updateExistingContext(ctx: Object): void {
for (let propName of Object.keys(ctx)) {
(<any>this._viewRef.context)[propName] = (<any>this.ngTemplateOutletContext)[propName];
(<any>this._viewRef !.context)[propName] = (<any>this.ngTemplateOutletContext)[propName];
}
}
}

View File

@ -174,11 +174,6 @@ export function extractDirectiveMetadata(
const providers: Expression|null =
directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null;
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = members.some(
member => !member.isStatic && member.kind === ClassMemberKind.Method &&
member.name === 'ngOnChanges');
// Parse exportAs.
let exportAs: string[]|null = null;
if (directive.has('exportAs')) {
@ -197,9 +192,6 @@ export function extractDirectiveMetadata(
const metadata: R3DirectiveMetadata = {
name: clazz.name !.text,
deps: getConstructorDependencies(clazz, reflector, isCore), host,
lifecycle: {
usesOnChanges,
},
inputs: {...inputsFromMeta, ...inputsFromFields},
outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector,
type: new WrappedNodeExpr(clazz.name !),

View File

@ -2117,7 +2117,6 @@ describe('compiler compliance', () => {
selectors: [["lifecycle-comp"]],
factory: function LifecycleComp_Factory(t) { return new (t || LifecycleComp)(); },
inputs: {nameMin: ["name", "nameMin"]},
features: [$r3$.ɵNgOnChangesFeature],
consts: 0,
vars: 0,
template: function LifecycleComp_Template(rf, ctx) {},
@ -2242,7 +2241,6 @@ describe('compiler compliance', () => {
factory: function ForOfDirective_Factory(t) {
return new (t || ForOfDirective)($r3$.ɵdirectiveInject(ViewContainerRef), $r3$.ɵdirectiveInject(TemplateRef));
},
features: [$r3$.ɵNgOnChangesFeature],
inputs: {forOf: "forOf"}
});
`;
@ -2318,7 +2316,6 @@ describe('compiler compliance', () => {
factory: function ForOfDirective_Factory(t) {
return new (t || ForOfDirective)($r3$.ɵdirectiveInject(ViewContainerRef), $r3$.ɵdirectiveInject(TemplateRef));
},
features: [$r3$.ɵNgOnChangesFeature],
inputs: {forOf: "forOf"}
});
`;

View File

@ -115,7 +115,6 @@ export interface R3DirectiveMetadataFacade {
queries: R3QueryMetadataFacade[];
host: {[key: string]: string};
propMetadata: {[key: string]: any[]};
lifecycle: {usesOnChanges: boolean;};
inputs: string[];
outputs: string[];
usesInheritance: boolean;

View File

@ -185,8 +185,6 @@ export class Identifiers {
static registerContentQuery:
o.ExternalReference = {name: 'ɵregisterContentQuery', moduleName: CORE};
static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE};
static InheritDefinitionFeature:
o.ExternalReference = {name: 'ɵInheritDefinitionFeature', moduleName: CORE};

View File

@ -74,17 +74,6 @@ export interface R3DirectiveMetadata {
properties: {[key: string]: string};
};
/**
* Information about usage of specific lifecycle events which require special treatment in the
* code generator.
*/
lifecycle: {
/**
* Whether the directive uses NgOnChanges.
*/
usesOnChanges: boolean;
};
/**
* A mapping of input field names to the property names.
*/

View File

@ -128,7 +128,6 @@ function baseDirectiveFields(
*/
function addFeatures(
definitionMap: DefinitionMap, meta: R3DirectiveMetadata | R3ComponentMetadata) {
// e.g. `features: [NgOnChangesFeature]`
const features: o.Expression[] = [];
const providers = meta.providers;
@ -144,9 +143,7 @@ function addFeatures(
if (meta.usesInheritance) {
features.push(o.importExpr(R3.InheritDefinitionFeature));
}
if (meta.lifecycle.usesOnChanges) {
features.push(o.importExpr(R3.NgOnChangesFeature));
}
if (features.length) {
definitionMap.set('features', o.literalArr(features));
}
@ -427,10 +424,6 @@ function directiveMetadataFromGlobalMetadata(
selector: directive.selector,
deps: dependenciesFromGlobalMetadata(directive.type, outputCtx, reflector),
queries: queriesFromGlobalMetadata(directive.queries, outputCtx),
lifecycle: {
usesOnChanges:
directive.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges),
},
host: {
attributes: directive.hostAttributes,
listeners: summary.hostListeners,

View File

@ -11,8 +11,7 @@ import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
import {IterableDifferFactory, IterableDiffers} from './differs/iterable_differs';
import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
export {SimpleChanges} from '../metadata/lifecycle_hooks';
export {SimpleChange, WrappedValue, devModeEqual} from './change_detection_util';
export {WrappedValue, devModeEqual} from './change_detection_util';
export {ChangeDetectorRef} from './change_detector_ref';
export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants';
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
@ -21,6 +20,7 @@ export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
export {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
export {PipeTransform} from './pipe_transform';
export {SimpleChange, SimpleChanges} from './simple_change';

View File

@ -64,20 +64,6 @@ export class WrappedValue {
static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; }
}
/**
* Represents a basic change from a previous to a new value.
*
* @publicApi
*/
export class SimpleChange {
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
/**
* Check whether the new value is the first value assigned.
*/
isFirstChange(): boolean { return this.firstChange; }
}
export function isListLikeIterable(obj: any): boolean {
if (!isJsObject(obj)) return false;
return Array.isArray(obj) ||

View File

@ -0,0 +1,35 @@
/**
* @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
*/
/**
* Represents a basic change from a previous to a new value for a single
* property on a directive instance. Passed as a value in a
* {@link SimpleChanges} object to the `ngOnChanges` hook.
*
* @see `OnChanges`
*
* @publicApi
*/
export class SimpleChange {
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
/**
* Check whether the new value is the first value assigned.
*/
isFirstChange(): boolean { return this.firstChange; }
}
/**
* A hashtable of changes represented by {@link SimpleChange} objects stored
* at the declared property name they belong to on a Directive or Component. This is
* the type passed to the `ngOnChanges` hook.
*
* @see `OnChanges`
*
* @publicApi
*/
export interface SimpleChanges { [propName: string]: SimpleChange; }

View File

@ -28,7 +28,6 @@ export {
templateRefExtractor as ɵtemplateRefExtractor,
ProvidersFeature as ɵProvidersFeature,
InheritDefinitionFeature as ɵInheritDefinitionFeature,
NgOnChangesFeature as ɵNgOnChangesFeature,
LifecycleHooksFeature as ɵLifecycleHooksFeature,
NgModuleType as ɵNgModuleType,
NgModuleRef as ɵRender3NgModuleRef,

View File

@ -9,13 +9,12 @@
import {ChangeDetectionStrategy} from '../change_detection/constants';
import {Provider} from '../di';
import {Type} from '../interface/type';
import {NG_BASE_DEF} from '../render3/fields';
import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../render3/fields';
import {compileComponent as render3CompileComponent, compileDirective as render3CompileDirective} from '../render3/jit/directive';
import {compilePipe as render3CompilePipe} from '../render3/jit/pipe';
import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators';
import {noop} from '../util/noop';
import {fillProperties} from '../util/property';
import {ViewEncapsulation} from './view';
@ -715,21 +714,46 @@ const initializeBaseDef = (target: any): void => {
};
/**
* Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators.
* @param key "inputs" or "outputs"
* Returns a function that will update the static definition on a class to have the
* appropriate input or output mapping.
*
* Will also add an {@link ngBaseDef} property to a directive if no `ngDirectiveDef`
* or `ngComponentDef` is present. This is done because a class may have {@link InputDecorator}s and
* {@link OutputDecorator}s without having a {@link ComponentDecorator} or {@link DirectiveDecorator},
* and those inputs and outputs should still be inheritable, we need to add an
* `ngBaseDef` property if there are no existing `ngComponentDef` or `ngDirectiveDef`
* properties, so that we can track the inputs and outputs for inheritance purposes.
*
* @param getPropertyToUpdate A function that maps to either the `inputs` property or the
* `outputs` property of a definition.
* @returns A function that, the called, will add a `ngBaseDef` if no other definition is present,
* then update the `inputs` or `outputs` on it, depending on what was selected by `getPropertyToUpdate`
*
*
* @see InputDecorator
* @see OutputDecorator
* @see InheritenceFeature
*/
const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any}) => any) =>
(target: any, name: string, ...args: any[]) => {
const constructor = target.constructor;
function getOrCreateDefinitionAndUpdateMappingFor(
getPropertyToUpdate: (baseDef: {inputs?: any, outputs?: any}) => any) {
return function updateIOProp(target: any, name: string, ...args: any[]) {
const constructor = target.constructor;
if (!constructor.hasOwnProperty(NG_BASE_DEF)) {
initializeBaseDef(target);
}
let def: any =
constructor[NG_COMPONENT_DEF] || constructor[NG_DIRECTIVE_DEF] || constructor[NG_BASE_DEF];
const baseDef = constructor.ngBaseDef;
const defProp = getProp(baseDef);
if (!def) {
initializeBaseDef(target);
def = constructor[NG_BASE_DEF];
}
const defProp = getPropertyToUpdate(def);
// Use of `in` because we *do* want to check the prototype chain here.
if (!(name in defProp)) {
defProp[name] = args[0];
};
}
};
}
/**
* @Annotation
@ -737,7 +761,7 @@ const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any
*/
export const Input: InputDecorator = makePropDecorator(
'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.inputs || {}));
getOrCreateDefinitionAndUpdateMappingFor(def => def.inputs || {}));
/**
* Type of the Output decorator / constructor function.
@ -777,7 +801,7 @@ export interface Output { bindingPropertyName?: string; }
*/
export const Output: OutputDecorator = makePropDecorator(
'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.outputs || {}));
getOrCreateDefinitionAndUpdateMappingFor(def => def.outputs || {}));

View File

@ -5,19 +5,8 @@
* 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 {SimpleChanges} from '../change_detection/simple_change';
import {SimpleChange} from '../change_detection/change_detection_util';
/**
* Defines an object that associates properties with
* instances of `SimpleChange`.
*
* @see `OnChanges`
*
* @publicApi
*/
export interface SimpleChanges { [propName: string]: SimpleChange; }
/**
* @description

View File

@ -17,7 +17,7 @@ import {assertComponentType} from './assert';
import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {publishDefaultGlobalUtils} from './global_utils';
import {queueInitHooks, queueLifecycleHooks} from './hooks';
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
import {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
@ -237,10 +237,11 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): v
const rootTView = readPatchedLView(component) ![TVIEW];
const dirIndex = rootTView.data.length - 1;
queueInitHooks(dirIndex, def.onInit, def.doCheck, rootTView);
registerPreOrderHooks(dirIndex, def, rootTView);
// TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on
// LNode).
queueLifecycleHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
registerPostOrderHooks(
rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
}
/**

View File

@ -12,6 +12,7 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
@ -257,7 +258,7 @@ function findViaDirective(lView: LView, directiveInstance: {}): number {
const directiveIndexStart = tNode.directiveStart;
const directiveIndexEnd = tNode.directiveEnd;
for (let i = directiveIndexStart; i < directiveIndexEnd; i++) {
if (lView[i] === directiveInstance) {
if (unwrapOnChangesDirectiveWrapper(lView[i]) === directiveInstance) {
return tNode.index;
}
}

View File

@ -202,7 +202,7 @@ export function defineComponent<T>(componentDefinition: {
/**
* A list of optional features to apply.
*
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}
* See: {@link ProvidersFeature}
*/
features?: ComponentDefFeature[];
@ -265,6 +265,7 @@ export function defineComponent<T>(componentDefinition: {
inputs: null !, // assigned in noSideEffects
outputs: null !, // assigned in noSideEffects
exportAs: componentDefinition.exportAs || null,
onChanges: typePrototype.ngOnChanges || null,
onInit: typePrototype.ngOnInit || null,
doCheck: typePrototype.ngDoCheck || null,
afterContentInit: typePrototype.ngAfterContentInit || null,
@ -583,7 +584,7 @@ export const defineDirective = defineComponent as any as<T>(directiveDefinition:
/**
* A list of optional features to apply.
*
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature}
* See: {@link ProvidersFeature}, {@link InheritDefinitionFeature}
*/
features?: DirectiveDefFeature[];

View File

@ -20,6 +20,7 @@ import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInject
import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node';
import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state';
import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util';
@ -514,6 +515,8 @@ export function getNodeInjectable(
factory.resolving = false;
setTNodeAndViewData(savePreviousOrParentTNode, saveLView);
}
} else {
value = unwrapOnChangesDirectiveWrapper(value);
}
return value;
}

View File

@ -35,8 +35,6 @@ function getSuperType(type: Type<any>): Type<any>&
export function InheritDefinitionFeature(definition: DirectiveDef<any>| ComponentDef<any>): void {
let superType = getSuperType(definition.type);
debugger;
while (superType) {
let superDef: DirectiveDef<any>|ComponentDef<any>|undefined = undefined;
if (isComponentDef(definition)) {
@ -62,7 +60,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
}
if (baseDef) {
// Merge inputs and outputs
fillProperties(definition.inputs, baseDef.inputs);
fillProperties(definition.declaredInputs, baseDef.declaredInputs);
fillProperties(definition.outputs, baseDef.outputs);
@ -127,7 +124,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
}
}
// Merge inputs and outputs
fillProperties(definition.inputs, superDef.inputs);
fillProperties(definition.declaredInputs, superDef.declaredInputs);
@ -143,6 +139,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
definition.doCheck = definition.doCheck || superDef.doCheck;
definition.onDestroy = definition.onDestroy || superDef.onDestroy;
definition.onInit = definition.onInit || superDef.onInit;
definition.onChanges = definition.onChanges || superDef.onChanges;
// Run parent features
const features = superDef.features;

View File

@ -1,124 +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 {SimpleChange} from '../../change_detection/change_detection_util';
import {OnChanges, SimpleChanges} from '../../metadata/lifecycle_hooks';
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
const PRIVATE_PREFIX = '__ngOnChanges_';
type OnChangesExpando = OnChanges & {
__ngOnChanges_: SimpleChanges|null|undefined;
// tslint:disable-next-line:no-any Can hold any value
[key: string]: any;
};
/**
* The NgOnChangesFeature decorates a component with support for the ngOnChanges
* lifecycle hook, so it should be included in any component that implements
* that hook.
*
* If the component or directive uses inheritance, the NgOnChangesFeature MUST
* be included as a feature AFTER {@link InheritDefinitionFeature}, otherwise
* inherited properties will not be propagated to the ngOnChanges lifecycle
* hook.
*
* Example usage:
*
* ```
* static ngComponentDef = defineComponent({
* ...
* inputs: {name: 'publicName'},
* features: [NgOnChangesFeature]
* });
* ```
*/
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
const publicToDeclaredInputs = definition.declaredInputs;
const publicToMinifiedInputs = definition.inputs;
const proto = definition.type.prototype;
for (const publicName in publicToDeclaredInputs) {
if (publicToDeclaredInputs.hasOwnProperty(publicName)) {
const minifiedKey = publicToMinifiedInputs[publicName];
const declaredKey = publicToDeclaredInputs[publicName];
const privateMinKey = PRIVATE_PREFIX + minifiedKey;
// Walk the prototype chain to see if we find a property descriptor
// That way we can honor setters and getters that were inherited.
let originalProperty: PropertyDescriptor|undefined = undefined;
let checkProto = proto;
while (!originalProperty && checkProto &&
Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) {
originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey);
checkProto = Object.getPrototypeOf(checkProto);
}
const getter = originalProperty && originalProperty.get;
const setter = originalProperty && originalProperty.set;
// create a getter and setter for property
Object.defineProperty(proto, minifiedKey, {
get: getter ||
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }),
set<T>(this: OnChangesExpando, value: T) {
let simpleChanges = this[PRIVATE_PREFIX];
if (!simpleChanges) {
simpleChanges = {};
// Place where we will store SimpleChanges if there is a change
Object.defineProperty(this, PRIVATE_PREFIX, {value: simpleChanges, writable: true});
}
const isFirstChange = !this.hasOwnProperty(privateMinKey);
const currentChange = simpleChanges[declaredKey];
if (currentChange) {
currentChange.currentValue = value;
} else {
simpleChanges[declaredKey] =
new SimpleChange(this[privateMinKey], value, isFirstChange);
}
if (isFirstChange) {
// Create a place where the actual value will be stored and make it non-enumerable
Object.defineProperty(this, privateMinKey, {value, writable: true});
} else {
this[privateMinKey] = value;
}
if (setter) setter.call(this, value);
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode
});
}
}
// If an onInit hook is defined, it will need to wrap the ngOnChanges call
// so the call order is changes-init-check in creation mode. In subsequent
// change detection runs, only the check wrapper will be called.
if (definition.onInit != null) {
definition.onInit = onChangesWrapper(definition.onInit);
}
definition.doCheck = onChangesWrapper(definition.doCheck);
}
// This option ensures that the ngOnChanges lifecycle hook will be inherited
// from superclasses (in InheritDefinitionFeature).
(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
function onChangesWrapper(delegateHook: (() => void) | null) {
return function(this: OnChangesExpando) {
const simpleChanges = this[PRIVATE_PREFIX];
if (simpleChanges != null) {
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
if (delegateHook) delegateHook.apply(this);
};
}

View File

@ -6,92 +6,117 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChanges} from '../change_detection/simple_change';
import {assertEqual} from '../util/assert';
import {DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node';
import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view';
import {OnChangesDirectiveWrapper, unwrapOnChangesDirectiveWrapper} from './onchanges_util';
/**
* If this is the first template pass, any ngOnInit or ngDoCheck hooks will be queued into
* TView.initHooks during directiveCreate.
* Adds all directive lifecycle hooks from the given `DirectiveDef` to the given `TView`.
*
* The directive index and hook type are encoded into one number (1st bit: type, remaining bits:
* directive index), then saved in the even indices of the initHooks array. The odd indices
* hold the hook functions themselves.
* Must be run *only* on the first template pass.
*
* @param index The index of the directive in LView
* @param hooks The static hooks map on the directive def
* The TView's hooks arrays are arranged in alternating pairs of directiveIndex and hookFunction,
* i.e.: `[directiveIndexA, hookFunctionA, directiveIndexB, hookFunctionB, ...]`. For `OnChanges`
* hooks, the `directiveIndex` will be *negative*, signaling {@link callHooks} that the
* `hookFunction` must be passed the the appropriate {@link SimpleChanges} object.
*
* @param directiveIndex The index of the directive in LView
* @param directiveDef The definition containing the hooks to setup in tView
* @param tView The current TView
*/
export function queueInitHooks(
index: number, onInit: (() => void) | null, doCheck: (() => void) | null, tView: TView): void {
export function registerPreOrderHooks(
directiveIndex: number, directiveDef: DirectiveDef<any>, tView: TView): void {
ngDevMode &&
assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass');
const {onChanges, onInit, doCheck} = directiveDef;
if (onChanges) {
(tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onChanges);
(tView.checkHooks || (tView.checkHooks = [])).push(-directiveIndex, onChanges);
}
if (onInit) {
(tView.initHooks || (tView.initHooks = [])).push(index, onInit);
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit);
}
if (doCheck) {
(tView.initHooks || (tView.initHooks = [])).push(index, doCheck);
(tView.checkHooks || (tView.checkHooks = [])).push(index, doCheck);
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, doCheck);
(tView.checkHooks || (tView.checkHooks = [])).push(directiveIndex, doCheck);
}
}
/**
* Loops through the directives on a node and queues all their hooks except ngOnInit
* and ngDoCheck, which are queued separately in directiveCreate.
*
* Loops through the directives on the provided `tNode` and queues hooks to be
* run that are not initialization hooks.
*
* Should be executed during `elementEnd()` and similar to
* preserve hook execution order. Content, view, and destroy hooks for projected
* components and directives must be called *before* their hosts.
*
* Sets up the content, view, and destroy hooks on the provided `tView` such that
* they're added in alternating pairs of directiveIndex and hookFunction,
* i.e.: `[directiveIndexA, hookFunctionA, directiveIndexB, hookFunctionB, ...]`
*
* NOTE: This does not set up `onChanges`, `onInit` or `doCheck`, those are set up
* separately at `elementStart`.
*
* @param tView The current TView
* @param tNode The TNode whose directives are to be searched for hooks to queue
*/
export function queueLifecycleHooks(tView: TView, tNode: TNode): void {
export function registerPostOrderHooks(tView: TView, tNode: TNode): void {
if (tView.firstTemplatePass) {
// It's necessary to loop through the directives at elementEnd() (rather than processing in
// directiveCreate) so we can preserve the current hook order. Content, view, and destroy
// hooks for projected components and directives must be called *before* their hosts.
for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>;
queueContentHooks(def, tView, i);
queueViewHooks(def, tView, i);
queueDestroyHooks(def, tView, i);
const directiveDef = tView.data[i] as DirectiveDef<any>;
if (directiveDef.afterContentInit) {
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit);
}
if (directiveDef.afterContentChecked) {
(tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentChecked);
(tView.contentCheckHooks || (tView.contentCheckHooks = [
])).push(i, directiveDef.afterContentChecked);
}
if (directiveDef.afterViewInit) {
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewInit);
}
if (directiveDef.afterViewChecked) {
(tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewChecked);
(tView.viewCheckHooks || (tView.viewCheckHooks = [
])).push(i, directiveDef.afterViewChecked);
}
if (directiveDef.onDestroy != null) {
(tView.destroyHooks || (tView.destroyHooks = [])).push(i, directiveDef.onDestroy);
}
}
}
}
/** Queues afterContentInit and afterContentChecked hooks on TView */
function queueContentHooks(def: DirectiveDef<any>, tView: TView, i: number): void {
if (def.afterContentInit) {
(tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentInit);
}
if (def.afterContentChecked) {
(tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentChecked);
(tView.contentCheckHooks || (tView.contentCheckHooks = [])).push(i, def.afterContentChecked);
}
}
/** Queues afterViewInit and afterViewChecked hooks on TView */
function queueViewHooks(def: DirectiveDef<any>, tView: TView, i: number): void {
if (def.afterViewInit) {
(tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewInit);
}
if (def.afterViewChecked) {
(tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewChecked);
(tView.viewCheckHooks || (tView.viewCheckHooks = [])).push(i, def.afterViewChecked);
}
}
/** Queues onDestroy hooks on TView */
function queueDestroyHooks(def: DirectiveDef<any>, tView: TView, i: number): void {
if (def.onDestroy != null) {
(tView.destroyHooks || (tView.destroyHooks = [])).push(i, def.onDestroy);
}
}
/**
* Calls onInit and doCheck calls if they haven't already been called.
* Executes necessary hooks at the start of executing a template.
*
* @param currentView The current view
* Executes hooks that are to be run during the initialization of a directive such
* as `onChanges`, `onInit`, and `doCheck`.
*
* Has the side effect of updating the RunInit flag in `lView` to be `0`, so that
* this isn't run a second time.
*
* @param lView The current view
* @param tView Static data for the view containing the hooks to be executed
* @param creationMode Whether or not we're in creation mode.
*/
export function executeInitHooks(
currentView: LView, tView: TView, checkNoChangesMode: boolean): void {
@ -102,16 +127,20 @@ export function executeInitHooks(
}
/**
* Iterates over afterViewInit and afterViewChecked functions and calls them.
* Executes hooks against the given `LView` based off of whether or not
* This is the first pass.
*
* @param currentView The current view
* @param lView The view instance data to run the hooks against
* @param firstPassHooks An array of hooks to run if we're in the first view pass
* @param checkHooks An Array of hooks to run if we're not in the first view pass.
* @param checkNoChangesMode Whether or not we're in no changes mode.
*/
export function executeHooks(
currentView: LView, allHooks: HookData | null, checkHooks: HookData | null,
currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
checkNoChangesMode: boolean): void {
if (checkNoChangesMode) return;
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? allHooks : checkHooks;
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks;
if (hooksToCall) {
callHooks(currentView, hooksToCall);
}
@ -119,13 +148,31 @@ export function executeHooks(
/**
* Calls lifecycle hooks with their contexts, skipping init hooks if it's not
* the first LView pass.
* the first LView pass, and skipping onChanges hooks if there are no changes present.
*
* @param currentView The current view
* @param arr The array in which the hooks are found
*/
export function callHooks(currentView: any[], arr: HookData): void {
export function callHooks(currentView: LView, arr: HookData): void {
for (let i = 0; i < arr.length; i += 2) {
(arr[i + 1] as() => void).call(currentView[arr[i] as number]);
const directiveIndex = arr[i] as number;
const hook = arr[i + 1] as((() => void) | ((changes: SimpleChanges) => void));
// Negative indices signal that we're dealing with an `onChanges` hook.
const isOnChangesHook = directiveIndex < 0;
const directiveOrWrappedDirective =
currentView[isOnChangesHook ? -directiveIndex : directiveIndex];
const directive = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective);
if (isOnChangesHook) {
const onChanges: OnChangesDirectiveWrapper = directiveOrWrappedDirective;
const changes = onChanges.changes;
if (changes) {
onChanges.previous = changes;
onChanges.changes = null;
hook.call(onChanges.instance, changes);
}
} else {
hook.call(directive);
}
}
}

View File

@ -9,7 +9,6 @@ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'
import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {getComponent, getHostElement, getRenderedText} from './discovery_utils';
import {InheritDefinitionFeature} from './features/inherit_definition_feature';
import {NgOnChangesFeature} from './features/ng_onchanges_feature';
import {ProvidersFeature} from './features/providers_feature';
import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition';
@ -158,7 +157,6 @@ export {
DirectiveDefFlags,
DirectiveDefWithMeta,
DirectiveType,
NgOnChangesFeature,
InheritDefinitionFeature,
ProvidersFeature,
PipeDef,

View File

@ -22,7 +22,7 @@ import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from
import {attachPatchData, getComponentViewByInstance} from './context_discovery';
import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di';
import {throwMultipleComponentError} from './errors';
import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks';
import {executeHooks, executeInitHooks, registerPostOrderHooks, registerPreOrderHooks} from './hooks';
import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector';
@ -36,6 +36,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLA
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {OnChangesDirectiveWrapper, isOnChangesDirectiveWrapper, recordChange, unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state';
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory';
@ -120,7 +121,7 @@ export function setHostBindings(tView: TView, viewData: LView): void {
if (instruction !== null) {
viewData[BINDING_INDEX] = bindingRootIndex;
instruction(
RenderFlags.Update, readElementValue(viewData[currentDirectiveIndex]),
RenderFlags.Update, unwrapOnChangesDirectiveWrapper(viewData[currentDirectiveIndex]),
currentElementIndex);
}
currentDirectiveIndex++;
@ -522,7 +523,7 @@ export function elementContainerEnd(): void {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementContainerNode);
}
queueLifecycleHooks(tView, previousOrParentTNode);
registerPostOrderHooks(tView, previousOrParentTNode);
}
/**
@ -719,6 +720,7 @@ export function createTView(
expandoStartIndex: initialViewLength,
expandoInstructions: null,
firstTemplatePass: true,
changesHooks: null,
initHooks: null,
checkHooks: null,
contentHooks: null,
@ -883,9 +885,14 @@ export function listener(
const propsLength = props.length;
if (propsLength) {
const lCleanup = getCleanup(lView);
for (let i = 0; i < propsLength; i += 2) {
ngDevMode && assertDataInRange(lView, props[i] as number);
const subscription = lView[props[i] as number][props[i + 1]].subscribe(listenerFn);
// Subscribe to listeners for each output, and setup clean up for each.
for (let i = 0; i < propsLength;) {
const directiveIndex = props[i++] as number;
const minifiedName = props[i++] as string;
const declaredName = props[i++] as string;
ngDevMode && assertDataInRange(lView, directiveIndex as number);
const directive = unwrapOnChangesDirectiveWrapper(lView[directiveIndex]);
const subscription = directive[minifiedName].subscribe(listenerFn);
const idx = lCleanup.length;
lCleanup.push(listenerFn, subscription);
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
@ -943,7 +950,7 @@ export function elementEnd(): void {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementNode);
}
queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode);
registerPostOrderHooks(getLView()[TVIEW], previousOrParentTNode);
decreaseElementDepthCount();
// this is fired at the end of elementEnd because ALL of the stylingBindings code
@ -952,7 +959,7 @@ export function elementEnd(): void {
if (hasClassInput(previousOrParentTNode)) {
const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
setInputsForProperty(
lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
lView, previousOrParentTNode.inputs !, 'class', getInitialClassNameValue(stylingContext));
}
}
@ -1051,7 +1058,7 @@ function elementPropertyInternal<T>(
let dataValue: PropertyAliasValue|undefined;
if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) &&
(dataValue = inputData[propName])) {
setInputsForProperty(lView, dataValue, value);
setInputsForProperty(lView, inputData, propName, value);
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
if (ngDevMode) {
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
@ -1121,22 +1128,35 @@ export function createTNode(
}
/**
* Given a list of directive indices and minified input names, sets the
* input properties on the corresponding directives.
* Set the inputs of directives at the current node to corresponding value.
*
* @param lView the `LView` which contains the directives.
* @param inputAliases mapping between the public "input" name and privately-known,
* possibly minified, property names to write to.
* @param publicName public binding name. (This is the `<div [publicName]=value>`)
* @param value Value to set.
*/
function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void {
for (let i = 0; i < inputs.length; i += 2) {
ngDevMode && assertDataInRange(lView, inputs[i] as number);
lView[inputs[i] as number][inputs[i + 1]] = value;
function setInputsForProperty(
lView: LView, inputAliases: PropertyAliases, publicName: string, value: any): void {
const inputs = inputAliases[publicName];
for (let i = 0; i < inputs.length;) {
const directiveIndex = inputs[i++] as number;
const privateName = inputs[i++] as string;
const declaredName = inputs[i++] as string;
ngDevMode && assertDataInRange(lView, directiveIndex);
recordChangeAndUpdateProperty(lView[directiveIndex], declaredName, privateName, value);
}
}
function setNgReflectProperties(
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
value: any) {
for (let i = 0; i < inputs.length; i += 2) {
for (let i = 0; i < inputs.length;) {
const directiveIndex = inputs[i++] as number;
const privateName = inputs[i++] as string;
const declaredName = inputs[i++] as string;
const renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(inputs[i + 1] as string);
const attrName = normalizeDebugBindingName(privateName);
const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ?
@ -1172,15 +1192,20 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
for (let i = start; i < end; i++) {
const directiveDef = defs[i] as DirectiveDef<any>;
const propertyAliasMap: {[publicName: string]: string} =
const publicToMinifiedNames: {[publicName: string]: string} =
isInput ? directiveDef.inputs : directiveDef.outputs;
for (let publicName in propertyAliasMap) {
if (propertyAliasMap.hasOwnProperty(publicName)) {
const publicToDeclaredNames: {[publicName: string]: string}|null =
isInput ? directiveDef.declaredInputs : null;
for (let publicName in publicToMinifiedNames) {
if (publicToMinifiedNames.hasOwnProperty(publicName)) {
propStore = propStore || {};
const internalName = propertyAliasMap[publicName];
const hasProperty = propStore.hasOwnProperty(publicName);
hasProperty ? propStore[publicName].push(i, internalName) :
(propStore[publicName] = [i, internalName]);
const minifiedName = publicToMinifiedNames[publicName];
const declaredName =
publicToDeclaredNames ? publicToDeclaredNames[publicName] : minifiedName;
const aliases: PropertyAliasValue = propStore.hasOwnProperty(publicName) ?
propStore[publicName] :
propStore[publicName] = [];
aliases.push(i, minifiedName, declaredName);
}
}
}
@ -1382,7 +1407,7 @@ export function elementStylingMap<T>(
const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
setInputsForProperty(lView, tNode.inputs !, 'class', classInputVal);
} else {
updateStylingMap(stylingContext, classes, styles);
}
@ -1493,7 +1518,7 @@ function resolveDirectives(
// Init hooks are queued now so ngOnInit is called in host components before
// any projected components.
queueInitHooks(directiveDefIdx, def.onInit, def.doCheck, tView);
registerPreOrderHooks(directiveDefIdx, def, tView);
}
}
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap);
@ -1515,6 +1540,7 @@ function instantiateAllDirectives(tView: TView, lView: LView, tNode: TNode) {
addComponentLogic(lView, tNode, def as ComponentDef<any>);
}
const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode);
postProcessDirective(lView, directive, def, i);
}
}
@ -1526,7 +1552,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const firstTemplatePass = getFirstTemplatePass();
for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i];
const directive = unwrapOnChangesDirectiveWrapper(viewData[i]);
if (def.hostBindings) {
const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def);
@ -1583,12 +1609,17 @@ function prefillHostVars(tView: TView, lView: LView, totalHostVars: number): voi
* Process a directive on the current node after its creation.
*/
function postProcessDirective<T>(
viewData: LView, directive: T, def: DirectiveDef<T>, directiveDefIdx: number): void {
lView: LView, directive: T, def: DirectiveDef<T>, directiveDefIdx: number): void {
if (def.onChanges) {
// We have onChanges, wrap it so that we can track changes.
lView[directiveDefIdx] = new OnChangesDirectiveWrapper(lView[directiveDefIdx]);
}
const previousOrParentTNode = getPreviousOrParentTNode();
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def);
postProcessBaseDirective(lView, previousOrParentTNode, directive, def);
ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode');
if (previousOrParentTNode && previousOrParentTNode.attrs) {
setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode);
setInputsFromAttrs(lView, directiveDefIdx, def, previousOrParentTNode);
}
if (def.contentQueries) {
@ -1596,7 +1627,7 @@ function postProcessDirective<T>(
}
if (isComponentDef(def)) {
const componentView = getComponentViewByIndex(previousOrParentTNode.index, viewData);
const componentView = getComponentViewByIndex(previousOrParentTNode.index, lView);
componentView[CONTEXT] = directive;
}
}
@ -1794,20 +1825,53 @@ function addComponentLogic<T>(
* @param tNode The static data for this node
*/
function setInputsFromAttrs<T>(
directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void {
lView: LView, directiveIndex: number, def: DirectiveDef<any>, tNode: TNode): void {
let initialInputData = tNode.initialInputs as InitialInputData | undefined;
if (initialInputData === undefined || directiveIndex >= initialInputData.length) {
initialInputData = generateInitialInputs(directiveIndex, inputs, tNode);
initialInputData = generateInitialInputs(directiveIndex, def, tNode);
}
const initialInputs: InitialInputs|null = initialInputData[directiveIndex];
if (initialInputs) {
for (let i = 0; i < initialInputs.length; i += 2) {
(instance as any)[initialInputs[i]] = initialInputs[i + 1];
const directiveOrWrappedDirective = lView[directiveIndex];
for (let i = 0; i < initialInputs.length;) {
const privateName = initialInputs[i++];
const declaredName = initialInputs[i++];
const attrValue = initialInputs[i++];
recordChangeAndUpdateProperty(
directiveOrWrappedDirective, declaredName, privateName, attrValue);
}
}
}
/**
* Checks to see if the instanced passed as `directiveOrWrappedDirective` is wrapped in {@link
* OnChangesDirectiveWrapper} or not.
* If it is, it will update the related {@link SimpleChanges} object with the change to signal
* `ngOnChanges` hook
* should fire, then it will unwrap the instance. After that, it will set the property with the key
* provided
* in `privateName` on the instance with the passed value.
* @param directiveOrWrappedDirective The directive instance or a directive instance wrapped in
* {@link OnChangesDirectiveWrapper}
* @param declaredName The original, declared name of the property to update.
* @param privateName The private, possibly minified name of the property to update.
* @param value The value to update the property with.
*/
function recordChangeAndUpdateProperty<T, K extends keyof T>(
directiveOrWrappedDirective: OnChangesDirectiveWrapper<T>| T, declaredName: string,
privateName: K, value: any) {
let instance: T;
if (isOnChangesDirectiveWrapper(directiveOrWrappedDirective)) {
instance = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective);
recordChange(directiveOrWrappedDirective, declaredName, value);
} else {
instance = directiveOrWrappedDirective;
}
instance[privateName] = value;
}
/**
* Generates initialInputData for a node and stores it in the template's static storage
* so subsequent template invocations don't have to recalculate it.
@ -1824,7 +1888,7 @@ function setInputsFromAttrs<T>(
* @param tNode The static data on this node
*/
function generateInitialInputs(
directiveIndex: number, inputs: {[key: string]: string}, tNode: TNode): InitialInputData {
directiveIndex: number, directiveDef: DirectiveDef<any>, tNode: TNode): InitialInputData {
const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []);
initialInputData[directiveIndex] = null;
@ -1832,19 +1896,23 @@ function generateInitialInputs(
let i = 0;
while (i < attrs.length) {
const attrName = attrs[i];
if (attrName === AttributeMarker.SelectOnly) break;
// If we hit Select-Only, Classes or Styles, we're done anyway. None of those are valid inputs.
if (attrName === AttributeMarker.SelectOnly || attrName === AttributeMarker.Classes ||
attrName === AttributeMarker.Styles)
break;
if (attrName === AttributeMarker.NamespaceURI) {
// We do not allow inputs on namespaced attributes.
i += 4;
continue;
}
const minifiedInputName = inputs[attrName];
const privateName = directiveDef.inputs[attrName];
const declaredName = directiveDef.declaredInputs[attrName];
const attrValue = attrs[i + 1];
if (minifiedInputName !== undefined) {
if (privateName !== undefined) {
const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(minifiedInputName, attrValue as string);
inputsToStore.push(privateName, declaredName, attrValue as string);
}
i += 2;
@ -1919,7 +1987,7 @@ export function template(
if (currentQueries) {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode);
}
queueLifecycleHooks(tView, tNode);
registerPostOrderHooks(tView, tNode);
setIsParent(false);
}
@ -2881,7 +2949,7 @@ export function registerContentQuery<Q>(
export const CLEAN_PROMISE = _CLEAN_PROMISE;
function initializeTNodeInputs(tNode: TNode | null) {
function initializeTNodeInputs(tNode: TNode | null): PropertyAliases|null {
// If tNode.inputs is undefined, a listener has created outputs, but inputs haven't
// yet been checked.
if (tNode) {

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ViewEncapsulation} from '../../core';
import {SimpleChanges, ViewEncapsulation} from '../../core';
import {Type} from '../../interface/type';
import {CssSelectorList} from './projection';
@ -150,6 +151,7 @@ export interface DirectiveDef<T> extends BaseDef<T> {
/* The following are lifecycle hooks for this component */
onInit: (() => void)|null;
doCheck: (() => void)|null;
onChanges: ((changes: SimpleChanges) => void)|null;
afterContentInit: (() => void)|null;
afterContentChecked: (() => void)|null;
afterViewInit: (() => void)|null;

View File

@ -464,10 +464,12 @@ export type PropertyAliases = {
/**
* Store the runtime input or output names for all the directives.
*
* - Even indices: directive index
* - Odd indices: minified / internal name
* Values are stored in triplets:
* - i + 0: directive index
* - i + 1: minified / internal name
* - i + 2: declared name
*
* e.g. [0, 'change-minified']
* e.g. [0, 'minifiedName', 'declaredPropertyName']
*/
export type PropertyAliasValue = (number | string)[];
@ -495,10 +497,12 @@ export type InitialInputData = (InitialInputs | null)[];
* Used by InitialInputData to store input properties
* that should be set once from attributes.
*
* Even indices: minified/internal input name
* Odd indices: initial value
* The inputs come in triplets of:
* i + 0: minified/internal input name
* i + 1: declared input name (needed for OnChanges)
* i + 2: initial value
*
* e.g. ['role-min', 'button']
* e.g. ['minifiedName', 'declaredName', 'value']
*/
export type InitialInputs = string[];

View File

@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChanges} from '../../change_detection/simple_change';
import {InjectionToken} from '../../di/injection_token';
import {Injector} from '../../di/injector';
import {Type} from '../../interface/type';
import {QueryList} from '../../linker';
import {Sanitizer} from '../../sanitization/security';
import {LContainer} from './container';
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList} from './definition';
import {I18nUpdateOpCodes, TI18n} from './i18n';
@ -22,7 +22,6 @@ import {RElement, Renderer3, RendererFactory3} from './renderer';
import {StylingContext} from './styling';
// Below are constants for LView indices to help us look up LView members
// without having to remember the specific indices.
// Uglify will inline these when minifying so there shouldn't be a cost.
@ -533,7 +532,7 @@ export interface RootContext {
* Even indices: Directive index
* Odd indices: Hook function
*/
export type HookData = (number | (() => void))[];
export type HookData = (number | (() => void) | ((changes: SimpleChanges) => void))[];
/**
* Static data that corresponds to the instance-specific data array on an LView.

View File

@ -115,7 +115,6 @@ export interface R3DirectiveMetadataFacade {
queries: R3QueryMetadataFacade[];
host: {[key: string]: string};
propMetadata: {[key: string]: any[]};
lifecycle: {usesOnChanges: boolean;};
inputs: string[];
outputs: string[];
usesInheritance: boolean;

View File

@ -145,9 +145,6 @@ function directiveMetadata(type: Type<any>, metadata: Directive): R3DirectiveMet
inputs: metadata.inputs || EMPTY_ARRAY,
outputs: metadata.outputs || EMPTY_ARRAY,
queries: extractQueriesMetadata(type, propMetadata, isContentQuery),
lifecycle: {
usesOnChanges: type.prototype.ngOnChanges !== undefined,
},
typeSourceSpan: null !,
usesInheritance: !extendsDirectlyFromObject(type),
exportAs: extractExportAs(metadata.exportAs),

View File

@ -31,7 +31,6 @@ export const angularCoreEnv: {[name: string]: Function} = {
'inject': inject,
'ɵinjectAttribute': r3.injectAttribute,
'ɵtemplateRefExtractor': r3.templateRefExtractor,
'ɵNgOnChangesFeature': r3.NgOnChangesFeature,
'ɵProvidersFeature': r3.ProvidersFeature,
'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature,
'ɵelementAttribute': r3.elementAttribute,

View File

@ -0,0 +1,71 @@
/**
* @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 {SimpleChange, SimpleChanges} from '../change_detection/simple_change';
type Constructor<T> = new (...args: any[]) => T;
/**
* Checks an object to see if it's an exact instance of a particular type
* without traversing the inheritance hierarchy like `instanceof` does.
* @param obj The object to check
* @param type The type to check the object against
*/
export function isExactInstanceOf<T>(obj: any, type: Constructor<T>): obj is T {
return obj != null && typeof obj == 'object' && Object.getPrototypeOf(obj) == type.prototype;
}
/**
* Checks to see if an object is an instance of {@link OnChangesDirectiveWrapper}
* @param obj the object to check (generally from `LView`)
*/
export function isOnChangesDirectiveWrapper(obj: any): obj is OnChangesDirectiveWrapper<any> {
return isExactInstanceOf(obj, OnChangesDirectiveWrapper);
}
/**
* Removes the `OnChangesDirectiveWrapper` if present.
*
* @param obj to unwrap.
*/
export function unwrapOnChangesDirectiveWrapper<T>(obj: T | OnChangesDirectiveWrapper<T>): T {
return isOnChangesDirectiveWrapper(obj) ? obj.instance : obj;
}
/**
* A class that wraps directive instances for storage in LView when directives
* have onChanges hooks to deal with.
*/
export class OnChangesDirectiveWrapper<T = any> {
seenProps = new Set<string>();
previous: SimpleChanges = {};
changes: SimpleChanges|null = null;
constructor(public instance: T) {}
}
/**
* Updates the `changes` property on the `wrapper` instance, such that when it's
* checked in {@link callHooks} it will fire the related `onChanges` hook.
* @param wrapper the wrapper for the directive instance
* @param declaredName the declared name to be used in `SimpleChange`
* @param value The new value for the property
*/
export function recordChange(wrapper: OnChangesDirectiveWrapper, declaredName: string, value: any) {
const simpleChanges = wrapper.changes || (wrapper.changes = {});
const firstChange = !wrapper.seenProps.has(declaredName);
if (firstChange) {
wrapper.seenProps.add(declaredName);
}
const previous = wrapper.previous;
const previousValue: SimpleChange|undefined = previous[declaredName];
simpleChanges[declaredName] = new SimpleChange(
firstChange ? undefined : previousValue && previousValue.currentValue, value, firstChange);
}

View File

@ -17,6 +17,7 @@ import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './inte
import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
import {isOnChangesDirectiveWrapper} from './onchanges_util';
/**
@ -67,9 +68,14 @@ export function flatten(list: any[]): any[] {
/** Retrieves a value from any `LView` or `TData`. */
export function loadInternal<T>(view: LView | TData, index: number): T {
ngDevMode && assertDataInRange(view, index + HEADER_OFFSET);
return view[index + HEADER_OFFSET];
const record = view[index + HEADER_OFFSET];
// If we're storing an array because of a directive or component with ngOnChanges,
// return the directive or component instance.
return isOnChangesDirectiveWrapper(record) ? record.instance : record;
}
/**
* Takes the value of a slot in `LView` and returns the element node.
*
@ -288,4 +294,4 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) {
export function resolveBody(element: RElement & {ownerDocument: Document}) {
return {name: 'body', target: element.ownerDocument.body};
}
}

View File

@ -11,10 +11,11 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {checkNoChanges, checkNoChangesInRootView, checkView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions';
import {checkNoChanges, checkNoChangesInRootView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions';
import {TNode, TNodeType, TViewNode} from './interfaces/node';
import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT, RENDERER_FACTORY} from './interfaces/view';
import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT} from './interfaces/view';
import {destroyLView} from './node_manipulation';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getNativeByTNode} from './util';
@ -271,7 +272,8 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
}
private _lookUpContext(): T {
return this._context = this._lView[PARENT] ![this._componentIndex] as T;
return this._context =
unwrapOnChangesDirectiveWrapper(this._lView[PARENT] ![this._componentIndex] as T);
}
}

View File

@ -86,24 +86,21 @@
{
"name": "NO_PARENT_INJECTOR"
},
{
"name": "NgOnChangesFeature"
},
{
"name": "NodeInjectorFactory"
},
{
"name": "ObjectUnsubscribedErrorImpl"
},
{
"name": "OnChangesDirectiveWrapper"
},
{
"name": "PARENT"
},
{
"name": "PARENT_INJECTOR"
},
{
"name": "PRIVATE_PREFIX"
},
{
"name": "RENDERER"
},
@ -113,9 +110,6 @@
{
"name": "SANITIZER"
},
{
"name": "SimpleChange"
},
{
"name": "TVIEW"
},
@ -335,9 +329,15 @@
{
"name": "isCreationMode"
},
{
"name": "isExactInstanceOf"
},
{
"name": "isFactory"
},
{
"name": "isOnChangesDirectiveWrapper"
},
{
"name": "isProceduralRenderer"
},
@ -365,9 +365,6 @@
{
"name": "noSideEffects"
},
{
"name": "onChangesWrapper"
},
{
"name": "postProcessBaseDirective"
},
@ -449,6 +446,9 @@
{
"name": "tickRootContext"
},
{
"name": "unwrapOnChangesDirectiveWrapper"
},
{
"name": "updateViewQuery"
},

View File

@ -35,9 +35,6 @@
{
"name": "NULL_INJECTOR$2"
},
{
"name": "NgOnChangesFeature"
},
{
"name": "NullInjector"
},
@ -50,9 +47,6 @@
{
"name": "PARAMETERS"
},
{
"name": "PRIVATE_PREFIX"
},
{
"name": "R3Injector"
},
@ -62,9 +56,6 @@
{
"name": "Self"
},
{
"name": "SimpleChange"
},
{
"name": "SkipSelf"
},
@ -164,9 +155,6 @@
{
"name": "makeRecord"
},
{
"name": "onChangesWrapper"
},
{
"name": "providerToFactory"
},

View File

@ -143,9 +143,6 @@
{
"name": "NgModuleRef"
},
{
"name": "NgOnChangesFeature"
},
{
"name": "NodeInjector"
},
@ -155,6 +152,9 @@
{
"name": "ObjectUnsubscribedErrorImpl"
},
{
"name": "OnChangesDirectiveWrapper"
},
{
"name": "Optional"
},
@ -167,9 +167,6 @@
{
"name": "PARENT_INJECTOR"
},
{
"name": "PRIVATE_PREFIX"
},
{
"name": "QUERIES"
},
@ -902,6 +899,9 @@
{
"name": "isDirty"
},
{
"name": "isExactInstanceOf"
},
{
"name": "isFactory"
},
@ -920,6 +920,9 @@
{
"name": "isNodeMatchingSelectorList"
},
{
"name": "isOnChangesDirectiveWrapper"
},
{
"name": "isPositive"
},
@ -998,9 +1001,6 @@
{
"name": "noSideEffects"
},
{
"name": "onChangesWrapper"
},
{
"name": "pointers"
},
@ -1019,21 +1019,6 @@
{
"name": "queueComponentIndexForCheck"
},
{
"name": "queueContentHooks"
},
{
"name": "queueDestroyHooks"
},
{
"name": "queueInitHooks"
},
{
"name": "queueLifecycleHooks"
},
{
"name": "queueViewHooks"
},
{
"name": "readElementValue"
},
@ -1043,6 +1028,12 @@
{
"name": "readPatchedLView"
},
{
"name": "recordChange"
},
{
"name": "recordChangeAndUpdateProperty"
},
{
"name": "reference"
},
@ -1058,6 +1049,12 @@
{
"name": "refreshDynamicEmbeddedViews"
},
{
"name": "registerPostOrderHooks"
},
{
"name": "registerPreOrderHooks"
},
{
"name": "removeListeners"
},
@ -1214,6 +1211,9 @@
{
"name": "trackByIdentity"
},
{
"name": "unwrapOnChangesDirectiveWrapper"
},
{
"name": "updateClassProp"
},

View File

@ -9,7 +9,7 @@
import {NgForOf as NgForOfDef, NgIf as NgIfDef, NgTemplateOutlet as NgTemplateOutletDef} from '@angular/common';
import {IterableDiffers, TemplateRef, ViewContainerRef} from '@angular/core';
import {DirectiveType, NgOnChangesFeature, defineDirective, directiveInject} from '../../src/render3/index';
import {DirectiveType, defineDirective, directiveInject} from '../../src/render3/index';
export const NgForOf: DirectiveType<NgForOfDef<any>> = NgForOfDef as any;
export const NgIf: DirectiveType<NgIfDef> = NgIfDef as any;
@ -40,7 +40,6 @@ NgForOf.ngDirectiveDef = defineDirective({
type: NgTemplateOutletDef,
selectors: [['', 'ngTemplateOutlet', '']],
factory: () => new NgTemplateOutletDef(directiveInject(ViewContainerRef as any)),
features: [NgOnChangesFeature],
inputs:
{ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'}
});

View File

@ -8,7 +8,7 @@
import {ElementRef, QueryList} from '@angular/core';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature} from '../../src/render3/index';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature} from '../../src/render3/index';
import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions';
import {query, queryRefresh} from '../../src/render3/query';
import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -357,7 +357,6 @@ describe('host bindings', () => {
template: (rf: RenderFlags, ctx: InitHookComp) => {},
consts: 0,
vars: 0,
features: [NgOnChangesFeature],
hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => {
if (rf & RenderFlags.Create) {
allocHostVars(1);

View File

@ -7,7 +7,7 @@
*/
import {Inject, InjectionToken} from '../../src/core';
import {ComponentDef, DirectiveDef, InheritDefinitionFeature, NgOnChangesFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty, load} from '../../src/render3/index';
import {ComponentDef, DirectiveDef, InheritDefinitionFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty} from '../../src/render3/index';
import {ComponentFixture, createComponent} from './render_util';
@ -501,8 +501,7 @@ describe('InheritDefinitionFeature', () => {
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {someInput: 'someInput'}
inputs: {someInput: 'someInput'},
});
}
@ -519,6 +518,9 @@ describe('InheritDefinitionFeature', () => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['subDir', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'someInput', bind('Weee'));
}
}, 1, 0, [SubDirective]);
const fixture = new ComponentFixture(App);

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ComponentFactoryResolver, OnDestroy, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, template, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1941,10 +1941,15 @@ describe('lifecycles', () => {
});
describe('onChanges', () => {
let events: string[];
let events: ({type: string, name: string, [key: string]: any})[];
beforeEach(() => { events = []; });
/**
* <div>
* <ng-content/>
* </div>
*/
const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
projectionDef();
@ -1953,15 +1958,20 @@ describe('lifecycles', () => {
elementEnd();
}
}, 2);
/**
* <comp [val1]="a" [publicVal2]="b"/>
*/
const Parent = createOnChangesComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'comp');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.a));
elementProperty(0, 'publicName', bind(ctx.b));
elementProperty(0, 'publicVal2', bind(ctx.b));
}
}, 1, 2, [Comp]);
const ProjectedComp = createOnChangesComponent('projected', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
text(0, 'content');
@ -1974,22 +1984,28 @@ describe('lifecycles', () => {
directives: any[] = []) {
return class Component {
// @Input() val1: string;
// @Input('publicName') val2: string;
// @Input('publicVal2') val2: string;
a: string = 'wasVal1BeforeMinification';
b: string = 'wasVal2BeforeMinification';
ngOnChanges(simpleChanges: SimpleChanges) {
events.push(
`comp=${name} val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`);
ngOnChanges(changes: SimpleChanges) {
if (changes.a && this.a !== changes.a.currentValue) {
throw Error(
`SimpleChanges invalid expected this.a ${this.a} to equal currentValue ${changes.a.currentValue}`);
}
if (changes.b && this.b !== changes.b.currentValue) {
throw Error(
`SimpleChanges invalid expected this.b ${this.b} to equal currentValue ${changes.b.currentValue}`);
}
events.push({type: 'onChanges', name: 'comp - ' + name, changes});
}
static ngComponentDef = defineComponent({
type: Component,
selectors: [[name]],
factory: () => new Component(),
features: [NgOnChangesFeature],
consts: consts,
vars: vars,
inputs: {a: 'val1', b: ['publicName', 'val2']}, template,
inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template,
directives: directives
});
};
@ -1997,49 +2013,64 @@ describe('lifecycles', () => {
class Directive {
// @Input() val1: string;
// @Input('publicName') val2: string;
// @Input('publicVal2') val2: string;
a: string = 'wasVal1BeforeMinification';
b: string = 'wasVal2BeforeMinification';
ngOnChanges(simpleChanges: SimpleChanges) {
events.push(
`dir - val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`);
ngOnChanges(changes: SimpleChanges) {
events.push({type: 'onChanges', name: 'dir - dir', changes});
}
static ngDirectiveDef = defineDirective({
type: Directive,
selectors: [['', 'dir', '']],
factory: () => new Directive(),
features: [NgOnChangesFeature],
inputs: {a: 'val1', b: ['publicName', 'val2']}
inputs: {a: 'val1', b: ['publicVal2', 'val2']}
});
}
const defs = [Comp, Parent, Directive, ProjectedComp];
it('should call onChanges method after inputs are set in creation and update mode', () => {
/** <comp [val1]="val1" [publicName]="val2"></comp> */
/** <comp [val1]="val1" [publicVal2]="val2"></comp> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'comp');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2));
elementProperty(0, 'publicVal2', bind(ctx.val2));
}
}, 1, 2, defs);
// First changes happen here.
const fixture = new ComponentFixture(App);
events = [];
fixture.component.val1 = '1';
fixture.component.val2 = 'a';
fixture.update();
expect(events).toEqual(['comp=comp val1=1 val2=a - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(
undefined, '1', false), // we cleared `events` above, this is the second change
'val2': new SimpleChange(undefined, 'a', false),
}
}]);
events = [];
fixture.component.val1 = '2';
fixture.component.val2 = 'b';
fixture.update();
expect(events).toEqual(['comp=comp val1=2 val2=b - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange('1', '2', false),
'val2': new SimpleChange('a', 'b', false),
}
}]);
});
it('should call parent onChanges before child onChanges', () => {
@ -2053,28 +2084,42 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2));
elementProperty(0, 'publicVal2', bind(ctx.val2));
}
}, 1, 2, defs);
const fixture = new ComponentFixture(App);
// We're clearing events after the first change here
events = [];
fixture.component.val1 = '1';
fixture.component.val2 = 'a';
fixture.update();
expect(events).toEqual([
'comp=parent val1=1 val2=a - changed=[val1,val2]',
'comp=comp val1=1 val2=a - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, '1', false),
'val2': new SimpleChange(undefined, 'a', false),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, '1', false),
'val2': new SimpleChange(undefined, 'a', false),
}
},
]);
});
it('should call all parent onChanges across view before calling children onChanges', () => {
/**
* <parent [val]="1"></parent>
* <parent [val]="2"></parent>
*
* parent temp: <comp [val]="val"></comp>
* <parent [val1]="1"></parent>
* <parent [val1]="2"></parent>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2084,18 +2129,46 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2));
elementProperty(1, 'publicVal2', bind(2));
}
}, 2, 4, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=parent val1=1 val2=1 - changed=[val1,val2]',
'comp=parent val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=2 val2=2 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
]);
});
@ -2121,7 +2194,7 @@ describe('lifecycles', () => {
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
}
embeddedViewEnd();
}
@ -2131,19 +2204,51 @@ describe('lifecycles', () => {
}, 1, 0, defs);
const fixture = new ComponentFixture(App);
// Show the `comp` component, causing it to initialize. (first change is true)
fixture.component.condition = true;
fixture.update();
expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}]);
// Hide the `comp` component, no onChanges should fire
fixture.component.condition = false;
fixture.update();
expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}]);
// Show the `comp` component, it initializes again. (first change is true)
fixture.component.condition = true;
fixture.update();
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}
]);
});
@ -2161,26 +2266,40 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2));
elementProperty(1, 'publicVal2', bind(2));
}
}, 2, 4, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=projected val1=2 val2=2 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - projected',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
]);
});
it('should call onChanges in host and its content children before next host', () => {
/**
* <comp [val]="1">
* <projected [val]="1"></projected>
* <comp [val1]="1" [publicVal2]="1">
* <projected [val1]="2" [publicVal2]="2"></projected>
* </comp>
* <comp [val]="2">
* <projected [val]="1"></projected>
* <comp [val1]="3" [publicVal2]="3">
* <projected [val1]="4" [publicVal2]="4"></projected>
* </comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2194,75 +2313,130 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2));
elementProperty(1, 'publicVal2', bind(2));
elementProperty(2, 'val1', bind(3));
elementProperty(2, 'publicName', bind(3));
elementProperty(2, 'publicVal2', bind(3));
elementProperty(3, 'val1', bind(4));
elementProperty(3, 'publicName', bind(4));
elementProperty(3, 'publicVal2', bind(4));
}
}, 4, 8, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=projected val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=3 val2=3 - changed=[val1,val2]',
'comp=projected val1=4 val2=4 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - projected',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - projected',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
]);
});
it('should be called on directives after component', () => {
/** <comp directive></comp> */
/**
* <comp dir [val1]="1" [publicVal2]="1"></comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'comp', ['dir', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
}
}, 1, 2, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'dir - dir',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
]);
// Update causes no changes to be fired, since the bindings didn't change.
events = [];
fixture.update();
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
]);
expect(events).toEqual([]);
});
it('should be called on directives on an element', () => {
/** <div directive></div> */
/**
* <div dir [val]="1" [publicVal2]="1"></div>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', ['dir', '']);
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
}
}, 1, 2, defs);
const fixture = new ComponentFixture(App);
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([{
type: 'onChanges',
name: 'dir - dir',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
}]);
events = [];
fixture.update();
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']);
expect(events).toEqual([]);
});
it('should call onChanges properly in for loop', () => {
/**
* <comp [val]="1"></comp>
* <comp [val1]="1" [publicVal2]="1"></comp>
* % for (let j = 2; j < 5; j++) {
* <comp [val]="j"></comp>
* <comp [val1]="j" [publicVal2]="j"></comp>
* % }
* <comp [val]="5"></comp>
* <comp [val1]="5" [publicVal2]="5"></comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2273,9 +2447,9 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(2, 'val1', bind(5));
elementProperty(2, 'publicName', bind(5));
elementProperty(2, 'publicVal2', bind(5));
containerRefreshStart(1);
{
for (let j = 2; j < 5; j++) {
@ -2285,7 +2459,7 @@ describe('lifecycles', () => {
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j));
elementProperty(0, 'publicVal2', bind(j));
}
embeddedViewEnd();
}
@ -2299,21 +2473,56 @@ describe('lifecycles', () => {
// onChanges is called top to bottom, so top level comps (1 and 5) are called
// before the comps inside the for loop's embedded view (2, 3, and 4)
expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=5 val2=5 - changed=[val1,val2]',
'comp=comp val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=3 val2=3 - changed=[val1,val2]',
'comp=comp val1=4 val2=4 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 5, true),
'val2': new SimpleChange(undefined, 5, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
]);
});
it('should call onChanges properly in for loop with children', () => {
/**
* <parent [val]="1"></parent>
* <parent [val1]="1" [publicVal2]="1"></parent>
* % for (let j = 2; j < 5; j++) {
* <parent [val]="j"></parent>
* <parent [val1]="j" [publicVal2]="j"></parent>
* % }
* <parent [val]="5"></parent>
* <parent [val1]="5" [publicVal2]="5"></parent>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2324,9 +2533,9 @@ describe('lifecycles', () => {
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1));
elementProperty(0, 'publicVal2', bind(1));
elementProperty(2, 'val1', bind(5));
elementProperty(2, 'publicName', bind(5));
elementProperty(2, 'publicVal2', bind(5));
containerRefreshStart(1);
{
for (let j = 2; j < 5; j++) {
@ -2336,7 +2545,7 @@ describe('lifecycles', () => {
}
if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j));
elementProperty(0, 'publicVal2', bind(j));
}
embeddedViewEnd();
}
@ -2350,19 +2559,144 @@ describe('lifecycles', () => {
// onChanges is called top to bottom, so top level comps (1 and 5) are called
// before the comps inside the for loop's embedded view (2, 3, and 4)
expect(events).toEqual([
'comp=parent val1=1 val2=1 - changed=[val1,val2]',
'comp=parent val1=5 val2=5 - changed=[val1,val2]',
'comp=parent val1=2 val2=2 - changed=[val1,val2]',
'comp=comp val1=2 val2=2 - changed=[val1,val2]',
'comp=parent val1=3 val2=3 - changed=[val1,val2]',
'comp=comp val1=3 val2=3 - changed=[val1,val2]',
'comp=parent val1=4 val2=4 - changed=[val1,val2]',
'comp=comp val1=4 val2=4 - changed=[val1,val2]',
'comp=comp val1=1 val2=1 - changed=[val1,val2]',
'comp=comp val1=5 val2=5 - changed=[val1,val2]'
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 5, true),
'val2': new SimpleChange(undefined, 5, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 2, true),
'val2': new SimpleChange(undefined, 2, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 3, true),
'val2': new SimpleChange(undefined, 3, true),
}
},
{
type: 'onChanges',
name: 'comp - parent',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 4, true),
'val2': new SimpleChange(undefined, 4, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 1, true),
'val2': new SimpleChange(undefined, 1, true),
}
},
{
type: 'onChanges',
name: 'comp - comp',
changes: {
'val1': new SimpleChange(undefined, 5, true),
'val2': new SimpleChange(undefined, 5, true),
}
},
]);
});
it('should not call onChanges if props are set directly', () => {
let events: SimpleChanges[] = [];
let compInstance: MyComp;
class MyComp {
value = 0;
ngOnChanges(changes: SimpleChanges) { events.push(changes); }
static ngComponentDef = defineComponent({
type: MyComp,
factory: () => {
// Capture the instance so we can test setting the property directly
compInstance = new MyComp();
return compInstance;
},
template: (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'data-a', bind(ctx.a));
}
},
selectors: [['mycomp']],
inputs: {
value: 'value',
},
consts: 1,
vars: 1,
});
}
/**
* <my-comp [value]="1"></my-comp>
*/
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'mycomp');
}
if (rf & RenderFlags.Update) {
elementProperty(0, 'value', bind(1));
}
}, 1, 1, [MyComp]);
const fixture = new ComponentFixture(App);
events = [];
// Try setting the property directly
compInstance !.value = 2;
fixture.update();
expect(events).toEqual([]);
});
});
describe('hook order', () => {
@ -2394,7 +2728,6 @@ describe('lifecycles', () => {
consts: consts,
vars: vars,
inputs: {val: 'val'}, template,
features: [NgOnChangesFeature],
directives: directives
});
};
@ -2412,6 +2745,11 @@ describe('lifecycles', () => {
element(0, 'comp');
element(1, 'comp');
}
// This template function is a little weird in that the `elementProperty` calls
// below are directly setting values `1` and `2`, where normally there would be
// a call to `bind()` that would do the work of seeing if something changed.
// This means when `fixture.update()` is called below, ngOnChanges should fire,
// even though the *value* itself never changed.
if (rf & RenderFlags.Update) {
elementProperty(0, 'val', 1);
elementProperty(1, 'val', 2);
@ -2426,7 +2764,7 @@ describe('lifecycles', () => {
]);
events = [];
fixture.update();
fixture.update(); // Changes are made due to lack of `bind()` call in template fn.
expect(events).toEqual([
'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1',
'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2'

View File

@ -1,325 +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 {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core';
import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature';
import {DirectiveDef, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index';
describe('NgOnChangesFeature', () => {
it('should patch class', () => {
class MyDirective implements OnChanges, DoCheck {
public log: Array<string|SimpleChange> = [];
public valA: string = 'initValue';
public set valB(value: string) { this.log.push(value); }
public get valB() { return 'works'; }
ngDoCheck(): void { this.log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
this.log.push('ngOnChanges');
this.log.push('valA', changes['valA']);
this.log.push('valB', changes['valB']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(myDir.log).toEqual(['second']);
expect(myDir.valB).toEqual('works');
myDir.log.length = 0;
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']);
});
it('should inherit the behavior from super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
set valB(value: string) { log.push(value); }
get valB() { return 'works'; }
ngDoCheck(): void { log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
log.push('ngOnChanges');
log.push('valA', changes['valA']);
log.push('valB', changes['valB']);
log.push('valC', changes['valC']);
}
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'},
});
}
class SubDirective extends SuperDirective {
valC = 'initValue';
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valC: 'valC'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(myDir.valB).toEqual('works');
myDir.valC = 'third';
expect(myDir.valC).toEqual('third');
log.length = 0;
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
const changeC = new SimpleChange(undefined, 'third', true);
expect(log).toEqual(
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
});
it('should not run the parent doCheck if it is not called explicitly on super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
ngDoCheck(): void { log.push('ERROR: Child overrides it without super call'); }
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA'},
});
}
class SubDirective extends SuperDirective implements DoCheck {
valB = 'initValue';
ngDoCheck(): void { log.push('sub ngDoCheck'); }
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valB: 'valB'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(log).toEqual([changeA, changeB, 'sub ngDoCheck']);
});
it('should run the parent doCheck if it is inherited from super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
ngDoCheck(): void { log.push('super ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA'},
});
}
class SubDirective extends SuperDirective implements DoCheck {
valB = 'initValue';
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valB: 'valB'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(log).toEqual([changeA, changeB, 'super ngDoCheck']);
});
it('should apply the feature to inherited properties if on sub class', () => {
const log: any[] = [];
class SuperDirective {
valC = 'initValue';
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'subDir', '']],
factory: () => new SuperDirective(),
features: [],
inputs: {valC: 'valC'},
});
}
class SubDirective extends SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
set valB(value: string) { log.push(value); }
get valB() { return 'works'; }
ngDoCheck(): void { log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
log.push('ngOnChanges');
log.push('valA', changes['valA']);
log.push('valB', changes['valB']);
log.push('valC', changes['valC']);
}
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'superDir', '']],
factory: () => new SubDirective(),
// Inheritance must always be before OnChanges feature.
features: [
InheritDefinitionFeature,
NgOnChangesFeature,
],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(log).toEqual(['second']);
expect(myDir.valB).toEqual('works');
myDir.valC = 'third';
expect(myDir.valC).toEqual('third');
log.length = 0;
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
const changeC = new SimpleChange(undefined, 'third', true);
expect(log).toEqual(
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
});
it('correctly computes firstChange', () => {
class MyDirective implements OnChanges {
public log: Array<string|SimpleChange|undefined> = [];
public valA: string = 'initValue';
// TODO(issue/24571): remove '!'.
public valB !: string;
ngOnChanges(changes: SimpleChanges): void {
this.log.push('valA', changes['valA']);
this.log.push('valB', changes['valB']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA1 = new SimpleChange(undefined, 'first', true);
const changeB1 = new SimpleChange(undefined, 'second', true);
expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]);
myDir.log.length = 0;
myDir.valA = 'third';
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA2 = new SimpleChange('first', 'third', false);
expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]);
});
it('should not create a getter when only a setter is originally defined', () => {
class MyDirective implements OnChanges {
public log: Array<string|SimpleChange> = [];
public set onlySetter(value: string) { this.log.push(value); }
ngOnChanges(changes: SimpleChanges): void {
this.log.push('ngOnChanges');
this.log.push('onlySetter', changes['onlySetter']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {onlySetter: 'onlySetter'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.onlySetter = 'someValue';
expect(myDir.onlySetter).toBeUndefined();
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeSetter = new SimpleChange(undefined, 'someValue', true);
expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]);
});
});

View File

@ -8,7 +8,7 @@
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {ViewEncapsulation} from '../../src/metadata';
import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1631,7 +1631,6 @@ describe('ViewContainerRef', () => {
textBinding(0, interpolation1('', cmp.name, ''));
}
},
features: [NgOnChangesFeature],
inputs: {name: 'name'}
});
}
@ -1796,12 +1795,13 @@ describe('ViewContainerRef', () => {
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>');
expect(log).toEqual([]);
// Below will *NOT* cause onChanges to fire, because only bindings trigger onChanges
componentRef.instance.name = 'D';
log.length = 0;
fixture.update();
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>');
expect(log).toEqual([
'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A',
'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
]);

View File

@ -9,6 +9,7 @@
import {SecurityContext} from '@angular/core';
import {ArgumentType, BindingFlags, NodeCheckFn, NodeFlags, Services, ViewData, ViewFlags, ViewState, asElementData, directiveDef, elementDef, rootRenderNodes} from '@angular/core/src/view/index';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {fixmeIvy} from '@angular/private/testing';
import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper';

View File

@ -11,6 +11,7 @@ import {getDebugContext} from '@angular/core/src/errors';
import {BindingFlags, NodeFlags, Services, ViewData, ViewDefinition, asElementData, elementDef} from '@angular/core/src/view/index';
import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {fixmeIvy} from '@angular/private/testing';
import {ARG_TYPE_VALUES, callMostRecentEventListenerHandler, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes, isBrowser, recordNodeToRemove} from './helper';
@ -184,6 +185,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
return result;
}
it('should listen to DOM events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent');
const removeListenerSpy =
@ -251,6 +253,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
expect(removeListenerSpy).toHaveBeenCalled();
});
it('should preventDefault only if the handler returns false', () => {
let eventHandlerResult: any;
let preventDefaultSpy: jasmine.Spy = undefined !;
@ -279,6 +282,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should report debug info on event errors', () => {
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough();

View File

@ -319,239 +319,235 @@ withEachNg1Version(() => {
});
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should bind properties, events', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []).value(
$EXCEPTION_HANDLER, (err: any) => { throw err; });
it('should bind properties, events', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module =
angular.module('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; });
ng1Module.run(($rootScope: any) => {
$rootScope.name = 'world';
$rootScope.dataA = 'A';
$rootScope.dataB = 'B';
$rootScope.modelA = 'initModelA';
$rootScope.modelB = 'initModelB';
$rootScope.eventA = '?';
$rootScope.eventB = '?';
});
@Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange',
'twoWayBEmitter: twoWayBChange'
],
template: 'ignore: {{ignore}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
})
class Ng2 {
ngOnChangesCount = 0;
ignore = '-';
literal = '?';
interpolate = '?';
oneWayA = '?';
oneWayB = '?';
twoWayA = '?';
twoWayB = '?';
eventA = new EventEmitter();
eventB = new EventEmitter();
twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => {
if ((this as any)[prop] != value) {
throw new Error(
`Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`);
}
};
ng1Module.run(($rootScope: any) => {
$rootScope.name = 'world';
$rootScope.dataA = 'A';
$rootScope.dataB = 'B';
$rootScope.modelA = 'initModelA';
$rootScope.modelB = 'initModelB';
$rootScope.eventA = '?';
$rootScope.eventB = '?';
});
@Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange'
],
template: 'ignore: {{ignore}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
})
class Ng2 {
ngOnChangesCount = 0;
ignore = '-';
literal = '?';
interpolate = '?';
oneWayA = '?';
oneWayB = '?';
twoWayA = '?';
twoWayB = '?';
eventA = new EventEmitter();
eventB = new EventEmitter();
twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => {
if ((this as any)[prop] != value) {
throw new Error(
`Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`);
}
};
const assertChange = (prop: string, value: any) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
const actValue = changes[prop].currentValue;
if (actValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
}
};
const assertChange = (prop: string, value: any) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
const actValue = changes[prop].currentValue;
if (actValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
}
};
switch (this.ngOnChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
switch (this.ngOnChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired');
this.eventB.emit('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB');
break;
case 2:
assertChange('interpolate', 'Hello everyone');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired');
this.eventB.emit('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB');
break;
case 2:
assertChange('interpolate', 'Hello everyone');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@NgModule({
declarations: [Ng2],
imports: [BrowserModule],
})
class Ng2Module {
}
@NgModule({
declarations: [Ng2],
imports: [BrowserModule],
})
class Ng2Module {
}
const element = html(`<div>
const element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB"
bindon-two-way-a="modelA" [(two-way-b)]="modelB"
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.ng1RootScope.$apply('name = "everyone"');
expect(multiTrim(document.body.textContent !))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.ng1RootScope.$apply('name = "everyone"');
expect(multiTrim(document.body.textContent !))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.dispose();
});
ref.dispose();
});
}));
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should support two-way binding and event listener', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const listenerSpy = jasmine.createSpy('$rootScope.listener');
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['value'] = 'world';
$rootScope['listener'] = listenerSpy;
});
it('should support two-way binding and event listener', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const listenerSpy = jasmine.createSpy('$rootScope.listener');
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['value'] = 'world';
$rootScope['listener'] = listenerSpy;
});
@Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
@Input() model = '?';
@Output() modelChange = new EventEmitter();
@Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
@Input() model = '?';
@Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) {
case 0:
expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC');
break;
case 1:
expect(changes.model.currentValue).toBe('newC');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) {
case 0:
expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC');
break;
case 1:
expect(changes.model.currentValue).toBe('newC');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@NgModule({declarations: [Ng2Component], imports: [BrowserModule]})
class Ng2Module {
ngDoBootstrap() {}
}
@NgModule({declarations: [Ng2Component], imports: [BrowserModule]})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
const element = html(`
<div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}}
</div>
`);
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC');
ref.dispose();
});
}));
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC');
ref.dispose();
});
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should initialize inputs in time for `ngOnChanges`', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
it('should initialize inputs in time for `ngOnChanges`', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@Component({
selector: 'ng2',
template: `
@Component({
selector: 'ng2',
template: `
ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}`
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
// TODO(issue/24571): remove '!'.
initialValue !: string;
// TODO(issue/24571): remove '!'.
@Input() foo !: string;
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
// TODO(issue/24571): remove '!'.
initialValue !: string;
// TODO(issue/24571): remove '!'.
@Input() foo !: string;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module {
}
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module {
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', adapter.downgradeNg2Component(Ng2Component));
const ng1Module = angular.module('ng1', []).directive(
'ng2', adapter.downgradeNg2Component(Ng2Component));
const element = html(`
const element = html(`
<ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2>
`);
adapter.bootstrap(element, ['ng1']).ready(ref => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
adapter.bootstrap(element, ['ng1']).ready(ref => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
ref.dispose();
});
}));
ref.dispose();
});
}));
it('should bind to ng-model', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));

View File

@ -22,106 +22,104 @@ withEachNg1Version(() => {
beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform());
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should bind properties, events', async(() => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['name'] = 'world';
$rootScope['dataA'] = 'A';
$rootScope['dataB'] = 'B';
$rootScope['modelA'] = 'initModelA';
$rootScope['modelB'] = 'initModelB';
$rootScope['eventA'] = '?';
$rootScope['eventB'] = '?';
});
it('should bind properties, events', async(() => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['name'] = 'world';
$rootScope['dataA'] = 'A';
$rootScope['dataB'] = 'B';
$rootScope['modelA'] = 'initModelA';
$rootScope['modelB'] = 'initModelB';
$rootScope['eventA'] = '?';
$rootScope['eventB'] = '?';
});
@Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange',
'twoWayBEmitter: twoWayBChange'
],
template: 'ignore: {{ignore}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
ignore = '-';
literal = '?';
interpolate = '?';
oneWayA = '?';
oneWayB = '?';
twoWayA = '?';
twoWayB = '?';
eventA = new EventEmitter();
eventB = new EventEmitter();
twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
@Component({
selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange'
],
template: 'ignore: {{ignore}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
ignore = '-';
literal = '?';
interpolate = '?';
oneWayA = '?';
oneWayB = '?';
twoWayA = '?';
twoWayB = '?';
eventA = new EventEmitter();
eventB = new EventEmitter();
twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => {
const propVal = (this as any)[prop];
if (propVal != value) {
throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`);
}
};
ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => {
const propVal = (this as any)[prop];
if (propVal != value) {
throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`);
}
};
const assertChange = (prop: string, value: any) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
const actualValue = changes[prop].currentValue;
if (actualValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`);
}
};
const assertChange = (prop: string, value: any) => {
assert(prop, value);
if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`);
}
const actualValue = changes[prop].currentValue;
if (actualValue != value) {
throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`);
}
};
switch (this.ngOnChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
switch (this.ngOnChangesCount++) {
case 0:
assert('ignore', '-');
assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired');
this.eventB.emit('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB');
break;
case 2:
assertChange('interpolate', 'Hello everyone');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired');
this.eventB.emit('bFired');
break;
case 1:
assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB');
break;
case 2:
assertChange('interpolate', 'Hello everyone');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', downgradeComponent({
component: Ng2Component,
}));
ng1Module.directive('ng2', downgradeComponent({
component: Ng2Component,
}));
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
const element = html(`
<div>
<ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB"
@ -130,23 +128,23 @@ withEachNg1Version(() => {
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
$apply(upgrade, 'name = "everyone"');
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
});
}));
$apply(upgrade, 'name = "everyone"');
expect(multiTrim(document.body.textContent))
.toEqual(
'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
});
}));
it('should bind properties to onpush components', async(() => {
const ng1Module = angular.module('ng1', []).run(
@ -189,58 +187,57 @@ withEachNg1Version(() => {
});
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should support two-way binding and event listener', async(() => {
const listenerSpy = jasmine.createSpy('$rootScope.listener');
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['value'] = 'world';
$rootScope['listener'] = listenerSpy;
});
it('should support two-way binding and event listener', async(() => {
const listenerSpy = jasmine.createSpy('$rootScope.listener');
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['value'] = 'world';
$rootScope['listener'] = listenerSpy;
});
@Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
@Input() model = '?';
@Output() modelChange = new EventEmitter();
@Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
@Input() model = '?';
@Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) {
case 0:
expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC');
break;
case 1:
expect(changes.model.currentValue).toBe('newC');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) {
case 0:
expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC');
break;
case 1:
expect(changes.model.currentValue).toBe('newC');
break;
default:
throw new Error('Called too many times! ' + JSON.stringify(changes));
}
}
}
ng1Module.directive('ng2', downgradeComponent({component: Ng2Component}));
ng1Module.directive('ng2', downgradeComponent({component: Ng2Component}));
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule]
})
class Ng2Module {
ngDoBootstrap() {}
}
const element = html(`
const element = html(`
<div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}}
</div>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC');
});
}));
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC');
});
}));
it('should run change-detection on every digest (by default)', async(() => {
let ng2Component: Ng2Component;
@ -404,66 +401,65 @@ withEachNg1Version(() => {
});
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should initialize inputs in time for `ngOnChanges`', async(() => {
@Component({
selector: 'ng2',
template: `
it('should initialize inputs in time for `ngOnChanges`', async(() => {
@Component({
selector: 'ng2',
template: `
ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}`
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
// TODO(issue/24571): remove '!'.
initialValue !: string;
// TODO(issue/24571): remove '!'.
@Input() foo !: string;
})
class Ng2Component implements OnChanges {
ngOnChangesCount = 0;
firstChangesCount = 0;
// TODO(issue/24571): remove '!'.
initialValue !: string;
// TODO(issue/24571): remove '!'.
@Input() foo !: string;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo;
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++;
}
}
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
@NgModule({
imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component],
entryComponents: [Ng2Component]
})
class Ng2Module {
ngDoBootstrap() {}
}
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component}));
const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component}));
const element = html(`
const element = html(`
<ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2>
`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
});
}));
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
});
}));
it('should bind to ng-model', async(() => {
const ng1Module = angular.module('ng1', []).run(

View File

@ -725,66 +725,63 @@ withEachNg1Version(() => {
});
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should propagate input changes inside the Angular zone', async(() => {
let ng2Component: Ng2Component;
it('should propagate input changes inside the Angular zone', async(() => {
let ng2Component: Ng2Component;
@Component({selector: 'ng2', template: ''})
class Ng2Component implements OnChanges {
@Input() attrInput = 'foo';
@Input() propInput = 'foo';
@Component({selector: 'ng2', template: ''})
class Ng2Component implements OnChanges {
@Input() attrInput = 'foo';
@Input() propInput = 'foo';
constructor() { ng2Component = this; }
ngOnChanges() {}
}
constructor() { ng2Component = this; }
ngOnChanges() {}
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule],
})
class Ng2Module {
ngDoBootstrap() {}
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule],
})
class Ng2Module {
ngDoBootstrap() {}
}
const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module =
angular.module('ng1', [lazyModuleName])
.directive(
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope.attrVal = 'bar';
$rootScope.propVal = 'bar';
});
const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module =
angular.module('ng1', [lazyModuleName])
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
.run(($rootScope: angular.IRootScopeService) => {
$rootScope.attrVal = 'bar';
$rootScope.propVal = 'bar';
});
const element =
html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>');
const $injector = angular.bootstrap(element, [ng1Module.name]);
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
const element = html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>');
const $injector = angular.bootstrap(element, [ng1Module.name]);
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true);
const changesSpy =
spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone);
setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true);
const changesSpy =
spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone);
expect(ng2Component.attrInput).toBe('bar');
expect(ng2Component.propInput).toBe('bar');
expect(ng2Component.attrInput).toBe('bar');
expect(ng2Component.propInput).toBe('bar');
$rootScope.$apply('attrVal = "baz"');
expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('bar');
expect(changesSpy).toHaveBeenCalledTimes(1);
$rootScope.$apply('attrVal = "baz"');
expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('bar');
expect(changesSpy).toHaveBeenCalledTimes(1);
$rootScope.$apply('propVal = "qux"');
expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('qux');
expect(changesSpy).toHaveBeenCalledTimes(2);
});
});
}));
$rootScope.$apply('propVal = "qux"');
expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('qux');
expect(changesSpy).toHaveBeenCalledTimes(2);
});
});
}));
fixmeIvy('FW-714: ng1 projected content is not being rendered')
.it('should create and destroy nested, asynchronously instantiated components inside the Angular zone',
@ -951,167 +948,165 @@ withEachNg1Version(() => {
});
}));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
.it('should run the lifecycle hooks in the correct order', async(() => {
const logs: string[] = [];
let rootScope: angular.IRootScopeService;
it('should run the lifecycle hooks in the correct order', async(() => {
const logs: string[] = [];
let rootScope: angular.IRootScopeService;
@Component({
selector: 'ng2',
template: `
@Component({
selector: 'ng2',
template: `
{{ value }}
<button (click)="value = 'qux'"></button>
<ng-content></ng-content>
`
})
class Ng2Component implements AfterContentChecked,
AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges,
OnDestroy, OnInit {
@Input() value = 'foo';
})
class Ng2Component implements AfterContentChecked,
AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy,
OnInit {
@Input() value = 'foo';
ngAfterContentChecked() { this.log('AfterContentChecked'); }
ngAfterContentInit() { this.log('AfterContentInit'); }
ngAfterViewChecked() { this.log('AfterViewChecked'); }
ngAfterViewInit() { this.log('AfterViewInit'); }
ngDoCheck() { this.log('DoCheck'); }
ngOnChanges() { this.log('OnChanges'); }
ngOnDestroy() { this.log('OnDestroy'); }
ngOnInit() { this.log('OnInit'); }
ngAfterContentChecked() { this.log('AfterContentChecked'); }
ngAfterContentInit() { this.log('AfterContentInit'); }
ngAfterViewChecked() { this.log('AfterViewChecked'); }
ngAfterViewInit() { this.log('AfterViewInit'); }
ngDoCheck() { this.log('DoCheck'); }
ngOnChanges() { this.log('OnChanges'); }
ngOnDestroy() { this.log('OnDestroy'); }
ngOnInit() { this.log('OnInit'); }
private log(hook: string) { logs.push(`${hook}(${this.value})`); }
}
private log(hook: string) { logs.push(`${hook}(${this.value})`); }
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule],
})
class Ng2Module {
ngDoBootstrap() {}
}
@NgModule({
declarations: [Ng2Component],
entryComponents: [Ng2Component],
imports: [BrowserModule],
})
class Ng2Module {
ngDoBootstrap() {}
}
const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module =
angular.module('ng1', [lazyModuleName])
.directive(
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
.run(($rootScope: angular.IRootScopeService) => {
rootScope = $rootScope;
rootScope.value = 'bar';
});
const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module =
angular.module('ng1', [lazyModuleName])
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
.run(($rootScope: angular.IRootScopeService) => {
rootScope = $rootScope;
rootScope.value = 'bar';
});
const element =
html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>');
angular.bootstrap(element, [ng1Module.name]);
const element =
html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>');
angular.bootstrap(element, [ng1Module.name]);
setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const button = element.querySelector('button') !;
setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const button = element.querySelector('button') !;
// Once initialized.
expect(multiTrim(element.textContent)).toBe('bar Content');
expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher.
'OnChanges(bar)',
// Initial CD triggered directly through the `detectChanges()` or
// `inputChanges`
// $watcher (for `propagateDigest` true/false respectively).
'OnInit(bar)',
'DoCheck(bar)',
'AfterContentInit(bar)',
'AfterContentChecked(bar)',
'AfterViewInit(bar)',
'AfterViewChecked(bar)',
...(propagateDigest ?
[
// CD triggered directly through the `detectChanges()` $watcher (2nd
// $digest).
'DoCheck(bar)',
'AfterContentChecked(bar)',
'AfterViewChecked(bar)',
] :
[]),
// CD triggered due to entering/leaving the NgZone (in `downgradeFn()`).
'DoCheck(bar)',
'AfterContentChecked(bar)',
'AfterViewChecked(bar)',
]);
logs.length = 0;
// Once initialized.
expect(multiTrim(element.textContent)).toBe('bar Content');
expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher.
'OnChanges(bar)',
// Initial CD triggered directly through the `detectChanges()` or
// `inputChanges`
// $watcher (for `propagateDigest` true/false respectively).
'OnInit(bar)',
'DoCheck(bar)',
'AfterContentInit(bar)',
'AfterContentChecked(bar)',
'AfterViewInit(bar)',
'AfterViewChecked(bar)',
...(propagateDigest ?
[
// CD triggered directly through the `detectChanges()` $watcher (2nd
// $digest).
'DoCheck(bar)',
'AfterContentChecked(bar)',
'AfterViewChecked(bar)',
] :
[]),
// CD triggered due to entering/leaving the NgZone (in `downgradeFn()`).
'DoCheck(bar)',
'AfterContentChecked(bar)',
'AfterViewChecked(bar)',
]);
logs.length = 0;
// Change inputs and run `$digest`.
rootScope.$apply('value = "baz"');
expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher.
'OnChanges(baz)',
// `propagateDigest: true` (3 CD runs):
// - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher).
// - CD triggered directly through the `detectChanges()` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `detectChanges`
// $watcher).
// `propagateDigest: false` (2 CD runs):
// - CD triggered directly through the `inputChanges` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher).
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
...(propagateDigest ?
[
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
] :
[]),
]);
logs.length = 0;
// Change inputs and run `$digest`.
rootScope.$apply('value = "baz"');
expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher.
'OnChanges(baz)',
// `propagateDigest: true` (3 CD runs):
// - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher).
// - CD triggered directly through the `detectChanges()` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `detectChanges`
// $watcher).
// `propagateDigest: false` (2 CD runs):
// - CD triggered directly through the `inputChanges` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher).
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
...(propagateDigest ?
[
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
] :
[]),
]);
logs.length = 0;
// Run `$digest` (without changing inputs).
rootScope.$digest();
expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual(
propagateDigest ?
[
// CD triggered directly through the `detectChanges()` $watcher.
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
// CD triggered due to entering/leaving the NgZone (in the above
// $watcher).
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
] :
[]);
logs.length = 0;
// Run `$digest` (without changing inputs).
rootScope.$digest();
expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual(
propagateDigest ?
[
// CD triggered directly through the `detectChanges()` $watcher.
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
// CD triggered due to entering/leaving the NgZone (in the above
// $watcher).
'DoCheck(baz)',
'AfterContentChecked(baz)',
'AfterViewChecked(baz)',
] :
[]);
logs.length = 0;
// Trigger change detection (without changing inputs).
button.click();
expect(multiTrim(element.textContent)).toBe('qux Content');
expect(logs).toEqual([
'DoCheck(qux)',
'AfterContentChecked(qux)',
'AfterViewChecked(qux)',
]);
logs.length = 0;
// Trigger change detection (without changing inputs).
button.click();
expect(multiTrim(element.textContent)).toBe('qux Content');
expect(logs).toEqual([
'DoCheck(qux)',
'AfterContentChecked(qux)',
'AfterViewChecked(qux)',
]);
logs.length = 0;
// Destroy the component.
rootScope.$apply('hideNg2 = true');
expect(logs).toEqual([
'OnDestroy(qux)',
]);
logs.length = 0;
});
});
}));
// Destroy the component.
rootScope.$apply('hideNg2 = true');
expect(logs).toEqual([
'OnDestroy(qux)',
]);
logs.length = 0;
});
});
}));
it('should detach hostViews from the ApplicationRef once destroyed', async(() => {
let ng2Component: Ng2Component;

View File

@ -306,8 +306,8 @@ export declare class NgSwitchDefault {
}
export declare class NgTemplateOutlet implements OnChanges {
ngTemplateOutlet: TemplateRef<any>;
ngTemplateOutletContext: Object;
ngTemplateOutlet: TemplateRef<any> | null;
ngTemplateOutletContext: Object | null;
constructor(_viewContainerRef: ViewContainerRef);
ngOnChanges(changes: SimpleChanges): void;
}