refactor(ivy): Add newer, smaller NgOnChangesFeature (#28187)

PR Close #28187
This commit is contained in:
Ben Lesh 2019-01-16 09:35:35 -08:00 committed by Alex Rickabaugh
parent 5552661fd7
commit a95e81978b
22 changed files with 752 additions and 1071 deletions

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

@ -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

@ -5,19 +5,9 @@
* 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, SimpleChange} from './simple_change'; import {SimpleChanges} from './simple_change';
/**
* Defines an object that associates properties with
* instances of `SimpleChange`.
*
* @see `OnChanges`
*
* @publicApi
*/
export interface SimpleChanges { [propName: string]: SimpleChange; }
/** /**
* @description * @description
* A lifecycle hook that is called when any data-bound property of a directive changes. * A lifecycle hook that is called when any data-bound property of a directive changes.

View File

@ -17,6 +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 {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';
@ -25,7 +26,6 @@ import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './inte
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view';
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state'; import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state';
import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util'; import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
import { registerPreOrderHooks, registerPostOrderHooks } from './hooks';
@ -240,7 +240,8 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): v
registerPreOrderHooks(dirIndex, def, 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).
registerPostOrderHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode); registerPostOrderHooks(
rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
} }
/** /**

View File

@ -276,6 +276,7 @@ export function defineComponent<T>(componentDefinition: {
id: 'c', id: 'c',
styles: componentDefinition.styles || EMPTY_ARRAY, styles: componentDefinition.styles || EMPTY_ARRAY,
_: null as never, _: null as never,
setInput: null,
}; };
def._ = noSideEffects(() => { def._ = noSideEffects(() => {
const directiveTypes = componentDefinition.directives !; const directiveTypes = componentDefinition.directives !;
@ -380,12 +381,14 @@ export function defineNgModule<T>(def: {type: T} & Partial<NgModuleDef<T>>): nev
* *
*/ */
function invertObject(obj: any, secondary?: any): any { function invertObject<T>(
if (obj == null) return EMPTY_OBJ; obj?: {[P in keyof T]?: string | [string, string]},
secondary?: {[key: string]: string}): {[P in keyof T]: string} {
if (obj == null) return EMPTY_OBJ as any;
const newLookup: any = {}; const newLookup: any = {};
for (const minifiedKey in obj) { for (const minifiedKey in obj) {
if (obj.hasOwnProperty(minifiedKey)) { if (obj.hasOwnProperty(minifiedKey)) {
let publicName: string = obj[minifiedKey]; let publicName: string|[string, string] = obj[minifiedKey] !;
let declaredName = publicName; let declaredName = publicName;
if (Array.isArray(publicName)) { if (Array.isArray(publicName)) {
declaredName = publicName[1]; declaredName = publicName[1];
@ -393,7 +396,7 @@ function invertObject(obj: any, secondary?: any): any {
} }
newLookup[publicName] = minifiedKey; newLookup[publicName] = minifiedKey;
if (secondary) { if (secondary) {
(secondary[publicName] = declaredName); (secondary[publicName] = declaredName as string);
} }
} }
} }
@ -470,11 +473,11 @@ export function defineBase<T>(baseDefinition: {
*/ */
outputs?: {[P in keyof T]?: string}; outputs?: {[P in keyof T]?: string};
}): BaseDef<T> { }): BaseDef<T> {
const declaredInputs: {[P in keyof T]: P} = {} as any; const declaredInputs: {[P in keyof T]: string} = {} as any;
return { return {
inputs: invertObject(baseDefinition.inputs, declaredInputs), inputs: invertObject<T>(baseDefinition.inputs as any, declaredInputs),
declaredInputs: declaredInputs, declaredInputs: declaredInputs,
outputs: invertObject(baseDefinition.outputs), outputs: invertObject<T>(baseDefinition.outputs as any),
}; };
} }

View File

@ -7,10 +7,10 @@
*/ */
import {Type} from '../../interface/type'; import {Type} from '../../interface/type';
import {Component} from '../../metadata/directives';
import {fillProperties} from '../../util/property'; import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
import { Component } from '../../metadata/directives';

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SimpleChange} from '../../change_detection/change_detection_util';
import {SimpleChanges} from '../../interface/simple_change';
import {OnChanges} from '../../interface/lifecycle_hooks'; import {OnChanges} from '../../interface/lifecycle_hooks';
import {SimpleChange, SimpleChanges} from '../../interface/simple_change';
import {EMPTY_OBJ} from '../empty';
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition'; import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
const PRIVATE_PREFIX = '__ngOnChanges_'; const PRIVATE_PREFIX = '__ngOnChanges_';
@ -40,86 +40,63 @@ type OnChangesExpando = OnChanges & {
* ``` * ```
*/ */
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void { export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
const publicToDeclaredInputs = definition.declaredInputs; if (definition.type.prototype.ngOnChanges) {
const publicToMinifiedInputs = definition.inputs; definition.setInput = ngOnChangesSetInput;
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 const prevDoCheck = definition.doCheck;
// That way we can honor setters and getters that were inherited. const prevOnInit = definition.onInit;
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; definition.onInit = wrapOnChanges(prevOnInit);
const setter = originalProperty && originalProperty.set; definition.doCheck = wrapOnChanges(prevDoCheck);
}
}
// create a getter and setter for property function wrapOnChanges(hook: (() => void) | null) {
Object.defineProperty(proto, minifiedKey, { return function(this: OnChanges) {
get: getter || const simpleChangesStore = getSimpleChangesStore(this);
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }), const current = simpleChangesStore && simpleChangesStore.current;
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); if (current) {
const currentChange = simpleChanges[declaredKey]; simpleChangesStore !.previous = current;
simpleChangesStore !.current = null;
if (currentChange) { this.ngOnChanges(current);
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 hook && hook.call(this);
// 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); function ngOnChangesSetInput<T>(
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
const simpleChangesStore = getSimpleChangesStore(instance) ||
setSimpleChangesStore(instance, {previous: EMPTY_OBJ, current: null});
const current = simpleChangesStore.current || (simpleChangesStore.current = {});
const previous = simpleChangesStore.previous;
const declaredName = (this.declaredInputs as{[key: string]: string})[publicName];
const previousChange = previous[declaredName];
current[declaredName] = new SimpleChange(
previousChange && previousChange.currentValue, value, previous === EMPTY_OBJ);
(instance as any)[privateName] = value;
}
const SIMPLE_CHANGES_STORE = '__ngSimpleChanges__';
function getSimpleChangesStore(instance: any): null|NgSimpleChangesStore {
return instance[SIMPLE_CHANGES_STORE] || null;
}
function setSimpleChangesStore(instance: any, store: NgSimpleChangesStore): NgSimpleChangesStore {
return instance[SIMPLE_CHANGES_STORE] = store;
} }
// This option ensures that the ngOnChanges lifecycle hook will be inherited // This option ensures that the ngOnChanges lifecycle hook will be inherited
// from superclasses (in InheritDefinitionFeature). // from superclasses (in InheritDefinitionFeature).
(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true; (NgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
function onChangesWrapper(delegateHook: (() => void) | null) {
return function(this: OnChangesExpando) { interface NgSimpleChangesStore {
const simpleChanges = this[PRIVATE_PREFIX]; previous: SimpleChanges;
if (simpleChanges != null) { current: SimpleChanges|null;
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
if (delegateHook) delegateHook.apply(this);
};
} }

View File

@ -959,10 +959,10 @@ function listenerInternal(
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) { for (let i = 0; i < propsLength; i += 3) {
const index = props[i] as number; const index = props[i] as number;
ngDevMode && assertDataInRange(lView, index); ngDevMode && assertDataInRange(lView, index);
const minifiedName = props[i + 1]; const minifiedName = props[i + 2];
const directiveInstance = lView[index]; const directiveInstance = lView[index];
const output = directiveInstance[minifiedName]; const output = directiveInstance[minifiedName];
@ -1214,18 +1214,29 @@ export function createTNode(
* @param value Value to set. * @param value Value to set.
*/ */
function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void { function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void {
for (let i = 0; i < inputs.length; i += 2) { const tView = lView[TVIEW];
ngDevMode && assertDataInRange(lView, inputs[i] as number); for (let i = 0; i < inputs.length;) {
lView[inputs[i] as number][inputs[i + 1]] = value; const index = inputs[i++] as number;
const publicName = inputs[i++] as string;
const privateName = inputs[i++] as string;
const instance = lView[index];
ngDevMode && assertDataInRange(lView, index);
const def = tView.data[index] as DirectiveDef<any>;
const setInput = def.setInput;
if (setInput) {
def.setInput !(instance, value, publicName, privateName);
} else {
instance[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; i += 3) {
const renderer = lView[RENDERER]; const renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(inputs[i + 1] as string); const attrName = normalizeDebugBindingName(inputs[i + 2] as string);
const debugValue = normalizeDebugBindingValue(value); const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) { if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
@ -1268,8 +1279,8 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
propStore = propStore || {}; propStore = propStore || {};
const internalName = propertyAliasMap[publicName]; const internalName = propertyAliasMap[publicName];
const hasProperty = propStore.hasOwnProperty(publicName); const hasProperty = propStore.hasOwnProperty(publicName);
hasProperty ? propStore[publicName].push(i, internalName) : hasProperty ? propStore[publicName].push(i, publicName, internalName) :
(propStore[publicName] = [i, internalName]); (propStore[publicName] = [i, publicName, internalName]);
} }
} }
} }
@ -1702,7 +1713,7 @@ function postProcessDirective<T>(
postProcessBaseDirective(viewData, previousOrParentTNode, directive, def); postProcessBaseDirective(viewData, 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(directiveDefIdx, directive, def, previousOrParentTNode);
} }
if (def.contentQueries) { if (def.contentQueries) {
@ -1903,16 +1914,24 @@ 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 { directiveIndex: number, instance: T, def: DirectiveDef<T>, 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.inputs, 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 setInput = def.setInput;
(instance as any)[initialInputs[i]] = initialInputs[i + 1]; for (let i = 0; i < initialInputs.length;) {
const publicName = initialInputs[i++];
const privateName = initialInputs[i++];
const value = initialInputs[i++];
if (setInput) {
def.setInput !(instance, value, publicName, privateName);
} else {
(instance as any)[privateName] = value;
}
} }
} }
} }
@ -1956,7 +1975,7 @@ function generateInitialInputs(
if (minifiedInputName !== undefined) { if (minifiedInputName !== undefined) {
const inputsToStore: InitialInputs = const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(minifiedInputName, attrValue as string); inputsToStore.push(attrName, minifiedInputName, attrValue as string);
} }
i += 2; i += 2;

View File

@ -84,14 +84,14 @@ export interface BaseDef<T> {
* @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of * @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of
* public or minified name. * public or minified name.
*/ */
readonly declaredInputs: {[P in keyof T]: P}; readonly declaredInputs: {[P in keyof T]: string};
/** /**
* A dictionary mapping the outputs' minified property names to their public API names, which * A dictionary mapping the outputs' minified property names to their public API names, which
* are their aliases if any, or their original unminified property names * are their aliases if any, or their original unminified property names
* (as in `@Output('alias') propertyName: any;`). * (as in `@Output('alias') propertyName: any;`).
*/ */
readonly outputs: {[P in keyof T]: P}; readonly outputs: {[P in keyof T]: string};
} }
/** /**
@ -152,6 +152,10 @@ export interface DirectiveDef<T> extends BaseDef<T> {
* The features applied to this directive * The features applied to this directive
*/ */
readonly features: DirectiveDefFeature[]|null; readonly features: DirectiveDefFeature[]|null;
setInput:
((this: DirectiveDef<T>, instance: T, value: any, publicName: string,
privateName: string) => void)|null;
} }
export type ComponentDefWithMeta< export type ComponentDefWithMeta<

View File

@ -468,10 +468,11 @@ 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 * i+0: directive instance index
* - Odd indices: minified / internal name * i+1: publicName
* i+2: privateName
* *
* e.g. [0, 'change-minified'] * e.g. [0, 'change', 'change-minified']
*/ */
export type PropertyAliasValue = (number | string)[]; export type PropertyAliasValue = (number | string)[];
@ -484,14 +485,15 @@ export type PropertyAliasValue = (number | string)[];
* *
* Within each sub-array: * Within each sub-array:
* *
* Even indices: minified/internal input name * i+0: attribute name
* Odd indices: initial value * i+1: minified/internal input name
* i+2: initial value
* *
* If a directive on a node does not have any input properties * If a directive on a node does not have any input properties
* that should be set from attributes, its index is set to null * that should be set from attributes, its index is set to null
* to avoid a sparse array. * to avoid a sparse array.
* *
* e.g. [null, ['role-min', 'button']] * e.g. [null, ['role-min', 'minified-input', 'button']]
*/ */
export type InitialInputData = (InitialInputs | null)[]; export type InitialInputData = (InitialInputs | null)[];
@ -499,10 +501,11 @@ 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 * i+0: attribute name
* Odd indices: initial value * i+1: minified/internal input name
* i+2: initial value
* *
* e.g. ['role-min', 'button'] * e.g. ['role-min', 'minified-input', 'button']
*/ */
export type InitialInputs = string[]; export type InitialInputs = string[];

View File

@ -8,7 +8,6 @@
import {InjectionToken} from '../../di/injection_token'; import {InjectionToken} from '../../di/injection_token';
import {Injector} from '../../di/injector'; import {Injector} from '../../di/injector';
import {SimpleChanges} from '../../interface/simple_change';
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';

View File

@ -95,9 +95,6 @@
{ {
"name": "PARENT_INJECTOR" "name": "PARENT_INJECTOR"
}, },
{
"name": "PRIVATE_PREFIX"
},
{ {
"name": "RENDERER" "name": "RENDERER"
}, },
@ -107,6 +104,9 @@
{ {
"name": "SANITIZER" "name": "SANITIZER"
}, },
{
"name": "SIMPLE_CHANGES_STORE"
},
{ {
"name": "SimpleChange" "name": "SimpleChange"
}, },
@ -308,6 +308,9 @@
{ {
"name": "getRootView" "name": "getRootView"
}, },
{
"name": "getSimpleChangesStore"
},
{ {
"name": "hasParentInjector" "name": "hasParentInjector"
}, },
@ -366,10 +369,10 @@
"name": "nextNgElementId" "name": "nextNgElementId"
}, },
{ {
"name": "noSideEffects" "name": "ngOnChangesSetInput"
}, },
{ {
"name": "onChangesWrapper" "name": "noSideEffects"
}, },
{ {
"name": "postProcessBaseDirective" "name": "postProcessBaseDirective"
@ -437,6 +440,9 @@
{ {
"name": "setPreviousOrParentTNode" "name": "setPreviousOrParentTNode"
}, },
{
"name": "setSimpleChangesStore"
},
{ {
"name": "setTNodeAndViewData" "name": "setTNodeAndViewData"
}, },
@ -454,5 +460,8 @@
}, },
{ {
"name": "viewAttached" "name": "viewAttached"
},
{
"name": "wrapOnChanges"
} }
] ]

