diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index 489ed87878..0500caf36c 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -34,20 +34,14 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange */ @Directive({selector: '[ngTemplateOutlet]'}) export class NgTemplateOutlet implements OnChanges { - private _viewRef: EmbeddedViewRef|null = null; + // TODO(issue/24571): remove '!'. + private _viewRef !: EmbeddedViewRef; - /** - * 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 ngTemplateOutletContext !: Object; - /** - * A string defining the template reference and optionally the context object for the template. - */ - @Input() public ngTemplateOutlet: TemplateRef|null = null; + // TODO(issue/24571): remove '!'. + @Input() public ngTemplateOutlet !: TemplateRef; constructor(private _viewContainerRef: ViewContainerRef) {} @@ -103,7 +97,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/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index ca77677893..27f569af19 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -174,6 +174,11 @@ export function extractDirectiveMetadata( const providers: Expression|null = directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null; + // Determine if `ngOnChanges` is a lifecycle hook defined on the component. + const usesOnChanges = members.some( + member => !member.isStatic && member.kind === ClassMemberKind.Method && + member.name === 'ngOnChanges'); + // Parse exportAs. let exportAs: string[]|null = null; if (directive.has('exportAs')) { @@ -192,6 +197,9 @@ export function extractDirectiveMetadata( const metadata: R3DirectiveMetadata = { name: clazz.name !.text, deps: getConstructorDependencies(clazz, reflector, isCore), host, + lifecycle: { + usesOnChanges, + }, inputs: {...inputsFromMeta, ...inputsFromFields}, outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector, type: new WrappedNodeExpr(clazz.name !), diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 477de86321..f986e71771 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -2117,6 +2117,7 @@ describe('compiler compliance', () => { selectors: [["lifecycle-comp"]], factory: function LifecycleComp_Factory(t) { return new (t || LifecycleComp)(); }, inputs: {nameMin: ["name", "nameMin"]}, + features: [$r3$.ɵNgOnChangesFeature], consts: 0, vars: 0, template: function LifecycleComp_Template(rf, ctx) {}, @@ -2238,6 +2239,7 @@ describe('compiler compliance', () => { factory: function ForOfDirective_Factory(t) { return new (t || ForOfDirective)($r3$.ɵdirectiveInject(ViewContainerRef), $r3$.ɵdirectiveInject(TemplateRef)); }, + features: [$r3$.ɵNgOnChangesFeature], inputs: {forOf: "forOf"} }); `; @@ -2313,6 +2315,7 @@ describe('compiler compliance', () => { factory: function ForOfDirective_Factory(t) { return new (t || ForOfDirective)($r3$.ɵdirectiveInject(ViewContainerRef), $r3$.ɵdirectiveInject(TemplateRef)); }, + features: [$r3$.ɵNgOnChangesFeature], inputs: {forOf: "forOf"} }); `; diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index a9dae6bff9..5e659a47af 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -115,6 +115,7 @@ export interface R3DirectiveMetadataFacade { queries: R3QueryMetadataFacade[]; host: {[key: string]: string}; propMetadata: {[key: string]: any[]}; + lifecycle: {usesOnChanges: boolean;}; inputs: string[]; outputs: string[]; usesInheritance: boolean; diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 602e3470b0..224e05c232 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -188,6 +188,8 @@ export class Identifiers { static registerContentQuery: o.ExternalReference = {name: 'ɵregisterContentQuery', moduleName: CORE}; + static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; + static InheritDefinitionFeature: o.ExternalReference = {name: 'ɵInheritDefinitionFeature', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 99ae438db6..afe470a482 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -74,6 +74,17 @@ export interface R3DirectiveMetadata { properties: {[key: string]: string}; }; + /** + * Information about usage of specific lifecycle events which require special treatment in the + * code generator. + */ + lifecycle: { + /** + * Whether the directive uses NgOnChanges. + */ + usesOnChanges: boolean; + }; + /** * A mapping of input field names to the property names. */ diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index e8efe7f798..4adc4cbd39 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -125,6 +125,7 @@ function baseDirectiveFields( */ function addFeatures( definitionMap: DefinitionMap, meta: R3DirectiveMetadata | R3ComponentMetadata) { + // e.g. `features: [NgOnChangesFeature]` const features: o.Expression[] = []; const providers = meta.providers; @@ -140,7 +141,9 @@ function addFeatures( if (meta.usesInheritance) { features.push(o.importExpr(R3.InheritDefinitionFeature)); } - + if (meta.lifecycle.usesOnChanges) { + features.push(o.importExpr(R3.NgOnChangesFeature)); + } if (features.length) { definitionMap.set('features', o.literalArr(features)); } @@ -421,6 +424,10 @@ function directiveMetadataFromGlobalMetadata( selector: directive.selector, deps: dependenciesFromGlobalMetadata(directive.type, outputCtx, reflector), queries: queriesFromGlobalMetadata(directive.queries, outputCtx), + lifecycle: { + usesOnChanges: + directive.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges), + }, host: { attributes: directive.hostAttributes, listeners: summary.hostListeners, diff --git a/packages/core/src/change_detection/change_detection_util.ts b/packages/core/src/change_detection/change_detection_util.ts index b98a3756e7..da963bb641 100644 --- a/packages/core/src/change_detection/change_detection_util.ts +++ b/packages/core/src/change_detection/change_detection_util.ts @@ -64,6 +64,20 @@ 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/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index edcaddd769..8c424454b3 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -115,6 +115,7 @@ export interface R3DirectiveMetadataFacade { queries: R3QueryMetadataFacade[]; host: {[key: string]: string}; propMetadata: {[key: string]: any[]}; + lifecycle: {usesOnChanges: boolean;}; inputs: string[]; outputs: string[]; usesInheritance: boolean; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 1a4c72b187..eb7da1d585 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -28,6 +28,7 @@ export { templateRefExtractor as ɵtemplateRefExtractor, ProvidersFeature as ɵProvidersFeature, InheritDefinitionFeature as ɵInheritDefinitionFeature, + NgOnChangesFeature as ɵNgOnChangesFeature, LifecycleHooksFeature as ɵLifecycleHooksFeature, NgModuleType as ɵNgModuleType, NgModuleRef as ɵRender3NgModuleRef, diff --git a/packages/core/src/interface/lifecycle_hooks.ts b/packages/core/src/interface/lifecycle_hooks.ts index bf92bd54da..1d0a6be42b 100644 --- a/packages/core/src/interface/lifecycle_hooks.ts +++ b/packages/core/src/interface/lifecycle_hooks.ts @@ -5,9 +5,19 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {SimpleChanges} from './simple_change'; +import {SimpleChanges, SimpleChange} 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/metadata/directives.ts b/packages/core/src/metadata/directives.ts index dc7f588325..0e34da068a 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -9,12 +9,13 @@ import {ChangeDetectionStrategy} from '../change_detection/constants'; import {Provider} from '../di'; import {Type} from '../interface/type'; -import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../render3/fields'; +import {NG_BASE_DEF} from '../render3/fields'; import {compileComponent as render3CompileComponent, compileDirective as render3CompileDirective} from '../render3/jit/directive'; import {compilePipe as render3CompilePipe} from '../render3/jit/pipe'; import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators'; import {noop} from '../util/noop'; import {fillProperties} from '../util/property'; + import {ViewEncapsulation} from './view'; @@ -714,46 +715,21 @@ const initializeBaseDef = (target: any): void => { }; /** - * Returns a function that will update the static definition on a class to have the - * appropriate input or output mapping. - * - * Will also add an {@link ngBaseDef} property to a directive if no `ngDirectiveDef` - * or `ngComponentDef` is present. This is done because a class may have {@link InputDecorator}s and - * {@link OutputDecorator}s without having a {@link ComponentDecorator} or {@link DirectiveDecorator}, - * and those inputs and outputs should still be inheritable, we need to add an - * `ngBaseDef` property if there are no existing `ngComponentDef` or `ngDirectiveDef` - * properties, so that we can track the inputs and outputs for inheritance purposes. - * - * @param getPropertyToUpdate A function that maps to either the `inputs` property or the - * `outputs` property of a definition. - * @returns A function that, the called, will add a `ngBaseDef` if no other definition is present, - * then update the `inputs` or `outputs` on it, depending on what was selected by `getPropertyToUpdate` - * - * - * @see InputDecorator - * @see OutputDecorator - * @see InheritenceFeature + * Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators. + * @param key "inputs" or "outputs" */ -function getOrCreateDefinitionAndUpdateMappingFor( - getPropertyToUpdate: (baseDef: {inputs?: any, outputs?: any}) => any) { - return function updateIOProp(target: any, name: string, ...args: any[]) { - const constructor = target.constructor; +const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any}) => any) => + (target: any, name: string, ...args: any[]) => { + const constructor = target.constructor; - let def: any = - constructor[NG_COMPONENT_DEF] || constructor[NG_DIRECTIVE_DEF] || constructor[NG_BASE_DEF]; + if (!constructor.hasOwnProperty(NG_BASE_DEF)) { + initializeBaseDef(target); + } - if (!def) { - initializeBaseDef(target); - def = constructor[NG_BASE_DEF]; - } - - const defProp = getPropertyToUpdate(def); - // Use of `in` because we *do* want to check the prototype chain here. - if (!(name in defProp)) { + const baseDef = constructor.ngBaseDef; + const defProp = getProp(baseDef); defProp[name] = args[0]; - } - }; -} + }; /** * @Annotation @@ -761,7 +737,7 @@ function getOrCreateDefinitionAndUpdateMappingFor( */ export const Input: InputDecorator = makePropDecorator( 'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined, - getOrCreateDefinitionAndUpdateMappingFor(def => def.inputs || {})); + updateBaseDefFromIOProp(baseDef => baseDef.inputs || {})); /** * Type of the Output decorator / constructor function. @@ -801,7 +777,7 @@ export interface Output { bindingPropertyName?: string; } */ export const Output: OutputDecorator = makePropDecorator( 'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined, - getOrCreateDefinitionAndUpdateMappingFor(def => def.outputs || {})); + updateBaseDefFromIOProp(baseDef => baseDef.outputs || {})); diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 0bc8cbaff3..3bfba4cacf 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -17,7 +17,6 @@ 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'; @@ -26,6 +25,7 @@ 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,8 +240,7 @@ 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/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index a41ffd128c..3a69d2f6a9 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -12,7 +12,6 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {TNode, TNodeFlags} from './interfaces/node'; import {RElement} from './interfaces/renderer'; import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; -import {unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; @@ -258,7 +257,7 @@ function findViaDirective(lView: LView, directiveInstance: {}): number { const directiveIndexStart = tNode.directiveStart; const directiveIndexEnd = tNode.directiveEnd; for (let i = directiveIndexStart; i < directiveIndexEnd; i++) { - if (unwrapOnChangesDirectiveWrapper(lView[i]) === directiveInstance) { + if (lView[i] === directiveInstance) { return tNode.index; } } diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 11fef9fa21..62c59963ab 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -194,7 +194,7 @@ export function defineComponent(componentDefinition: { /** * A list of optional features to apply. * - * See: {@link ProvidersFeature} + * See: {@link NgOnChangesFeature}, {@link ProvidersFeature} */ features?: ComponentDefFeature[]; @@ -256,7 +256,6 @@ export function defineComponent(componentDefinition: { inputs: null !, // assigned in noSideEffects outputs: null !, // assigned in noSideEffects exportAs: componentDefinition.exportAs || null, - onChanges: typePrototype.ngOnChanges || null, onInit: typePrototype.ngOnInit || null, doCheck: typePrototype.ngDoCheck || null, afterContentInit: typePrototype.ngAfterContentInit || null, @@ -567,7 +566,7 @@ export const defineDirective = defineComponent as any as(directiveDefinition: /** * A list of optional features to apply. * - * See: {@link ProvidersFeature}, {@link InheritDefinitionFeature} + * See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature} */ features?: DirectiveDefFeature[]; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index c49956353c..11035c4a7d 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -20,7 +20,6 @@ import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInject import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node'; import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes} from './node_assert'; -import {unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, renderStringify} from './util'; @@ -523,8 +522,6 @@ export function getNodeInjectable( factory.resolving = false; setTNodeAndViewData(savePreviousOrParentTNode, saveLView); } - } else { - value = unwrapOnChangesDirectiveWrapper(value); } return value; } diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index b07cfaa857..8da5c9b641 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -10,6 +10,7 @@ import {Type} from '../../interface/type'; 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'; @@ -60,6 +61,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen } if (baseDef) { + // Merge inputs and outputs fillProperties(definition.inputs, baseDef.inputs); fillProperties(definition.declaredInputs, baseDef.declaredInputs); fillProperties(definition.outputs, baseDef.outputs); @@ -124,6 +126,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen } } + // Merge inputs and outputs fillProperties(definition.inputs, superDef.inputs); fillProperties(definition.declaredInputs, superDef.declaredInputs); @@ -139,7 +142,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen definition.doCheck = definition.doCheck || superDef.doCheck; definition.onDestroy = definition.onDestroy || superDef.onDestroy; definition.onInit = definition.onInit || superDef.onInit; - definition.onChanges = definition.onChanges || superDef.onChanges; // Run parent features const features = superDef.features; @@ -166,7 +168,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen definition.doCheck = definition.doCheck || superPrototype.ngDoCheck; definition.onDestroy = definition.onDestroy || superPrototype.ngOnDestroy; definition.onInit = definition.onInit || superPrototype.ngOnInit; - definition.onChanges = definition.onChanges || superPrototype.ngOnChanges; } } diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts new file mode 100644 index 0000000000..347688f0b0 --- /dev/null +++ b/packages/core/src/render3/features/ng_onchanges_feature.ts @@ -0,0 +1,125 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SimpleChange} from '../../change_detection/change_detection_util'; +import {SimpleChanges} from '../../interface/simple_change'; +import {OnChanges} from '../../interface/lifecycle_hooks'; +import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition'; + +const PRIVATE_PREFIX = '__ngOnChanges_'; + +type OnChangesExpando = OnChanges & { + __ngOnChanges_: SimpleChanges|null|undefined; + // tslint:disable-next-line:no-any Can hold any value + [key: string]: any; +}; + +/** + * The NgOnChangesFeature decorates a component with support for the ngOnChanges + * lifecycle hook, so it should be included in any component that implements + * that hook. + * + * If the component or directive uses inheritance, the NgOnChangesFeature MUST + * be included as a feature AFTER {@link InheritDefinitionFeature}, otherwise + * inherited properties will not be propagated to the ngOnChanges lifecycle + * hook. + * + * Example usage: + * + * ``` + * static ngComponentDef = defineComponent({ + * ... + * inputs: {name: 'publicName'}, + * features: [NgOnChangesFeature] + * }); + * ``` + */ +export function NgOnChangesFeature(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; + + // Walk the prototype chain to see if we find a property descriptor + // That way we can honor setters and getters that were inherited. + let originalProperty: PropertyDescriptor|undefined = undefined; + let checkProto = proto; + while (!originalProperty && checkProto && + Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) { + originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey); + checkProto = Object.getPrototypeOf(checkProto); + } + + const getter = originalProperty && originalProperty.get; + const setter = originalProperty && originalProperty.set; + + // create a getter and setter for property + Object.defineProperty(proto, minifiedKey, { + get: getter || + (setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }), + set(this: OnChangesExpando, value: T) { + let simpleChanges = this[PRIVATE_PREFIX]; + if (!simpleChanges) { + simpleChanges = {}; + // Place where we will store SimpleChanges if there is a change + Object.defineProperty(this, PRIVATE_PREFIX, {value: simpleChanges, writable: true}); + } + + const isFirstChange = !this.hasOwnProperty(privateMinKey); + const currentChange = simpleChanges[declaredKey]; + + if (currentChange) { + currentChange.currentValue = value; + } else { + simpleChanges[declaredKey] = + new SimpleChange(this[privateMinKey], value, isFirstChange); + } + + if (isFirstChange) { + // Create a place where the actual value will be stored and make it non-enumerable + Object.defineProperty(this, privateMinKey, {value, writable: true}); + } else { + this[privateMinKey] = value; + } + + if (setter) setter.call(this, value); + }, + // Make the property configurable in dev mode to allow overriding in tests + configurable: !!ngDevMode + }); + } + } + + // If an onInit hook is defined, it will need to wrap the ngOnChanges call + // so the call order is changes-init-check in creation mode. In subsequent + // change detection runs, only the check wrapper will be called. + if (definition.onInit != null) { + definition.onInit = onChangesWrapper(definition.onInit); + } + + definition.doCheck = onChangesWrapper(definition.doCheck); +} + +// This option ensures that the ngOnChanges lifecycle hook will be inherited +// from superclasses (in InheritDefinitionFeature). +(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true; + +function onChangesWrapper(delegateHook: (() => void) | null) { + return function(this: OnChangesExpando) { + const simpleChanges = this[PRIVATE_PREFIX]; + if (simpleChanges != null) { + this.ngOnChanges(simpleChanges); + this[PRIVATE_PREFIX] = null; + } + if (delegateHook) delegateHook.apply(this); + }; +} diff --git a/packages/core/src/render3/hooks.ts b/packages/core/src/render3/hooks.ts index 59062152bb..8675d11673 100644 --- a/packages/core/src/render3/hooks.ts +++ b/packages/core/src/render3/hooks.ts @@ -12,7 +12,6 @@ import {assertEqual} from '../util/assert'; import {DirectiveDef} from './interfaces/definition'; import {TNode} from './interfaces/node'; import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view'; -import {OnChangesDirectiveWrapper, unwrapOnChangesDirectiveWrapper} from './onchanges_util'; @@ -35,12 +34,7 @@ export function registerPreOrderHooks( ngDevMode && assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass'); - const {onChanges, onInit, doCheck} = directiveDef; - - if (onChanges) { - (tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onChanges); - (tView.checkHooks || (tView.checkHooks = [])).push(-directiveIndex, onChanges); - } + const {onInit, doCheck} = directiveDef; if (onInit) { (tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit); @@ -148,31 +142,13 @@ export function executeHooks( /** * Calls lifecycle hooks with their contexts, skipping init hooks if it's not - * the first LView pass, and skipping onChanges hooks if there are no changes present. + * the first LView pass * * @param currentView The current view * @param arr The array in which the hooks are found */ export function callHooks(currentView: LView, arr: HookData): void { for (let i = 0; i < arr.length; i += 2) { - const directiveIndex = arr[i] as number; - const hook = arr[i + 1] as((() => void) | ((changes: SimpleChanges) => void)); - // Negative indices signal that we're dealing with an `onChanges` hook. - const isOnChangesHook = directiveIndex < 0; - const directiveOrWrappedDirective = - currentView[isOnChangesHook ? -directiveIndex : directiveIndex]; - const directive = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective); - - if (isOnChangesHook) { - const onChanges: OnChangesDirectiveWrapper = directiveOrWrappedDirective; - const changes = onChanges.changes; - if (changes) { - onChanges.previous = changes; - onChanges.changes = null; - hook.call(onChanges.instance, changes); - } - } else { - hook.call(directive); - } + (arr[i + 1] as() => void).call(currentView[arr[i] as number]); } } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 15013a5aff..f90eec0741 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -9,6 +9,7 @@ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component' import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; import {getComponent, getDirectives, getHostElement, getRenderedText} from './discovery_utils'; import {InheritDefinitionFeature} from './features/inherit_definition_feature'; +import {NgOnChangesFeature} from './features/ng_onchanges_feature'; import {ProvidersFeature} from './features/providers_feature'; import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition'; @@ -158,6 +159,7 @@ export { DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, + NgOnChangesFeature, InheritDefinitionFeature, ProvidersFeature, PipeDef, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index fbe0a010d2..6b3d9654b8 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -36,7 +36,6 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLA import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; -import {OnChangesDirectiveWrapper, isOnChangesDirectiveWrapper, recordChange, unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; @@ -121,7 +120,7 @@ export function setHostBindings(tView: TView, viewData: LView): void { if (instruction !== null) { viewData[BINDING_INDEX] = bindingRootIndex; instruction( - RenderFlags.Update, unwrapOnChangesDirectiveWrapper(viewData[currentDirectiveIndex]), + RenderFlags.Update, readElementValue(viewData[currentDirectiveIndex]), currentElementIndex); } currentDirectiveIndex++; @@ -726,7 +725,6 @@ export function createTView( expandoStartIndex: initialViewLength, expandoInstructions: null, firstTemplatePass: true, - changesHooks: null, initHooks: null, checkHooks: null, contentHooks: null, @@ -961,18 +959,16 @@ function listenerInternal( const propsLength = props.length; if (propsLength) { const lCleanup = getCleanup(lView); - // Subscribe to listeners for each output, and setup clean up for each. - for (let i = 0; i < propsLength;) { - const directiveIndex = props[i++] as number; - const minifiedName = props[i++] as string; - const declaredName = props[i++] as string; - ngDevMode && assertDataInRange(lView, directiveIndex as number); - const directive = unwrapOnChangesDirectiveWrapper(lView[directiveIndex]); - const output = directive[minifiedName]; + for (let i = 0; i < propsLength; i += 2) { + const index = props[i] as number; + ngDevMode && assertDataInRange(lView, index); + const minifiedName = props[i + 1]; + const directiveInstance = lView[index]; + const output = directiveInstance[minifiedName]; if (ngDevMode && !isObservable(output)) { throw new Error( - `@Output ${minifiedName} not initialized in '${directive.constructor.name}'.`); + `@Output ${minifiedName} not initialized in '${directiveInstance.constructor.name}'.`); } const subscription = output.subscribe(listenerFn); @@ -1042,7 +1038,7 @@ export function elementEnd(): void { if (hasClassInput(previousOrParentTNode)) { const stylingContext = getStylingContext(previousOrParentTNode.index, lView); setInputsForProperty( - lView, previousOrParentTNode.inputs !, 'class', getInitialClassNameValue(stylingContext)); + lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext)); } } @@ -1137,7 +1133,7 @@ function elementPropertyInternal( let dataValue: PropertyAliasValue|undefined; if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) && (dataValue = inputData[propName])) { - setInputsForProperty(lView, inputData, propName, value); + setInputsForProperty(lView, dataValue, value); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (ngDevMode) { if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) { @@ -1215,30 +1211,21 @@ export function createTNode( * @param lView the `LView` which contains the directives. * @param inputAliases mapping between the public "input" name and privately-known, * possibly minified, property names to write to. - * @param publicName public binding name. (This is the `
`) * @param value Value to set. */ -function setInputsForProperty( - lView: LView, inputAliases: PropertyAliases, publicName: string, value: any): void { - const inputs = inputAliases[publicName]; - for (let i = 0; i < inputs.length;) { - const directiveIndex = inputs[i++] as number; - const privateName = inputs[i++] as string; - const declaredName = inputs[i++] as string; - ngDevMode && assertDataInRange(lView, directiveIndex); - recordChangeAndUpdateProperty(lView[directiveIndex], declaredName, privateName, value); +function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void { + for (let i = 0; i < inputs.length; i += 2) { + ngDevMode && assertDataInRange(lView, inputs[i] as number); + lView[inputs[i] as number][inputs[i + 1]] = value; } } function setNgReflectProperties( lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, value: any) { - for (let i = 0; i < inputs.length;) { - const directiveIndex = inputs[i++] as number; - const privateName = inputs[i++] as string; - const declaredName = inputs[i++] as string; + for (let i = 0; i < inputs.length; i += 2) { const renderer = lView[RENDERER]; - const attrName = normalizeDebugBindingName(privateName); + const attrName = normalizeDebugBindingName(inputs[i + 1] as string); const debugValue = normalizeDebugBindingValue(value); if (type === TNodeType.Element) { isProceduralRenderer(renderer) ? @@ -1274,20 +1261,15 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro for (let i = start; i < end; i++) { const directiveDef = defs[i] as DirectiveDef; - const publicToMinifiedNames: {[publicName: string]: string} = + const propertyAliasMap: {[publicName: string]: string} = isInput ? directiveDef.inputs : directiveDef.outputs; - const publicToDeclaredNames: {[publicName: string]: string}|null = - isInput ? directiveDef.declaredInputs : null; - for (let publicName in publicToMinifiedNames) { - if (publicToMinifiedNames.hasOwnProperty(publicName)) { + for (let publicName in propertyAliasMap) { + if (propertyAliasMap.hasOwnProperty(publicName)) { propStore = propStore || {}; - const minifiedName = publicToMinifiedNames[publicName]; - const declaredName = - publicToDeclaredNames ? publicToDeclaredNames[publicName] : minifiedName; - const aliases: PropertyAliasValue = propStore.hasOwnProperty(publicName) ? - propStore[publicName] : - propStore[publicName] = []; - aliases.push(i, minifiedName, declaredName); + const internalName = propertyAliasMap[publicName]; + const hasProperty = propStore.hasOwnProperty(publicName); + hasProperty ? propStore[publicName].push(i, internalName) : + (propStore[publicName] = [i, internalName]); } } } @@ -1514,7 +1496,7 @@ export function elementStylingMap( const initialClasses = getInitialClassNameValue(stylingContext); const classInputVal = (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); - setInputsForProperty(lView, tNode.inputs !, 'class', classInputVal); + setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); } else { updateStylingMap(stylingContext, classes, styles); } @@ -1647,7 +1629,6 @@ function instantiateAllDirectives(tView: TView, lView: LView, tNode: TNode) { addComponentLogic(lView, tNode, def as ComponentDef); } const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode); - postProcessDirective(lView, directive, def, i); } } @@ -1659,7 +1640,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const firstTemplatePass = getFirstTemplatePass(); for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; - const directive = unwrapOnChangesDirectiveWrapper(viewData[i]); + const directive = viewData[i]; if (def.hostBindings) { const previousExpandoLength = expando.length; setCurrentDirectiveDef(def); @@ -1716,17 +1697,12 @@ function prefillHostVars(tView: TView, lView: LView, totalHostVars: number): voi * Process a directive on the current node after its creation. */ function postProcessDirective( - lView: LView, directive: T, def: DirectiveDef, directiveDefIdx: number): void { - if (def.onChanges) { - // We have onChanges, wrap it so that we can track changes. - lView[directiveDefIdx] = new OnChangesDirectiveWrapper(lView[directiveDefIdx]); - } - + viewData: LView, directive: T, def: DirectiveDef, directiveDefIdx: number): void { const previousOrParentTNode = getPreviousOrParentTNode(); - postProcessBaseDirective(lView, previousOrParentTNode, directive, def); + postProcessBaseDirective(viewData, previousOrParentTNode, directive, def); ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode'); if (previousOrParentTNode && previousOrParentTNode.attrs) { - setInputsFromAttrs(lView, directiveDefIdx, def, previousOrParentTNode); + setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode); } if (def.contentQueries) { @@ -1734,7 +1710,7 @@ function postProcessDirective( } if (isComponentDef(def)) { - const componentView = getComponentViewByIndex(previousOrParentTNode.index, lView); + const componentView = getComponentViewByIndex(previousOrParentTNode.index, viewData); componentView[CONTEXT] = directive; } } @@ -1927,53 +1903,20 @@ function addComponentLogic( * @param tNode The static data for this node */ function setInputsFromAttrs( - lView: LView, directiveIndex: number, def: DirectiveDef, tNode: TNode): void { + directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void { let initialInputData = tNode.initialInputs as InitialInputData | undefined; if (initialInputData === undefined || directiveIndex >= initialInputData.length) { - initialInputData = generateInitialInputs(directiveIndex, def, tNode); + initialInputData = generateInitialInputs(directiveIndex, inputs, tNode); } const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; if (initialInputs) { - const directiveOrWrappedDirective = lView[directiveIndex]; - - for (let i = 0; i < initialInputs.length;) { - const privateName = initialInputs[i++]; - const declaredName = initialInputs[i++]; - const attrValue = initialInputs[i++]; - recordChangeAndUpdateProperty( - directiveOrWrappedDirective, declaredName, privateName, attrValue); + for (let i = 0; i < initialInputs.length; i += 2) { + (instance as any)[initialInputs[i]] = initialInputs[i + 1]; } } } -/** - * Checks to see if the instanced passed as `directiveOrWrappedDirective` is wrapped in {@link - * OnChangesDirectiveWrapper} or not. - * If it is, it will update the related {@link SimpleChanges} object with the change to signal - * `ngOnChanges` hook - * should fire, then it will unwrap the instance. After that, it will set the property with the key - * provided - * in `privateName` on the instance with the passed value. - * @param directiveOrWrappedDirective The directive instance or a directive instance wrapped in - * {@link OnChangesDirectiveWrapper} - * @param declaredName The original, declared name of the property to update. - * @param privateName The private, possibly minified name of the property to update. - * @param value The value to update the property with. - */ -function recordChangeAndUpdateProperty( - directiveOrWrappedDirective: OnChangesDirectiveWrapper| T, declaredName: string, - privateName: K, value: any) { - let instance: T; - if (isOnChangesDirectiveWrapper(directiveOrWrappedDirective)) { - instance = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective); - recordChange(directiveOrWrappedDirective, declaredName, value); - } else { - instance = directiveOrWrappedDirective; - } - instance[privateName] = value; -} - /** * Generates initialInputData for a node and stores it in the template's static storage * so subsequent template invocations don't have to recalculate it. @@ -1990,7 +1933,7 @@ function recordChangeAndUpdateProperty( * @param tNode The static data on this node */ function generateInitialInputs( - directiveIndex: number, directiveDef: DirectiveDef, tNode: TNode): InitialInputData { + directiveIndex: number, inputs: {[key: string]: string}, tNode: TNode): InitialInputData { const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []); initialInputData[directiveIndex] = null; @@ -2007,14 +1950,13 @@ function generateInitialInputs( i += 4; continue; } - const privateName = directiveDef.inputs[attrName]; - const declaredName = directiveDef.declaredInputs[attrName]; + const minifiedInputName = inputs[attrName]; const attrValue = attrs[i + 1]; - if (privateName !== undefined) { + if (minifiedInputName !== undefined) { const inputsToStore: InitialInputs = initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); - inputsToStore.push(privateName, declaredName, attrValue as string); + inputsToStore.push(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 e3ecf2c0fd..76a9228abf 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -6,9 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {SimpleChanges, ViewEncapsulation} from '../../core'; +import {ViewEncapsulation} from '../../core'; import {Type} from '../../interface/type'; - import {CssSelectorList} from './projection'; @@ -143,7 +142,6 @@ export interface DirectiveDef extends BaseDef { /* The following are lifecycle hooks for this component */ onInit: (() => void)|null; doCheck: (() => void)|null; - onChanges: ((changes: SimpleChanges) => void)|null; afterContentInit: (() => void)|null; afterContentChecked: (() => void)|null; afterViewInit: (() => void)|null; diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index a86ac83727..16f5589a45 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -468,12 +468,10 @@ export type PropertyAliases = { /** * Store the runtime input or output names for all the directives. * - * Values are stored in triplets: - * - i + 0: directive index - * - i + 1: minified / internal name - * - i + 2: declared name + * - Even indices: directive index + * - Odd indices: minified / internal name * - * e.g. [0, 'minifiedName', 'declaredPropertyName'] + * e.g. [0, 'change-minified'] */ export type PropertyAliasValue = (number | string)[]; @@ -501,12 +499,10 @@ export type InitialInputData = (InitialInputs | null)[]; * Used by InitialInputData to store input properties * that should be set once from attributes. * - * The inputs come in triplets of: - * i + 0: minified/internal input name - * i + 1: declared input name (needed for OnChanges) - * i + 2: initial value + * Even indices: minified/internal input name + * Odd indices: initial value * - * e.g. ['minifiedName', 'declaredName', 'value'] + * e.g. ['role-min', 'button'] */ export type InitialInputs = string[]; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 700185bc47..7c618c48a6 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -534,7 +534,7 @@ export interface RootContext { * Even indices: Directive index * Odd indices: Hook function */ -export type HookData = (number | (() => void) | ((changes: SimpleChanges) => void))[]; +export type HookData = (number | (() => void))[]; /** * Static data that corresponds to the instance-specific data array on an LView. diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index cf10571f15..375e232205 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -145,6 +145,9 @@ function directiveMetadata(type: Type, metadata: Directive): R3DirectiveMet inputs: metadata.inputs || EMPTY_ARRAY, outputs: metadata.outputs || EMPTY_ARRAY, queries: extractQueriesMetadata(type, propMetadata, isContentQuery), + lifecycle: { + usesOnChanges: type.prototype.ngOnChanges !== undefined, + }, typeSourceSpan: null !, usesInheritance: !extendsDirectlyFromObject(type), exportAs: extractExportAs(metadata.exportAs), diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 44d01358e5..0587dfa53c 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -31,6 +31,7 @@ export const angularCoreEnv: {[name: string]: Function} = { 'inject': inject, 'ɵinjectAttribute': r3.injectAttribute, 'ɵtemplateRefExtractor': r3.templateRefExtractor, + 'ɵNgOnChangesFeature': r3.NgOnChangesFeature, 'ɵProvidersFeature': r3.ProvidersFeature, 'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature, 'ɵelementAttribute': r3.elementAttribute, diff --git a/packages/core/src/render3/onchanges_util.ts b/packages/core/src/render3/onchanges_util.ts deleted file mode 100644 index 3b6ab9d5ae..0000000000 --- a/packages/core/src/render3/onchanges_util.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import {SimpleChange, SimpleChanges} from '../interface/simple_change'; - - -type Constructor = new (...args: any[]) => T; - -/** - * Checks an object to see if it's an exact instance of a particular type - * without traversing the inheritance hierarchy like `instanceof` does. - * @param obj The object to check - * @param type The type to check the object against - */ -export function isExactInstanceOf(obj: any, type: Constructor): obj is T { - return obj != null && typeof obj == 'object' && Object.getPrototypeOf(obj) == type.prototype; -} - -/** - * Checks to see if an object is an instance of {@link OnChangesDirectiveWrapper} - * @param obj the object to check (generally from `LView`) - */ -export function isOnChangesDirectiveWrapper(obj: any): obj is OnChangesDirectiveWrapper { - return isExactInstanceOf(obj, OnChangesDirectiveWrapper); -} - -/** - * Removes the `OnChangesDirectiveWrapper` if present. - * - * @param obj to unwrap. - */ -export function unwrapOnChangesDirectiveWrapper(obj: T | OnChangesDirectiveWrapper): T { - return isOnChangesDirectiveWrapper(obj) ? obj.instance : obj; -} - -/** - * A class that wraps directive instances for storage in LView when directives - * have onChanges hooks to deal with. - */ -export class OnChangesDirectiveWrapper { - seenProps = new Set(); - previous: SimpleChanges = {}; - changes: SimpleChanges|null = null; - - constructor(public instance: T) {} -} - -/** - * Updates the `changes` property on the `wrapper` instance, such that when it's - * checked in {@link callHooks} it will fire the related `onChanges` hook. - * @param wrapper the wrapper for the directive instance - * @param declaredName the declared name to be used in `SimpleChange` - * @param value The new value for the property - */ -export function recordChange(wrapper: OnChangesDirectiveWrapper, declaredName: string, value: any) { - const simpleChanges = wrapper.changes || (wrapper.changes = {}); - - const firstChange = !wrapper.seenProps.has(declaredName); - if (firstChange) { - wrapper.seenProps.add(declaredName); - } - - const previous = wrapper.previous; - const previousValue: SimpleChange|undefined = previous[declaredName]; - simpleChanges[declaredName] = new SimpleChange( - firstChange ? undefined : previousValue && previousValue.currentValue, value, firstChange); -} diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 31d03f94dc..c2d97fb503 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -17,7 +17,6 @@ import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './inte import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer'; import {StylingContext} from './interfaces/styling'; import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view'; -import {isOnChangesDirectiveWrapper} from './onchanges_util'; /** @@ -71,14 +70,9 @@ export function flatten(list: any[]): any[] { /** Retrieves a value from any `LView` or `TData`. */ export function loadInternal(view: LView | TData, index: number): T { ngDevMode && assertDataInRange(view, index + HEADER_OFFSET); - const record = view[index + HEADER_OFFSET]; - // If we're storing an array because of a directive or component with ngOnChanges, - // return the directive or component instance. - return isOnChangesDirectiveWrapper(record) ? record.instance : record; + return view[index + HEADER_OFFSET]; } - - /** * Takes the value of a slot in `LView` and returns the element node. * @@ -297,4 +291,4 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) { export function resolveBody(element: RElement & {ownerDocument: Document}) { return {name: 'body', target: element.ownerDocument.body}; -} +} \ No newline at end of file diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index cc6794e23a..b88ec8cb94 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -11,11 +11,10 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; -import {checkNoChanges, checkNoChangesInRootView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; +import {checkNoChanges, checkNoChangesInRootView, checkView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; import {TNode, TNodeType, TViewNode} from './interfaces/node'; -import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT} from './interfaces/view'; +import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT, RENDERER_FACTORY} from './interfaces/view'; import {destroyLView} from './node_manipulation'; -import {unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {getNativeByTNode} from './util'; @@ -272,8 +271,7 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int } private _lookUpContext(): T { - return this._context = - unwrapOnChangesDirectiveWrapper(this._lView[PARENT] ![this._componentIndex] as T); + return this._context = this._lView[PARENT] ![this._componentIndex] as T; } } 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 bff40dcc6f..146e8556d2 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -80,21 +80,24 @@ { "name": "NO_PARENT_INJECTOR" }, + { + "name": "NgOnChangesFeature" + }, { "name": "NodeInjectorFactory" }, { "name": "ObjectUnsubscribedErrorImpl" }, - { - "name": "OnChangesDirectiveWrapper" - }, { "name": "PARENT" }, { "name": "PARENT_INJECTOR" }, + { + "name": "PRIVATE_PREFIX" + }, { "name": "RENDERER" }, @@ -104,6 +107,9 @@ { "name": "SANITIZER" }, + { + "name": "SimpleChange" + }, { "name": "TVIEW" }, @@ -326,15 +332,9 @@ { "name": "isCreationMode" }, - { - "name": "isExactInstanceOf" - }, { "name": "isFactory" }, - { - "name": "isOnChangesDirectiveWrapper" - }, { "name": "isProceduralRenderer" }, @@ -368,6 +368,9 @@ { "name": "noSideEffects" }, + { + "name": "onChangesWrapper" + }, { "name": "postProcessBaseDirective" }, @@ -446,9 +449,6 @@ { "name": "tickRootContext" }, - { - "name": "unwrapOnChangesDirectiveWrapper" - }, { "name": "updateViewQuery" }, diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index 33e1162f8f..673ef89e01 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -35,6 +35,9 @@ { "name": "NULL_INJECTOR" }, + { + "name": "NgOnChangesFeature" + }, { "name": "NullInjector" }, @@ -47,6 +50,9 @@ { "name": "PARAMETERS" }, + { + "name": "PRIVATE_PREFIX" + }, { "name": "R3Injector" }, @@ -56,6 +62,9 @@ { "name": "Self" }, + { + "name": "SimpleChange" + }, { "name": "SkipSelf" }, @@ -155,6 +164,9 @@ { "name": "makeRecord" }, + { + "name": "onChangesWrapper" + }, { "name": "providerToFactory" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 442a8dc109..93b17b2f5b 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -143,6 +143,9 @@ { "name": "NgModuleRef" }, + { + "name": "NgOnChangesFeature" + }, { "name": "NodeInjector" }, @@ -152,9 +155,6 @@ { "name": "ObjectUnsubscribedErrorImpl" }, - { - "name": "OnChangesDirectiveWrapper" - }, { "name": "Optional" }, @@ -167,6 +167,9 @@ { "name": "PARENT_INJECTOR" }, + { + "name": "PRIVATE_PREFIX" + }, { "name": "QUERIES" }, @@ -908,9 +911,6 @@ { "name": "isDirty" }, - { - "name": "isExactInstanceOf" - }, { "name": "isFactory" }, @@ -929,9 +929,6 @@ { "name": "isNodeMatchingSelectorList" }, - { - "name": "isOnChangesDirectiveWrapper" - }, { "name": "isPositive" }, @@ -1019,6 +1016,9 @@ { "name": "noSideEffects" }, + { + "name": "onChangesWrapper" + }, { "name": "pointers" }, @@ -1049,12 +1049,6 @@ { "name": "readPatchedLView" }, - { - "name": "recordChange" - }, - { - "name": "recordChangeAndUpdateProperty" - }, { "name": "reference" }, @@ -1232,9 +1226,6 @@ { "name": "trackByIdentity" }, - { - "name": "unwrapOnChangesDirectiveWrapper" - }, { "name": "updateClassProp" }, diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 6b036a4683..5abc8650f4 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -536,6 +536,7 @@ 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( '
'); @@ -738,6 +739,7 @@ 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/common_with_def.ts b/packages/core/test/render3/common_with_def.ts index 32e7bd1875..73cbda6bf8 100644 --- a/packages/core/test/render3/common_with_def.ts +++ b/packages/core/test/render3/common_with_def.ts @@ -9,7 +9,7 @@ import {NgForOf as NgForOfDef, NgIf as NgIfDef, NgTemplateOutlet as NgTemplateOutletDef} from '@angular/common'; import {IterableDiffers, TemplateRef, ViewContainerRef} from '@angular/core'; -import {DirectiveType, defineDirective, directiveInject} from '../../src/render3/index'; +import {DirectiveType, NgOnChangesFeature, defineDirective, directiveInject} from '../../src/render3/index'; export const NgForOf: DirectiveType> = NgForOfDef as any; export const NgIf: DirectiveType = NgIfDef as any; @@ -40,6 +40,7 @@ NgForOf.ngDirectiveDef = defineDirective({ type: NgTemplateOutletDef, selectors: [['', 'ngTemplateOutlet', '']], factory: () => new NgTemplateOutletDef(directiveInject(ViewContainerRef as any)), + features: [NgOnChangesFeature], inputs: {ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'} }); diff --git a/packages/core/test/render3/host_binding_spec.ts b/packages/core/test/render3/host_binding_spec.ts index a01e8a4dbb..35ab940486 100644 --- a/packages/core/test/render3/host_binding_spec.ts +++ b/packages/core/test/render3/host_binding_spec.ts @@ -8,7 +8,7 @@ import {ElementRef, QueryList, ViewContainerRef} from '@angular/core'; -import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature} from '../../src/render3/index'; +import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature} from '../../src/render3/index'; import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions'; import {query, queryRefresh} from '../../src/render3/query'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -357,6 +357,7 @@ describe('host bindings', () => { template: (rf: RenderFlags, ctx: InitHookComp) => {}, consts: 0, vars: 0, + features: [NgOnChangesFeature], hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => { if (rf & RenderFlags.Create) { allocHostVars(1); diff --git a/packages/core/test/render3/inherit_definition_feature_spec.ts b/packages/core/test/render3/inherit_definition_feature_spec.ts index d2a059e8d1..d19b58d8eb 100644 --- a/packages/core/test/render3/inherit_definition_feature_spec.ts +++ b/packages/core/test/render3/inherit_definition_feature_spec.ts @@ -7,7 +7,7 @@ */ import {Inject, InjectionToken} from '../../src/core'; -import {ComponentDef, DirectiveDef, InheritDefinitionFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty} from '../../src/render3/index'; +import {ComponentDef, DirectiveDef, InheritDefinitionFeature, NgOnChangesFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty, load} from '../../src/render3/index'; import {ComponentFixture, createComponent} from './render_util'; @@ -501,7 +501,8 @@ describe('InheritDefinitionFeature', () => { type: SuperDirective, selectors: [['', 'superDir', '']], factory: () => new SuperDirective(), - inputs: {someInput: 'someInput'}, + features: [NgOnChangesFeature], + inputs: {someInput: 'someInput'} }); } @@ -518,9 +519,6 @@ describe('InheritDefinitionFeature', () => { if (rf & RenderFlags.Create) { element(0, 'div', ['subDir', '']); } - if (rf & RenderFlags.Update) { - elementProperty(0, 'someInput', bind('Weee')); - } }, 1, 0, [SubDirective]); const fixture = new ComponentFixture(App); diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index da7181479c..44be88dc17 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -14,6 +14,7 @@ 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'; describe('lifecycles', () => { @@ -1940,6 +1941,7 @@ describe('lifecycles', () => { }); + fixmeIvy('FW-956: refactor onChanges'). describe('onChanges', () => { let events: ({type: string, name: string, [key: string]: any})[]; @@ -2699,6 +2701,7 @@ describe('lifecycles', () => { }); + fixmeIvy('FW-956: refactor onChanges'). describe('hook order', () => { let events: string[]; diff --git a/packages/core/test/render3/ng_on_changes_feature_spec.ts b/packages/core/test/render3/ng_on_changes_feature_spec.ts new file mode 100644 index 0000000000..617884a5bb --- /dev/null +++ b/packages/core/test/render3/ng_on_changes_feature_spec.ts @@ -0,0 +1,327 @@ +/** + * @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 f1c54e17a0..b669c9ede4 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -8,7 +8,7 @@ import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core'; import {ViewEncapsulation} from '../../src/metadata'; -import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index'; +import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index'; import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding, elementHostAttrs} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -1631,6 +1631,7 @@ describe('ViewContainerRef', () => { textBinding(0, interpolation1('', cmp.name, '')); } }, + features: [NgOnChangesFeature], inputs: {name: 'name'} }); } @@ -1795,13 +1796,12 @@ describe('ViewContainerRef', () => { expect(fixture.html).toEqual('AB'); expect(log).toEqual([]); - // Below will *NOT* cause onChanges to fire, because only bindings trigger onChanges componentRef.instance.name = 'D'; log.length = 0; fixture.update(); expect(fixture.html).toEqual('ADB'); expect(log).toEqual([ - 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', + 'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D', 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' ]); diff --git a/packages/core/test/view/component_view_spec.ts b/packages/core/test/view/component_view_spec.ts index e9d7f66dfb..2c5aa1a0e1 100644 --- a/packages/core/test/view/component_view_spec.ts +++ b/packages/core/test/view/component_view_spec.ts @@ -9,7 +9,6 @@ import {SecurityContext} from '@angular/core'; import {ArgumentType, BindingFlags, NodeCheckFn, NodeFlags, Services, ViewData, ViewFlags, ViewState, asElementData, directiveDef, elementDef, rootRenderNodes} from '@angular/core/src/view/index'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {fixmeIvy} from '@angular/private/testing'; import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper'; diff --git a/packages/core/test/view/element_spec.ts b/packages/core/test/view/element_spec.ts index 3ee576f0a6..5e9fe24afd 100644 --- a/packages/core/test/view/element_spec.ts +++ b/packages/core/test/view/element_spec.ts @@ -11,7 +11,6 @@ import {getDebugContext} from '@angular/core/src/errors'; import {BindingFlags, NodeFlags, Services, ViewData, ViewDefinition, asElementData, elementDef} from '@angular/core/src/view/index'; import {TestBed} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; -import {fixmeIvy} from '@angular/private/testing'; import {ARG_TYPE_VALUES, callMostRecentEventListenerHandler, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes, isBrowser, recordNodeToRemove} from './helper'; @@ -185,7 +184,6 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent return result; } - it('should listen to DOM events', () => { const handleEventSpy = jasmine.createSpy('handleEvent'); const removeListenerSpy = @@ -253,7 +251,6 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent expect(removeListenerSpy).toHaveBeenCalled(); }); - it('should preventDefault only if the handler returns false', () => { let eventHandlerResult: any; let preventDefaultSpy: jasmine.Spy = undefined !; @@ -282,7 +279,6 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent expect(preventDefaultSpy).toHaveBeenCalled(); }); - it('should report debug info on event errors', () => { const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError'); const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough(); diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index 32e8626355..91b9c59735 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -315,235 +315,239 @@ withEachNg1Version(() => { }); })); - 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; }); + 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; }); - 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(); + }); - })); + })); - 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; - }); + 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; + }); - @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(); + }); + })); - it('should initialize inputs in time for `ngOnChanges`', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + 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)); - @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)); @@ -1868,6 +1872,7 @@ 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 fe78f26f03..c34348dfc3 100644 --- a/packages/upgrade/test/static/integration/downgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_component_spec.ts @@ -22,104 +22,106 @@ withEachNg1Version(() => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); - 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'] = '?'; - }); + 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'] = '?'; + }); - @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( @@ -187,57 +189,58 @@ withEachNg1Version(() => { }); })); - 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; - }); + 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; + }); - @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; @@ -401,65 +404,66 @@ withEachNg1Version(() => { }); })); - it('should initialize inputs in time for `ngOnChanges`', async(() => { - @Component({ - selector: 'ng2', - template: ` + fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') + .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 663158ae11..f8a5ed5035 100644 --- a/packages/upgrade/test/static/integration/downgrade_module_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_module_spec.ts @@ -721,63 +721,66 @@ withEachNg1Version(() => { }); })); - it('should propagate input changes inside the Angular zone', async(() => { - let ng2Component: Ng2Component; + fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') + .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(() => { @@ -940,165 +943,167 @@ withEachNg1Version(() => { }); })); - it('should run the lifecycle hooks in the correct order', async(() => { - const logs: string[] = []; - let rootScope: angular.IRootScopeService; + 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; - @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; diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index c77d0dac54..58fbcfd538 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -306,8 +306,8 @@ export declare class NgSwitchDefault { } export declare class NgTemplateOutlet implements OnChanges { - ngTemplateOutlet: TemplateRef | null; - ngTemplateOutletContext: Object | null; + ngTemplateOutlet: TemplateRef; + ngTemplateOutletContext: Object; constructor(_viewContainerRef: ViewContainerRef); ngOnChanges(changes: SimpleChanges): void; }