diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 30c7a708cd..5136ceafb2 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -3,7 +3,7 @@ "master": { "uncompressed": { "runtime": 1497, - "main": 187134, + "main": 187437, "polyfills": 59608 } } diff --git a/packages/common/src/directives/ng_template_outlet.ts b/packages/common/src/directives/ng_template_outlet.ts index 0500caf36c..489ed87878 100644 --- a/packages/common/src/directives/ng_template_outlet.ts +++ b/packages/common/src/directives/ng_template_outlet.ts @@ -34,14 +34,20 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange */ @Directive({selector: '[ngTemplateOutlet]'}) export class NgTemplateOutlet implements OnChanges { - // TODO(issue/24571): remove '!'. - private _viewRef !: EmbeddedViewRef; + private _viewRef: EmbeddedViewRef|null = null; - // TODO(issue/24571): remove '!'. - @Input() public ngTemplateOutletContext !: Object; + /** + * A context object to attach to the {@link EmbeddedViewRef}. This should be an + * object, the object's keys will be available for binding by the local template `let` + * declarations. + * Using the key `$implicit` in the context object will set its value as default. + */ + @Input() public ngTemplateOutletContext: Object|null = null; - // TODO(issue/24571): remove '!'. - @Input() public ngTemplateOutlet !: TemplateRef; + /** + * A string defining the template reference and optionally the context object for the template. + */ + @Input() public ngTemplateOutlet: TemplateRef|null = null; constructor(private _viewContainerRef: ViewContainerRef) {} @@ -97,7 +103,7 @@ export class NgTemplateOutlet implements OnChanges { private _updateExistingContext(ctx: Object): void { for (let propName of Object.keys(ctx)) { - (this._viewRef.context)[propName] = (this.ngTemplateOutletContext)[propName]; + (this._viewRef !.context)[propName] = (this.ngTemplateOutletContext)[propName]; } } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 27f569af19..ca77677893 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -174,11 +174,6 @@ export function extractDirectiveMetadata( const providers: Expression|null = directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null; - // Determine if `ngOnChanges` is a lifecycle hook defined on the component. - const usesOnChanges = members.some( - member => !member.isStatic && member.kind === ClassMemberKind.Method && - member.name === 'ngOnChanges'); - // Parse exportAs. let exportAs: string[]|null = null; if (directive.has('exportAs')) { @@ -197,9 +192,6 @@ export function extractDirectiveMetadata( const metadata: R3DirectiveMetadata = { name: clazz.name !.text, deps: getConstructorDependencies(clazz, reflector, isCore), host, - lifecycle: { - usesOnChanges, - }, inputs: {...inputsFromMeta, ...inputsFromFields}, outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector, type: new WrappedNodeExpr(clazz.name !), 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 21092cb120..5771a21638 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -2117,7 +2117,6 @@ describe('compiler compliance', () => { selectors: [["lifecycle-comp"]], factory: function LifecycleComp_Factory(t) { return new (t || LifecycleComp)(); }, inputs: {nameMin: ["name", "nameMin"]}, - features: [$r3$.ɵNgOnChangesFeature], consts: 0, vars: 0, template: function LifecycleComp_Template(rf, ctx) {}, @@ -2242,7 +2241,6 @@ describe('compiler compliance', () => { factory: function ForOfDirective_Factory(t) { return new (t || ForOfDirective)($r3$.ɵdirectiveInject(ViewContainerRef), $r3$.ɵdirectiveInject(TemplateRef)); }, - features: [$r3$.ɵNgOnChangesFeature], inputs: {forOf: "forOf"} }); `; @@ -2318,7 +2316,6 @@ describe('compiler compliance', () => { factory: function ForOfDirective_Factory(t) { return new (t || ForOfDirective)($r3$.ɵdirectiveInject(ViewContainerRef), $r3$.ɵdirectiveInject(TemplateRef)); }, - features: [$r3$.ɵNgOnChangesFeature], inputs: {forOf: "forOf"} }); `; diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 5e659a47af..a9dae6bff9 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -115,7 +115,6 @@ export interface R3DirectiveMetadataFacade { queries: R3QueryMetadataFacade[]; host: {[key: string]: string}; propMetadata: {[key: string]: any[]}; - lifecycle: {usesOnChanges: boolean;}; inputs: string[]; outputs: string[]; usesInheritance: boolean; diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 83139fa7da..387f6e1543 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -185,8 +185,6 @@ export class Identifiers { static registerContentQuery: o.ExternalReference = {name: 'ɵregisterContentQuery', moduleName: CORE}; - static NgOnChangesFeature: o.ExternalReference = {name: 'ɵNgOnChangesFeature', moduleName: CORE}; - static InheritDefinitionFeature: o.ExternalReference = {name: 'ɵInheritDefinitionFeature', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index afe470a482..99ae438db6 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -74,17 +74,6 @@ export interface R3DirectiveMetadata { properties: {[key: string]: string}; }; - /** - * Information about usage of specific lifecycle events which require special treatment in the - * code generator. - */ - lifecycle: { - /** - * Whether the directive uses NgOnChanges. - */ - usesOnChanges: boolean; - }; - /** * A mapping of input field names to the property names. */ diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 0fc19b159b..5a0880e2a1 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -128,7 +128,6 @@ function baseDirectiveFields( */ function addFeatures( definitionMap: DefinitionMap, meta: R3DirectiveMetadata | R3ComponentMetadata) { - // e.g. `features: [NgOnChangesFeature]` const features: o.Expression[] = []; const providers = meta.providers; @@ -144,9 +143,7 @@ function addFeatures( if (meta.usesInheritance) { features.push(o.importExpr(R3.InheritDefinitionFeature)); } - if (meta.lifecycle.usesOnChanges) { - features.push(o.importExpr(R3.NgOnChangesFeature)); - } + if (features.length) { definitionMap.set('features', o.literalArr(features)); } @@ -427,10 +424,6 @@ function directiveMetadataFromGlobalMetadata( selector: directive.selector, deps: dependenciesFromGlobalMetadata(directive.type, outputCtx, reflector), queries: queriesFromGlobalMetadata(directive.queries, outputCtx), - lifecycle: { - usesOnChanges: - directive.type.lifecycleHooks.some(lifecycle => lifecycle == LifecycleHooks.OnChanges), - }, host: { attributes: directive.hostAttributes, listeners: summary.hostListeners, diff --git a/packages/core/src/change_detection/change_detection.ts b/packages/core/src/change_detection/change_detection.ts index f8dc1f18ac..1ce69f4662 100644 --- a/packages/core/src/change_detection/change_detection.ts +++ b/packages/core/src/change_detection/change_detection.ts @@ -11,8 +11,7 @@ import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ'; import {IterableDifferFactory, IterableDiffers} from './differs/iterable_differs'; import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs'; -export {SimpleChanges} from '../metadata/lifecycle_hooks'; -export {SimpleChange, WrappedValue, devModeEqual} from './change_detection_util'; +export {WrappedValue, devModeEqual} from './change_detection_util'; export {ChangeDetectorRef} from './change_detector_ref'; export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants'; export {DefaultIterableDifferFactory} from './differs/default_iterable_differ'; @@ -21,6 +20,7 @@ export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ'; export {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFunction} from './differs/iterable_differs'; export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs'; export {PipeTransform} from './pipe_transform'; +export {SimpleChange, SimpleChanges} from './simple_change'; diff --git a/packages/core/src/change_detection/change_detection_util.ts b/packages/core/src/change_detection/change_detection_util.ts index da963bb641..b98a3756e7 100644 --- a/packages/core/src/change_detection/change_detection_util.ts +++ b/packages/core/src/change_detection/change_detection_util.ts @@ -64,20 +64,6 @@ export class WrappedValue { static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; } } -/** - * Represents a basic change from a previous to a new value. - * - * @publicApi - */ -export class SimpleChange { - constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {} - - /** - * Check whether the new value is the first value assigned. - */ - isFirstChange(): boolean { return this.firstChange; } -} - export function isListLikeIterable(obj: any): boolean { if (!isJsObject(obj)) return false; return Array.isArray(obj) || diff --git a/packages/core/src/change_detection/simple_change.ts b/packages/core/src/change_detection/simple_change.ts new file mode 100644 index 0000000000..6152cbd7ad --- /dev/null +++ b/packages/core/src/change_detection/simple_change.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * Represents a basic change from a previous to a new value for a single + * property on a directive instance. Passed as a value in a + * {@link SimpleChanges} object to the `ngOnChanges` hook. + * + * @see `OnChanges` + * + * @publicApi + */ +export class SimpleChange { + constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {} + /** + * Check whether the new value is the first value assigned. + */ + isFirstChange(): boolean { return this.firstChange; } +} + +/** + * A hashtable of changes represented by {@link SimpleChange} objects stored + * at the declared property name they belong to on a Directive or Component. This is + * the type passed to the `ngOnChanges` hook. + * + * @see `OnChanges` + * + * @publicApi + */ +export interface SimpleChanges { [propName: string]: SimpleChange; } diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index e0a8e77a03..3afc6c83d4 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -28,7 +28,6 @@ export { templateRefExtractor as ɵtemplateRefExtractor, ProvidersFeature as ɵProvidersFeature, InheritDefinitionFeature as ɵInheritDefinitionFeature, - NgOnChangesFeature as ɵNgOnChangesFeature, LifecycleHooksFeature as ɵLifecycleHooksFeature, NgModuleType as ɵNgModuleType, NgModuleRef as ɵRender3NgModuleRef, diff --git a/packages/core/src/metadata/directives.ts b/packages/core/src/metadata/directives.ts index e6ceea61b1..fedc38d190 100644 --- a/packages/core/src/metadata/directives.ts +++ b/packages/core/src/metadata/directives.ts @@ -9,13 +9,12 @@ import {ChangeDetectionStrategy} from '../change_detection/constants'; import {Provider} from '../di'; import {Type} from '../interface/type'; -import {NG_BASE_DEF} from '../render3/fields'; +import {NG_BASE_DEF, NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../render3/fields'; import {compileComponent as render3CompileComponent, compileDirective as render3CompileDirective} from '../render3/jit/directive'; import {compilePipe as render3CompilePipe} from '../render3/jit/pipe'; import {TypeDecorator, makeDecorator, makePropDecorator} from '../util/decorators'; import {noop} from '../util/noop'; import {fillProperties} from '../util/property'; - import {ViewEncapsulation} from './view'; @@ -715,21 +714,46 @@ const initializeBaseDef = (target: any): void => { }; /** - * Does the work of creating the `ngBaseDef` property for the @Input and @Output decorators. - * @param key "inputs" or "outputs" + * Returns a function that will update the static definition on a class to have the + * appropriate input or output mapping. + * + * Will also add an {@link ngBaseDef} property to a directive if no `ngDirectiveDef` + * or `ngComponentDef` is present. This is done because a class may have {@link InputDecorator}s and + * {@link OutputDecorator}s without having a {@link ComponentDecorator} or {@link DirectiveDecorator}, + * and those inputs and outputs should still be inheritable, we need to add an + * `ngBaseDef` property if there are no existing `ngComponentDef` or `ngDirectiveDef` + * properties, so that we can track the inputs and outputs for inheritance purposes. + * + * @param getPropertyToUpdate A function that maps to either the `inputs` property or the + * `outputs` property of a definition. + * @returns A function that, the called, will add a `ngBaseDef` if no other definition is present, + * then update the `inputs` or `outputs` on it, depending on what was selected by `getPropertyToUpdate` + * + * + * @see InputDecorator + * @see OutputDecorator + * @see InheritenceFeature */ -const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any}) => any) => - (target: any, name: string, ...args: any[]) => { - const constructor = target.constructor; +function getOrCreateDefinitionAndUpdateMappingFor( + getPropertyToUpdate: (baseDef: {inputs?: any, outputs?: any}) => any) { + return function updateIOProp(target: any, name: string, ...args: any[]) { + const constructor = target.constructor; - if (!constructor.hasOwnProperty(NG_BASE_DEF)) { - initializeBaseDef(target); - } + let def: any = + constructor[NG_COMPONENT_DEF] || constructor[NG_DIRECTIVE_DEF] || constructor[NG_BASE_DEF]; - const baseDef = constructor.ngBaseDef; - const defProp = getProp(baseDef); + if (!def) { + initializeBaseDef(target); + def = constructor[NG_BASE_DEF]; + } + + const defProp = getPropertyToUpdate(def); + // Use of `in` because we *do* want to check the prototype chain here. + if (!(name in defProp)) { defProp[name] = args[0]; - }; + } + }; +} /** * @Annotation @@ -737,7 +761,7 @@ const updateBaseDefFromIOProp = (getProp: (baseDef: {inputs?: any, outputs?: any */ export const Input: InputDecorator = makePropDecorator( 'Input', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined, - updateBaseDefFromIOProp(baseDef => baseDef.inputs || {})); + getOrCreateDefinitionAndUpdateMappingFor(def => def.inputs || {})); /** * Type of the Output decorator / constructor function. @@ -777,7 +801,7 @@ export interface Output { bindingPropertyName?: string; } */ export const Output: OutputDecorator = makePropDecorator( 'Output', (bindingPropertyName?: string) => ({bindingPropertyName}), undefined, - updateBaseDefFromIOProp(baseDef => baseDef.outputs || {})); + getOrCreateDefinitionAndUpdateMappingFor(def => def.outputs || {})); diff --git a/packages/core/src/metadata/lifecycle_hooks.ts b/packages/core/src/metadata/lifecycle_hooks.ts index 40a261e2d6..5b8949b82f 100644 --- a/packages/core/src/metadata/lifecycle_hooks.ts +++ b/packages/core/src/metadata/lifecycle_hooks.ts @@ -5,19 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {SimpleChanges} from '../change_detection/simple_change'; -import {SimpleChange} from '../change_detection/change_detection_util'; - - -/** - * Defines an object that associates properties with - * instances of `SimpleChange`. - * - * @see `OnChanges` - * - * @publicApi - */ -export interface SimpleChanges { [propName: string]: SimpleChange; } /** * @description diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index e1288eec33..e932734c71 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -17,7 +17,7 @@ import {assertComponentType} from './assert'; import {getComponentDef} from './definition'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; import {publishDefaultGlobalUtils} from './global_utils'; -import {queueInitHooks, queueLifecycleHooks} from './hooks'; +import {registerPostOrderHooks, registerPreOrderHooks} from './hooks'; import {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions'; import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; @@ -237,10 +237,11 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef): v const rootTView = readPatchedLView(component) ![TVIEW]; const dirIndex = rootTView.data.length - 1; - queueInitHooks(dirIndex, def.onInit, def.doCheck, rootTView); + registerPreOrderHooks(dirIndex, def, rootTView); // TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on // LNode). - queueLifecycleHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode); + registerPostOrderHooks( + rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode); } /** diff --git a/packages/core/src/render3/context_discovery.ts b/packages/core/src/render3/context_discovery.ts index c866403e67..feafb9be59 100644 --- a/packages/core/src/render3/context_discovery.ts +++ b/packages/core/src/render3/context_discovery.ts @@ -12,6 +12,7 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {TNode, TNodeFlags} from './interfaces/node'; import {RElement} from './interfaces/renderer'; import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; +import {unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; @@ -257,7 +258,7 @@ function findViaDirective(lView: LView, directiveInstance: {}): number { const directiveIndexStart = tNode.directiveStart; const directiveIndexEnd = tNode.directiveEnd; for (let i = directiveIndexStart; i < directiveIndexEnd; i++) { - if (lView[i] === directiveInstance) { + if (unwrapOnChangesDirectiveWrapper(lView[i]) === directiveInstance) { return tNode.index; } } diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index c0e8b5ac65..8b0d755a5d 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -202,7 +202,7 @@ export function defineComponent(componentDefinition: { /** * A list of optional features to apply. * - * See: {@link NgOnChangesFeature}, {@link ProvidersFeature} + * See: {@link ProvidersFeature} */ features?: ComponentDefFeature[]; @@ -265,6 +265,7 @@ export function defineComponent(componentDefinition: { inputs: null !, // assigned in noSideEffects outputs: null !, // assigned in noSideEffects exportAs: componentDefinition.exportAs || null, + onChanges: typePrototype.ngOnChanges || null, onInit: typePrototype.ngOnInit || null, doCheck: typePrototype.ngDoCheck || null, afterContentInit: typePrototype.ngAfterContentInit || null, @@ -583,7 +584,7 @@ export const defineDirective = defineComponent as any as(directiveDefinition: /** * A list of optional features to apply. * - * See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature} + * See: {@link ProvidersFeature}, {@link InheritDefinitionFeature} */ features?: DirectiveDefFeature[]; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 0e409af616..9d210fd34b 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -20,6 +20,7 @@ import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInject import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node'; import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes} from './node_assert'; +import {unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util'; @@ -514,6 +515,8 @@ export function getNodeInjectable( factory.resolving = false; setTNodeAndViewData(savePreviousOrParentTNode, saveLView); } + } else { + value = unwrapOnChangesDirectiveWrapper(value); } return value; } diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 4f33b971fa..b07cfaa857 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -35,8 +35,6 @@ function getSuperType(type: Type): Type& export function InheritDefinitionFeature(definition: DirectiveDef| ComponentDef): void { let superType = getSuperType(definition.type); - debugger; - while (superType) { let superDef: DirectiveDef|ComponentDef|undefined = undefined; if (isComponentDef(definition)) { @@ -62,7 +60,6 @@ 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); @@ -127,7 +124,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef| Componen } } - // Merge inputs and outputs fillProperties(definition.inputs, superDef.inputs); fillProperties(definition.declaredInputs, superDef.declaredInputs); @@ -143,6 +139,7 @@ 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; diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts deleted file mode 100644 index 2b2d5468bc..0000000000 --- a/packages/core/src/render3/features/ng_onchanges_feature.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {SimpleChange} from '../../change_detection/change_detection_util'; -import {OnChanges, SimpleChanges} from '../../metadata/lifecycle_hooks'; -import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition'; - -const PRIVATE_PREFIX = '__ngOnChanges_'; - -type OnChangesExpando = OnChanges & { - __ngOnChanges_: SimpleChanges|null|undefined; - // tslint:disable-next-line:no-any Can hold any value - [key: string]: any; -}; - -/** - * The NgOnChangesFeature decorates a component with support for the ngOnChanges - * lifecycle hook, so it should be included in any component that implements - * that hook. - * - * If the component or directive uses inheritance, the NgOnChangesFeature MUST - * be included as a feature AFTER {@link InheritDefinitionFeature}, otherwise - * inherited properties will not be propagated to the ngOnChanges lifecycle - * hook. - * - * Example usage: - * - * ``` - * static ngComponentDef = defineComponent({ - * ... - * inputs: {name: 'publicName'}, - * features: [NgOnChangesFeature] - * }); - * ``` - */ -export function NgOnChangesFeature(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 5cdd95c648..b64f9a2146 100644 --- a/packages/core/src/render3/hooks.ts +++ b/packages/core/src/render3/hooks.ts @@ -6,92 +6,117 @@ * found in the LICENSE file at https://angular.io/license */ +import {SimpleChanges} from '../change_detection/simple_change'; import {assertEqual} from '../util/assert'; + import {DirectiveDef} from './interfaces/definition'; import {TNode} from './interfaces/node'; import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view'; +import {OnChangesDirectiveWrapper, unwrapOnChangesDirectiveWrapper} from './onchanges_util'; /** - * If this is the first template pass, any ngOnInit or ngDoCheck hooks will be queued into - * TView.initHooks during directiveCreate. + * Adds all directive lifecycle hooks from the given `DirectiveDef` to the given `TView`. * - * The directive index and hook type are encoded into one number (1st bit: type, remaining bits: - * directive index), then saved in the even indices of the initHooks array. The odd indices - * hold the hook functions themselves. + * Must be run *only* on the first template pass. * - * @param index The index of the directive in LView - * @param hooks The static hooks map on the directive def + * The TView's hooks arrays are arranged in alternating pairs of directiveIndex and hookFunction, + * i.e.: `[directiveIndexA, hookFunctionA, directiveIndexB, hookFunctionB, ...]`. For `OnChanges` + * hooks, the `directiveIndex` will be *negative*, signaling {@link callHooks} that the + * `hookFunction` must be passed the the appropriate {@link SimpleChanges} object. + * + * @param directiveIndex The index of the directive in LView + * @param directiveDef The definition containing the hooks to setup in tView * @param tView The current TView */ -export function queueInitHooks( - index: number, onInit: (() => void) | null, doCheck: (() => void) | null, tView: TView): void { +export function registerPreOrderHooks( + directiveIndex: number, directiveDef: DirectiveDef, tView: TView): void { ngDevMode && assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass'); + + const {onChanges, onInit, doCheck} = directiveDef; + + if (onChanges) { + (tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onChanges); + (tView.checkHooks || (tView.checkHooks = [])).push(-directiveIndex, onChanges); + } + if (onInit) { - (tView.initHooks || (tView.initHooks = [])).push(index, onInit); + (tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit); } if (doCheck) { - (tView.initHooks || (tView.initHooks = [])).push(index, doCheck); - (tView.checkHooks || (tView.checkHooks = [])).push(index, doCheck); + (tView.initHooks || (tView.initHooks = [])).push(directiveIndex, doCheck); + (tView.checkHooks || (tView.checkHooks = [])).push(directiveIndex, doCheck); } } /** - * Loops through the directives on a node and queues all their hooks except ngOnInit - * and ngDoCheck, which are queued separately in directiveCreate. + * + * Loops through the directives on the provided `tNode` and queues hooks to be + * run that are not initialization hooks. + * + * Should be executed during `elementEnd()` and similar to + * preserve hook execution order. Content, view, and destroy hooks for projected + * components and directives must be called *before* their hosts. + * + * Sets up the content, view, and destroy hooks on the provided `tView` such that + * they're added in alternating pairs of directiveIndex and hookFunction, + * i.e.: `[directiveIndexA, hookFunctionA, directiveIndexB, hookFunctionB, ...]` + * + * NOTE: This does not set up `onChanges`, `onInit` or `doCheck`, those are set up + * separately at `elementStart`. + * + * @param tView The current TView + * @param tNode The TNode whose directives are to be searched for hooks to queue */ -export function queueLifecycleHooks(tView: TView, tNode: TNode): void { +export function registerPostOrderHooks(tView: TView, tNode: TNode): void { if (tView.firstTemplatePass) { // It's necessary to loop through the directives at elementEnd() (rather than processing in // directiveCreate) so we can preserve the current hook order. Content, view, and destroy // hooks for projected components and directives must be called *before* their hosts. for (let i = tNode.directiveStart, end = tNode.directiveEnd; i < end; i++) { - const def = tView.data[i] as DirectiveDef; - queueContentHooks(def, tView, i); - queueViewHooks(def, tView, i); - queueDestroyHooks(def, tView, i); + const directiveDef = tView.data[i] as DirectiveDef; + if (directiveDef.afterContentInit) { + (tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentInit); + } + + if (directiveDef.afterContentChecked) { + (tView.contentHooks || (tView.contentHooks = [])).push(i, directiveDef.afterContentChecked); + (tView.contentCheckHooks || (tView.contentCheckHooks = [ + ])).push(i, directiveDef.afterContentChecked); + } + + if (directiveDef.afterViewInit) { + (tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewInit); + } + + if (directiveDef.afterViewChecked) { + (tView.viewHooks || (tView.viewHooks = [])).push(i, directiveDef.afterViewChecked); + (tView.viewCheckHooks || (tView.viewCheckHooks = [ + ])).push(i, directiveDef.afterViewChecked); + } + + if (directiveDef.onDestroy != null) { + (tView.destroyHooks || (tView.destroyHooks = [])).push(i, directiveDef.onDestroy); + } } } } -/** Queues afterContentInit and afterContentChecked hooks on TView */ -function queueContentHooks(def: DirectiveDef, tView: TView, i: number): void { - if (def.afterContentInit) { - (tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentInit); - } - - if (def.afterContentChecked) { - (tView.contentHooks || (tView.contentHooks = [])).push(i, def.afterContentChecked); - (tView.contentCheckHooks || (tView.contentCheckHooks = [])).push(i, def.afterContentChecked); - } -} - -/** Queues afterViewInit and afterViewChecked hooks on TView */ -function queueViewHooks(def: DirectiveDef, tView: TView, i: number): void { - if (def.afterViewInit) { - (tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewInit); - } - - if (def.afterViewChecked) { - (tView.viewHooks || (tView.viewHooks = [])).push(i, def.afterViewChecked); - (tView.viewCheckHooks || (tView.viewCheckHooks = [])).push(i, def.afterViewChecked); - } -} - -/** Queues onDestroy hooks on TView */ -function queueDestroyHooks(def: DirectiveDef, tView: TView, i: number): void { - if (def.onDestroy != null) { - (tView.destroyHooks || (tView.destroyHooks = [])).push(i, def.onDestroy); - } -} - /** - * Calls onInit and doCheck calls if they haven't already been called. + * Executes necessary hooks at the start of executing a template. * - * @param currentView The current view + * Executes hooks that are to be run during the initialization of a directive such + * as `onChanges`, `onInit`, and `doCheck`. + * + * Has the side effect of updating the RunInit flag in `lView` to be `0`, so that + * this isn't run a second time. + * + * @param lView The current view + * @param tView Static data for the view containing the hooks to be executed + * @param creationMode Whether or not we're in creation mode. */ export function executeInitHooks( currentView: LView, tView: TView, checkNoChangesMode: boolean): void { @@ -102,16 +127,20 @@ export function executeInitHooks( } /** - * Iterates over afterViewInit and afterViewChecked functions and calls them. + * Executes hooks against the given `LView` based off of whether or not + * This is the first pass. * - * @param currentView The current view + * @param lView The view instance data to run the hooks against + * @param firstPassHooks An array of hooks to run if we're in the first view pass + * @param checkHooks An Array of hooks to run if we're not in the first view pass. + * @param checkNoChangesMode Whether or not we're in no changes mode. */ export function executeHooks( - currentView: LView, allHooks: HookData | null, checkHooks: HookData | null, + currentView: LView, firstPassHooks: HookData | null, checkHooks: HookData | null, checkNoChangesMode: boolean): void { if (checkNoChangesMode) return; - const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? allHooks : checkHooks; + const hooksToCall = currentView[FLAGS] & LViewFlags.FirstLViewPass ? firstPassHooks : checkHooks; if (hooksToCall) { callHooks(currentView, hooksToCall); } @@ -119,13 +148,31 @@ export function executeHooks( /** * Calls lifecycle hooks with their contexts, skipping init hooks if it's not - * the first LView pass. + * the first LView pass, and skipping onChanges hooks if there are no changes present. * * @param currentView The current view * @param arr The array in which the hooks are found */ -export function callHooks(currentView: any[], arr: HookData): void { +export function callHooks(currentView: LView, arr: HookData): void { for (let i = 0; i < arr.length; i += 2) { - (arr[i + 1] as() => void).call(currentView[arr[i] as number]); + const directiveIndex = arr[i] as number; + const hook = arr[i + 1] as((() => void) | ((changes: SimpleChanges) => void)); + // Negative indices signal that we're dealing with an `onChanges` hook. + const isOnChangesHook = directiveIndex < 0; + const directiveOrWrappedDirective = + currentView[isOnChangesHook ? -directiveIndex : directiveIndex]; + const directive = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective); + + if (isOnChangesHook) { + const onChanges: OnChangesDirectiveWrapper = directiveOrWrappedDirective; + const changes = onChanges.changes; + if (changes) { + onChanges.previous = changes; + onChanges.changes = null; + hook.call(onChanges.instance, changes); + } + } else { + hook.call(directive); + } } } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 6d1ac2ea6a..30004f9d08 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -9,7 +9,6 @@ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component' import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; import {getComponent, getHostElement, getRenderedText} from './discovery_utils'; import {InheritDefinitionFeature} from './features/inherit_definition_feature'; -import {NgOnChangesFeature} from './features/ng_onchanges_feature'; import {ProvidersFeature} from './features/providers_feature'; import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition'; @@ -158,7 +157,6 @@ export { DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, - NgOnChangesFeature, InheritDefinitionFeature, ProvidersFeature, PipeDef, diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 2240f90158..688f5c92d1 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -22,7 +22,7 @@ import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4} from import {attachPatchData, getComponentViewByInstance} from './context_discovery'; import {diPublicInInjector, getNodeInjectable, getOrCreateInjectable, getOrCreateNodeInjectorForNode, injectAttributeImpl} from './di'; import {throwMultipleComponentError} from './errors'; -import {executeHooks, executeInitHooks, queueInitHooks, queueLifecycleHooks} from './hooks'; +import {executeHooks, executeInitHooks, registerPostOrderHooks, registerPreOrderHooks} from './hooks'; import {ACTIVE_INDEX, LContainer, VIEWS} from './interfaces/container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags} from './interfaces/definition'; import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from './interfaces/injector'; @@ -36,6 +36,7 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLA import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; +import {OnChangesDirectiveWrapper, isOnChangesDirectiveWrapper, recordChange, unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; @@ -120,7 +121,7 @@ export function setHostBindings(tView: TView, viewData: LView): void { if (instruction !== null) { viewData[BINDING_INDEX] = bindingRootIndex; instruction( - RenderFlags.Update, readElementValue(viewData[currentDirectiveIndex]), + RenderFlags.Update, unwrapOnChangesDirectiveWrapper(viewData[currentDirectiveIndex]), currentElementIndex); } currentDirectiveIndex++; @@ -522,7 +523,7 @@ export function elementContainerEnd(): void { lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementContainerNode); } - queueLifecycleHooks(tView, previousOrParentTNode); + registerPostOrderHooks(tView, previousOrParentTNode); } /** @@ -719,6 +720,7 @@ export function createTView( expandoStartIndex: initialViewLength, expandoInstructions: null, firstTemplatePass: true, + changesHooks: null, initHooks: null, checkHooks: null, contentHooks: null, @@ -883,9 +885,14 @@ export function listener( const propsLength = props.length; if (propsLength) { const lCleanup = getCleanup(lView); - for (let i = 0; i < propsLength; i += 2) { - ngDevMode && assertDataInRange(lView, props[i] as number); - const subscription = lView[props[i] as number][props[i + 1]].subscribe(listenerFn); + // Subscribe to listeners for each output, and setup clean up for each. + for (let i = 0; i < propsLength;) { + const directiveIndex = props[i++] as number; + const minifiedName = props[i++] as string; + const declaredName = props[i++] as string; + ngDevMode && assertDataInRange(lView, directiveIndex as number); + const directive = unwrapOnChangesDirectiveWrapper(lView[directiveIndex]); + const subscription = directive[minifiedName].subscribe(listenerFn); const idx = lCleanup.length; lCleanup.push(listenerFn, subscription); tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1)); @@ -943,7 +950,7 @@ export function elementEnd(): void { lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TElementNode); } - queueLifecycleHooks(getLView()[TVIEW], previousOrParentTNode); + registerPostOrderHooks(getLView()[TVIEW], previousOrParentTNode); decreaseElementDepthCount(); // this is fired at the end of elementEnd because ALL of the stylingBindings code @@ -952,7 +959,7 @@ export function elementEnd(): void { if (hasClassInput(previousOrParentTNode)) { const stylingContext = getStylingContext(previousOrParentTNode.index, lView); setInputsForProperty( - lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext)); + lView, previousOrParentTNode.inputs !, 'class', getInitialClassNameValue(stylingContext)); } } @@ -1051,7 +1058,7 @@ function elementPropertyInternal( let dataValue: PropertyAliasValue|undefined; if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) && (dataValue = inputData[propName])) { - setInputsForProperty(lView, dataValue, value); + setInputsForProperty(lView, inputData, propName, value); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (ngDevMode) { if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) { @@ -1121,22 +1128,35 @@ export function createTNode( } /** - * Given a list of directive indices and minified input names, sets the - * input properties on the corresponding directives. + * Set the inputs of directives at the current node to corresponding value. + * + * @param lView the `LView` which contains the directives. + * @param inputAliases mapping between the public "input" name and privately-known, + * possibly minified, property names to write to. + * @param publicName public binding name. (This is the `
`) + * @param value Value to set. */ -function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void { - for (let i = 0; i < inputs.length; i += 2) { - ngDevMode && assertDataInRange(lView, inputs[i] as number); - lView[inputs[i] as number][inputs[i + 1]] = value; +function setInputsForProperty( + lView: LView, inputAliases: PropertyAliases, publicName: string, value: any): void { + const inputs = inputAliases[publicName]; + for (let i = 0; i < inputs.length;) { + const directiveIndex = inputs[i++] as number; + const privateName = inputs[i++] as string; + const declaredName = inputs[i++] as string; + ngDevMode && assertDataInRange(lView, directiveIndex); + recordChangeAndUpdateProperty(lView[directiveIndex], declaredName, privateName, value); } } function setNgReflectProperties( lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, value: any) { - for (let i = 0; i < inputs.length; i += 2) { + for (let i = 0; i < inputs.length;) { + const directiveIndex = inputs[i++] as number; + const privateName = inputs[i++] as string; + const declaredName = inputs[i++] as string; const renderer = lView[RENDERER]; - const attrName = normalizeDebugBindingName(inputs[i + 1] as string); + const attrName = normalizeDebugBindingName(privateName); const debugValue = normalizeDebugBindingValue(value); if (type === TNodeType.Element) { isProceduralRenderer(renderer) ? @@ -1172,15 +1192,20 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro for (let i = start; i < end; i++) { const directiveDef = defs[i] as DirectiveDef; - const propertyAliasMap: {[publicName: string]: string} = + const publicToMinifiedNames: {[publicName: string]: string} = isInput ? directiveDef.inputs : directiveDef.outputs; - for (let publicName in propertyAliasMap) { - if (propertyAliasMap.hasOwnProperty(publicName)) { + const publicToDeclaredNames: {[publicName: string]: string}|null = + isInput ? directiveDef.declaredInputs : null; + for (let publicName in publicToMinifiedNames) { + if (publicToMinifiedNames.hasOwnProperty(publicName)) { propStore = propStore || {}; - const internalName = propertyAliasMap[publicName]; - const hasProperty = propStore.hasOwnProperty(publicName); - hasProperty ? propStore[publicName].push(i, internalName) : - (propStore[publicName] = [i, internalName]); + const minifiedName = publicToMinifiedNames[publicName]; + const declaredName = + publicToDeclaredNames ? publicToDeclaredNames[publicName] : minifiedName; + const aliases: PropertyAliasValue = propStore.hasOwnProperty(publicName) ? + propStore[publicName] : + propStore[publicName] = []; + aliases.push(i, minifiedName, declaredName); } } } @@ -1382,7 +1407,7 @@ export function elementStylingMap( const initialClasses = getInitialClassNameValue(stylingContext); const classInputVal = (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); - setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal); + setInputsForProperty(lView, tNode.inputs !, 'class', classInputVal); } else { updateStylingMap(stylingContext, classes, styles); } @@ -1493,7 +1518,7 @@ function resolveDirectives( // Init hooks are queued now so ngOnInit is called in host components before // any projected components. - queueInitHooks(directiveDefIdx, def.onInit, def.doCheck, tView); + registerPreOrderHooks(directiveDefIdx, def, tView); } } if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap); @@ -1515,6 +1540,7 @@ function instantiateAllDirectives(tView: TView, lView: LView, tNode: TNode) { addComponentLogic(lView, tNode, def as ComponentDef); } const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode); + postProcessDirective(lView, directive, def, i); } } @@ -1526,7 +1552,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const firstTemplatePass = getFirstTemplatePass(); for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; - const directive = viewData[i]; + const directive = unwrapOnChangesDirectiveWrapper(viewData[i]); if (def.hostBindings) { const previousExpandoLength = expando.length; setCurrentDirectiveDef(def); @@ -1583,12 +1609,17 @@ function prefillHostVars(tView: TView, lView: LView, totalHostVars: number): voi * Process a directive on the current node after its creation. */ function postProcessDirective( - viewData: LView, directive: T, def: DirectiveDef, directiveDefIdx: number): void { + 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]); + } + const previousOrParentTNode = getPreviousOrParentTNode(); - postProcessBaseDirective(viewData, previousOrParentTNode, directive, def); + postProcessBaseDirective(lView, previousOrParentTNode, directive, def); ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode'); if (previousOrParentTNode && previousOrParentTNode.attrs) { - setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode); + setInputsFromAttrs(lView, directiveDefIdx, def, previousOrParentTNode); } if (def.contentQueries) { @@ -1596,7 +1627,7 @@ function postProcessDirective( } if (isComponentDef(def)) { - const componentView = getComponentViewByIndex(previousOrParentTNode.index, viewData); + const componentView = getComponentViewByIndex(previousOrParentTNode.index, lView); componentView[CONTEXT] = directive; } } @@ -1794,20 +1825,53 @@ function addComponentLogic( * @param tNode The static data for this node */ function setInputsFromAttrs( - directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void { + lView: LView, directiveIndex: number, def: DirectiveDef, tNode: TNode): void { let initialInputData = tNode.initialInputs as InitialInputData | undefined; if (initialInputData === undefined || directiveIndex >= initialInputData.length) { - initialInputData = generateInitialInputs(directiveIndex, inputs, tNode); + initialInputData = generateInitialInputs(directiveIndex, def, tNode); } const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; if (initialInputs) { - for (let i = 0; i < initialInputs.length; i += 2) { - (instance as any)[initialInputs[i]] = initialInputs[i + 1]; + const directiveOrWrappedDirective = lView[directiveIndex]; + + for (let i = 0; i < initialInputs.length;) { + const privateName = initialInputs[i++]; + const declaredName = initialInputs[i++]; + const attrValue = initialInputs[i++]; + recordChangeAndUpdateProperty( + directiveOrWrappedDirective, declaredName, privateName, attrValue); } } } +/** + * Checks to see if the instanced passed as `directiveOrWrappedDirective` is wrapped in {@link + * OnChangesDirectiveWrapper} or not. + * If it is, it will update the related {@link SimpleChanges} object with the change to signal + * `ngOnChanges` hook + * should fire, then it will unwrap the instance. After that, it will set the property with the key + * provided + * in `privateName` on the instance with the passed value. + * @param directiveOrWrappedDirective The directive instance or a directive instance wrapped in + * {@link OnChangesDirectiveWrapper} + * @param declaredName The original, declared name of the property to update. + * @param privateName The private, possibly minified name of the property to update. + * @param value The value to update the property with. + */ +function recordChangeAndUpdateProperty( + 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. @@ -1824,7 +1888,7 @@ function setInputsFromAttrs( * @param tNode The static data on this node */ function generateInitialInputs( - directiveIndex: number, inputs: {[key: string]: string}, tNode: TNode): InitialInputData { + directiveIndex: number, directiveDef: DirectiveDef, tNode: TNode): InitialInputData { const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []); initialInputData[directiveIndex] = null; @@ -1832,19 +1896,23 @@ function generateInitialInputs( let i = 0; while (i < attrs.length) { const attrName = attrs[i]; - if (attrName === AttributeMarker.SelectOnly) break; + // If we hit Select-Only, Classes or Styles, we're done anyway. None of those are valid inputs. + if (attrName === AttributeMarker.SelectOnly || attrName === AttributeMarker.Classes || + attrName === AttributeMarker.Styles) + break; if (attrName === AttributeMarker.NamespaceURI) { // We do not allow inputs on namespaced attributes. i += 4; continue; } - const minifiedInputName = inputs[attrName]; + const privateName = directiveDef.inputs[attrName]; + const declaredName = directiveDef.declaredInputs[attrName]; const attrValue = attrs[i + 1]; - if (minifiedInputName !== undefined) { + if (privateName !== undefined) { const inputsToStore: InitialInputs = initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); - inputsToStore.push(minifiedInputName, attrValue as string); + inputsToStore.push(privateName, declaredName, attrValue as string); } i += 2; @@ -1919,7 +1987,7 @@ export function template( if (currentQueries) { lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode); } - queueLifecycleHooks(tView, tNode); + registerPostOrderHooks(tView, tNode); setIsParent(false); } @@ -2881,7 +2949,7 @@ export function registerContentQuery( export const CLEAN_PROMISE = _CLEAN_PROMISE; -function initializeTNodeInputs(tNode: TNode | null) { +function initializeTNodeInputs(tNode: TNode | null): PropertyAliases|null { // If tNode.inputs is undefined, a listener has created outputs, but inputs haven't // yet been checked. if (tNode) { diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 92e0a05dcf..3a7995a77b 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {ViewEncapsulation} from '../../core'; +import {SimpleChanges, ViewEncapsulation} from '../../core'; import {Type} from '../../interface/type'; + import {CssSelectorList} from './projection'; @@ -150,6 +151,7 @@ export interface DirectiveDef 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 dcf5468846..07a6ed6971 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -464,10 +464,12 @@ export type PropertyAliases = { /** * Store the runtime input or output names for all the directives. * - * - Even indices: directive index - * - Odd indices: minified / internal name + * Values are stored in triplets: + * - i + 0: directive index + * - i + 1: minified / internal name + * - i + 2: declared name * - * e.g. [0, 'change-minified'] + * e.g. [0, 'minifiedName', 'declaredPropertyName'] */ export type PropertyAliasValue = (number | string)[]; @@ -495,10 +497,12 @@ export type InitialInputData = (InitialInputs | null)[]; * Used by InitialInputData to store input properties * that should be set once from attributes. * - * Even indices: minified/internal input name - * Odd indices: initial value + * The inputs come in triplets of: + * i + 0: minified/internal input name + * i + 1: declared input name (needed for OnChanges) + * i + 2: initial value * - * e.g. ['role-min', 'button'] + * e.g. ['minifiedName', 'declaredName', 'value'] */ export type InitialInputs = string[]; diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 88ab31b085..fea5012d53 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {SimpleChanges} from '../../change_detection/simple_change'; import {InjectionToken} from '../../di/injection_token'; import {Injector} from '../../di/injector'; import {Type} from '../../interface/type'; import {QueryList} from '../../linker'; import {Sanitizer} from '../../sanitization/security'; - import {LContainer} from './container'; import {ComponentDef, ComponentQuery, ComponentTemplate, DirectiveDef, DirectiveDefList, HostBindingsFunction, PipeDef, PipeDefList} from './definition'; import {I18nUpdateOpCodes, TI18n} from './i18n'; @@ -22,7 +22,6 @@ import {RElement, Renderer3, RendererFactory3} from './renderer'; import {StylingContext} from './styling'; - // Below are constants for LView indices to help us look up LView members // without having to remember the specific indices. // Uglify will inline these when minifying so there shouldn't be a cost. @@ -533,7 +532,7 @@ export interface RootContext { * Even indices: Directive index * Odd indices: Hook function */ -export type HookData = (number | (() => void))[]; +export type HookData = (number | (() => void) | ((changes: SimpleChanges) => void))[]; /** * Static data that corresponds to the instance-specific data array on an LView. diff --git a/packages/core/src/render3/jit/compiler_facade_interface.ts b/packages/core/src/render3/jit/compiler_facade_interface.ts index 8c424454b3..edcaddd769 100644 --- a/packages/core/src/render3/jit/compiler_facade_interface.ts +++ b/packages/core/src/render3/jit/compiler_facade_interface.ts @@ -115,7 +115,6 @@ export interface R3DirectiveMetadataFacade { queries: R3QueryMetadataFacade[]; host: {[key: string]: string}; propMetadata: {[key: string]: any[]}; - lifecycle: {usesOnChanges: boolean;}; inputs: string[]; outputs: string[]; usesInheritance: boolean; diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 26e4948689..2bc3cbb967 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -145,9 +145,6 @@ 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 0eccfc4b75..9951233f18 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -31,7 +31,6 @@ export const angularCoreEnv: {[name: string]: Function} = { 'inject': inject, 'ɵinjectAttribute': r3.injectAttribute, 'ɵtemplateRefExtractor': r3.templateRefExtractor, - 'ɵNgOnChangesFeature': r3.NgOnChangesFeature, 'ɵProvidersFeature': r3.ProvidersFeature, 'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature, 'ɵelementAttribute': r3.elementAttribute, diff --git a/packages/core/src/render3/onchanges_util.ts b/packages/core/src/render3/onchanges_util.ts new file mode 100644 index 0000000000..df44196a1c --- /dev/null +++ b/packages/core/src/render3/onchanges_util.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {SimpleChange, SimpleChanges} from '../change_detection/simple_change'; + + +type Constructor = 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 583f30e517..77c5c5bb41 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -17,6 +17,7 @@ import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './inte import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer'; import {StylingContext} from './interfaces/styling'; import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view'; +import {isOnChangesDirectiveWrapper} from './onchanges_util'; /** @@ -67,9 +68,14 @@ export function flatten(list: any[]): any[] { /** Retrieves a value from any `LView` or `TData`. */ export function loadInternal(view: LView | TData, index: number): T { ngDevMode && assertDataInRange(view, index + HEADER_OFFSET); - return view[index + HEADER_OFFSET]; + const record = view[index + HEADER_OFFSET]; + // If we're storing an array because of a directive or component with ngOnChanges, + // return the directive or component instance. + return isOnChangesDirectiveWrapper(record) ? record.instance : record; } + + /** * Takes the value of a slot in `LView` and returns the element node. * @@ -288,4 +294,4 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) { export function resolveBody(element: RElement & {ownerDocument: Document}) { return {name: 'body', target: element.ownerDocument.body}; -} \ 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 b88ec8cb94..cc6794e23a 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -11,10 +11,11 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; -import {checkNoChanges, checkNoChangesInRootView, checkView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; +import {checkNoChanges, checkNoChangesInRootView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; import {TNode, TNodeType, TViewNode} from './interfaces/node'; -import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT, RENDERER_FACTORY} from './interfaces/view'; +import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT} from './interfaces/view'; import {destroyLView} from './node_manipulation'; +import {unwrapOnChangesDirectiveWrapper} from './onchanges_util'; import {getNativeByTNode} from './util'; @@ -271,7 +272,8 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int } private _lookUpContext(): T { - return this._context = this._lView[PARENT] ![this._componentIndex] as T; + return this._context = + unwrapOnChangesDirectiveWrapper(this._lView[PARENT] ![this._componentIndex] as T); } } 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 c1a61277f8..e1caf4b8fb 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -86,24 +86,21 @@ { "name": "NO_PARENT_INJECTOR" }, - { - "name": "NgOnChangesFeature" - }, { "name": "NodeInjectorFactory" }, { "name": "ObjectUnsubscribedErrorImpl" }, + { + "name": "OnChangesDirectiveWrapper" + }, { "name": "PARENT" }, { "name": "PARENT_INJECTOR" }, - { - "name": "PRIVATE_PREFIX" - }, { "name": "RENDERER" }, @@ -113,9 +110,6 @@ { "name": "SANITIZER" }, - { - "name": "SimpleChange" - }, { "name": "TVIEW" }, @@ -335,9 +329,15 @@ { "name": "isCreationMode" }, + { + "name": "isExactInstanceOf" + }, { "name": "isFactory" }, + { + "name": "isOnChangesDirectiveWrapper" + }, { "name": "isProceduralRenderer" }, @@ -365,9 +365,6 @@ { "name": "noSideEffects" }, - { - "name": "onChangesWrapper" - }, { "name": "postProcessBaseDirective" }, @@ -449,6 +446,9 @@ { "name": "tickRootContext" }, + { + "name": "unwrapOnChangesDirectiveWrapper" + }, { "name": "updateViewQuery" }, diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json index d3e45977b3..f15c3c359d 100644 --- a/packages/core/test/bundling/injection/bundle.golden_symbols.json +++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json @@ -35,9 +35,6 @@ { "name": "NULL_INJECTOR$2" }, - { - "name": "NgOnChangesFeature" - }, { "name": "NullInjector" }, @@ -50,9 +47,6 @@ { "name": "PARAMETERS" }, - { - "name": "PRIVATE_PREFIX" - }, { "name": "R3Injector" }, @@ -62,9 +56,6 @@ { "name": "Self" }, - { - "name": "SimpleChange" - }, { "name": "SkipSelf" }, @@ -164,9 +155,6 @@ { "name": "makeRecord" }, - { - "name": "onChangesWrapper" - }, { "name": "providerToFactory" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index c1e32f2570..328fce36ab 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -143,9 +143,6 @@ { "name": "NgModuleRef" }, - { - "name": "NgOnChangesFeature" - }, { "name": "NodeInjector" }, @@ -155,6 +152,9 @@ { "name": "ObjectUnsubscribedErrorImpl" }, + { + "name": "OnChangesDirectiveWrapper" + }, { "name": "Optional" }, @@ -167,9 +167,6 @@ { "name": "PARENT_INJECTOR" }, - { - "name": "PRIVATE_PREFIX" - }, { "name": "QUERIES" }, @@ -902,6 +899,9 @@ { "name": "isDirty" }, + { + "name": "isExactInstanceOf" + }, { "name": "isFactory" }, @@ -920,6 +920,9 @@ { "name": "isNodeMatchingSelectorList" }, + { + "name": "isOnChangesDirectiveWrapper" + }, { "name": "isPositive" }, @@ -998,9 +1001,6 @@ { "name": "noSideEffects" }, - { - "name": "onChangesWrapper" - }, { "name": "pointers" }, @@ -1019,21 +1019,6 @@ { "name": "queueComponentIndexForCheck" }, - { - "name": "queueContentHooks" - }, - { - "name": "queueDestroyHooks" - }, - { - "name": "queueInitHooks" - }, - { - "name": "queueLifecycleHooks" - }, - { - "name": "queueViewHooks" - }, { "name": "readElementValue" }, @@ -1043,6 +1028,12 @@ { "name": "readPatchedLView" }, + { + "name": "recordChange" + }, + { + "name": "recordChangeAndUpdateProperty" + }, { "name": "reference" }, @@ -1058,6 +1049,12 @@ { "name": "refreshDynamicEmbeddedViews" }, + { + "name": "registerPostOrderHooks" + }, + { + "name": "registerPreOrderHooks" + }, { "name": "removeListeners" }, @@ -1214,6 +1211,9 @@ { "name": "trackByIdentity" }, + { + "name": "unwrapOnChangesDirectiveWrapper" + }, { "name": "updateClassProp" }, diff --git a/packages/core/test/render3/common_with_def.ts b/packages/core/test/render3/common_with_def.ts index 73cbda6bf8..32e7bd1875 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, NgOnChangesFeature, defineDirective, directiveInject} from '../../src/render3/index'; +import {DirectiveType, defineDirective, directiveInject} from '../../src/render3/index'; export const NgForOf: DirectiveType> = NgForOfDef as any; export const NgIf: DirectiveType = NgIfDef as any; @@ -40,7 +40,6 @@ NgForOf.ngDirectiveDef = defineDirective({ type: NgTemplateOutletDef, selectors: [['', 'ngTemplateOutlet', '']], factory: () => new NgTemplateOutletDef(directiveInject(ViewContainerRef as any)), - features: [NgOnChangesFeature], inputs: {ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'} }); diff --git a/packages/core/test/render3/host_binding_spec.ts b/packages/core/test/render3/host_binding_spec.ts index 424456d816..f2c7f8fa3a 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} from '@angular/core'; -import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature} from '../../src/render3/index'; +import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature} from '../../src/render3/index'; import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions'; import {query, queryRefresh} from '../../src/render3/query'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -357,7 +357,6 @@ describe('host bindings', () => { template: (rf: RenderFlags, ctx: InitHookComp) => {}, consts: 0, vars: 0, - features: [NgOnChangesFeature], hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => { if (rf & RenderFlags.Create) { allocHostVars(1); diff --git a/packages/core/test/render3/inherit_definition_feature_spec.ts b/packages/core/test/render3/inherit_definition_feature_spec.ts index d19b58d8eb..d2a059e8d1 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, NgOnChangesFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty, load} from '../../src/render3/index'; +import {ComponentDef, DirectiveDef, InheritDefinitionFeature, ProvidersFeature, RenderFlags, allocHostVars, bind, defineBase, defineComponent, defineDirective, directiveInject, element, elementProperty} from '../../src/render3/index'; import {ComponentFixture, createComponent} from './render_util'; @@ -501,8 +501,7 @@ describe('InheritDefinitionFeature', () => { type: SuperDirective, selectors: [['', 'superDir', '']], factory: () => new SuperDirective(), - features: [NgOnChangesFeature], - inputs: {someInput: 'someInput'} + inputs: {someInput: 'someInput'}, }); } @@ -519,6 +518,9 @@ describe('InheritDefinitionFeature', () => { if (rf & RenderFlags.Create) { element(0, 'div', ['subDir', '']); } + if (rf & RenderFlags.Update) { + elementProperty(0, 'someInput', bind('Weee')); + } }, 1, 0, [SubDirective]); const fixture = new ComponentFixture(App); diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index b501cb0847..da7181479c 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ComponentFactoryResolver, OnDestroy, SimpleChanges, ViewContainerRef} from '../../src/core'; -import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index'; +import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core'; +import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, template, text} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -1941,10 +1941,15 @@ describe('lifecycles', () => { }); describe('onChanges', () => { - let events: string[]; + let events: ({type: string, name: string, [key: string]: any})[]; beforeEach(() => { events = []; }); + /** + *
+ * + *
+ */ const Comp = createOnChangesComponent('comp', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { projectionDef(); @@ -1953,15 +1958,20 @@ describe('lifecycles', () => { elementEnd(); } }, 2); + + /** + * + */ const Parent = createOnChangesComponent('parent', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { element(0, 'comp'); } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(ctx.a)); - elementProperty(0, 'publicName', bind(ctx.b)); + elementProperty(0, 'publicVal2', bind(ctx.b)); } }, 1, 2, [Comp]); + const ProjectedComp = createOnChangesComponent('projected', (rf: RenderFlags, ctx: any) => { if (rf & RenderFlags.Create) { text(0, 'content'); @@ -1974,22 +1984,28 @@ describe('lifecycles', () => { directives: any[] = []) { return class Component { // @Input() val1: string; - // @Input('publicName') val2: string; + // @Input('publicVal2') val2: string; a: string = 'wasVal1BeforeMinification'; b: string = 'wasVal2BeforeMinification'; - ngOnChanges(simpleChanges: SimpleChanges) { - events.push( - `comp=${name} val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`); + ngOnChanges(changes: SimpleChanges) { + if (changes.a && this.a !== changes.a.currentValue) { + throw Error( + `SimpleChanges invalid expected this.a ${this.a} to equal currentValue ${changes.a.currentValue}`); + } + if (changes.b && this.b !== changes.b.currentValue) { + throw Error( + `SimpleChanges invalid expected this.b ${this.b} to equal currentValue ${changes.b.currentValue}`); + } + events.push({type: 'onChanges', name: 'comp - ' + name, changes}); } static ngComponentDef = defineComponent({ type: Component, selectors: [[name]], factory: () => new Component(), - features: [NgOnChangesFeature], consts: consts, vars: vars, - inputs: {a: 'val1', b: ['publicName', 'val2']}, template, + inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template, directives: directives }); }; @@ -1997,49 +2013,64 @@ describe('lifecycles', () => { class Directive { // @Input() val1: string; - // @Input('publicName') val2: string; + // @Input('publicVal2') val2: string; a: string = 'wasVal1BeforeMinification'; b: string = 'wasVal2BeforeMinification'; - ngOnChanges(simpleChanges: SimpleChanges) { - events.push( - `dir - val1=${this.a} val2=${this.b} - changed=[${Object.getOwnPropertyNames(simpleChanges).join(',')}]`); + ngOnChanges(changes: SimpleChanges) { + events.push({type: 'onChanges', name: 'dir - dir', changes}); } static ngDirectiveDef = defineDirective({ type: Directive, selectors: [['', 'dir', '']], factory: () => new Directive(), - features: [NgOnChangesFeature], - inputs: {a: 'val1', b: ['publicName', 'val2']} + inputs: {a: 'val1', b: ['publicVal2', 'val2']} }); } const defs = [Comp, Parent, Directive, ProjectedComp]; it('should call onChanges method after inputs are set in creation and update mode', () => { - /** */ + /** */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'comp'); } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(ctx.val1)); - elementProperty(0, 'publicName', bind(ctx.val2)); + elementProperty(0, 'publicVal2', bind(ctx.val2)); } }, 1, 2, defs); + // First changes happen here. const fixture = new ComponentFixture(App); + events = []; fixture.component.val1 = '1'; fixture.component.val2 = 'a'; fixture.update(); - expect(events).toEqual(['comp=comp val1=1 val2=a - changed=[val1,val2]']); + expect(events).toEqual([{ + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange( + undefined, '1', false), // we cleared `events` above, this is the second change + 'val2': new SimpleChange(undefined, 'a', false), + } + }]); events = []; fixture.component.val1 = '2'; fixture.component.val2 = 'b'; fixture.update(); - expect(events).toEqual(['comp=comp val1=2 val2=b - changed=[val1,val2]']); + expect(events).toEqual([{ + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange('1', '2', false), + 'val2': new SimpleChange('a', 'b', false), + } + }]); }); it('should call parent onChanges before child onChanges', () => { @@ -2053,28 +2084,42 @@ describe('lifecycles', () => { } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(ctx.val1)); - elementProperty(0, 'publicName', bind(ctx.val2)); + elementProperty(0, 'publicVal2', bind(ctx.val2)); } }, 1, 2, defs); const fixture = new ComponentFixture(App); + + // We're clearing events after the first change here events = []; fixture.component.val1 = '1'; fixture.component.val2 = 'a'; fixture.update(); expect(events).toEqual([ - 'comp=parent val1=1 val2=a - changed=[val1,val2]', - 'comp=comp val1=1 val2=a - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, '1', false), + 'val2': new SimpleChange(undefined, 'a', false), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, '1', false), + 'val2': new SimpleChange(undefined, 'a', false), + } + }, ]); }); it('should call all parent onChanges across view before calling children onChanges', () => { /** - * - * - * - * parent temp: + * + * */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { @@ -2084,18 +2129,46 @@ describe('lifecycles', () => { } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); elementProperty(1, 'val1', bind(2)); - elementProperty(1, 'publicName', bind(2)); + elementProperty(1, 'publicVal2', bind(2)); } }, 2, 4, defs); const fixture = new ComponentFixture(App); expect(events).toEqual([ - 'comp=parent val1=1 val2=1 - changed=[val1,val2]', - 'comp=parent val1=2 val2=2 - changed=[val1,val2]', - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', - 'comp=comp val1=2 val2=2 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, ]); }); @@ -2121,7 +2194,7 @@ describe('lifecycles', () => { } if (rf1 & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); } embeddedViewEnd(); } @@ -2131,19 +2204,51 @@ describe('lifecycles', () => { }, 1, 0, defs); const fixture = new ComponentFixture(App); + + // Show the `comp` component, causing it to initialize. (first change is true) fixture.component.condition = true; fixture.update(); - expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']); + expect(events).toEqual([{ + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }]); + // Hide the `comp` component, no onChanges should fire fixture.component.condition = false; fixture.update(); - expect(events).toEqual(['comp=comp val1=1 val2=1 - changed=[val1,val2]']); + expect(events).toEqual([{ + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }]); + // Show the `comp` component, it initializes again. (first change is true) fixture.component.condition = true; fixture.update(); expect(events).toEqual([ - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', - 'comp=comp val1=1 val2=1 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + } ]); }); @@ -2161,26 +2266,40 @@ describe('lifecycles', () => { } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); elementProperty(1, 'val1', bind(2)); - elementProperty(1, 'publicName', bind(2)); + elementProperty(1, 'publicVal2', bind(2)); } }, 2, 4, defs); const fixture = new ComponentFixture(App); expect(events).toEqual([ - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', - 'comp=projected val1=2 val2=2 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - projected', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, ]); }); it('should call onChanges in host and its content children before next host', () => { /** - * - * + * + * * - * - * + * + * * */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { @@ -2194,75 +2313,130 @@ describe('lifecycles', () => { } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); elementProperty(1, 'val1', bind(2)); - elementProperty(1, 'publicName', bind(2)); + elementProperty(1, 'publicVal2', bind(2)); elementProperty(2, 'val1', bind(3)); - elementProperty(2, 'publicName', bind(3)); + elementProperty(2, 'publicVal2', bind(3)); elementProperty(3, 'val1', bind(4)); - elementProperty(3, 'publicName', bind(4)); + elementProperty(3, 'publicVal2', bind(4)); } }, 4, 8, defs); const fixture = new ComponentFixture(App); expect(events).toEqual([ - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', - 'comp=projected val1=2 val2=2 - changed=[val1,val2]', - 'comp=comp val1=3 val2=3 - changed=[val1,val2]', - 'comp=projected val1=4 val2=4 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - projected', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 3, true), + 'val2': new SimpleChange(undefined, 3, true), + } + }, + { + type: 'onChanges', + name: 'comp - projected', + changes: { + 'val1': new SimpleChange(undefined, 4, true), + 'val2': new SimpleChange(undefined, 4, true), + } + }, ]); }); it('should be called on directives after component', () => { - /** */ + /** + * + */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'comp', ['dir', '']); } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); } }, 1, 2, defs); const fixture = new ComponentFixture(App); expect(events).toEqual([ - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'dir - dir', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, ]); + // Update causes no changes to be fired, since the bindings didn't change. + events = []; fixture.update(); - expect(events).toEqual([ - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', 'dir - val1=1 val2=1 - changed=[val1,val2]' - ]); + expect(events).toEqual([]); }); it('should be called on directives on an element', () => { - /**
*/ + /** + *
+ */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['dir', '']); } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); } }, 1, 2, defs); const fixture = new ComponentFixture(App); - expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']); + expect(events).toEqual([{ + type: 'onChanges', + name: 'dir - dir', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }]); + events = []; fixture.update(); - expect(events).toEqual(['dir - val1=1 val2=1 - changed=[val1,val2]']); + expect(events).toEqual([]); }); it('should call onChanges properly in for loop', () => { /** - * + * * % for (let j = 2; j < 5; j++) { - * + * * % } - * + * */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { @@ -2273,9 +2447,9 @@ describe('lifecycles', () => { } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); elementProperty(2, 'val1', bind(5)); - elementProperty(2, 'publicName', bind(5)); + elementProperty(2, 'publicVal2', bind(5)); containerRefreshStart(1); { for (let j = 2; j < 5; j++) { @@ -2285,7 +2459,7 @@ describe('lifecycles', () => { } if (rf1 & RenderFlags.Update) { elementProperty(0, 'val1', bind(j)); - elementProperty(0, 'publicName', bind(j)); + elementProperty(0, 'publicVal2', bind(j)); } embeddedViewEnd(); } @@ -2299,21 +2473,56 @@ describe('lifecycles', () => { // onChanges is called top to bottom, so top level comps (1 and 5) are called // before the comps inside the for loop's embedded view (2, 3, and 4) expect(events).toEqual([ - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', - 'comp=comp val1=5 val2=5 - changed=[val1,val2]', - 'comp=comp val1=2 val2=2 - changed=[val1,val2]', - 'comp=comp val1=3 val2=3 - changed=[val1,val2]', - 'comp=comp val1=4 val2=4 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 5, true), + 'val2': new SimpleChange(undefined, 5, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 3, true), + 'val2': new SimpleChange(undefined, 3, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 4, true), + 'val2': new SimpleChange(undefined, 4, true), + } + }, ]); }); it('should call onChanges properly in for loop with children', () => { /** - * + * * % for (let j = 2; j < 5; j++) { - * + * * % } - * + * */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { @@ -2324,9 +2533,9 @@ describe('lifecycles', () => { } if (rf & RenderFlags.Update) { elementProperty(0, 'val1', bind(1)); - elementProperty(0, 'publicName', bind(1)); + elementProperty(0, 'publicVal2', bind(1)); elementProperty(2, 'val1', bind(5)); - elementProperty(2, 'publicName', bind(5)); + elementProperty(2, 'publicVal2', bind(5)); containerRefreshStart(1); { for (let j = 2; j < 5; j++) { @@ -2336,7 +2545,7 @@ describe('lifecycles', () => { } if (rf1 & RenderFlags.Update) { elementProperty(0, 'val1', bind(j)); - elementProperty(0, 'publicName', bind(j)); + elementProperty(0, 'publicVal2', bind(j)); } embeddedViewEnd(); } @@ -2350,19 +2559,144 @@ describe('lifecycles', () => { // onChanges is called top to bottom, so top level comps (1 and 5) are called // before the comps inside the for loop's embedded view (2, 3, and 4) expect(events).toEqual([ - 'comp=parent val1=1 val2=1 - changed=[val1,val2]', - 'comp=parent val1=5 val2=5 - changed=[val1,val2]', - 'comp=parent val1=2 val2=2 - changed=[val1,val2]', - 'comp=comp val1=2 val2=2 - changed=[val1,val2]', - 'comp=parent val1=3 val2=3 - changed=[val1,val2]', - 'comp=comp val1=3 val2=3 - changed=[val1,val2]', - 'comp=parent val1=4 val2=4 - changed=[val1,val2]', - 'comp=comp val1=4 val2=4 - changed=[val1,val2]', - 'comp=comp val1=1 val2=1 - changed=[val1,val2]', - 'comp=comp val1=5 val2=5 - changed=[val1,val2]' + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 5, true), + 'val2': new SimpleChange(undefined, 5, true), + } + }, + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 2, true), + 'val2': new SimpleChange(undefined, 2, true), + } + }, + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 3, true), + 'val2': new SimpleChange(undefined, 3, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 3, true), + 'val2': new SimpleChange(undefined, 3, true), + } + }, + { + type: 'onChanges', + name: 'comp - parent', + changes: { + 'val1': new SimpleChange(undefined, 4, true), + 'val2': new SimpleChange(undefined, 4, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 4, true), + 'val2': new SimpleChange(undefined, 4, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 1, true), + 'val2': new SimpleChange(undefined, 1, true), + } + }, + { + type: 'onChanges', + name: 'comp - comp', + changes: { + 'val1': new SimpleChange(undefined, 5, true), + 'val2': new SimpleChange(undefined, 5, true), + } + }, ]); }); + it('should not call onChanges if props are set directly', () => { + let events: SimpleChanges[] = []; + let compInstance: MyComp; + class MyComp { + value = 0; + + ngOnChanges(changes: SimpleChanges) { events.push(changes); } + + static ngComponentDef = defineComponent({ + type: MyComp, + factory: () => { + // Capture the instance so we can test setting the property directly + compInstance = new MyComp(); + return compInstance; + }, + template: (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'div'); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'data-a', bind(ctx.a)); + } + }, + selectors: [['mycomp']], + inputs: { + value: 'value', + }, + consts: 1, + vars: 1, + }); + } + + /** + * + */ + + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'mycomp'); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'value', bind(1)); + } + }, 1, 1, [MyComp]); + + const fixture = new ComponentFixture(App); + events = []; + + // Try setting the property directly + compInstance !.value = 2; + + fixture.update(); + expect(events).toEqual([]); + }); + }); describe('hook order', () => { @@ -2394,7 +2728,6 @@ describe('lifecycles', () => { consts: consts, vars: vars, inputs: {val: 'val'}, template, - features: [NgOnChangesFeature], directives: directives }); }; @@ -2412,6 +2745,11 @@ describe('lifecycles', () => { element(0, 'comp'); element(1, 'comp'); } + // This template function is a little weird in that the `elementProperty` calls + // below are directly setting values `1` and `2`, where normally there would be + // a call to `bind()` that would do the work of seeing if something changed. + // This means when `fixture.update()` is called below, ngOnChanges should fire, + // even though the *value* itself never changed. if (rf & RenderFlags.Update) { elementProperty(0, 'val', 1); elementProperty(1, 'val', 2); @@ -2426,7 +2764,7 @@ describe('lifecycles', () => { ]); events = []; - fixture.update(); + fixture.update(); // Changes are made due to lack of `bind()` call in template fn. expect(events).toEqual([ 'changes comp1', 'check comp1', 'changes comp2', 'check comp2', 'contentCheck comp1', 'contentCheck comp2', 'viewCheck comp1', 'viewCheck comp2' diff --git a/packages/core/test/render3/ng_on_changes_feature_spec.ts b/packages/core/test/render3/ng_on_changes_feature_spec.ts deleted file mode 100644 index 69e8012c79..0000000000 --- a/packages/core/test/render3/ng_on_changes_feature_spec.ts +++ /dev/null @@ -1,325 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChange, SimpleChanges} from '../../src/core'; -import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature'; -import {DirectiveDef, NgOnChangesFeature, defineComponent, defineDirective} from '../../src/render3/index'; - -describe('NgOnChangesFeature', () => { - it('should patch class', () => { - class MyDirective implements OnChanges, DoCheck { - public log: Array = []; - 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 fe64165925..00df5eea08 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, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index'; +import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index'; import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; @@ -1631,7 +1631,6 @@ describe('ViewContainerRef', () => { textBinding(0, interpolation1('', cmp.name, '')); } }, - features: [NgOnChangesFeature], inputs: {name: 'name'} }); } @@ -1796,12 +1795,13 @@ describe('ViewContainerRef', () => { expect(fixture.html).toEqual('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', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D', + 'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' ]); diff --git a/packages/core/test/view/component_view_spec.ts b/packages/core/test/view/component_view_spec.ts index 2c5aa1a0e1..e9d7f66dfb 100644 --- a/packages/core/test/view/component_view_spec.ts +++ b/packages/core/test/view/component_view_spec.ts @@ -9,6 +9,7 @@ import {SecurityContext} from '@angular/core'; import {ArgumentType, BindingFlags, NodeCheckFn, NodeFlags, Services, ViewData, ViewFlags, ViewState, asElementData, directiveDef, elementDef, rootRenderNodes} from '@angular/core/src/view/index'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; +import {fixmeIvy} from '@angular/private/testing'; import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper'; diff --git a/packages/core/test/view/element_spec.ts b/packages/core/test/view/element_spec.ts index 5e9fe24afd..3ee576f0a6 100644 --- a/packages/core/test/view/element_spec.ts +++ b/packages/core/test/view/element_spec.ts @@ -11,6 +11,7 @@ import {getDebugContext} from '@angular/core/src/errors'; import {BindingFlags, NodeFlags, Services, ViewData, ViewDefinition, asElementData, elementDef} from '@angular/core/src/view/index'; import {TestBed} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; +import {fixmeIvy} from '@angular/private/testing'; import {ARG_TYPE_VALUES, callMostRecentEventListenerHandler, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes, isBrowser, recordNodeToRemove} from './helper'; @@ -184,6 +185,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent return result; } + it('should listen to DOM events', () => { const handleEventSpy = jasmine.createSpy('handleEvent'); const removeListenerSpy = @@ -251,6 +253,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent expect(removeListenerSpy).toHaveBeenCalled(); }); + it('should preventDefault only if the handler returns false', () => { let eventHandlerResult: any; let preventDefaultSpy: jasmine.Spy = undefined !; @@ -279,6 +282,7 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent expect(preventDefaultSpy).toHaveBeenCalled(); }); + it('should report debug info on event errors', () => { const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError'); const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough(); diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index eae2a62923..8277f6e0b8 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -319,239 +319,235 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should bind properties, events', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const ng1Module = angular.module('ng1', []).value( - $EXCEPTION_HANDLER, (err: any) => { throw err; }); + it('should bind properties, events', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const ng1Module = + angular.module('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; }); - ng1Module.run(($rootScope: any) => { - $rootScope.name = 'world'; - $rootScope.dataA = 'A'; - $rootScope.dataB = 'B'; - $rootScope.modelA = 'initModelA'; - $rootScope.modelB = 'initModelB'; - $rootScope.eventA = '?'; - $rootScope.eventB = '?'; - }); - @Component({ - selector: 'ng2', - inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], - outputs: [ - 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', - 'twoWayBEmitter: twoWayBChange' - ], - template: 'ignore: {{ignore}}; ' + - 'literal: {{literal}}; interpolate: {{interpolate}}; ' + - 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + - 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' - }) - class Ng2 { - ngOnChangesCount = 0; - ignore = '-'; - literal = '?'; - interpolate = '?'; - oneWayA = '?'; - oneWayB = '?'; - twoWayA = '?'; - twoWayB = '?'; - eventA = new EventEmitter(); - eventB = new EventEmitter(); - twoWayAEmitter = new EventEmitter(); - twoWayBEmitter = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - const assert = (prop: string, value: any) => { - if ((this as any)[prop] != value) { - throw new Error( - `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); - } - }; + ng1Module.run(($rootScope: any) => { + $rootScope.name = 'world'; + $rootScope.dataA = 'A'; + $rootScope.dataB = 'B'; + $rootScope.modelA = 'initModelA'; + $rootScope.modelB = 'initModelB'; + $rootScope.eventA = '?'; + $rootScope.eventB = '?'; + }); + @Component({ + selector: 'ng2', + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' + ], + template: 'ignore: {{ignore}}; ' + + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' + }) + class Ng2 { + ngOnChangesCount = 0; + ignore = '-'; + literal = '?'; + interpolate = '?'; + oneWayA = '?'; + oneWayB = '?'; + twoWayA = '?'; + twoWayB = '?'; + eventA = new EventEmitter(); + eventB = new EventEmitter(); + twoWayAEmitter = new EventEmitter(); + twoWayBEmitter = new EventEmitter(); + ngOnChanges(changes: SimpleChanges) { + const assert = (prop: string, value: any) => { + if ((this as any)[prop] != value) { + throw new Error( + `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); + } + }; - const assertChange = (prop: string, value: any) => { - assert(prop, value); - if (!changes[prop]) { - throw new Error(`Changes record for '${prop}' not found.`); - } - const actValue = changes[prop].currentValue; - if (actValue != value) { - throw new Error( - `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); - } - }; + const assertChange = (prop: string, value: any) => { + assert(prop, value); + if (!changes[prop]) { + throw new Error(`Changes record for '${prop}' not found.`); + } + const actValue = changes[prop].currentValue; + if (actValue != value) { + throw new Error( + `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); + } + }; - switch (this.ngOnChangesCount++) { - case 0: - assert('ignore', '-'); - assertChange('literal', 'Text'); - assertChange('interpolate', 'Hello world'); - assertChange('oneWayA', 'A'); - assertChange('oneWayB', 'B'); - assertChange('twoWayA', 'initModelA'); - assertChange('twoWayB', 'initModelB'); + switch (this.ngOnChangesCount++) { + case 0: + assert('ignore', '-'); + assertChange('literal', 'Text'); + assertChange('interpolate', 'Hello world'); + assertChange('oneWayA', 'A'); + assertChange('oneWayB', 'B'); + assertChange('twoWayA', 'initModelA'); + assertChange('twoWayB', 'initModelB'); - this.twoWayAEmitter.emit('newA'); - this.twoWayBEmitter.emit('newB'); - this.eventA.emit('aFired'); - this.eventB.emit('bFired'); - break; - case 1: - assertChange('twoWayA', 'newA'); - assertChange('twoWayB', 'newB'); - break; - case 2: - assertChange('interpolate', 'Hello everyone'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } - ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); + this.twoWayAEmitter.emit('newA'); + this.twoWayBEmitter.emit('newB'); + this.eventA.emit('aFired'); + this.eventB.emit('bFired'); + break; + case 1: + assertChange('twoWayA', 'newA'); + assertChange('twoWayB', 'newB'); + break; + case 2: + assertChange('interpolate', 'Hello everyone'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); - @NgModule({ - declarations: [Ng2], - imports: [BrowserModule], - }) - class Ng2Module { - } + @NgModule({ + declarations: [Ng2], + imports: [BrowserModule], + }) + class Ng2Module { + } - const element = html(`
+ const element = html(`
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect(multiTrim(document.body.textContent !)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello world; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect(multiTrim(document.body.textContent !)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello world; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - ref.ng1RootScope.$apply('name = "everyone"'); - expect(multiTrim(document.body.textContent !)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello everyone; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + ref.ng1RootScope.$apply('name = "everyone"'); + expect(multiTrim(document.body.textContent !)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello everyone; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - ref.dispose(); - }); + ref.dispose(); + }); - })); + })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should support two-way binding and event listener', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - const listenerSpy = jasmine.createSpy('$rootScope.listener'); - const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { - $rootScope['value'] = 'world'; - $rootScope['listener'] = listenerSpy; - }); + it('should support two-way binding and event listener', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + const listenerSpy = jasmine.createSpy('$rootScope.listener'); + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['value'] = 'world'; + $rootScope['listener'] = listenerSpy; + }); - @Component({selector: 'ng2', template: `model: {{model}};`}) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - @Input() model = '?'; - @Output() modelChange = new EventEmitter(); + @Component({selector: 'ng2', template: `model: {{model}};`}) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + @Input() model = '?'; + @Output() modelChange = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - switch (this.ngOnChangesCount++) { - case 0: - expect(changes.model.currentValue).toBe('world'); - this.modelChange.emit('newC'); - break; - case 1: - expect(changes.model.currentValue).toBe('newC'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } + ngOnChanges(changes: SimpleChanges) { + switch (this.ngOnChangesCount++) { + case 0: + expect(changes.model.currentValue).toBe('world'); + this.modelChange.emit('newC'); + break; + case 1: + expect(changes.model.currentValue).toBe('newC'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } - ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); + ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); - @NgModule({declarations: [Ng2Component], imports: [BrowserModule]}) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({declarations: [Ng2Component], imports: [BrowserModule]}) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(` + const element = html(`
| value: {{value}}
`); - adapter.bootstrap(element, ['ng1']).ready((ref) => { - expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); - expect(listenerSpy).toHaveBeenCalledWith('newC'); - ref.dispose(); - }); - })); + adapter.bootstrap(element, ['ng1']).ready((ref) => { + expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); + expect(listenerSpy).toHaveBeenCalledWith('newC'); + ref.dispose(); + }); + })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should initialize inputs in time for `ngOnChanges`', async(() => { - const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); + it('should initialize inputs in time for `ngOnChanges`', async(() => { + const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); - @Component({ - selector: 'ng2', - template: ` + @Component({ + selector: 'ng2', + template: ` ngOnChangesCount: {{ ngOnChangesCount }} | firstChangesCount: {{ firstChangesCount }} | initialValue: {{ initialValue }}` - }) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - firstChangesCount = 0; - // TODO(issue/24571): remove '!'. - initialValue !: string; - // TODO(issue/24571): remove '!'. - @Input() foo !: string; + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + firstChangesCount = 0; + // TODO(issue/24571): remove '!'. + initialValue !: string; + // TODO(issue/24571): remove '!'. + @Input() foo !: string; - ngOnChanges(changes: SimpleChanges) { - this.ngOnChangesCount++; + ngOnChanges(changes: SimpleChanges) { + this.ngOnChangesCount++; - if (this.ngOnChangesCount === 1) { - this.initialValue = this.foo; - } + if (this.ngOnChangesCount === 1) { + this.initialValue = this.foo; + } - if (changes['foo'] && changes['foo'].isFirstChange()) { - this.firstChangesCount++; - } - } - } + if (changes['foo'] && changes['foo'].isFirstChange()) { + this.firstChangesCount++; + } + } + } - @NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) - class Ng2Module { - } + @NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) + class Ng2Module { + } - const ng1Module = angular.module('ng1', []).directive( - 'ng2', adapter.downgradeNg2Component(Ng2Component)); + const ng1Module = angular.module('ng1', []).directive( + 'ng2', adapter.downgradeNg2Component(Ng2Component)); - const element = html(` + const element = html(` `); - adapter.bootstrap(element, ['ng1']).ready(ref => { - const nodes = element.querySelectorAll('ng2'); - const expectedTextWith = (value: string) => - `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; + adapter.bootstrap(element, ['ng1']).ready(ref => { + const nodes = element.querySelectorAll('ng2'); + const expectedTextWith = (value: string) => + `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; - expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); - expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); - expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); - expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); + expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); + expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); + expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); + expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); - ref.dispose(); - }); - })); + ref.dispose(); + }); + })); it('should bind to ng-model', async(() => { const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); diff --git a/packages/upgrade/test/static/integration/downgrade_component_spec.ts b/packages/upgrade/test/static/integration/downgrade_component_spec.ts index 36c2fd9bc7..5ca71fd7fc 100644 --- a/packages/upgrade/test/static/integration/downgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_component_spec.ts @@ -22,106 +22,104 @@ withEachNg1Version(() => { beforeEach(() => destroyPlatform()); afterEach(() => destroyPlatform()); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should bind properties, events', async(() => { - const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { - $rootScope['name'] = 'world'; - $rootScope['dataA'] = 'A'; - $rootScope['dataB'] = 'B'; - $rootScope['modelA'] = 'initModelA'; - $rootScope['modelB'] = 'initModelB'; - $rootScope['eventA'] = '?'; - $rootScope['eventB'] = '?'; - }); + it('should bind properties, events', async(() => { + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['name'] = 'world'; + $rootScope['dataA'] = 'A'; + $rootScope['dataB'] = 'B'; + $rootScope['modelA'] = 'initModelA'; + $rootScope['modelB'] = 'initModelB'; + $rootScope['eventA'] = '?'; + $rootScope['eventB'] = '?'; + }); - @Component({ - selector: 'ng2', - inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], - outputs: [ - 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', - 'twoWayBEmitter: twoWayBChange' - ], - template: 'ignore: {{ignore}}; ' + - 'literal: {{literal}}; interpolate: {{interpolate}}; ' + - 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + - 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' - }) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - ignore = '-'; - literal = '?'; - interpolate = '?'; - oneWayA = '?'; - oneWayB = '?'; - twoWayA = '?'; - twoWayB = '?'; - eventA = new EventEmitter(); - eventB = new EventEmitter(); - twoWayAEmitter = new EventEmitter(); - twoWayBEmitter = new EventEmitter(); + @Component({ + selector: 'ng2', + inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], + outputs: [ + 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' + ], + template: 'ignore: {{ignore}}; ' + + 'literal: {{literal}}; interpolate: {{interpolate}}; ' + + 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + + 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + ignore = '-'; + literal = '?'; + interpolate = '?'; + oneWayA = '?'; + oneWayB = '?'; + twoWayA = '?'; + twoWayB = '?'; + eventA = new EventEmitter(); + eventB = new EventEmitter(); + twoWayAEmitter = new EventEmitter(); + twoWayBEmitter = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - const assert = (prop: string, value: any) => { - const propVal = (this as any)[prop]; - if (propVal != value) { - throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); - } - }; + ngOnChanges(changes: SimpleChanges) { + const assert = (prop: string, value: any) => { + const propVal = (this as any)[prop]; + if (propVal != value) { + throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); + } + }; - const assertChange = (prop: string, value: any) => { - assert(prop, value); - if (!changes[prop]) { - throw new Error(`Changes record for '${prop}' not found.`); - } - const actualValue = changes[prop].currentValue; - if (actualValue != value) { - throw new Error( - `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); - } - }; + const assertChange = (prop: string, value: any) => { + assert(prop, value); + if (!changes[prop]) { + throw new Error(`Changes record for '${prop}' not found.`); + } + const actualValue = changes[prop].currentValue; + if (actualValue != value) { + throw new Error( + `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); + } + }; - switch (this.ngOnChangesCount++) { - case 0: - assert('ignore', '-'); - assertChange('literal', 'Text'); - assertChange('interpolate', 'Hello world'); - assertChange('oneWayA', 'A'); - assertChange('oneWayB', 'B'); - assertChange('twoWayA', 'initModelA'); - assertChange('twoWayB', 'initModelB'); + switch (this.ngOnChangesCount++) { + case 0: + assert('ignore', '-'); + assertChange('literal', 'Text'); + assertChange('interpolate', 'Hello world'); + assertChange('oneWayA', 'A'); + assertChange('oneWayB', 'B'); + assertChange('twoWayA', 'initModelA'); + assertChange('twoWayB', 'initModelB'); - this.twoWayAEmitter.emit('newA'); - this.twoWayBEmitter.emit('newB'); - this.eventA.emit('aFired'); - this.eventB.emit('bFired'); - break; - case 1: - assertChange('twoWayA', 'newA'); - assertChange('twoWayB', 'newB'); - break; - case 2: - assertChange('interpolate', 'Hello everyone'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } + this.twoWayAEmitter.emit('newA'); + this.twoWayBEmitter.emit('newB'); + this.eventA.emit('aFired'); + this.eventB.emit('bFired'); + break; + case 1: + assertChange('twoWayA', 'newA'); + assertChange('twoWayB', 'newB'); + break; + case 2: + assertChange('interpolate', 'Hello everyone'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } - ng1Module.directive('ng2', downgradeComponent({ - component: Ng2Component, - })); + ng1Module.directive('ng2', downgradeComponent({ + component: Ng2Component, + })); - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule, UpgradeModule] - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(` + const element = html(`
{ | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
`); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { - expect(multiTrim(document.body.textContent)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello world; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(document.body.textContent)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello world; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - $apply(upgrade, 'name = "everyone"'); - expect(multiTrim(document.body.textContent)) - .toEqual( - 'ignore: -; ' + - 'literal: Text; interpolate: Hello everyone; ' + - 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + - 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); - }); - })); + $apply(upgrade, 'name = "everyone"'); + expect(multiTrim(document.body.textContent)) + .toEqual( + 'ignore: -; ' + + 'literal: Text; interpolate: Hello everyone; ' + + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + + 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); + }); + })); it('should bind properties to onpush components', async(() => { const ng1Module = angular.module('ng1', []).run( @@ -189,58 +187,57 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should support two-way binding and event listener', async(() => { - const listenerSpy = jasmine.createSpy('$rootScope.listener'); - const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { - $rootScope['value'] = 'world'; - $rootScope['listener'] = listenerSpy; - }); + it('should support two-way binding and event listener', async(() => { + const listenerSpy = jasmine.createSpy('$rootScope.listener'); + const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { + $rootScope['value'] = 'world'; + $rootScope['listener'] = listenerSpy; + }); - @Component({selector: 'ng2', template: `model: {{model}};`}) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - @Input() model = '?'; - @Output() modelChange = new EventEmitter(); + @Component({selector: 'ng2', template: `model: {{model}};`}) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + @Input() model = '?'; + @Output() modelChange = new EventEmitter(); - ngOnChanges(changes: SimpleChanges) { - switch (this.ngOnChangesCount++) { - case 0: - expect(changes.model.currentValue).toBe('world'); - this.modelChange.emit('newC'); - break; - case 1: - expect(changes.model.currentValue).toBe('newC'); - break; - default: - throw new Error('Called too many times! ' + JSON.stringify(changes)); - } - } - } + ngOnChanges(changes: SimpleChanges) { + switch (this.ngOnChangesCount++) { + case 0: + expect(changes.model.currentValue).toBe('world'); + this.modelChange.emit('newC'); + break; + case 1: + expect(changes.model.currentValue).toBe('newC'); + break; + default: + throw new Error('Called too many times! ' + JSON.stringify(changes)); + } + } + } - ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); + ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule, UpgradeModule] - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule, UpgradeModule] + }) + class Ng2Module { + ngDoBootstrap() {} + } - const element = html(` + const element = html(`
| value: {{value}}
`); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { - expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); - expect(listenerSpy).toHaveBeenCalledWith('newC'); - }); - })); + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { + expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); + expect(listenerSpy).toHaveBeenCalledWith('newC'); + }); + })); it('should run change-detection on every digest (by default)', async(() => { let ng2Component: Ng2Component; @@ -404,66 +401,65 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should initialize inputs in time for `ngOnChanges`', async(() => { - @Component({ - selector: 'ng2', - template: ` + it('should initialize inputs in time for `ngOnChanges`', async(() => { + @Component({ + selector: 'ng2', + template: ` ngOnChangesCount: {{ ngOnChangesCount }} | firstChangesCount: {{ firstChangesCount }} | initialValue: {{ initialValue }}` - }) - class Ng2Component implements OnChanges { - ngOnChangesCount = 0; - firstChangesCount = 0; - // TODO(issue/24571): remove '!'. - initialValue !: string; - // TODO(issue/24571): remove '!'. - @Input() foo !: string; + }) + class Ng2Component implements OnChanges { + ngOnChangesCount = 0; + firstChangesCount = 0; + // TODO(issue/24571): remove '!'. + initialValue !: string; + // TODO(issue/24571): remove '!'. + @Input() foo !: string; - ngOnChanges(changes: SimpleChanges) { - this.ngOnChangesCount++; + ngOnChanges(changes: SimpleChanges) { + this.ngOnChangesCount++; - if (this.ngOnChangesCount === 1) { - this.initialValue = this.foo; - } + if (this.ngOnChangesCount === 1) { + this.initialValue = this.foo; + } - if (changes['foo'] && changes['foo'].isFirstChange()) { - this.firstChangesCount++; - } - } - } + if (changes['foo'] && changes['foo'].isFirstChange()) { + this.firstChangesCount++; + } + } + } - @NgModule({ - imports: [BrowserModule, UpgradeModule], - declarations: [Ng2Component], - entryComponents: [Ng2Component] - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } - const ng1Module = angular.module('ng1', []).directive( - 'ng2', downgradeComponent({component: Ng2Component})); + const ng1Module = angular.module('ng1', []).directive( + 'ng2', downgradeComponent({component: Ng2Component})); - const element = html(` + const element = html(` `); - bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { - const nodes = element.querySelectorAll('ng2'); - const expectedTextWith = (value: string) => - `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { + const nodes = element.querySelectorAll('ng2'); + const expectedTextWith = (value: string) => + `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; - expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); - expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); - expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); - expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); - }); - })); + expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); + expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); + expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); + expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); + }); + })); it('should bind to ng-model', async(() => { const ng1Module = angular.module('ng1', []).run( diff --git a/packages/upgrade/test/static/integration/downgrade_module_spec.ts b/packages/upgrade/test/static/integration/downgrade_module_spec.ts index 8dcb06bd80..8462a0e710 100644 --- a/packages/upgrade/test/static/integration/downgrade_module_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_module_spec.ts @@ -725,66 +725,63 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should propagate input changes inside the Angular zone', async(() => { - let ng2Component: Ng2Component; + it('should propagate input changes inside the Angular zone', async(() => { + let ng2Component: Ng2Component; - @Component({selector: 'ng2', template: ''}) - class Ng2Component implements OnChanges { - @Input() attrInput = 'foo'; - @Input() propInput = 'foo'; + @Component({selector: 'ng2', template: ''}) + class Ng2Component implements OnChanges { + @Input() attrInput = 'foo'; + @Input() propInput = 'foo'; - constructor() { ng2Component = this; } - ngOnChanges() {} - } + constructor() { ng2Component = this; } + ngOnChanges() {} + } - @NgModule({ - declarations: [Ng2Component], - entryComponents: [Ng2Component], - imports: [BrowserModule], - }) - class Ng2Module { - ngDoBootstrap() {} - } + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule], + }) + class Ng2Module { + ngDoBootstrap() {} + } - const bootstrapFn = (extraProviders: StaticProvider[]) => - platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); - const lazyModuleName = downgradeModule(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); + }); + }); + })); fixmeIvy('FW-714: ng1 projected content is not being rendered') .it('should create and destroy nested, asynchronously instantiated components inside the Angular zone', @@ -951,167 +948,165 @@ withEachNg1Version(() => { }); })); - fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly') - .it('should run the lifecycle hooks in the correct order', async(() => { - const logs: string[] = []; - let rootScope: angular.IRootScopeService; + it('should run the lifecycle hooks in the correct order', async(() => { + const logs: string[] = []; + let rootScope: angular.IRootScopeService; - @Component({ - selector: 'ng2', - template: ` + @Component({ + selector: 'ng2', + template: ` {{ value }} ` - }) - 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 58fbcfd538..c77d0dac54 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; - ngTemplateOutletContext: Object; + ngTemplateOutlet: TemplateRef | null; + ngTemplateOutletContext: Object | null; constructor(_viewContainerRef: ViewContainerRef); ngOnChanges(changes: SimpleChanges): void; }