View File

@ -8,6 +8,9 @@
{ {
"name": "EMPTY_ARRAY" "name": "EMPTY_ARRAY"
}, },
{
"name": "EMPTY_OBJ"
},
{ {
"name": "EmptyErrorImpl" "name": "EmptyErrorImpl"
}, },
@ -51,10 +54,10 @@
"name": "PARAMETERS" "name": "PARAMETERS"
}, },
{ {
"name": "PRIVATE_PREFIX" "name": "R3Injector"
}, },
{ {
"name": "R3Injector" "name": "SIMPLE_CHANGES_STORE"
}, },
{ {
"name": "ScopedService" "name": "ScopedService"
@ -122,6 +125,9 @@
{ {
"name": "getNullInjector" "name": "getNullInjector"
}, },
{
"name": "getSimpleChangesStore"
},
{ {
"name": "hasDeps" "name": "hasDeps"
}, },
@ -165,7 +171,7 @@
"name": "makeRecord" "name": "makeRecord"
}, },
{ {
"name": "onChangesWrapper" "name": "ngOnChangesSetInput"
}, },
{ {
"name": "providerToFactory" "name": "providerToFactory"
@ -179,7 +185,13 @@
{ {
"name": "setCurrentInjector" "name": "setCurrentInjector"
}, },
{
"name": "setSimpleChangesStore"
},
{ {
"name": "stringify" "name": "stringify"
},
{
"name": "wrapOnChanges"
} }
] ]

