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": { "master": {
"uncompressed": { "uncompressed": {
"runtime": 1497, "runtime": 1497,
"main": 187134, "main": 187437,
"polyfills": 59608 "polyfills": 59608
} }
} }

View File

@ -34,14 +34,20 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange
*/ */
@Directive({selector: '[ngTemplateOutlet]'}) @Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges { export class NgTemplateOutlet implements OnChanges {
// TODO(issue/24571): remove '!'. private _viewRef: EmbeddedViewRef<any>|null = null;
private _viewRef !: EmbeddedViewRef<any>;
// 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) {} constructor(private _viewContainerRef: ViewContainerRef) {}
@ -97,7 +103,7 @@ export class NgTemplateOutlet implements OnChanges {
private _updateExistingContext(ctx: Object): void { private _updateExistingContext(ctx: Object): void {
for (let propName of Object.keys(ctx)) { 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 = const providers: Expression|null =
directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : 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. // Parse exportAs.
let exportAs: string[]|null = null; let exportAs: string[]|null = null;
if (directive.has('exportAs')) { if (directive.has('exportAs')) {
@ -197,9 +192,6 @@ export function extractDirectiveMetadata(
const metadata: R3DirectiveMetadata = { const metadata: R3DirectiveMetadata = {
name: clazz.name !.text, name: clazz.name !.text,
deps: getConstructorDependencies(clazz, reflector, isCore), host, deps: getConstructorDependencies(clazz, reflector, isCore), host,
lifecycle: {
usesOnChanges,
},
inputs: {...inputsFromMeta, ...inputsFromFields}, inputs: {...inputsFromMeta, ...inputsFromFields},
outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector, outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector,
type: new WrappedNodeExpr(clazz.name !), type: new WrappedNodeExpr(clazz.name !),

View File

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

View File

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

View File

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

View File

@ -74,17 +74,6 @@ export interface R3DirectiveMetadata {
properties: {[key: string]: string}; 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. * A mapping of input field names to the property names.
*/ */

View File

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

View File

@ -11,8 +11,7 @@ import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
import {IterableDifferFactory, IterableDiffers} from './differs/iterable_differs'; import {IterableDifferFactory, IterableDiffers} from './differs/iterable_differs';
import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs'; import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
export {SimpleChanges} from '../metadata/lifecycle_hooks'; export {WrappedValue, devModeEqual} from './change_detection_util';
export {SimpleChange, WrappedValue, devModeEqual} from './change_detection_util';
export {ChangeDetectorRef} from './change_detector_ref'; export {ChangeDetectorRef} from './change_detector_ref';
export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants'; export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants';
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ'; 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 {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs';
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs'; export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
export {PipeTransform} from './pipe_transform'; 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; } 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 { export function isListLikeIterable(obj: any): boolean {
if (!isJsObject(obj)) return false; if (!isJsObject(obj)) return false;
return Array.isArray(obj) || 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, templateRefExtractor as ɵtemplateRefExtractor,
ProvidersFeature as ɵProvidersFeature, ProvidersFeature as ɵProvidersFeature,
InheritDefinitionFeature as ɵInheritDefinitionFeature, InheritDefinitionFeature as ɵInheritDefinitionFeature,
NgOnChangesFeature as ɵNgOnChangesFeature,
LifecycleHooksFeature as ɵLifecycleHooksFeature, LifecycleHooksFeature as ɵLifecycleHooksFeature,
NgModuleType as ɵNgModuleType, NgModuleType as ɵNgModuleType,
NgModuleRef as ɵRender3NgModuleRef, NgModuleRef as ɵRender3NgModuleRef,

View File

@ -9,13 +9,12 @@
import {ChangeDetectionStrategy} from '../change_detection/constants'; import {ChangeDetectionStrategy} from '../change_detection/constants';
import {Provider} from '../di'; import {Provider} from '../di';
import {Type} from '../interface/type'; 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 {compileComponent as render3CompileComponent, compileDirective as render3CompileDirective} from '../render3/jit/directive';
import {compilePipe as render3CompilePipe} from '../render3/jit/pipe'; import {compilePipe as render3CompilePipe} from '../render3/jit/pipe';
import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators'; import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators';
import {noop} from '../util/noop'; import {noop} from '../util/noop';
import {fillProperties} from '../util/property'; import {fillProperties} from '../util/property';
import {ViewEncapsulation} from './view'; 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. * Returns a function that will update the static definition on a class to have the
* @param key "inputs" or "outputs" * 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) => function getOrCreateDefinitionAndUpdateMappingFor(
(target: any, name: string, ...args: any[]) => { getPropertyToUpdate: (baseDef: {inputs?: any, outputs?: any}) => any) {
const constructor = target.constructor; return function updateIOProp(target: any, name: string, ...args: any[]) {
const constructor = target.constructor;
if (!constructor.hasOwnProperty(NG_BASE_DEF)) { let def: any =
initializeBaseDef(target); constructor[NG_COMPONENT_DEF] || constructor[NG_DIRECTIVE_DEF] || constructor[NG_BASE_DEF];
}
const baseDef = constructor.ngBaseDef; if (!def) {
const defProp = getProp(baseDef); 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]; defProp[name] = args[0];
}; }
};
}
/** /**
* @Annotation * @Annotation
@ -737,7 +761,7 @@ const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any
*/ */
export const Input: InputDecorator = makePropDecorator( export const Input: InputDecorator = makePropDecorator(
'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined, 'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined,
updateBaseDefFromIOProp(baseDef => baseDef.inputs || {})); getOrCreateDefinitionAndUpdateMappingFor(def => def.inputs || {}));
/** /**
* Type of the Output decorator / constructor function. * Type of the Output decorator / constructor function.
@ -777,7 +801,7 @@ export interface Output { bindingPropertyName?: string; }
*/ */
export const Output: OutputDecorator = makePropDecorator( export const Output: OutputDecorator = makePropDecorator(
'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined, '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 * 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 * 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 * @description

View File

@ -17,7 +17,7 @@ import {assertComponentType} from './assert';
import {getComponentDef} from './definition'; import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {publishDefaultGlobalUtils} from './global_utils'; 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 {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; 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 rootTView = readPatchedLView(component) ![TVIEW];
const dirIndex = rootTView.data.length - 1; 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 // TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on
// LNode). // 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 {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer'; import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
@ -257,7 +258,7 @@ function findViaDirective(lView: LView, directiveInstance: {}): number {
const directiveIndexStart = tNode.directiveStart; const directiveIndexStart = tNode.directiveStart;
const directiveIndexEnd = tNode.directiveEnd; const directiveIndexEnd = tNode.directiveEnd;
for (let i = directiveIndexStart; i < directiveIndexEnd; i++) { for (let i = directiveIndexStart; i < directiveIndexEnd; i++) {
if (lView[i] === directiveInstance) { if (unwrapOnChangesDirectiveWrapper(lView[i]) === directiveInstance) {
return tNode.index; return tNode.index;
} }
} }

View File

@ -202,7 +202,7 @@ export function defineComponent<T>(componentDefinition: {
/** /**
* A list of optional features to apply. * A list of optional features to apply.
* *
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature} * See: {@link ProvidersFeature}
*/ */
features?: ComponentDefFeature[]; features?: ComponentDefFeature[];
@ -265,6 +265,7 @@ export function defineComponent<T>(componentDefinition: {
inputs: null !, // assigned in noSideEffects inputs: null !, // assigned in noSideEffects
outputs: null !, // assigned in noSideEffects outputs: null !, // assigned in noSideEffects
exportAs: componentDefinition.exportAs || null, exportAs: componentDefinition.exportAs || null,
onChanges: typePrototype.ngOnChanges || null,
onInit: typePrototype.ngOnInit || null, onInit: typePrototype.ngOnInit || null,
doCheck: typePrototype.ngDoCheck || null, doCheck: typePrototype.ngDoCheck || null,
afterContentInit: typePrototype.ngAfterContentInit || 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. * A list of optional features to apply.
* *
* See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature} * See: {@link ProvidersFeature}, {@link InheritDefinitionFeature}
*/ */
features?: DirectiveDefFeature[]; 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 {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 {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert'; import {assertNodeOfPossibleTypes} from './node_assert';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state';
import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util'; import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util';
@ -514,6 +515,8 @@ export function getNodeInjectable(
factory.resolving = false; factory.resolving = false;
setTNodeAndViewData(savePreviousOrParentTNode, saveLView); setTNodeAndViewData(savePreviousOrParentTNode, saveLView);
} }
} else {
value = unwrapOnChangesDirectiveWrapper(value);
} }
return 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 { export function InheritDefinitionFeature(definition: DirectiveDef<any>| ComponentDef<any>): void {
let superType = getSuperType(definition.type); let superType = getSuperType(definition.type);
debugger;
while (superType) { while (superType) {
let superDef: DirectiveDef<any>|ComponentDef<any>|undefined = undefined; let superDef: DirectiveDef<any>|ComponentDef<any>|undefined = undefined;
if (isComponentDef(definition)) { if (isComponentDef(definition)) {
@ -62,7 +60,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
} }
if (baseDef) { if (baseDef) {
// Merge inputs and outputs
fillProperties(definition.inputs, baseDef.inputs); fillProperties(definition.inputs, baseDef.inputs);
fillProperties(definition.declaredInputs, baseDef.declaredInputs); fillProperties(definition.declaredInputs, baseDef.declaredInputs);
fillProperties(definition.outputs, baseDef.outputs); fillProperties(definition.outputs, baseDef.outputs);
@ -127,7 +124,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
} }
} }
// Merge inputs and outputs // Merge inputs and outputs
fillProperties(definition.inputs, superDef.inputs); fillProperties(definition.inputs, superDef.inputs);
fillProperties(definition.declaredInputs, superDef.declaredInputs); fillProperties(definition.declaredInputs, superDef.declaredInputs);
@ -143,6 +139,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
definition.doCheck = definition.doCheck || superDef.doCheck; definition.doCheck = definition.doCheck || superDef.doCheck;
definition.onDestroy = definition.onDestroy || superDef.onDestroy; definition.onDestroy = definition.onDestroy || superDef.onDestroy;
definition.onInit = definition.onInit || superDef.onInit; definition.onInit = definition.onInit || superDef.onInit;
definition.onChanges = definition.onChanges || superDef.onChanges;
// Run parent features // Run parent features
const features = superDef.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 * found in the LICENSE file at https://angular.io/license
*/ */
import {SimpleChanges} from '../change_detection/simple_change';
import {assertEqual} from '../util/assert'; import {assertEqual} from '../util/assert';
import {DirectiveDef} from './interfaces/definition'; import {DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node'; import {TNode} from './interfaces/node';
import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view'; 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 * Adds all directive lifecycle hooks from the given `DirectiveDef` to the given `TView`.
* TView.initHooks during directiveCreate.
* *
* The directive index and hook type are encoded into one number (1st bit: type, remaining bits: * Must be run *only* on the first template pass.
* directive index), then saved in the even indices of the initHooks array. The odd indices
* hold the hook functions themselves.
* *
* @param index The index of the directive in LView * The TView's hooks arrays are arranged in alternating pairs of directiveIndex and hookFunction,
* @param hooks The static hooks map on the directive def * 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 * @param tView The current TView
*/ */
export function queueInitHooks( export function registerPreOrderHooks(
index: number, onInit: (() => void) | null, doCheck: (() => void) | null, tView: TView): void { directiveIndex: number, directiveDef: DirectiveDef<any>, tView: TView): void {
ngDevMode && ngDevMode &&
assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass'); 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) { if (onInit) {
(tView.initHooks || (tView.initHooks = [])).push(index, onInit); (tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit);
} }
if (doCheck) { if (doCheck) {
(tView.initHooks || (tView.initHooks = [])).push(index, doCheck); (tView.initHooks || (tView.initHooks = [])).push(directiveIndex, doCheck);
(tView.checkHooks || (tView.checkHooks = [])).push(index, 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) { if (tView.firstTemplatePass) {
// It's necessary to loop through the directives at elementEnd() (rather than processing in // 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 // 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. // hooks for projected components and directives must be called *before* their hosts.
for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) { for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>; const directiveDef = tView.data[i] as DirectiveDef<any>;
queueContentHooks(def, tView, i); if (directiveDef.afterContentInit) {
queueViewHooks(def, tView, i); (tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit);
queueDestroyHooks(def, tView, i); }
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( export function executeInitHooks(
currentView: LView, tView: TView, checkNoChangesMode: boolean): void { 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( export function executeHooks(
currentView: LView, allHooks: HookData | null, checkHooks: HookData | null, currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null,
checkNoChangesMode: boolean): void { checkNoChangesMode: boolean): void {
if (checkNoChangesMode) return; if (checkNoChangesMode) return;
const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? allHooks : checkHooks; const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks;
if (hooksToCall) { if (hooksToCall) {
callHooks(currentView, hooksToCall); callHooks(currentView, hooksToCall);
} }
@ -119,13 +148,31 @@ export function executeHooks(
/** /**
* Calls lifecycle hooks with their contexts, skipping init hooks if it's not * 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 currentView The current view
* @param arr The array in which the hooks are found * @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) { 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 {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {getComponent, getHostElement, getRenderedText} from './discovery_utils'; import {getComponent, getHostElement, getRenderedText} from './discovery_utils';
import {InheritDefinitionFeature} from './features/inherit_definition_feature'; import {InheritDefinitionFeature} from './features/inherit_definition_feature';
import {NgOnChangesFeature} from './features/ng_onchanges_feature';
import {ProvidersFeature} from './features/providers_feature'; import {ProvidersFeature} from './features/providers_feature';
import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition'; import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition';
@ -158,7 +157,6 @@ export {
DirectiveDefFlags, DirectiveDefFlags,
DirectiveDefWithMeta, DirectiveDefWithMeta,
DirectiveType, DirectiveType,
NgOnChangesFeature,
InheritDefinitionFeature, InheritDefinitionFeature,
ProvidersFeature, ProvidersFeature,
PipeDef, PipeDef,

View File

@ -22,7 +22,7 @@ import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from
import {attachPatchData, getComponentViewByInstance} from './context_discovery'; import {attachPatchData, getComponentViewByInstance} from './context_discovery';
import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di'; import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di';
import {throwMultipleComponentError} from './errors'; 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 {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container';
import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector'; 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 {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; 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 {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 {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'; import {BoundPlayerFactory} from './styling/player_factory';
@ -120,7 +121,7 @@ export function setHostBindings(tView: TView, viewData: LView): void {
if (instruction !== null) { if (instruction !== null) {
viewData[BINDING_INDEX] = bindingRootIndex; viewData[BINDING_INDEX] = bindingRootIndex;
instruction( instruction(
RenderFlags.Update, readElementValue(viewData[currentDirectiveIndex]), RenderFlags.Update, unwrapOnChangesDirectiveWrapper(viewData[currentDirectiveIndex]),
currentElementIndex); currentElementIndex);
} }
currentDirectiveIndex++; currentDirectiveIndex++;
@ -522,7 +523,7 @@ export function elementContainerEnd(): void {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementContainerNode); lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementContainerNode);
} }
queueLifecycleHooks(tView, previousOrParentTNode); registerPostOrderHooks(tView, previousOrParentTNode);
} }
/** /**
@ -719,6 +720,7 @@ export function createTView(
expandoStartIndex: initialViewLength, expandoStartIndex: initialViewLength,
expandoInstructions: null, expandoInstructions: null,
firstTemplatePass: true, firstTemplatePass: true,
changesHooks: null,
initHooks: null, initHooks: null,
checkHooks: null, checkHooks: null,
contentHooks: null, contentHooks: null,
@ -883,9 +885,14 @@ export function listener(
const propsLength = props.length; const propsLength = props.length;
if (propsLength) { if (propsLength) {
const lCleanup = getCleanup(lView); const lCleanup = getCleanup(lView);
for (let i = 0; i < propsLength; i += 2) { // Subscribe to listeners for each output, and setup clean up for each.
ngDevMode && assertDataInRange(lView, props[i] as number); for (let i = 0; i < propsLength;) {
const subscription = lView[props[i] as number][props[i + 1]].subscribe(listenerFn); 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; const idx = lCleanup.length;
lCleanup.push(listenerFn, subscription); lCleanup.push(listenerFn, subscription);
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1)); tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
@ -943,7 +950,7 @@ export function elementEnd(): void {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementNode); lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementNode);
} }
queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode); registerPostOrderHooks(getLView()[TVIEW], previousOrParentTNode);
decreaseElementDepthCount(); decreaseElementDepthCount();
// this is fired at the end of elementEnd because ALL of the stylingBindings code // 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)) { if (hasClassInput(previousOrParentTNode)) {
const stylingContext = getStylingContext(previousOrParentTNode.index, lView); const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
setInputsForProperty( setInputsForProperty(
lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext)); lView, previousOrParentTNode.inputs !, 'class', getInitialClassNameValue(stylingContext));
} }
} }
@ -1051,7 +1058,7 @@ function elementPropertyInternal<T>(
let dataValue: PropertyAliasValue|undefined; let dataValue: PropertyAliasValue|undefined;
if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) && if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) &&
(dataValue = inputData[propName])) { (dataValue = inputData[propName])) {
setInputsForProperty(lView, dataValue, value); setInputsForProperty(lView, inputData, propName, value);
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
if (ngDevMode) { if (ngDevMode) {
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) { 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 * Set the inputs of directives at the current node to corresponding value.
* input properties on the corresponding directives. *
* @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 { function setInputsForProperty(
for (let i = 0; i < inputs.length; i += 2) { lView: LView, inputAliases: PropertyAliases, publicName: string, value: any): void {
ngDevMode && assertDataInRange(lView, inputs[i] as number); const inputs = inputAliases[publicName];
lView[inputs[i] as number][inputs[i + 1]] = value; 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( function setNgReflectProperties(
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
value: any) { 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 renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(inputs[i + 1] as string); const attrName = normalizeDebugBindingName(privateName);
const debugValue = normalizeDebugBindingValue(value); const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) { if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
@ -1172,15 +1192,20 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
const directiveDef = defs[i] as DirectiveDef<any>; const directiveDef = defs[i] as DirectiveDef<any>;
const propertyAliasMap: {[publicName: string]: string} = const publicToMinifiedNames: {[publicName: string]: string} =
isInput ? directiveDef.inputs : directiveDef.outputs; isInput ? directiveDef.inputs : directiveDef.outputs;
for (let publicName in propertyAliasMap) { const publicToDeclaredNames: {[publicName: string]: string}|null =
if (propertyAliasMap.hasOwnProperty(publicName)) { isInput ? directiveDef.declaredInputs : null;
for (let publicName in publicToMinifiedNames) {
if (publicToMinifiedNames.hasOwnProperty(publicName)) {
propStore = propStore || {}; propStore = propStore || {};
const internalName = propertyAliasMap[publicName]; const minifiedName = publicToMinifiedNames[publicName];
const hasProperty = propStore.hasOwnProperty(publicName); const declaredName =
hasProperty ? propStore[publicName].push(i, internalName) : publicToDeclaredNames ? publicToDeclaredNames[publicName] : minifiedName;
(propStore[publicName] = [i, internalName]); 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 initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal = const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); setInputsForProperty(lView, tNode.inputs !, 'class', classInputVal);
} else { } else {
updateStylingMap(stylingContext, classes, styles); updateStylingMap(stylingContext, classes, styles);
} }
@ -1493,7 +1518,7 @@ function resolveDirectives(
// Init hooks are queued now so ngOnInit is called in host components before // Init hooks are queued now so ngOnInit is called in host components before
// any projected components. // any projected components.
queueInitHooks(directiveDefIdx, def.onInit, def.doCheck, tView); registerPreOrderHooks(directiveDefIdx, def, tView);
} }
} }
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap); 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>); addComponentLogic(lView, tNode, def as ComponentDef<any>);
} }
const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode); const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode);
postProcessDirective(lView, directive, def, i); postProcessDirective(lView, directive, def, i);
} }
} }
@ -1526,7 +1552,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const firstTemplatePass = getFirstTemplatePass(); const firstTemplatePass = getFirstTemplatePass();
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>; const def = tView.data[i] as DirectiveDef<any>;
const directive = viewData[i]; const directive = unwrapOnChangesDirectiveWrapper(viewData[i]);
if (def.hostBindings) { if (def.hostBindings) {
const previousExpandoLength = expando.length; const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def); 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. * Process a directive on the current node after its creation.
*/ */
function postProcessDirective<T>( 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(); const previousOrParentTNode = getPreviousOrParentTNode();
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def); postProcessBaseDirective(lView, previousOrParentTNode, directive, def);
ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode'); ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode');
if (previousOrParentTNode && previousOrParentTNode.attrs) { if (previousOrParentTNode && previousOrParentTNode.attrs) {
setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode); setInputsFromAttrs(lView, directiveDefIdx, def, previousOrParentTNode);
} }
if (def.contentQueries) { if (def.contentQueries) {
@ -1596,7 +1627,7 @@ function postProcessDirective<T>(
} }
if (isComponentDef(def)) { if (isComponentDef(def)) {
const componentView = getComponentViewByIndex(previousOrParentTNode.index, viewData); const componentView = getComponentViewByIndex(previousOrParentTNode.index, lView);
componentView[CONTEXT] = directive; componentView[CONTEXT] = directive;
} }
} }
@ -1794,20 +1825,53 @@ function addComponentLogic<T>(
* @param tNode The static data for this node * @param tNode The static data for this node
*/ */
function setInputsFromAttrs<T>( 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; let initialInputData = tNode.initialInputs as InitialInputData | undefined;
if (initialInputData === undefined || directiveIndex >= initialInputData.length) { if (initialInputData === undefined || directiveIndex >= initialInputData.length) {
initialInputData = generateInitialInputs(directiveIndex, inputs, tNode); initialInputData = generateInitialInputs(directiveIndex, def, tNode);
} }
const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; const initialInputs: InitialInputs|null = initialInputData[directiveIndex];
if (initialInputs) { if (initialInputs) {
for (let i = 0; i < initialInputs.length; i += 2) { const directiveOrWrappedDirective = lView[directiveIndex];
(instance as any)[initialInputs[i]] = initialInputs[i + 1];
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 * Generates initialInputData for a node and stores it in the template's static storage
* so subsequent template invocations don't have to recalculate it. * 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 * @param tNode The static data on this node
*/ */
function generateInitialInputs( 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 = []); const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []);
initialInputData[directiveIndex] = null; initialInputData[directiveIndex] = null;
@ -1832,19 +1896,23 @@ function generateInitialInputs(
let i = 0; let i = 0;
while (i < attrs.length) { while (i < attrs.length) {
const attrName = attrs[i]; 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) { if (attrName === AttributeMarker.NamespaceURI) {
// We do not allow inputs on namespaced attributes. // We do not allow inputs on namespaced attributes.
i += 4; i += 4;
continue; continue;
} }
const minifiedInputName = inputs[attrName]; const privateName = directiveDef.inputs[attrName];
const declaredName = directiveDef.declaredInputs[attrName];
const attrValue = attrs[i + 1]; const attrValue = attrs[i + 1];
if (minifiedInputName !== undefined) { if (privateName !== undefined) {
const inputsToStore: InitialInputs = const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(minifiedInputName, attrValue as string); inputsToStore.push(privateName, declaredName, attrValue as string);
} }
i += 2; i += 2;
@ -1919,7 +1987,7 @@ export function template(
if (currentQueries) { if (currentQueries) {
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode); lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode);
} }
queueLifecycleHooks(tView, tNode); registerPostOrderHooks(tView, tNode);
setIsParent(false); setIsParent(false);
} }
@ -2881,7 +2949,7 @@ export function registerContentQuery<Q>(
export const CLEAN_PROMISE = _CLEAN_PROMISE; 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 // If tNode.inputs is undefined, a listener has created outputs, but inputs haven't
// yet been checked. // yet been checked.
if (tNode) { if (tNode) {

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license * 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 {Type} from '../../interface/type';
import {CssSelectorList} from './projection'; import {CssSelectorList} from './projection';
@ -150,6 +151,7 @@ export interface DirectiveDef<T> extends BaseDef<T> {
/* The following are lifecycle hooks for this component */ /* The following are lifecycle hooks for this component */
onInit: (() => void)|null; onInit: (() => void)|null;
doCheck: (() => void)|null; doCheck: (() => void)|null;
onChanges: ((changes: SimpleChanges) => void)|null;
afterContentInit: (() => void)|null; afterContentInit: (() => void)|null;
afterContentChecked: (() => void)|null; afterContentChecked: (() => void)|null;
afterViewInit: (() => 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. * Store the runtime input or output names for all the directives.
* *
* - Even indices: directive index * Values are stored in triplets:
* - Odd indices: minified / internal name * - 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)[]; export type PropertyAliasValue = (number | string)[];
@ -495,10 +497,12 @@ export type InitialInputData = (InitialInputs | null)[];
* Used by InitialInputData to store input properties * Used by InitialInputData to store input properties
* that should be set once from attributes. * that should be set once from attributes.
* *
* Even indices: minified/internal input name * The inputs come in triplets of:
* Odd indices: initial value * 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[]; export type InitialInputs = string[];

View File

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

View File

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

View File

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

View File

@ -31,7 +31,6 @@ export const angularCoreEnv: {[name: string]: Function} = {
'inject': inject, 'inject': inject,
'ɵinjectAttribute': r3.injectAttribute, 'ɵinjectAttribute': r3.injectAttribute,
'ɵtemplateRefExtractor': r3.templateRefExtractor, 'ɵtemplateRefExtractor': r3.templateRefExtractor,
'ɵNgOnChangesFeature': r3.NgOnChangesFeature,
'ɵProvidersFeature': r3.ProvidersFeature, 'ɵProvidersFeature': r3.ProvidersFeature,
'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature, 'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature,
'ɵelementAttribute': r3.elementAttribute, 'ɵ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 {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling'; 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 {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`. */ /** Retrieves a value from any `LView` or `TData`. */
export function loadInternal<T>(view: LView | TData, index: number): T { export function loadInternal<T>(view: LView | TData, index: number): T {
ngDevMode && assertDataInRange(view, index + HEADER_OFFSET); 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. * 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}) { export function resolveBody(element: RElement & {ownerDocument: Document}) {
return {name: 'body', target: element.ownerDocument.body}; 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 {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_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 {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 {destroyLView} from './node_manipulation';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getNativeByTNode} from './util'; import {getNativeByTNode} from './util';
@ -271,7 +272,8 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
} }
private _lookUpContext(): T { 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": "NO_PARENT_INJECTOR"
}, },
{
"name": "NgOnChangesFeature"
},
{ {
"name": "NodeInjectorFactory" "name": "NodeInjectorFactory"
}, },
{ {
"name": "ObjectUnsubscribedErrorImpl" "name": "ObjectUnsubscribedErrorImpl"
}, },
{
"name": "OnChangesDirectiveWrapper"
},
{ {
"name": "PARENT" "name": "PARENT"
}, },
{ {
"name": "PARENT_INJECTOR" "name": "PARENT_INJECTOR"
}, },
{
"name": "PRIVATE_PREFIX"
},
{ {
"name": "RENDERER" "name": "RENDERER"
}, },
@ -113,9 +110,6 @@
{ {
"name": "SANITIZER" "name": "SANITIZER"
}, },
{
"name": "SimpleChange"
},
{ {
"name": "TVIEW" "name": "TVIEW"
}, },
@ -335,9 +329,15 @@
{ {
"name": "isCreationMode" "name": "isCreationMode"
}, },
{
"name": "isExactInstanceOf"
},
{ {
"name": "isFactory" "name": "isFactory"
}, },
{
"name": "isOnChangesDirectiveWrapper"
},
{ {
"name": "isProceduralRenderer" "name": "isProceduralRenderer"
}, },
@ -365,9 +365,6 @@
{ {
"name": "noSideEffects" "name": "noSideEffects"
}, },
{
"name": "onChangesWrapper"
},
{ {
"name": "postProcessBaseDirective" "name": "postProcessBaseDirective"
}, },
@ -449,6 +446,9 @@
{ {
"name": "tickRootContext" "name": "tickRootContext"
}, },
{
"name": "unwrapOnChangesDirectiveWrapper"
},
{ {
"name": "updateViewQuery" "name": "updateViewQuery"
}, },

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import {ElementRef, QueryList} from '@angular/core'; 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 {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 {query, queryRefresh} from '../../src/render3/query';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -357,7 +357,6 @@ describe('host bindings', () => {
template: (rf: RenderFlags, ctx: InitHookComp) => {}, template: (rf: RenderFlags, ctx: InitHookComp) => {},
consts: 0, consts: 0,
vars: 0, vars: 0,
features: [NgOnChangesFeature],
hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => { hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
allocHostVars(1); allocHostVars(1);

View File

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

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ComponentFactoryResolver, OnDestroy, SimpleChanges, ViewContainerRef} from '../../src/core'; import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index'; 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 {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'; import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1941,10 +1941,15 @@ describe('lifecycles', () => {
}); });
describe('onChanges', () => { describe('onChanges', () => {
let events: string[]; let events: ({type: string, name: string, [key: string]: any})[];
beforeEach(() => { events = []; }); beforeEach(() => { events = []; });
/**
* <div>
* <ng-content/>
* </div>
*/
const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => { const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
projectionDef(); projectionDef();
@ -1953,15 +1958,20 @@ describe('lifecycles', () => {
elementEnd(); elementEnd();
} }
}, 2); }, 2);
/**
* <comp [val1]="a" [publicVal2]="b"/>
*/
const Parent = createOnChangesComponent('parent', (rf: RenderFlags, ctx: any) => { const Parent = createOnChangesComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'comp'); element(0, 'comp');
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.a)); elementProperty(0, 'val1', bind(ctx.a));
elementProperty(0, 'publicName', bind(ctx.b)); elementProperty(0, 'publicVal2', bind(ctx.b));
} }
}, 1, 2, [Comp]); }, 1, 2, [Comp]);
const ProjectedComp = createOnChangesComponent('projected', (rf: RenderFlags, ctx: any) => { const ProjectedComp = createOnChangesComponent('projected', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
text(0, 'content'); text(0, 'content');
@ -1974,22 +1984,28 @@ describe('lifecycles', () => {
directives: any[] = []) { directives: any[] = []) {
return class Component { return class Component {
// @Input() val1: string; // @Input() val1: string;
// @Input('publicName') val2: string; // @Input('publicVal2') val2: string;
a: string = 'wasVal1BeforeMinification'; a: string = 'wasVal1BeforeMinification';
b: string = 'wasVal2BeforeMinification'; b: string = 'wasVal2BeforeMinification';
ngOnChanges(simpleChanges: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
events.push( if (changes.a && this.a !== changes.a.currentValue) {
`comp=${name} val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`); 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({ static ngComponentDef = defineComponent({
type: Component, type: Component,
selectors: [[name]], selectors: [[name]],
factory: () => new Component(), factory: () => new Component(),
features: [NgOnChangesFeature],
consts: consts, consts: consts,
vars: vars, vars: vars,
inputs: {a: 'val1', b: ['publicName', 'val2']}, template, inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template,
directives: directives directives: directives
}); });
}; };
@ -1997,49 +2013,64 @@ describe('lifecycles', () => {
class Directive { class Directive {
// @Input() val1: string; // @Input() val1: string;
// @Input('publicName') val2: string; // @Input('publicVal2') val2: string;
a: string = 'wasVal1BeforeMinification'; a: string = 'wasVal1BeforeMinification';
b: string = 'wasVal2BeforeMinification'; b: string = 'wasVal2BeforeMinification';
ngOnChanges(simpleChanges: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
events.push( events.push({type: 'onChanges', name: 'dir - dir', changes});
`dir - val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`);
} }
static ngDirectiveDef = defineDirective({ static ngDirectiveDef = defineDirective({
type: Directive, type: Directive,
selectors: [['', 'dir', '']], selectors: [['', 'dir', '']],
factory: () => new Directive(), factory: () => new Directive(),
features: [NgOnChangesFeature], inputs: {a: 'val1', b: ['publicVal2', 'val2']}
inputs: {a: 'val1', b: ['publicName', 'val2']}
}); });
} }
const defs = [Comp, Parent, Directive, ProjectedComp]; const defs = [Comp, Parent, Directive, ProjectedComp];
it('should call onChanges method after inputs are set in creation and update mode', () => { 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'comp'); element(0, 'comp');
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.val1)); elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2)); elementProperty(0, 'publicVal2', bind(ctx.val2));
} }
}, 1, 2, defs); }, 1, 2, defs);
// First changes happen here.
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
events = []; events = [];
fixture.component.val1 = '1'; fixture.component.val1 = '1';
fixture.component.val2 = 'a'; fixture.component.val2 = 'a';
fixture.update(); 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 = []; events = [];
fixture.component.val1 = '2'; fixture.component.val1 = '2';
fixture.component.val2 = 'b'; fixture.component.val2 = 'b';
fixture.update(); 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', () => { it('should call parent onChanges before child onChanges', () => {
@ -2053,28 +2084,42 @@ describe('lifecycles', () => {
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(ctx.val1)); elementProperty(0, 'val1', bind(ctx.val1));
elementProperty(0, 'publicName', bind(ctx.val2)); elementProperty(0, 'publicVal2', bind(ctx.val2));
} }
}, 1, 2, defs); }, 1, 2, defs);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
// We're clearing events after the first change here
events = []; events = [];
fixture.component.val1 = '1'; fixture.component.val1 = '1';
fixture.component.val2 = 'a'; fixture.component.val2 = 'a';
fixture.update(); fixture.update();
expect(events).toEqual([ 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', () => { it('should call all parent onChanges across view before calling children onChanges', () => {
/** /**
* <parent [val]="1"></parent> * <parent [val1]="1"></parent>
* <parent [val]="2"></parent> * <parent [val1]="2"></parent>
*
* parent temp: <comp [val]="val"></comp>
*/ */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2084,18 +2129,46 @@ describe('lifecycles', () => {
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2)); elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2)); elementProperty(1, 'publicVal2', bind(2));
} }
}, 2, 4, defs); }, 2, 4, defs);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
expect(events).toEqual([ expect(events).toEqual([
'comp=parent val1=1 val2=1 - changed=[val1,val2]', {
'comp=parent val1=2 val2=2 - changed=[val1,val2]', type: 'onChanges',
'comp=comp val1=1 val2=1 - changed=[val1,val2]', name: 'comp - parent',
'comp=comp val1=2 val2=2 - changed=[val1,val2]' 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) { if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
} }
embeddedViewEnd(); embeddedViewEnd();
} }
@ -2131,19 +2204,51 @@ describe('lifecycles', () => {
}, 1, 0, defs); }, 1, 0, defs);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
// Show the `comp` component, causing it to initialize. (first change is true)
fixture.component.condition = true; fixture.component.condition = true;
fixture.update(); 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.component.condition = false;
fixture.update(); 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.component.condition = true;
fixture.update(); fixture.update();
expect(events).toEqual([ 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) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2)); elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2)); elementProperty(1, 'publicVal2', bind(2));
} }
}, 2, 4, defs); }, 2, 4, defs);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
expect(events).toEqual([ 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', () => { it('should call onChanges in host and its content children before next host', () => {
/** /**
* <comp [val]="1"> * <comp [val1]="1" [publicVal2]="1">
* <projected [val]="1"></projected> * <projected [val1]="2" [publicVal2]="2"></projected>
* </comp> * </comp>
* <comp [val]="2"> * <comp [val1]="3" [publicVal2]="3">
* <projected [val]="1"></projected> * <projected [val1]="4" [publicVal2]="4"></projected>
* </comp> * </comp>
*/ */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2194,75 +2313,130 @@ describe('lifecycles', () => {
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
elementProperty(1, 'val1', bind(2)); elementProperty(1, 'val1', bind(2));
elementProperty(1, 'publicName', bind(2)); elementProperty(1, 'publicVal2', bind(2));
elementProperty(2, 'val1', bind(3)); elementProperty(2, 'val1', bind(3));
elementProperty(2, 'publicName', bind(3)); elementProperty(2, 'publicVal2', bind(3));
elementProperty(3, 'val1', bind(4)); elementProperty(3, 'val1', bind(4));
elementProperty(3, 'publicName', bind(4)); elementProperty(3, 'publicVal2', bind(4));
} }
}, 4, 8, defs); }, 4, 8, defs);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
expect(events).toEqual([ expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', {
'comp=projected val1=2 val2=2 - changed=[val1,val2]', type: 'onChanges',
'comp=comp val1=3 val2=3 - changed=[val1,val2]', name: 'comp - comp',
'comp=projected val1=4 val2=4 - changed=[val1,val2]' 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', () => { 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'comp', ['dir', '']); element(0, 'comp', ['dir', '']);
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
} }
}, 1, 2, defs); }, 1, 2, defs);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
expect(events).toEqual([ 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(); fixture.update();
expect(events).toEqual([ expect(events).toEqual([]);
'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]'
]);
}); });
it('should be called on directives on an element', () => { 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'div', ['dir', '']); element(0, 'div', ['dir', '']);
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
} }
}, 1, 2, defs); }, 1, 2, defs);
const fixture = new ComponentFixture(App); 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(); fixture.update();
expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']); expect(events).toEqual([]);
}); });
it('should call onChanges properly in for loop', () => { 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++) { * % 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2273,9 +2447,9 @@ describe('lifecycles', () => {
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
elementProperty(2, 'val1', bind(5)); elementProperty(2, 'val1', bind(5));
elementProperty(2, 'publicName', bind(5)); elementProperty(2, 'publicVal2', bind(5));
containerRefreshStart(1); containerRefreshStart(1);
{ {
for (let j = 2; j < 5; j++) { for (let j = 2; j < 5; j++) {
@ -2285,7 +2459,7 @@ describe('lifecycles', () => {
} }
if (rf1 & RenderFlags.Update) { if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(j)); elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j)); elementProperty(0, 'publicVal2', bind(j));
} }
embeddedViewEnd(); embeddedViewEnd();
} }
@ -2299,21 +2473,56 @@ describe('lifecycles', () => {
// onChanges is called top to bottom, so top level comps (1 and 5) are called // 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) // before the comps inside the for loop's embedded view (2, 3, and 4)
expect(events).toEqual([ expect(events).toEqual([
'comp=comp val1=1 val2=1 - changed=[val1,val2]', {
'comp=comp val1=5 val2=5 - changed=[val1,val2]', type: 'onChanges',
'comp=comp val1=2 val2=2 - changed=[val1,val2]', name: 'comp - comp',
'comp=comp val1=3 val2=3 - changed=[val1,val2]', changes: {
'comp=comp val1=4 val2=4 - changed=[val1,val2]' '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', () => { 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++) { * % 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) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -2324,9 +2533,9 @@ describe('lifecycles', () => {
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val1', bind(1)); elementProperty(0, 'val1', bind(1));
elementProperty(0, 'publicName', bind(1)); elementProperty(0, 'publicVal2', bind(1));
elementProperty(2, 'val1', bind(5)); elementProperty(2, 'val1', bind(5));
elementProperty(2, 'publicName', bind(5)); elementProperty(2, 'publicVal2', bind(5));
containerRefreshStart(1); containerRefreshStart(1);
{ {
for (let j = 2; j < 5; j++) { for (let j = 2; j < 5; j++) {
@ -2336,7 +2545,7 @@ describe('lifecycles', () => {
} }
if (rf1 & RenderFlags.Update) { if (rf1 & RenderFlags.Update) {
elementProperty(0, 'val1', bind(j)); elementProperty(0, 'val1', bind(j));
elementProperty(0, 'publicName', bind(j)); elementProperty(0, 'publicVal2', bind(j));
} }
embeddedViewEnd(); embeddedViewEnd();
} }
@ -2350,19 +2559,144 @@ describe('lifecycles', () => {
// onChanges is called top to bottom, so top level comps (1 and 5) are called // 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) // before the comps inside the for loop's embedded view (2, 3, and 4)
expect(events).toEqual([ expect(events).toEqual([
'comp=parent val1=1 val2=1 - changed=[val1,val2]', {
'comp=parent val1=5 val2=5 - changed=[val1,val2]', type: 'onChanges',
'comp=parent val1=2 val2=2 - changed=[val1,val2]', name: 'comp - parent',
'comp=comp val1=2 val2=2 - changed=[val1,val2]', changes: {
'comp=parent val1=3 val2=3 - changed=[val1,val2]', 'val1': new SimpleChange(undefined, 1, true),
'comp=comp val1=3 val2=3 - changed=[val1,val2]', 'val2': new SimpleChange(undefined, 1, true),
'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, 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', () => { describe('hook order', () => {
@ -2394,7 +2728,6 @@ describe('lifecycles', () => {
consts: consts, consts: consts,
vars: vars, vars: vars,
inputs: {val: 'val'}, template, inputs: {val: 'val'}, template,
features: [NgOnChangesFeature],
directives: directives directives: directives
}); });
}; };
@ -2412,6 +2745,11 @@ describe('lifecycles', () => {
element(0, 'comp'); element(0, 'comp');
element(1, '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) { if (rf & RenderFlags.Update) {
elementProperty(0, 'val', 1); elementProperty(0, 'val', 1);
elementProperty(1, 'val', 2); elementProperty(1, 'val', 2);
@ -2426,7 +2764,7 @@ describe('lifecycles', () => {
]); ]);
events = []; events = [];
fixture.update(); fixture.update(); // Changes are made due to lack of `bind()` call in template fn.
expect(events).toEqual([ expect(events).toEqual([
'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1', 'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1',
'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2' '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 {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 {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 {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'; import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1631,7 +1631,6 @@ describe('ViewContainerRef', () => {
textBinding(0, interpolation1('', cmp.name, '')); textBinding(0, interpolation1('', cmp.name, ''));
} }
}, },
features: [NgOnChangesFeature],
inputs: {name: 'name'} inputs: {name: 'name'}
}); });
} }
@ -1796,12 +1795,13 @@ describe('ViewContainerRef', () => {
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>'); expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>');
expect(log).toEqual([]); expect(log).toEqual([]);
// Below will *NOT* cause onChanges to fire, because only bindings trigger onChanges
componentRef.instance.name = 'D'; componentRef.instance.name = 'D';
log.length = 0; log.length = 0;
fixture.update(); fixture.update();
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>'); expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>');
expect(log).toEqual([ 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-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A',
'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
]); ]);

View File

@ -9,6 +9,7 @@
import {SecurityContext} from '@angular/core'; 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 {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 {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {fixmeIvy} from '@angular/private/testing';
import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper'; 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 {BindingFlags, NodeFlags, Services, ViewData, ViewDefinition, asElementData, elementDef} from '@angular/core/src/view/index';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; 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'; 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; return result;
} }
it('should listen to DOM events', () => { it('should listen to DOM events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent'); const handleEventSpy = jasmine.createSpy('handleEvent');
const removeListenerSpy = const removeListenerSpy =
@ -251,6 +253,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
expect(removeListenerSpy).toHaveBeenCalled(); expect(removeListenerSpy).toHaveBeenCalled();
}); });
it('should preventDefault only if the handler returns false', () => { it('should preventDefault only if the handler returns false', () => {
let eventHandlerResult: any; let eventHandlerResult: any;
let preventDefaultSpy: jasmine.Spy = undefined !; let preventDefaultSpy: jasmine.Spy = undefined !;
@ -279,6 +282,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
expect(preventDefaultSpy).toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalled();
}); });
it('should report debug info on event errors', () => { it('should report debug info on event errors', () => {
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError'); const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough(); 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(() => {
.it('should bind properties, events', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const ng1Module =
const ng1Module = angular.module('ng1', []).value( angular.module('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; });
$EXCEPTION_HANDLER, (err: any) => { throw err; });
ng1Module.run(($rootScope: any) => { ng1Module.run(($rootScope: any) => {
$rootScope.name = 'world'; $rootScope.name = 'world';
$rootScope.dataA = 'A'; $rootScope.dataA = 'A';
$rootScope.dataB = 'B'; $rootScope.dataB = 'B';
$rootScope.modelA = 'initModelA'; $rootScope.modelA = 'initModelA';
$rootScope.modelB = 'initModelB'; $rootScope.modelB = 'initModelB';
$rootScope.eventA = '?'; $rootScope.eventA = '?';
$rootScope.eventB = '?'; $rootScope.eventB = '?';
}); });
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [ outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange'
'twoWayBEmitter: twoWayBChange' ],
], template: 'ignore: {{ignore}}; ' +
template: 'ignore: {{ignore}}; ' + 'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' })
}) class Ng2 {
class Ng2 { ngOnChangesCount = 0;
ngOnChangesCount = 0; ignore = '-';
ignore = '-'; literal = '?';
literal = '?'; interpolate = '?';
interpolate = '?'; oneWayA = '?';
oneWayA = '?'; oneWayB = '?';
oneWayB = '?'; twoWayA = '?';
twoWayA = '?'; twoWayB = '?';
twoWayB = '?'; eventA = new EventEmitter();
eventA = new EventEmitter(); eventB = new EventEmitter();
eventB = new EventEmitter(); twoWayAEmitter = new EventEmitter();
twoWayAEmitter = new EventEmitter(); twoWayBEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter(); ngOnChanges(changes: SimpleChanges) {
ngOnChanges(changes: SimpleChanges) { const assert = (prop: string, value: any) => {
const assert = (prop: string, value: any) => { if ((this as any)[prop] != value) {
if ((this as any)[prop] != value) { throw new Error(
throw new Error( `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`);
`Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); }
} };
};
const assertChange = (prop: string, value: any) => { const assertChange = (prop: string, value: any) => {
assert(prop, value); assert(prop, value);
if (!changes[prop]) { if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`); throw new Error(`Changes record for '${prop}' not found.`);
} }
const actValue = changes[prop].currentValue; const actValue = changes[prop].currentValue;
if (actValue != value) { if (actValue != value) {
throw new Error( throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
} }
}; };
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
assert('ignore', '-'); assert('ignore', '-');
assertChange('literal', 'Text'); assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world'); assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A'); assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B'); assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA'); assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB'); assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA'); this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB'); this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired'); this.eventA.emit('aFired');
this.eventB.emit('bFired'); this.eventB.emit('bFired');
break; break;
case 1: case 1:
assertChange('twoWayA', 'newA'); assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB'); assertChange('twoWayB', 'newB');
break; break;
case 2: case 2:
assertChange('interpolate', 'Hello everyone'); assertChange('interpolate', 'Hello everyone');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@NgModule({ @NgModule({
declarations: [Ng2], declarations: [Ng2],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
} }
const element = html(`<div> const element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{name}}" <ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB" bind-one-way-a="dataA" [one-way-b]="dataB"
bindon-two-way-a="modelA" [(two-way-b)]="modelB" bindon-two-way-a="modelA" [(two-way-b)]="modelB"
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2> on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !)) expect(multiTrim(document.body.textContent !))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' + 'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.ng1RootScope.$apply('name = "everyone"'); ref.ng1RootScope.$apply('name = "everyone"');
expect(multiTrim(document.body.textContent !)) expect(multiTrim(document.body.textContent !))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' + 'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); '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(() => {
.it('should support two-way binding and event listener', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const listenerSpy = jasmine.createSpy('$rootScope.listener');
const listenerSpy = jasmine.createSpy('$rootScope.listener'); const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['value'] = 'world';
$rootScope['value'] = 'world'; $rootScope['listener'] = listenerSpy;
$rootScope['listener'] = listenerSpy; });
});
@Component({selector: 'ng2', template: `model: {{model}};`}) @Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
@Input() model = '?'; @Input() model = '?';
@Output() modelChange = new EventEmitter(); @Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
expect(changes.model.currentValue).toBe('world'); expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC'); this.modelChange.emit('newC');
break; break;
case 1: case 1:
expect(changes.model.currentValue).toBe('newC'); expect(changes.model.currentValue).toBe('newC');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); 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]}) @NgModule({declarations: [Ng2Component], imports: [BrowserModule]})
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const element = html(` const element = html(`
<div> <div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2> <ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}} | value: {{value}}
</div> </div>
`); `);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC'); expect(listenerSpy).toHaveBeenCalledWith('newC');
ref.dispose(); ref.dispose();
}); });
})); }));
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') it('should initialize inputs in time for `ngOnChanges`', async(() => {
.it('should initialize inputs in time for `ngOnChanges`', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: ` template: `
ngOnChangesCount: {{ ngOnChangesCount }} | ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} | firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}` initialValue: {{ initialValue }}`
}) })
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
firstChangesCount = 0; firstChangesCount = 0;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
initialValue !: string; initialValue !: string;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() foo !: string; @Input() foo !: string;
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++; this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) { if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo; this.initialValue = this.foo;
} }
if (changes['foo'] && changes['foo'].isFirstChange()) { if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++; this.firstChangesCount++;
} }
} }
} }
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) @NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module { class Ng2Module {
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', []).directive(
'ng2', adapter.downgradeNg2Component(Ng2Component)); 'ng2', adapter.downgradeNg2Component(Ng2Component));
const element = html(` const element = html(`
<ng2 [foo]="'foo'"></ng2> <ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2> <ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2> <ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2> <ng2 foo="qux" ng-if="true"></ng2>
`); `);
adapter.bootstrap(element, ['ng1']).ready(ref => { adapter.bootstrap(element, ['ng1']).ready(ref => {
const nodes = element.querySelectorAll('ng2'); const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) => const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
ref.dispose(); ref.dispose();
}); });
})); }));
it('should bind to ng-model', async(() => { it('should bind to ng-model', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));

View File

@ -22,106 +22,104 @@ withEachNg1Version(() => {
beforeEach(() => destroyPlatform()); beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform()); afterEach(() => destroyPlatform());
fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') it('should bind properties, events', async(() => {
.it('should bind properties, events', async(() => { const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['name'] = 'world';
$rootScope['name'] = 'world'; $rootScope['dataA'] = 'A';
$rootScope['dataA'] = 'A'; $rootScope['dataB'] = 'B';
$rootScope['dataB'] = 'B'; $rootScope['modelA'] = 'initModelA';
$rootScope['modelA'] = 'initModelA'; $rootScope['modelB'] = 'initModelB';
$rootScope['modelB'] = 'initModelB'; $rootScope['eventA'] = '?';
$rootScope['eventA'] = '?'; $rootScope['eventB'] = '?';
$rootScope['eventB'] = '?'; });
});
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [ outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange'
'twoWayBEmitter: twoWayBChange' ],
], template: 'ignore: {{ignore}}; ' +
template: 'ignore: {{ignore}}; ' + 'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'literal: {{literal}}; interpolate: {{interpolate}}; ' + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' })
}) class Ng2Component implements OnChanges {
class Ng2Component implements OnChanges { ngOnChangesCount = 0;
ngOnChangesCount = 0; ignore = '-';
ignore = '-'; literal = '?';
literal = '?'; interpolate = '?';
interpolate = '?'; oneWayA = '?';
oneWayA = '?'; oneWayB = '?';
oneWayB = '?'; twoWayA = '?';
twoWayA = '?'; twoWayB = '?';
twoWayB = '?'; eventA = new EventEmitter();
eventA = new EventEmitter(); eventB = new EventEmitter();
eventB = new EventEmitter(); twoWayAEmitter = new EventEmitter();
twoWayAEmitter = new EventEmitter(); twoWayBEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => { const assert = (prop: string, value: any) => {
const propVal = (this as any)[prop]; const propVal = (this as any)[prop];
if (propVal != value) { if (propVal != value) {
throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`);
} }
}; };
const assertChange = (prop: string, value: any) => { const assertChange = (prop: string, value: any) => {
assert(prop, value); assert(prop, value);
if (!changes[prop]) { if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`); throw new Error(`Changes record for '${prop}' not found.`);
} }
const actualValue = changes[prop].currentValue; const actualValue = changes[prop].currentValue;
if (actualValue != value) { if (actualValue != value) {
throw new Error( throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`);
} }
}; };
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
assert('ignore', '-'); assert('ignore', '-');
assertChange('literal', 'Text'); assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world'); assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A'); assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B'); assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA'); assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB'); assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA'); this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB'); this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired'); this.eventA.emit('aFired');
this.eventB.emit('bFired'); this.eventB.emit('bFired');
break; break;
case 1: case 1:
assertChange('twoWayA', 'newA'); assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB'); assertChange('twoWayB', 'newB');
break; break;
case 2: case 2:
assertChange('interpolate', 'Hello everyone'); assertChange('interpolate', 'Hello everyone');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', downgradeComponent({ ng1Module.directive('ng2', downgradeComponent({
component: Ng2Component, component: Ng2Component,
})); }));
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const element = html(` const element = html(`
<div> <div>
<ng2 literal="Text" interpolate="Hello {{name}}" <ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB" bind-one-way-a="dataA" [one-way-b]="dataB"
@ -130,23 +128,23 @@ withEachNg1Version(() => {
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' + 'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
$apply(upgrade, 'name = "everyone"'); $apply(upgrade, 'name = "everyone"');
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' + 'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
}); });
})); }));
it('should bind properties to onpush components', async(() => { it('should bind properties to onpush components', async(() => {
const ng1Module = angular.module('ng1', []).run( 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(() => {
.it('should support two-way binding and event listener', async(() => { const listenerSpy = jasmine.createSpy('$rootScope.listener');
const listenerSpy = jasmine.createSpy('$rootScope.listener'); const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { $rootScope['value'] = 'world';
$rootScope['value'] = 'world'; $rootScope['listener'] = listenerSpy;
$rootScope['listener'] = listenerSpy; });
});
@Component({selector: 'ng2', template: `model: {{model}};`}) @Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
@Input() model = '?'; @Input() model = '?';
@Output() modelChange = new EventEmitter(); @Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
expect(changes.model.currentValue).toBe('world'); expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC'); this.modelChange.emit('newC');
break; break;
case 1: case 1:
expect(changes.model.currentValue).toBe('newC'); expect(changes.model.currentValue).toBe('newC');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); ng1Module.directive('ng2', downgradeComponent({component: Ng2Component}));
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const element = html(` const element = html(`
<div> <div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2> <ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}} | value: {{value}}
</div> </div>
`); `);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC'); expect(listenerSpy).toHaveBeenCalledWith('newC');
}); });
})); }));
it('should run change-detection on every digest (by default)', async(() => { it('should run change-detection on every digest (by default)', async(() => {
let ng2Component: Ng2Component; 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(() => {
.it('should initialize inputs in time for `ngOnChanges`', async(() => { @Component({
@Component({ selector: 'ng2',
selector: 'ng2', template: `
template: `
ngOnChangesCount: {{ ngOnChangesCount }} | ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} | firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}` initialValue: {{ initialValue }}`
}) })
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
firstChangesCount = 0; firstChangesCount = 0;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
initialValue !: string; initialValue !: string;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() foo !: string; @Input() foo !: string;
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++; this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) { if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo; this.initialValue = this.foo;
} }
if (changes['foo'] && changes['foo'].isFirstChange()) { if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++; this.firstChangesCount++;
} }
} }
} }
@NgModule({ @NgModule({
imports: [BrowserModule, UpgradeModule], imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component] entryComponents: [Ng2Component]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component})); 'ng2', downgradeComponent({component: Ng2Component}));
const element = html(` const element = html(`
<ng2 [foo]="'foo'"></ng2> <ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2> <ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2> <ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2> <ng2 foo="qux" ng-if="true"></ng2>
`); `);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const nodes = element.querySelectorAll('ng2'); const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) => const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
}); });
})); }));
it('should bind to ng-model', async(() => { it('should bind to ng-model', async(() => {
const ng1Module = angular.module('ng1', []).run( 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(() => {
.it('should propagate input changes inside the Angular zone', async(() => { let ng2Component: Ng2Component;
let ng2Component: Ng2Component;
@Component({selector: 'ng2', template: ''}) @Component({selector: 'ng2', template: ''})
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
@Input() attrInput = 'foo'; @Input() attrInput = 'foo';
@Input() propInput = 'foo'; @Input() propInput = 'foo';
constructor() { ng2Component = this; } constructor() { ng2Component = this; }
ngOnChanges() {} ngOnChanges() {}
} }
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const bootstrapFn = (extraProviders: StaticProvider[]) => const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module = const ng1Module =
angular.module('ng1', [lazyModuleName]) angular.module('ng1', [lazyModuleName])
.directive( .directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
'ng2', downgradeComponent({component: Ng2Component, propagateDigest})) .run(($rootScope: angular.IRootScopeService) => {
.run(($rootScope: angular.IRootScopeService) => { $rootScope.attrVal = 'bar';
$rootScope.attrVal = 'bar'; $rootScope.propVal = 'bar';
$rootScope.propVal = 'bar'; });
});
const element = const element = html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>');
html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>'); const $injector = angular.bootstrap(element, [ng1Module.name]);
const $injector = angular.bootstrap(element, [ng1Module.name]); const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
setTimeout(() => { // Wait for the module to be bootstrapped. setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true); const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true);
const changesSpy = const changesSpy =
spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone); spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone);
expect(ng2Component.attrInput).toBe('bar'); expect(ng2Component.attrInput).toBe('bar');
expect(ng2Component.propInput).toBe('bar'); expect(ng2Component.propInput).toBe('bar');
$rootScope.$apply('attrVal = "baz"'); $rootScope.$apply('attrVal = "baz"');
expect(ng2Component.attrInput).toBe('baz'); expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('bar'); expect(ng2Component.propInput).toBe('bar');
expect(changesSpy).toHaveBeenCalledTimes(1); expect(changesSpy).toHaveBeenCalledTimes(1);
$rootScope.$apply('propVal = "qux"'); $rootScope.$apply('propVal = "qux"');
expect(ng2Component.attrInput).toBe('baz'); expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('qux'); expect(ng2Component.propInput).toBe('qux');
expect(changesSpy).toHaveBeenCalledTimes(2); expect(changesSpy).toHaveBeenCalledTimes(2);
}); });
}); });
})); }));
fixmeIvy('FW-714: ng1 projected content is not being rendered') fixmeIvy('FW-714: ng1 projected content is not being rendered')
.it('should create and destroy nested, asynchronously instantiated components inside the Angular zone', .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(() => {
.it('should run the lifecycle hooks in the correct order', async(() => { const logs: string[] = [];
const logs: string[] = []; let rootScope: angular.IRootScopeService;
let rootScope: angular.IRootScopeService;
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: ` template: `
{{ value }} {{ value }}
<button (click)="value = 'qux'"></button> <button (click)="value = 'qux'"></button>
<ng-content></ng-content> <ng-content></ng-content>
` `
}) })
class Ng2Component implements AfterContentChecked, class Ng2Component implements AfterContentChecked,
AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy,
OnDestroy, OnInit { OnInit {
@Input() value = 'foo'; @Input() value = 'foo';
ngAfterContentChecked() { this.log('AfterContentChecked'); } ngAfterContentChecked() { this.log('AfterContentChecked'); }
ngAfterContentInit() { this.log('AfterContentInit'); } ngAfterContentInit() { this.log('AfterContentInit'); }
ngAfterViewChecked() { this.log('AfterViewChecked'); } ngAfterViewChecked() { this.log('AfterViewChecked'); }
ngAfterViewInit() { this.log('AfterViewInit'); } ngAfterViewInit() { this.log('AfterViewInit'); }
ngDoCheck() { this.log('DoCheck'); } ngDoCheck() { this.log('DoCheck'); }
ngOnChanges() { this.log('OnChanges'); } ngOnChanges() { this.log('OnChanges'); }
ngOnDestroy() { this.log('OnDestroy'); } ngOnDestroy() { this.log('OnDestroy'); }
ngOnInit() { this.log('OnInit'); } ngOnInit() { this.log('OnInit'); }
private log(hook: string) { logs.push(`${hook}(${this.value})`); } private log(hook: string) { logs.push(`${hook}(${this.value})`); }
} }
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const bootstrapFn = (extraProviders: StaticProvider[]) => const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module = const ng1Module =
angular.module('ng1', [lazyModuleName]) angular.module('ng1', [lazyModuleName])
.directive( .directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
'ng2', downgradeComponent({component: Ng2Component, propagateDigest})) .run(($rootScope: angular.IRootScopeService) => {
.run(($rootScope: angular.IRootScopeService) => { rootScope = $rootScope;
rootScope = $rootScope; rootScope.value = 'bar';
rootScope.value = 'bar'; });
});
const element = const element =
html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>'); html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>');
angular.bootstrap(element, [ng1Module.name]); angular.bootstrap(element, [ng1Module.name]);
setTimeout(() => { // Wait for the module to be bootstrapped. setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const button = element.querySelector('button') !; const button = element.querySelector('button') !;
// Once initialized. // Once initialized.
expect(multiTrim(element.textContent)).toBe('bar Content'); expect(multiTrim(element.textContent)).toBe('bar Content');
expect(logs).toEqual([ expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges` // `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher. // $watcher.
'OnChanges(bar)', 'OnChanges(bar)',
// Initial CD triggered directly through the `detectChanges()` or // Initial CD triggered directly through the `detectChanges()` or
// `inputChanges` // `inputChanges`
// $watcher (for `propagateDigest` true/false respectively). // $watcher (for `propagateDigest` true/false respectively).
'OnInit(bar)', 'OnInit(bar)',
'DoCheck(bar)', 'DoCheck(bar)',
'AfterContentInit(bar)', 'AfterContentInit(bar)',
'AfterContentChecked(bar)', 'AfterContentChecked(bar)',
'AfterViewInit(bar)', 'AfterViewInit(bar)',
'AfterViewChecked(bar)', 'AfterViewChecked(bar)',
...(propagateDigest ? ...(propagateDigest ?
[ [
// CD triggered directly through the `detectChanges()` $watcher (2nd // CD triggered directly through the `detectChanges()` $watcher (2nd
// $digest). // $digest).
'DoCheck(bar)', 'DoCheck(bar)',
'AfterContentChecked(bar)', 'AfterContentChecked(bar)',
'AfterViewChecked(bar)', 'AfterViewChecked(bar)',
] : ] :
[]), []),
// CD triggered due to entering/leaving the NgZone (in `downgradeFn()`). // CD triggered due to entering/leaving the NgZone (in `downgradeFn()`).
'DoCheck(bar)', 'DoCheck(bar)',
'AfterContentChecked(bar)', 'AfterContentChecked(bar)',
'AfterViewChecked(bar)', 'AfterViewChecked(bar)',
]); ]);
logs.length = 0; logs.length = 0;
// Change inputs and run `$digest`. // Change inputs and run `$digest`.
rootScope.$apply('value = "baz"'); rootScope.$apply('value = "baz"');
expect(multiTrim(element.textContent)).toBe('baz Content'); expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual([ expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges` // `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher. // $watcher.
'OnChanges(baz)', 'OnChanges(baz)',
// `propagateDigest: true` (3 CD runs): // `propagateDigest: true` (3 CD runs):
// - CD triggered due to entering/leaving the NgZone (in `inputChanges` // - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher). // $watcher).
// - CD triggered directly through the `detectChanges()` $watcher. // - CD triggered directly through the `detectChanges()` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `detectChanges` // - CD triggered due to entering/leaving the NgZone (in `detectChanges`
// $watcher). // $watcher).
// `propagateDigest: false` (2 CD runs): // `propagateDigest: false` (2 CD runs):
// - CD triggered directly through the `inputChanges` $watcher. // - CD triggered directly through the `inputChanges` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `inputChanges` // - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher). // $watcher).
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
...(propagateDigest ? ...(propagateDigest ?
[ [
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
] : ] :
[]), []),
]); ]);
logs.length = 0; logs.length = 0;
// Run `$digest` (without changing inputs). // Run `$digest` (without changing inputs).
rootScope.$digest(); rootScope.$digest();
expect(multiTrim(element.textContent)).toBe('baz Content'); expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual( expect(logs).toEqual(
propagateDigest ? propagateDigest ?
[ [
// CD triggered directly through the `detectChanges()` $watcher. // CD triggered directly through the `detectChanges()` $watcher.
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
// CD triggered due to entering/leaving the NgZone (in the above // CD triggered due to entering/leaving the NgZone (in the above
// $watcher). // $watcher).
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
] : ] :
[]); []);
logs.length = 0; logs.length = 0;
// Trigger change detection (without changing inputs). // Trigger change detection (without changing inputs).
button.click(); button.click();
expect(multiTrim(element.textContent)).toBe('qux Content'); expect(multiTrim(element.textContent)).toBe('qux Content');
expect(logs).toEqual([ expect(logs).toEqual([
'DoCheck(qux)', 'DoCheck(qux)',
'AfterContentChecked(qux)', 'AfterContentChecked(qux)',
'AfterViewChecked(qux)', 'AfterViewChecked(qux)',
]); ]);
logs.length = 0; logs.length = 0;
// Destroy the component. // Destroy the component.
rootScope.$apply('hideNg2 = true'); rootScope.$apply('hideNg2 = true');
expect(logs).toEqual([ expect(logs).toEqual([
'OnDestroy(qux)', 'OnDestroy(qux)',
]); ]);
logs.length = 0; logs.length = 0;
}); });
}); });
})); }));
it('should detach hostViews from the ApplicationRef once destroyed', async(() => { it('should detach hostViews from the ApplicationRef once destroyed', async(() => {
let ng2Component: Ng2Component; let ng2Component: Ng2Component;

View File

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