diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index 0500caf36c..489ed87878 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -34,14 +34,20 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange */ @Directive({selector: '[ngTemplateOutlet]'}) export class NgTemplateOutlet implements OnChanges { - // TODO(issue/24571): remove '!'. - private _viewRef !: EmbeddedViewRef; + private _viewRef: EmbeddedViewRef|null = null; - // TODO(issue/24571): remove '!'. - @Input() public ngTemplateOutletContext !: Object; + /** + * A context object to attach to the {@link EmbeddedViewRef}. This should be an + * object, the object's keys will be available for binding by the local template `let` + * declarations. + * Using the key `$implicit` in the context object will set its value as default. + */ + @Input() public ngTemplateOutletContext: Object|null = null; - // TODO(issue/24571): remove '!'. - @Input() public ngTemplateOutlet !: TemplateRef; + /** + * A string defining the template reference and optionally the context object for the template. + */ + @Input() public ngTemplateOutlet: TemplateRef|null = null; constructor(private _viewContainerRef: ViewContainerRef) {} @@ -97,7 +103,7 @@ export class NgTemplateOutlet implements OnChanges { private _updateExistingContext(ctx: Object): void { for (let propName of Object.keys(ctx)) { - (this._viewRef.context)[propName] = (this.ngTemplateOutletContext)[propName]; + (this._viewRef !.context)[propName] = (this.ngTemplateOutletContext)[propName]; } } } diff --git a/packages/core/src/change_detection/change_detection_util.ts b/packages/core/src/change_detection/change_detection_util.ts index da963bb641..b98a3756e7 100644 --- a/packages/core/src/change_detection/change_detection_util.ts +++ b/packages/core/src/change_detection/change_detection_util.ts @@ -64,20 +64,6 @@ export class WrappedValue { static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; } } -/** - * Represents a basic change from a previous to a new value. - * - * @publicApi - */ -export class SimpleChange { - constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {} - - /** - * Check whether the new value is the first value assigned. - */ - isFirstChange(): boolean { return this.firstChange; } -} - export function isListLikeIterable(obj: any): boolean { if (!isJsObject(obj)) return false; return Array.isArray(obj) || diff --git a/packages/core/src/interface/lifecycle_hooks.ts b/packages/core/src/interface/lifecycle_hooks.ts index 1d0a6be42b..bf92bd54da 100644 --- a/packages/core/src/interface/lifecycle_hooks.ts +++ b/packages/core/src/interface/lifecycle_hooks.ts @@ -5,19 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {SimpleChanges, 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 * A lifecycle hook that is called when any data-bound property of a directive changes. diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 3bfba4cacf..0bc8cbaff3 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -17,6 +17,7 @@ import {assertComponentType} from './assert'; import {getComponentDef} from './definition'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; 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 {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; 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 {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state'; import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util'; -import { registerPreOrderHooks, registerPostOrderHooks } from './hooks'; @@ -240,7 +240,8 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef): v registerPreOrderHooks(dirIndex, def, rootTView); // TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on // LNode). - registerPostOrderHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode); + registerPostOrderHooks( + rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode); } /** diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 62c59963ab..e4e119c86c 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -276,6 +276,7 @@ export function defineComponent(componentDefinition: { id: 'c', styles: componentDefinition.styles || EMPTY_ARRAY, _: null as never, + setInput: null, }; def._ = noSideEffects(() => { const directiveTypes = componentDefinition.directives !; @@ -380,12 +381,14 @@ export function defineNgModule(def: {type: T} & Partial>): nev * */ -function invertObject(obj: any, secondary?: any): any { - if (obj == null) return EMPTY_OBJ; +function invertObject( + 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 = {}; for (const minifiedKey in obj) { if (obj.hasOwnProperty(minifiedKey)) { - let publicName: string = obj[minifiedKey]; + let publicName: string|[string, string] = obj[minifiedKey] !; let declaredName = publicName; if (Array.isArray(publicName)) { declaredName = publicName[1]; @@ -393,7 +396,7 @@ function invertObject(obj: any, secondary?: any): any { } newLookup[publicName] = minifiedKey; if (secondary) { - (secondary[publicName] = declaredName); + (secondary[publicName] = declaredName as string); } } } @@ -470,11 +473,11 @@ export function defineBase(baseDefinition: { */ outputs?: {[P in keyof T]?: string}; }): BaseDef { - const declaredInputs: {[P in keyof T]: P} = {} as any; + const declaredInputs: {[P in keyof T]: string} = {} as any; return { - inputs: invertObject(baseDefinition.inputs, declaredInputs), + inputs: invertObject(baseDefinition.inputs as any, declaredInputs), declaredInputs: declaredInputs, - outputs: invertObject(baseDefinition.outputs), + outputs: invertObject(baseDefinition.outputs as any), }; } diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 8da5c9b641..0f03ddb3a6 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -7,10 +7,10 @@ */ import {Type} from '../../interface/type'; +import {Component} from '../../metadata/directives'; import {fillProperties} from '../../util/property'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; -import { Component } from '../../metadata/directives'; diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts index 347688f0b0..9ebb00bc6f 100644 --- a/packages/core/src/render3/features/ng_onchanges_feature.ts +++ b/packages/core/src/render3/features/ng_onchanges_feature.ts @@ -6,9 +6,9 @@ * 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 {SimpleChange, SimpleChanges} from '../../interface/simple_change'; +import {EMPTY_OBJ} from '../empty'; import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition'; const PRIVATE_PREFIX = '__ngOnChanges_'; @@ -40,86 +40,63 @@ type OnChangesExpando = OnChanges & { * ``` */ export function NgOnChangesFeature(definition: DirectiveDef): 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; + if (definition.type.prototype.ngOnChanges) { + definition.setInput = ngOnChangesSetInput; - // 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 prevDoCheck = definition.doCheck; + const prevOnInit = definition.onInit; - const getter = originalProperty && originalProperty.get; - const setter = originalProperty && originalProperty.set; + definition.onInit = wrapOnChanges(prevOnInit); + definition.doCheck = wrapOnChanges(prevDoCheck); + } +} - // create a getter and setter for property - Object.defineProperty(proto, minifiedKey, { - get: getter || - (setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }), - set(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}); - } +function wrapOnChanges(hook: (() => void) | null) { + return function(this: OnChanges) { + const simpleChangesStore = getSimpleChangesStore(this); + const current = simpleChangesStore && simpleChangesStore.current; - 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 (current) { + simpleChangesStore !.previous = current; + simpleChangesStore !.current = null; + this.ngOnChanges(current); } - } - // 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); - } + hook && hook.call(this); + }; +} - definition.doCheck = onChangesWrapper(definition.doCheck); +function ngOnChangesSetInput( + this: DirectiveDef, 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 // 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); - }; + +interface NgSimpleChangesStore { + previous: SimpleChanges; + current: SimpleChanges|null; } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 6b3d9654b8..fa5706f2ab 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -959,10 +959,10 @@ function listenerInternal( const propsLength = props.length; if (propsLength) { 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; ngDevMode && assertDataInRange(lView, index); - const minifiedName = props[i + 1]; + const minifiedName = props[i + 2]; const directiveInstance = lView[index]; const output = directiveInstance[minifiedName]; @@ -1214,18 +1214,29 @@ export function createTNode( * @param value Value to set. */ function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void { - for (let i = 0; i < inputs.length; i += 2) { - ngDevMode && assertDataInRange(lView, inputs[i] as number); - lView[inputs[i] as number][inputs[i + 1]] = value; + const tView = lView[TVIEW]; + for (let i = 0; i < inputs.length;) { + 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; + const setInput = def.setInput; + if (setInput) { + def.setInput !(instance, value, publicName, privateName); + } else { + instance[privateName] = value; + } } } function setNgReflectProperties( lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, value: any) { - for (let i = 0; i < inputs.length; i += 2) { + for (let i = 0; i < inputs.length; i += 3) { const renderer = lView[RENDERER]; - const attrName = normalizeDebugBindingName(inputs[i + 1] as string); + const attrName = normalizeDebugBindingName(inputs[i + 2] as string); const debugValue = normalizeDebugBindingValue(value); if (type === TNodeType.Element) { isProceduralRenderer(renderer) ? @@ -1268,8 +1279,8 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro propStore = propStore || {}; const internalName = propertyAliasMap[publicName]; const hasProperty = propStore.hasOwnProperty(publicName); - hasProperty ? propStore[publicName].push(i, internalName) : - (propStore[publicName] = [i, internalName]); + hasProperty ? propStore[publicName].push(i, publicName, internalName) : + (propStore[publicName] = [i, publicName, internalName]); } } } @@ -1702,7 +1713,7 @@ function postProcessDirective( postProcessBaseDirective(viewData, previousOrParentTNode, directive, def); ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode'); if (previousOrParentTNode && previousOrParentTNode.attrs) { - setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode); + setInputsFromAttrs(directiveDefIdx, directive, def, previousOrParentTNode); } if (def.contentQueries) { @@ -1903,16 +1914,24 @@ function addComponentLogic( * @param tNode The static data for this node */ function setInputsFromAttrs( - directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void { + directiveIndex: number, instance: T, def: DirectiveDef, tNode: TNode): void { let initialInputData = tNode.initialInputs as InitialInputData | undefined; if (initialInputData === undefined || directiveIndex >= initialInputData.length) { - initialInputData = generateInitialInputs(directiveIndex, inputs, tNode); + initialInputData = generateInitialInputs(directiveIndex, def.inputs, tNode); } const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; if (initialInputs) { - for (let i = 0; i < initialInputs.length; i += 2) { - (instance as any)[initialInputs[i]] = initialInputs[i + 1]; + const setInput = def.setInput; + 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) { const inputsToStore: InitialInputs = initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); - inputsToStore.push(minifiedInputName, attrValue as string); + inputsToStore.push(attrName, minifiedInputName, attrValue as string); } i += 2; diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 76a9228abf..78be2de004 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -84,14 +84,14 @@ export interface BaseDef { * @deprecated This is only here because `NgOnChanges` incorrectly uses declared name instead of * 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 * are their aliases if any, or their original unminified property names * (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 extends BaseDef { * The features applied to this directive */ readonly features: DirectiveDefFeature[]|null; + + setInput: + ((this: DirectiveDef, instance: T, value: any, publicName: string, + privateName: string) => void)|null; } export type ComponentDefWithMeta< diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 16f5589a45..2faf312e7e 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -468,10 +468,11 @@ export type PropertyAliases = { /** * Store the runtime input or output names for all the directives. * - * - Even indices: directive index - * - Odd indices: minified / internal name + * i+0: directive instance index + * i+1: publicName + * i+2: privateName * - * e.g. [0, 'change-minified'] + * e.g. [0, 'change', 'change-minified'] */ export type PropertyAliasValue = (number | string)[]; @@ -484,14 +485,15 @@ export type PropertyAliasValue = (number | string)[]; * * Within each sub-array: * - * Even indices: minified/internal input name - * Odd indices: initial value + * i+0: attribute name + * i+1: minified/internal input name + * i+2: initial value * * If a directive on a node does not have any input properties * that should be set from attributes, its index is set to null * to avoid a sparse array. * - * e.g. [null, ['role-min', 'button']] + * e.g. [null, ['role-min', 'minified-input', 'button']] */ export type InitialInputData = (InitialInputs | null)[]; @@ -499,10 +501,11 @@ export type InitialInputData = (InitialInputs | null)[]; * Used by InitialInputData to store input properties * that should be set once from attributes. * - * Even indices: minified/internal input name - * Odd indices: initial value + * i+0: attribute name + * 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[]; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 7c618c48a6..88ab31b085 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -8,7 +8,6 @@ import {InjectionToken} from '../../di/injection_token'; import {Injector} from '../../di/injector'; -import {SimpleChanges} from '../../interface/simple_change'; import {Type} from '../../interface/type'; import {QueryList} from '../../linker'; import {Sanitizer} from '../../sanitization/security'; diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 146e8556d2..84854e952d 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -95,9 +95,6 @@ { "name": "PARENT_INJECTOR" }, - { - "name": "PRIVATE_PREFIX" - }, { "name": "RENDERER" }, @@ -107,6 +104,9 @@ { "name": "SANITIZER" }, + { + "name": "SIMPLE_CHANGES_STORE" + }, { "name": "SimpleChange" }, @@ -308,6 +308,9 @@ { "name": "getRootView" }, + { + "name": "getSimpleChangesStore" + }, { "name": "hasParentInjector" }, @@ -366,10 +369,10 @@ "name": "nextNgElementId" }, { - "name": "noSideEffects" + "name": "ngOnChangesSetInput" }, { - "name": "onChangesWrapper" + "name": "noSideEffects" }, { "name": "postProcessBaseDirective" @@ -437,6 +440,9 @@ { "name": "setPreviousOrParentTNode" }, + { + "name": "setSimpleChangesStore" + }, { "name": "setTNodeAndViewData" }, @@ -454,5 +460,8 @@ }, { "name": "viewAttached" + }, + { + "name": "wrapOnChanges" } ] \ No newline at end of file diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index 673ef89e01..2304efae4d 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -8,6 +8,9 @@ { "name": "EMPTY_ARRAY" }, + { + "name": "EMPTY_OBJ" + }, { "name": "EmptyErrorImpl" }, @@ -51,10 +54,10 @@ "name": "PARAMETERS" }, { - "name": "PRIVATE_PREFIX" + "name": "R3Injector" }, { - "name": "R3Injector" + "name": "SIMPLE_CHANGES_STORE" }, { "name": "ScopedService" @@ -122,6 +125,9 @@ { "name": "getNullInjector" }, + { + "name": "getSimpleChangesStore" + }, { "name": "hasDeps" }, @@ -165,7 +171,7 @@ "name": "makeRecord" }, { - "name": "onChangesWrapper" + "name": "ngOnChangesSetInput" }, { "name": "providerToFactory" @@ -179,7 +185,13 @@ { "name": "setCurrentInjector" }, + { + "name": "setSimpleChangesStore" + }, { "name": "stringify" + }, + { + "name": "wrapOnChanges" } ] \ No newline at end of file diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 93b17b2f5b..0ddada3067 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -167,9 +167,6 @@ { "name": "PARENT_INJECTOR" }, - { - "name": "PRIVATE_PREFIX" - }, { "name": "QUERIES" }, @@ -188,6 +185,9 @@ { "name": "SANITIZER" }, + { + "name": "SIMPLE_CHANGES_STORE" + }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, @@ -785,6 +785,9 @@ { "name": "getRootView" }, + { + "name": "getSimpleChangesStore" + }, { "name": "getSinglePropIndexValue" }, @@ -1014,10 +1017,10 @@ "name": "nextNgElementId" }, { - "name": "noSideEffects" + "name": "ngOnChangesSetInput" }, { - "name": "onChangesWrapper" + "name": "noSideEffects" }, { "name": "pointers" @@ -1184,6 +1187,9 @@ { "name": "setSanitizeFlag" }, + { + "name": "setSimpleChangesStore" + }, { "name": "setStyle" }, @@ -1249,5 +1255,8 @@ }, { "name": "wrapListenerWithPreventDefault" + }, + { + "name": "wrapOnChanges" } ] \ No newline at end of file diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 5abc8650f4..6b036a4683 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -536,7 +536,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ expect(renderLog.log).toEqual(['someProp=Megatron']); })); - fixmeIvy('FW-956: refactor onChanges'). it('should record unwrapped values via ngOnChanges', fakeAsync(() => { const ctx = createCompFixture( '
'); @@ -739,7 +738,6 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ }); describe('ngOnChanges', () => { - fixmeIvy('FW-956: refactor onChanges'). it('should notify the directive when a group of records changes', fakeAsync(() => { const ctx = createCompFixture( '
'); diff --git a/packages/core/test/render3/inherit_definition_feature_spec.ts b/packages/core/test/render3/inherit_definition_feature_spec.ts index d19b58d8eb..cd04aa05c2 100644 --- a/packages/core/test/render3/inherit_definition_feature_spec.ts +++ b/packages/core/test/render3/inherit_definition_feature_spec.ts @@ -519,7 +519,10 @@ describe('InheritDefinitionFeature', () => { if (rf & RenderFlags.Create) { element(0, 'div', ['subDir', '']); } - }, 1, 0, [SubDirective]); + if (rf & RenderFlags.Update) { + elementProperty(0, 'someInput', bind(1)); + } + }, 1, 1, [SubDirective]); const fixture = new ComponentFixture(App); expect(log).toEqual(['on changes!']); diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index 44be88dc17..b612c4b19d 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -7,14 +7,14 @@ */ 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 {RenderFlags} from '../../src/render3/interfaces/definition'; import {NgIf} from './common_with_def'; import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util'; -import { fixmeIvy } from '@angular/private/testing'; +import {fixmeIvy} from '@angular/private/testing'; describe('lifecycles', () => { @@ -1941,7 +1941,6 @@ describe('lifecycles', () => { }); - fixmeIvy('FW-956: refactor onChanges'). describe('onChanges', () => { let events: ({type: string, name: string, [key: string]: any})[]; @@ -2008,7 +2007,8 @@ describe('lifecycles', () => { consts: consts, vars: vars, inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template, - directives: directives + directives: directives, + features: [NgOnChangesFeature], }); }; } @@ -2026,7 +2026,8 @@ describe('lifecycles', () => { type: Directive, selectors: [['', 'dir', '']], 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', () => { let events: string[]; @@ -2731,7 +2731,8 @@ describe('lifecycles', () => { consts: consts, vars: vars, inputs: {val: 'val'}, template, - directives: directives + directives: directives, + features: [NgOnChangesFeature], }); }; } diff --git a/packages/core/test/render3/ng_on_changes_feature_spec.ts b/packages/core/test/render3/ng_on_changes_feature_spec.ts deleted file mode 100644 index 617884a5bb..0000000000 --- a/packages/core/test/render3/ng_on_changes_feature_spec.ts +++ /dev/null @@ -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 = []; - 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).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).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).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).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).factory(null) as SubDirective; - myDir.valA = 'first'; - myDir.valB = 'second'; - - (SubDirective.ngDirectiveDef as DirectiveDef).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).factory(null) as SubDirective; - myDir.valA = 'first'; - myDir.valB = 'second'; - - (SubDirective.ngDirectiveDef as DirectiveDef).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).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).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 = []; - 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).factory(null) as MyDirective; - myDir.valA = 'first'; - myDir.valB = 'second'; - (MyDirective.ngDirectiveDef as DirectiveDef).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).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 = []; - - 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).factory(null) as MyDirective; - myDir.onlySetter = 'someValue'; - expect(myDir.onlySetter).toBeUndefined(); - (MyDirective.ngDirectiveDef as DirectiveDef).doCheck !.call(myDir); - const changeSetter = new SimpleChange(undefined, 'someValue', true); - expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]); - }); -}); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index b669c9ede4..1283409be3 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -1677,7 +1677,8 @@ describe('ViewContainerRef', () => { elementProperty(3, 'name', bind('B')); } }, - directives: [ComponentWithHooks, DirectiveWithVCRef] + directives: [ComponentWithHooks, DirectiveWithVCRef], + features: [NgOnChangesFeature], }); } @@ -1769,7 +1770,8 @@ describe('ViewContainerRef', () => { elementProperty(1, 'name', bind('B')); } }, - directives: [ComponentWithHooks, DirectiveWithVCRef] + directives: [ComponentWithHooks, DirectiveWithVCRef], + features: [NgOnChangesFeature], }); } @@ -1801,7 +1803,7 @@ describe('ViewContainerRef', () => { fixture.update(); expect(fixture.html).toEqual('ADB'); expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D', + 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' ]); diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index 91b9c59735..32e8626355 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -315,239 +315,235 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should bind properties, events', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const ng1Module = angular.module('ng1', []).value( - $EXCEPTION_HANDLER, (err: any) => { throw err; }); + it('should bind properties, events', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const ng1Module = + angular.module('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; }); - ng1Module.run(($rootScope: any) => { - $rootScope.name = 'world'; - $rootScope.dataA = 'A'; - $rootScope.dataB = 'B'; - $rootScope.modelA = 'initModelA'; - $rootScope.modelB = 'initModelB'; - $rootScope.eventA = '?'; - $rootScope.eventB = '?'; - }); - @Component({ - selector: 'ng2', - inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], - outputs: [ - 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', - 'twoWayBEmitter: twoWayBChange' - ], - template: 'ignore: {{ignore}}; ' + - 'literal: {{literal}}; interpolate: {{interpolate}}; ' + - 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + - 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' - }) - class Ng2 { - ngOnChangesCount = 0; - ignore = '-'; - literal = '?'; - interpolate = '?'; - oneWayA = '?'; - oneWayB = '?'; - twoWayA = '?'; - twoWayB = '?'; - eventA = new EventEmitter(); - eventB = new EventEmitter(); - twoWayAEmitter = new EventEmitter(); - twoWayBEmitter = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - const assert = (prop: string, value: any) => { - if ((this as any)[prop] != value) { - throw new Error( - `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); - } - }; + ng1Module.run(($rootScope: any) => { + $rootScope.name = 'world'; + $rootScope.dataA = 'A'; + $rootScope.dataB = 'B'; + $rootScope.modelA = 'initModelA'; + $rootScope.modelB = 'initModelB'; + $rootScope.eventA = '?'; + $rootScope.eventB = '?'; + }); + @Component({ + selector: 'ng2', + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' + ], + template: 'ignore: {{ignore}}; ' + + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' + }) + class Ng2 { + ngOnChangesCount = 0; + ignore = '-'; + literal = '?'; + interpolate = '?'; + oneWayA = '?'; + oneWayB = '?'; + twoWayA = '?'; + twoWayB = '?'; + eventA = new EventEmitter(); + eventB = new EventEmitter(); + twoWayAEmitter = new EventEmitter(); + twoWayBEmitter = new EventEmitter(); + ngOnChanges(changes: SimpleChanges) { + const assert = (prop: string, value: any) => { + if ((this as any)[prop] != value) { + throw new Error( + `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); + } + }; - const assertChange = (prop: string, value: any) => { - assert(prop, value); - if (!changes[prop]) { - throw new Error(`Changes record for '${prop}' not found.`); - } - const actValue = changes[prop].currentValue; - if (actValue != value) { - throw new Error( - `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); - } - }; + const assertChange = (prop: string, value: any) => { + assert(prop, value); + if (!changes[prop]) { + throw new Error(`Changes record for '${prop}' not found.`); + } + const actValue = changes[prop].currentValue; + if (actValue != value) { + throw new Error( + `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); + } + }; - switch (this.ngOnChangesCount++) { - case 0: - assert('ignore', '-'); - assertChange('literal', 'Text'); - assertChange('interpolate', 'Hello world'); - assertChange('oneWayA', 'A'); - assertChange('oneWayB', 'B'); - assertChange('twoWayA', 'initModelA'); - assertChange('twoWayB', 'initModelB'); + switch (this.ngOnChangesCount++) { + case 0: + assert('ignore', '-'); + assertChange('literal', 'Text'); + assertChange('interpolate', 'Hello world'); + assertChange('oneWayA', 'A'); + assertChange('oneWayB', 'B'); + assertChange('twoWayA', 'initModelA'); + assertChange('twoWayB', 'initModelB'); - this.twoWayAEmitter.emit('newA'); - this.twoWayBEmitter.emit('newB'); - this.eventA.emit('aFired'); - this.eventB.emit('bFired'); - break; - case 1: - assertChange('twoWayA', 'newA'); - assertChange('twoWayB', 'newB'); - break; - case 2: - assertChange('interpolate', 'Hello everyone'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } - ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + this.twoWayAEmitter.emit('newA'); + this.twoWayBEmitter.emit('newB'); + this.eventA.emit('aFired'); + this.eventB.emit('bFired'); + break; + case 1: + assertChange('twoWayA', 'newA'); + assertChange('twoWayB', 'newB'); + break; + case 2: + assertChange('interpolate', 'Hello everyone'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); - @NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - }) - class Ng2Module { - } + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } - const element = html(`
+ const element = html(`
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect(multiTrim(document.body.textContent !)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello world; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect(multiTrim(document.body.textContent !)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello world; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - ref.ng1RootScope.$apply('name = "everyone"'); - expect(multiTrim(document.body.textContent !)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello everyone; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + ref.ng1RootScope.$apply('name = "everyone"'); + expect(multiTrim(document.body.textContent !)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello everyone; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - ref.dispose(); - }); + ref.dispose(); + }); - })); + })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should support two-way binding and event listener', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const listenerSpy = jasmine.createSpy('$rootScope.listener'); - const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { - $rootScope['value'] = 'world'; - $rootScope['listener'] = listenerSpy; - }); + it('should support two-way binding and event listener', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const listenerSpy = jasmine.createSpy('$rootScope.listener'); + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['value'] = 'world'; + $rootScope['listener'] = listenerSpy; + }); - @Component({selector: 'ng2', template: `model: {{model}};`}) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - @Input() model = '?'; - @Output() modelChange = new EventEmitter(); + @Component({selector: 'ng2', template: `model: {{model}};`}) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + @Input() model = '?'; + @Output() modelChange = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - switch (this.ngOnChangesCount++) { - case 0: - expect(changes.model.currentValue).toBe('world'); - this.modelChange.emit('newC'); - break; - case 1: - expect(changes.model.currentValue).toBe('newC'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } + ngOnChanges(changes: SimpleChanges) { + switch (this.ngOnChangesCount++) { + case 0: + expect(changes.model.currentValue).toBe('world'); + this.modelChange.emit('newC'); + break; + case 1: + expect(changes.model.currentValue).toBe('newC'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } - ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); - @NgModule({declarations: [Ng2Component], imports: [BrowserModule]}) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({declarations: [Ng2Component], imports: [BrowserModule]}) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(` + const element = html(`
| value: {{value}}
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); - expect(listenerSpy).toHaveBeenCalledWith('newC'); - ref.dispose(); - }); - })); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); + expect(listenerSpy).toHaveBeenCalledWith('newC'); + ref.dispose(); + }); + })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should initialize inputs in time for `ngOnChanges`', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + it('should initialize inputs in time for `ngOnChanges`', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - @Component({ - selector: 'ng2', - template: ` + @Component({ + selector: 'ng2', + template: ` ngOnChangesCount: {{ ngOnChangesCount }} | firstChangesCount: {{ firstChangesCount }} | initialValue: {{ initialValue }}` - }) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - firstChangesCount = 0; - // TODO(issue/24571): remove '!'. - initialValue !: string; - // TODO(issue/24571): remove '!'. - @Input() foo !: string; + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + firstChangesCount = 0; + // TODO(issue/24571): remove '!'. + initialValue !: string; + // TODO(issue/24571): remove '!'. + @Input() foo !: string; - ngOnChanges(changes: SimpleChanges) { - this.ngOnChangesCount++; + ngOnChanges(changes: SimpleChanges) { + this.ngOnChangesCount++; - if (this.ngOnChangesCount === 1) { - this.initialValue = this.foo; - } + if (this.ngOnChangesCount === 1) { + this.initialValue = this.foo; + } - if (changes['foo'] && changes['foo'].isFirstChange()) { - this.firstChangesCount++; - } - } - } + if (changes['foo'] && changes['foo'].isFirstChange()) { + this.firstChangesCount++; + } + } + } - @NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) - class Ng2Module { - } + @NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) + class Ng2Module { + } - const ng1Module = angular.module('ng1', []).directive( - 'ng2', adapter.downgradeNg2Component(Ng2Component)); + const ng1Module = angular.module('ng1', []).directive( + 'ng2', adapter.downgradeNg2Component(Ng2Component)); - const element = html(` + const element = html(` `); - adapter.bootstrap(element, ['ng1']).ready(ref => { - const nodes = element.querySelectorAll('ng2'); - const expectedTextWith = (value: string) => - `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; + adapter.bootstrap(element, ['ng1']).ready(ref => { + const nodes = element.querySelectorAll('ng2'); + const expectedTextWith = (value: string) => + `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; - expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); - expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); - expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); - expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); + expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); + expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); + expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); + expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); - ref.dispose(); - }); - })); + ref.dispose(); + }); + })); it('should bind to ng-model', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); @@ -1872,7 +1868,6 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-956: refactor onChanges'). it('should call `$onChanges()` on binding destination', fakeAsync(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA'); diff --git a/packages/upgrade/test/static/integration/downgrade_component_spec.ts b/packages/upgrade/test/static/integration/downgrade_component_spec.ts index c34348dfc3..fe78f26f03 100644 --- a/packages/upgrade/test/static/integration/downgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_component_spec.ts @@ -22,106 +22,104 @@ withEachNg1Version(() => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should bind properties, events', async(() => { - const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { - $rootScope['name'] = 'world'; - $rootScope['dataA'] = 'A'; - $rootScope['dataB'] = 'B'; - $rootScope['modelA'] = 'initModelA'; - $rootScope['modelB'] = 'initModelB'; - $rootScope['eventA'] = '?'; - $rootScope['eventB'] = '?'; - }); + it('should bind properties, events', async(() => { + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['name'] = 'world'; + $rootScope['dataA'] = 'A'; + $rootScope['dataB'] = 'B'; + $rootScope['modelA'] = 'initModelA'; + $rootScope['modelB'] = 'initModelB'; + $rootScope['eventA'] = '?'; + $rootScope['eventB'] = '?'; + }); - @Component({ - selector: 'ng2', - inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], - outputs: [ - 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', - 'twoWayBEmitter: twoWayBChange' - ], - template: 'ignore: {{ignore}}; ' + - 'literal: {{literal}}; interpolate: {{interpolate}}; ' + - 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + - 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' - }) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - ignore = '-'; - literal = '?'; - interpolate = '?'; - oneWayA = '?'; - oneWayB = '?'; - twoWayA = '?'; - twoWayB = '?'; - eventA = new EventEmitter(); - eventB = new EventEmitter(); - twoWayAEmitter = new EventEmitter(); - twoWayBEmitter = new EventEmitter(); + @Component({ + selector: 'ng2', + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' + ], + template: 'ignore: {{ignore}}; ' + + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + ignore = '-'; + literal = '?'; + interpolate = '?'; + oneWayA = '?'; + oneWayB = '?'; + twoWayA = '?'; + twoWayB = '?'; + eventA = new EventEmitter(); + eventB = new EventEmitter(); + twoWayAEmitter = new EventEmitter(); + twoWayBEmitter = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - const assert = (prop: string, value: any) => { - const propVal = (this as any)[prop]; - if (propVal != value) { - throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); - } - }; + ngOnChanges(changes: SimpleChanges) { + const assert = (prop: string, value: any) => { + const propVal = (this as any)[prop]; + if (propVal != value) { + throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); + } + }; - const assertChange = (prop: string, value: any) => { - assert(prop, value); - if (!changes[prop]) { - throw new Error(`Changes record for '${prop}' not found.`); - } - const actualValue = changes[prop].currentValue; - if (actualValue != value) { - throw new Error( - `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); - } - }; + const assertChange = (prop: string, value: any) => { + assert(prop, value); + if (!changes[prop]) { + throw new Error(`Changes record for '${prop}' not found.`); + } + const actualValue = changes[prop].currentValue; + if (actualValue != value) { + throw new Error( + `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); + } + }; - switch (this.ngOnChangesCount++) { - case 0: - assert('ignore', '-'); - assertChange('literal', 'Text'); - assertChange('interpolate', 'Hello world'); - assertChange('oneWayA', 'A'); - assertChange('oneWayB', 'B'); - assertChange('twoWayA', 'initModelA'); - assertChange('twoWayB', 'initModelB'); + switch (this.ngOnChangesCount++) { + case 0: + assert('ignore', '-'); + assertChange('literal', 'Text'); + assertChange('interpolate', 'Hello world'); + assertChange('oneWayA', 'A'); + assertChange('oneWayB', 'B'); + assertChange('twoWayA', 'initModelA'); + assertChange('twoWayB', 'initModelB'); - this.twoWayAEmitter.emit('newA'); - this.twoWayBEmitter.emit('newB'); - this.eventA.emit('aFired'); - this.eventB.emit('bFired'); - break; - case 1: - assertChange('twoWayA', 'newA'); - assertChange('twoWayB', 'newB'); - break; - case 2: - assertChange('interpolate', 'Hello everyone'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } + this.twoWayAEmitter.emit('newA'); + this.twoWayBEmitter.emit('newB'); + this.eventA.emit('aFired'); + this.eventB.emit('bFired'); + break; + case 1: + assertChange('twoWayA', 'newA'); + assertChange('twoWayB', 'newB'); + break; + case 2: + assertChange('interpolate', 'Hello everyone'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } - ng1Module.directive('ng2', downgradeComponent({ - component: Ng2Component, - })); + ng1Module.directive('ng2', downgradeComponent({ + component: Ng2Component, + })); - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule, UpgradeModule] - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(` + const element = html(`
{ | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { - expect(multiTrim(document.body.textContent)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello world; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(document.body.textContent)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello world; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - $apply(upgrade, 'name = "everyone"'); - expect(multiTrim(document.body.textContent)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello everyone; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - }); - })); + $apply(upgrade, 'name = "everyone"'); + expect(multiTrim(document.body.textContent)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello everyone; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + }); + })); it('should bind properties to onpush components', async(() => { const ng1Module = angular.module('ng1', []).run( @@ -189,58 +187,57 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should support two-way binding and event listener', async(() => { - const listenerSpy = jasmine.createSpy('$rootScope.listener'); - const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { - $rootScope['value'] = 'world'; - $rootScope['listener'] = listenerSpy; - }); + it('should support two-way binding and event listener', async(() => { + const listenerSpy = jasmine.createSpy('$rootScope.listener'); + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['value'] = 'world'; + $rootScope['listener'] = listenerSpy; + }); - @Component({selector: 'ng2', template: `model: {{model}};`}) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - @Input() model = '?'; - @Output() modelChange = new EventEmitter(); + @Component({selector: 'ng2', template: `model: {{model}};`}) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + @Input() model = '?'; + @Output() modelChange = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - switch (this.ngOnChangesCount++) { - case 0: - expect(changes.model.currentValue).toBe('world'); - this.modelChange.emit('newC'); - break; - case 1: - expect(changes.model.currentValue).toBe('newC'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } + ngOnChanges(changes: SimpleChanges) { + switch (this.ngOnChangesCount++) { + case 0: + expect(changes.model.currentValue).toBe('world'); + this.modelChange.emit('newC'); + break; + case 1: + expect(changes.model.currentValue).toBe('newC'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } - ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); + ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule, UpgradeModule] - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(` + const element = html(`
| value: {{value}}
`); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { - expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); - expect(listenerSpy).toHaveBeenCalledWith('newC'); - }); - })); + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); + expect(listenerSpy).toHaveBeenCalledWith('newC'); + }); + })); it('should run change-detection on every digest (by default)', async(() => { let ng2Component: Ng2Component; @@ -404,66 +401,65 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should initialize inputs in time for `ngOnChanges`', async(() => { - @Component({ - selector: 'ng2', - template: ` + it('should initialize inputs in time for `ngOnChanges`', async(() => { + @Component({ + selector: 'ng2', + template: ` ngOnChangesCount: {{ ngOnChangesCount }} | firstChangesCount: {{ firstChangesCount }} | initialValue: {{ initialValue }}` - }) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - firstChangesCount = 0; - // TODO(issue/24571): remove '!'. - initialValue !: string; - // TODO(issue/24571): remove '!'. - @Input() foo !: string; + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + firstChangesCount = 0; + // TODO(issue/24571): remove '!'. + initialValue !: string; + // TODO(issue/24571): remove '!'. + @Input() foo !: string; - ngOnChanges(changes: SimpleChanges) { - this.ngOnChangesCount++; + ngOnChanges(changes: SimpleChanges) { + this.ngOnChangesCount++; - if (this.ngOnChangesCount === 1) { - this.initialValue = this.foo; - } + if (this.ngOnChangesCount === 1) { + this.initialValue = this.foo; + } - if (changes['foo'] && changes['foo'].isFirstChange()) { - this.firstChangesCount++; - } - } - } + if (changes['foo'] && changes['foo'].isFirstChange()) { + this.firstChangesCount++; + } + } + } - @NgModule({ - imports: [BrowserModule, UpgradeModule], - declarations: [Ng2Component], - entryComponents: [Ng2Component] - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } - const ng1Module = angular.module('ng1', []).directive( - 'ng2', downgradeComponent({component: Ng2Component})); + const ng1Module = angular.module('ng1', []).directive( + 'ng2', downgradeComponent({component: Ng2Component})); - const element = html(` + const element = html(` `); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { - const nodes = element.querySelectorAll('ng2'); - const expectedTextWith = (value: string) => - `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { + const nodes = element.querySelectorAll('ng2'); + const expectedTextWith = (value: string) => + `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; - expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); - expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); - expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); - expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); - }); - })); + expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); + expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); + expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); + expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); + }); + })); it('should bind to ng-model', async(() => { const ng1Module = angular.module('ng1', []).run( diff --git a/packages/upgrade/test/static/integration/downgrade_module_spec.ts b/packages/upgrade/test/static/integration/downgrade_module_spec.ts index f8a5ed5035..663158ae11 100644 --- a/packages/upgrade/test/static/integration/downgrade_module_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_module_spec.ts @@ -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(() => { - let ng2Component: Ng2Component; + it('should propagate input changes inside the Angular zone', async(() => { + let ng2Component: Ng2Component; - @Component({selector: 'ng2', template: ''}) - class Ng2Component implements OnChanges { - @Input() attrInput = 'foo'; - @Input() propInput = 'foo'; + @Component({selector: 'ng2', template: ''}) + class Ng2Component implements OnChanges { + @Input() attrInput = 'foo'; + @Input() propInput = 'foo'; - constructor() { ng2Component = this; } - ngOnChanges() {} - } + constructor() { ng2Component = this; } + ngOnChanges() {} + } - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule], + }) + class Ng2Module { + ngDoBootstrap() {} + } - const bootstrapFn = (extraProviders: StaticProvider[]) => - platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); - const lazyModuleName = downgradeModule(bootstrapFn); - const ng1Module = - angular.module('ng1', [lazyModuleName]) - .directive( - 'ng2', downgradeComponent({component: Ng2Component, propagateDigest})) - .run(($rootScope: angular.IRootScopeService) => { - $rootScope.attrVal = 'bar'; - $rootScope.propVal = 'bar'; - }); + const bootstrapFn = (extraProviders: StaticProvider[]) => + platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); + const lazyModuleName = downgradeModule(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest})) + .run(($rootScope: angular.IRootScopeService) => { + $rootScope.attrVal = 'bar'; + $rootScope.propVal = 'bar'; + }); - const element = - html(''); - const $injector = angular.bootstrap(element, [ng1Module.name]); - const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; + const element = html(''); + const $injector = angular.bootstrap(element, [ng1Module.name]); + const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; - setTimeout(() => { // Wait for the module to be bootstrapped. - setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. - const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true); - const changesSpy = - spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone); + setTimeout(() => { // Wait for the module to be bootstrapped. + setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. + const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true); + const changesSpy = + spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone); - expect(ng2Component.attrInput).toBe('bar'); - expect(ng2Component.propInput).toBe('bar'); + expect(ng2Component.attrInput).toBe('bar'); + expect(ng2Component.propInput).toBe('bar'); - $rootScope.$apply('attrVal = "baz"'); - expect(ng2Component.attrInput).toBe('baz'); - expect(ng2Component.propInput).toBe('bar'); - expect(changesSpy).toHaveBeenCalledTimes(1); + $rootScope.$apply('attrVal = "baz"'); + expect(ng2Component.attrInput).toBe('baz'); + expect(ng2Component.propInput).toBe('bar'); + expect(changesSpy).toHaveBeenCalledTimes(1); - $rootScope.$apply('propVal = "qux"'); - expect(ng2Component.attrInput).toBe('baz'); - expect(ng2Component.propInput).toBe('qux'); - expect(changesSpy).toHaveBeenCalledTimes(2); - }); - }); - })); + $rootScope.$apply('propVal = "qux"'); + expect(ng2Component.attrInput).toBe('baz'); + expect(ng2Component.propInput).toBe('qux'); + expect(changesSpy).toHaveBeenCalledTimes(2); + }); + }); + })); it('should create and destroy nested, asynchronously instantiated components inside the Angular zone', 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(() => { - const logs: string[] = []; - let rootScope: angular.IRootScopeService; + it('should run the lifecycle hooks in the correct order', async(() => { + const logs: string[] = []; + let rootScope: angular.IRootScopeService; - @Component({ - selector: 'ng2', - template: ` + @Component({ + selector: 'ng2', + template: ` {{ value }} ` - }) - class Ng2Component implements AfterContentChecked, - AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, - OnDestroy, OnInit { - @Input() value = 'foo'; + }) + class Ng2Component implements AfterContentChecked, + AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy, + OnInit { + @Input() value = 'foo'; - ngAfterContentChecked() { this.log('AfterContentChecked'); } - ngAfterContentInit() { this.log('AfterContentInit'); } - ngAfterViewChecked() { this.log('AfterViewChecked'); } - ngAfterViewInit() { this.log('AfterViewInit'); } - ngDoCheck() { this.log('DoCheck'); } - ngOnChanges() { this.log('OnChanges'); } - ngOnDestroy() { this.log('OnDestroy'); } - ngOnInit() { this.log('OnInit'); } + ngAfterContentChecked() { this.log('AfterContentChecked'); } + ngAfterContentInit() { this.log('AfterContentInit'); } + ngAfterViewChecked() { this.log('AfterViewChecked'); } + ngAfterViewInit() { this.log('AfterViewInit'); } + ngDoCheck() { this.log('DoCheck'); } + ngOnChanges() { this.log('OnChanges'); } + ngOnDestroy() { this.log('OnDestroy'); } + ngOnInit() { this.log('OnInit'); } - private log(hook: string) { logs.push(`${hook}(${this.value})`); } - } + private log(hook: string) { logs.push(`${hook}(${this.value})`); } + } - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule], + }) + class Ng2Module { + ngDoBootstrap() {} + } - const bootstrapFn = (extraProviders: StaticProvider[]) => - platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); - const lazyModuleName = downgradeModule(bootstrapFn); - const ng1Module = - angular.module('ng1', [lazyModuleName]) - .directive( - 'ng2', downgradeComponent({component: Ng2Component, propagateDigest})) - .run(($rootScope: angular.IRootScopeService) => { - rootScope = $rootScope; - rootScope.value = 'bar'; - }); + const bootstrapFn = (extraProviders: StaticProvider[]) => + platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); + const lazyModuleName = downgradeModule(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest})) + .run(($rootScope: angular.IRootScopeService) => { + rootScope = $rootScope; + rootScope.value = 'bar'; + }); - const element = - html('
Content
'); - angular.bootstrap(element, [ng1Module.name]); + const element = + html('
Content
'); + angular.bootstrap(element, [ng1Module.name]); - setTimeout(() => { // Wait for the module to be bootstrapped. - setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. - const button = element.querySelector('button') !; + setTimeout(() => { // Wait for the module to be bootstrapped. + setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. + const button = element.querySelector('button') !; - // Once initialized. - expect(multiTrim(element.textContent)).toBe('bar Content'); - expect(logs).toEqual([ - // `ngOnChanges()` call triggered directly through the `inputChanges` - // $watcher. - 'OnChanges(bar)', - // Initial CD triggered directly through the `detectChanges()` or - // `inputChanges` - // $watcher (for `propagateDigest` true/false respectively). - 'OnInit(bar)', - 'DoCheck(bar)', - 'AfterContentInit(bar)', - 'AfterContentChecked(bar)', - 'AfterViewInit(bar)', - 'AfterViewChecked(bar)', - ...(propagateDigest ? - [ - // CD triggered directly through the `detectChanges()` $watcher (2nd - // $digest). - 'DoCheck(bar)', - 'AfterContentChecked(bar)', - 'AfterViewChecked(bar)', - ] : - []), - // CD triggered due to entering/leaving the NgZone (in `downgradeFn()`). - 'DoCheck(bar)', - 'AfterContentChecked(bar)', - 'AfterViewChecked(bar)', - ]); - logs.length = 0; + // Once initialized. + expect(multiTrim(element.textContent)).toBe('bar Content'); + expect(logs).toEqual([ + // `ngOnChanges()` call triggered directly through the `inputChanges` + // $watcher. + 'OnChanges(bar)', + // Initial CD triggered directly through the `detectChanges()` or + // `inputChanges` + // $watcher (for `propagateDigest` true/false respectively). + 'OnInit(bar)', + 'DoCheck(bar)', + 'AfterContentInit(bar)', + 'AfterContentChecked(bar)', + 'AfterViewInit(bar)', + 'AfterViewChecked(bar)', + ...(propagateDigest ? + [ + // CD triggered directly through the `detectChanges()` $watcher (2nd + // $digest). + 'DoCheck(bar)', + 'AfterContentChecked(bar)', + 'AfterViewChecked(bar)', + ] : + []), + // CD triggered due to entering/leaving the NgZone (in `downgradeFn()`). + 'DoCheck(bar)', + 'AfterContentChecked(bar)', + 'AfterViewChecked(bar)', + ]); + logs.length = 0; - // Change inputs and run `$digest`. - rootScope.$apply('value = "baz"'); - expect(multiTrim(element.textContent)).toBe('baz Content'); - expect(logs).toEqual([ - // `ngOnChanges()` call triggered directly through the `inputChanges` - // $watcher. - 'OnChanges(baz)', - // `propagateDigest: true` (3 CD runs): - // - CD triggered due to entering/leaving the NgZone (in `inputChanges` - // $watcher). - // - CD triggered directly through the `detectChanges()` $watcher. - // - CD triggered due to entering/leaving the NgZone (in `detectChanges` - // $watcher). - // `propagateDigest: false` (2 CD runs): - // - CD triggered directly through the `inputChanges` $watcher. - // - CD triggered due to entering/leaving the NgZone (in `inputChanges` - // $watcher). - 'DoCheck(baz)', - 'AfterContentChecked(baz)', - 'AfterViewChecked(baz)', - 'DoCheck(baz)', - 'AfterContentChecked(baz)', - 'AfterViewChecked(baz)', - ...(propagateDigest ? - [ - 'DoCheck(baz)', - 'AfterContentChecked(baz)', - 'AfterViewChecked(baz)', - ] : - []), - ]); - logs.length = 0; + // Change inputs and run `$digest`. + rootScope.$apply('value = "baz"'); + expect(multiTrim(element.textContent)).toBe('baz Content'); + expect(logs).toEqual([ + // `ngOnChanges()` call triggered directly through the `inputChanges` + // $watcher. + 'OnChanges(baz)', + // `propagateDigest: true` (3 CD runs): + // - CD triggered due to entering/leaving the NgZone (in `inputChanges` + // $watcher). + // - CD triggered directly through the `detectChanges()` $watcher. + // - CD triggered due to entering/leaving the NgZone (in `detectChanges` + // $watcher). + // `propagateDigest: false` (2 CD runs): + // - CD triggered directly through the `inputChanges` $watcher. + // - CD triggered due to entering/leaving the NgZone (in `inputChanges` + // $watcher). + 'DoCheck(baz)', + 'AfterContentChecked(baz)', + 'AfterViewChecked(baz)', + 'DoCheck(baz)', + 'AfterContentChecked(baz)', + 'AfterViewChecked(baz)', + ...(propagateDigest ? + [ + 'DoCheck(baz)', + 'AfterContentChecked(baz)', + 'AfterViewChecked(baz)', + ] : + []), + ]); + logs.length = 0; - // Run `$digest` (without changing inputs). - rootScope.$digest(); - expect(multiTrim(element.textContent)).toBe('baz Content'); - expect(logs).toEqual( - propagateDigest ? - [ - // CD triggered directly through the `detectChanges()` $watcher. - 'DoCheck(baz)', - 'AfterContentChecked(baz)', - 'AfterViewChecked(baz)', - // CD triggered due to entering/leaving the NgZone (in the above - // $watcher). - 'DoCheck(baz)', - 'AfterContentChecked(baz)', - 'AfterViewChecked(baz)', - ] : - []); - logs.length = 0; + // Run `$digest` (without changing inputs). + rootScope.$digest(); + expect(multiTrim(element.textContent)).toBe('baz Content'); + expect(logs).toEqual( + propagateDigest ? + [ + // CD triggered directly through the `detectChanges()` $watcher. + 'DoCheck(baz)', + 'AfterContentChecked(baz)', + 'AfterViewChecked(baz)', + // CD triggered due to entering/leaving the NgZone (in the above + // $watcher). + 'DoCheck(baz)', + 'AfterContentChecked(baz)', + 'AfterViewChecked(baz)', + ] : + []); + logs.length = 0; - // Trigger change detection (without changing inputs). - button.click(); - expect(multiTrim(element.textContent)).toBe('qux Content'); - expect(logs).toEqual([ - 'DoCheck(qux)', - 'AfterContentChecked(qux)', - 'AfterViewChecked(qux)', - ]); - logs.length = 0; + // Trigger change detection (without changing inputs). + button.click(); + expect(multiTrim(element.textContent)).toBe('qux Content'); + expect(logs).toEqual([ + 'DoCheck(qux)', + 'AfterContentChecked(qux)', + 'AfterViewChecked(qux)', + ]); + logs.length = 0; - // Destroy the component. - rootScope.$apply('hideNg2 = true'); - expect(logs).toEqual([ - 'OnDestroy(qux)', - ]); - logs.length = 0; - }); - }); - })); + // Destroy the component. + rootScope.$apply('hideNg2 = true'); + expect(logs).toEqual([ + 'OnDestroy(qux)', + ]); + logs.length = 0; + }); + }); + })); it('should detach hostViews from the ApplicationRef once destroyed', async(() => { let ng2Component: Ng2Component;