View File

@ -167,9 +167,6 @@
{ {
"name": "PARENT_INJECTOR" "name": "PARENT_INJECTOR"
}, },
{
"name": "PRIVATE_PREFIX"
},
{ {
"name": "QUERIES" "name": "QUERIES"
}, },
@ -188,6 +185,9 @@
{ {
"name": "SANITIZER" "name": "SANITIZER"
}, },
{
"name": "SIMPLE_CHANGES_STORE"
},
{ {
"name": "SWITCH_ELEMENT_REF_FACTORY" "name": "SWITCH_ELEMENT_REF_FACTORY"
}, },
@ -785,6 +785,9 @@
{ {
"name": "getRootView" "name": "getRootView"
}, },
{
"name": "getSimpleChangesStore"
},
{ {
"name": "getSinglePropIndexValue" "name": "getSinglePropIndexValue"
}, },
@ -1014,10 +1017,10 @@
"name": "nextNgElementId" "name": "nextNgElementId"
}, },
{ {
"name": "noSideEffects" "name": "ngOnChangesSetInput"
}, },
{ {
"name": "onChangesWrapper" "name": "noSideEffects"
}, },
{ {
"name": "pointers" "name": "pointers"
@ -1184,6 +1187,9 @@
{ {
"name": "setSanitizeFlag" "name": "setSanitizeFlag"
}, },
{
"name": "setSimpleChangesStore"
},
{ {
"name": "setStyle" "name": "setStyle"
}, },
@ -1249,5 +1255,8 @@
}, },
{ {
"name": "wrapListenerWithPreventDefault" "name": "wrapListenerWithPreventDefault"
},
{
"name": "wrapOnChanges"
} }
] ]

View File

@ -536,7 +536,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(renderLog.log).toEqual(['someProp=Megatron']); expect(renderLog.log).toEqual(['someProp=Megatron']);
})); }));
fixmeIvy('FW-956: refactor onChanges').
it('should record unwrapped values via ngOnChanges', fakeAsync(() => { it('should record unwrapped values via ngOnChanges', fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
'<div [testDirective]="\'aName\' | wrappedPipe" [a]="1" [b]="2 | wrappedPipe"></div>'); '<div [testDirective]="\'aName\' | wrappedPipe" [a]="1" [b]="2 | wrappedPipe"></div>');
@ -739,7 +738,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}); });
describe('ngOnChanges', () => { describe('ngOnChanges', () => {
fixmeIvy('FW-956: refactor onChanges').
it('should notify the directive when a group of records changes', fakeAsync(() => { it('should notify the directive when a group of records changes', fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
'<div [testDirective]="\'aName\'" [a]="1" [b]="2"></div><div [testDirective]="\'bName\'" [a]="4"></div>'); '<div [testDirective]="\'aName\'" [a]="1" [b]="2"></div><div [testDirective]="\'bName\'" [a]="4"></div>');

View File

@ -519,7 +519,10 @@ describe('InheritDefinitionFeature', () => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'div', ['subDir', '']); element(0, 'div', ['subDir', '']);
} }
}, 1, 0, [SubDirective]); if (rf & RenderFlags.Update) {
elementProperty(0, 'someInput', bind(1));
}
}, 1, 1, [SubDirective]);
const fixture = new ComponentFixture(App); const fixture = new ComponentFixture(App);
expect(log).toEqual(['on changes!']); expect(log).toEqual(['on changes!']);

View File

@ -7,14 +7,14 @@
*/ */
import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core'; import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index'; import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, 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';
import {NgIf} from './common_with_def'; import {NgIf} from './common_with_def';
import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util'; import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util';
import { fixmeIvy } from '@angular/private/testing'; import {fixmeIvy} from '@angular/private/testing';
describe('lifecycles', () => { describe('lifecycles', () => {
@ -1941,7 +1941,6 @@ describe('lifecycles', () => {
}); });
fixmeIvy('FW-956: refactor onChanges').
describe('onChanges', () => { describe('onChanges', () => {
let events: ({type: string, name: string, [key: string]: any})[]; let events: ({type: string, name: string, [key: string]: any})[];
@ -2008,7 +2007,8 @@ describe('lifecycles', () => {
consts: consts, consts: consts,
vars: vars, vars: vars,
inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template, inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template,
directives: directives directives: directives,
features: [NgOnChangesFeature],
}); });
}; };
} }
@ -2026,7 +2026,8 @@ describe('lifecycles', () => {
type: Directive, type: Directive,
selectors: [['', 'dir', '']], selectors: [['', 'dir', '']],
factory: () => new Directive(), factory: () => new Directive(),
inputs: {a: 'val1', b: ['publicVal2', 'val2']} inputs: {a: 'val1', b: ['publicVal2', 'val2']},
features: [NgOnChangesFeature],
}); });
} }
@ -2701,7 +2702,6 @@ describe('lifecycles', () => {
}); });
fixmeIvy('FW-956: refactor onChanges').
describe('hook order', () => { describe('hook order', () => {
let events: string[]; let events: string[];
@ -2731,7 +2731,8 @@ describe('lifecycles', () => {
consts: consts, consts: consts,
vars: vars, vars: vars,
inputs: {val: 'val'}, template, inputs: {val: 'val'}, template,
directives: directives directives: directives,
features: [NgOnChangesFeature],
}); });
}; };
} }

View File

@ -1,327 +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, OnChanges, SimpleChange, SimpleChanges} from '../../src/core';
import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature';
import {DirectiveDef, NgOnChangesFeature, defineDirective} from '../../src/render3/index';
import { fixmeIvy } from '@angular/private/testing';
fixmeIvy('FW-956: refactor onChanges').
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

@ -1677,7 +1677,8 @@ describe('ViewContainerRef', () => {
elementProperty(3, 'name', bind('B')); elementProperty(3, 'name', bind('B'));
} }
}, },
directives: [ComponentWithHooks, DirectiveWithVCRef] directives: [ComponentWithHooks, DirectiveWithVCRef],
features: [NgOnChangesFeature],
}); });
} }
@ -1769,7 +1770,8 @@ describe('ViewContainerRef', () => {
elementProperty(1, 'name', bind('B')); elementProperty(1, 'name', bind('B'));
} }
}, },
directives: [ComponentWithHooks, DirectiveWithVCRef] directives: [ComponentWithHooks, DirectiveWithVCRef],
features: [NgOnChangesFeature],
}); });
} }
@ -1801,7 +1803,7 @@ describe('ViewContainerRef', () => {
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

@ -315,239 +315,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));
@ -1872,7 +1868,6 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-956: refactor onChanges').
it('should call `$onChanges()` on binding destination', fakeAsync(() => { it('should call `$onChanges()` on binding destination', fakeAsync(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA'); const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA');

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

@ -721,66 +721,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);
}); });
}); });
})); }));
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',
async(() => { async(() => {
@ -943,167 +940,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;