refactor(ivy): revert onChanges change back to a feature (#28187)

- adds fixmeIvy annotation to tests that should remain updated so we can resolve those issues in the subsequent commits

PR Close #28187
This commit is contained in:
Ben Lesh 2019-01-14 17:39:21 -08:00 committed by Alex Rickabaugh
parent 030350f53e
commit 5552661fd7
45 changed files with 1229 additions and 898 deletions

View File

@ -34,20 +34,14 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange
*/ */
@Directive({selector: '[ngTemplateOutlet]'}) @Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges { export class NgTemplateOutlet implements OnChanges {
private _viewRef: EmbeddedViewRef<any>|null = null; // TODO(issue/24571): remove '!'.
private _viewRef !: EmbeddedViewRef<any>;
/** // TODO(issue/24571): remove '!'.
* A context object to attach to the {@link EmbeddedViewRef}. This should be an @Input() public ngTemplateOutletContext !: Object;
* 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 '!'.
* A string defining the template reference and optionally the context object for the template. @Input() public ngTemplateOutlet !: TemplateRef<any>;
*/
@Input() public ngTemplateOutlet: TemplateRef<any>|null = null;
constructor(private _viewContainerRef: ViewContainerRef) {} constructor(private _viewContainerRef: ViewContainerRef) {}
@ -103,7 +97,7 @@ export class NgTemplateOutlet implements OnChanges {
private _updateExistingContext(ctx: Object): void { private _updateExistingContext(ctx: Object): void {
for (let propName of Object.keys(ctx)) { for (let propName of Object.keys(ctx)) {
(<any>this._viewRef !.context)[propName] = (<any>this.ngTemplateOutletContext)[propName]; (<any>this._viewRef.context)[propName] = (<any>this.ngTemplateOutletContext)[propName];
} }
} }
} }

View File

@ -174,6 +174,11 @@ export function extractDirectiveMetadata(
const providers: Expression|null = const providers: Expression|null =
directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null; directive.has('providers') ? new WrappedNodeExpr(directive.get('providers') !) : null;
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = members.some(
member => !member.isStatic && member.kind === ClassMemberKind.Method &&
member.name === 'ngOnChanges');
// Parse exportAs. // Parse exportAs.
let exportAs: string[]|null = null; let exportAs: string[]|null = null;
if (directive.has('exportAs')) { if (directive.has('exportAs')) {
@ -192,6 +197,9 @@ export function extractDirectiveMetadata(
const metadata: R3DirectiveMetadata = { const metadata: R3DirectiveMetadata = {
name: clazz.name !.text, name: clazz.name !.text,
deps: getConstructorDependencies(clazz, reflector, isCore), host, deps: getConstructorDependencies(clazz, reflector, isCore), host,
lifecycle: {
usesOnChanges,
},
inputs: {...inputsFromMeta, ...inputsFromFields}, inputs: {...inputsFromMeta, ...inputsFromFields},
outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector, outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector,
type: new WrappedNodeExpr(clazz.name !), type: new WrappedNodeExpr(clazz.name !),

View File

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

View File

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

View File

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

View File

@ -74,6 +74,17 @@ export interface R3DirectiveMetadata {
properties: {[key: string]: string}; properties: {[key: string]: string};
}; };
/**
* Information about usage of specific lifecycle events which require special treatment in the
* code generator.
*/
lifecycle: {
/**
* Whether the directive uses NgOnChanges.
*/
usesOnChanges: boolean;
};
/** /**
* A mapping of input field names to the property names. * A mapping of input field names to the property names.
*/ */

View File

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

View File

@ -64,6 +64,20 @@ export class WrappedValue {
static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; } static isWrapped(value: any): value is WrappedValue { return value instanceof WrappedValue; }
} }
/**
* Represents a basic change from a previous to a new value.
*
* @publicApi
*/
export class SimpleChange {
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
/**
* Check whether the new value is the first value assigned.
*/
isFirstChange(): boolean { return this.firstChange; }
}
export function isListLikeIterable(obj: any): boolean { export function isListLikeIterable(obj: any): boolean {
if (!isJsObject(obj)) return false; if (!isJsObject(obj)) return false;
return Array.isArray(obj) || return Array.isArray(obj) ||

View File

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

View File

@ -28,6 +28,7 @@ export {
templateRefExtractor as ɵtemplateRefExtractor, templateRefExtractor as ɵtemplateRefExtractor,
ProvidersFeature as ɵProvidersFeature, ProvidersFeature as ɵProvidersFeature,
InheritDefinitionFeature as ɵInheritDefinitionFeature, InheritDefinitionFeature as ɵInheritDefinitionFeature,
NgOnChangesFeature as ɵNgOnChangesFeature,
LifecycleHooksFeature as ɵLifecycleHooksFeature, LifecycleHooksFeature as ɵLifecycleHooksFeature,
NgModuleType as ɵNgModuleType, NgModuleType as ɵNgModuleType,
NgModuleRef as ɵRender3NgModuleRef, NgModuleRef as ɵRender3NgModuleRef,

View File

@ -5,9 +5,19 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SimpleChanges} from './simple_change'; import {SimpleChanges, SimpleChange} from './simple_change';
/**
* Defines an object that associates properties with
* instances of `SimpleChange`.
*
* @see `OnChanges`
*
* @publicApi
*/
export interface SimpleChanges { [propName: string]: SimpleChange; }
/** /**
* @description * @description
* A lifecycle hook that is called when any data-bound property of a directive changes. * A lifecycle hook that is called when any data-bound property of a directive changes.

View File

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

View File

@ -17,7 +17,6 @@ import {assertComponentType} from './assert';
import {getComponentDef} from './definition'; import {getComponentDef} from './definition';
import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di'; import {diPublicInInjector, getOrCreateNodeInjectorForNode} from './di';
import {publishDefaultGlobalUtils} from './global_utils'; import {publishDefaultGlobalUtils} from './global_utils';
import {registerPostOrderHooks, registerPreOrderHooks} from './hooks';
import {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions'; import {CLEAN_PROMISE, createLView, createNodeAtIndex, createTNode, createTView, getOrCreateTView, initNodeFlags, instantiateRootComponent, locateHostElement, queueComponentIndexForCheck, refreshDescendantViews} from './instructions';
import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition';
import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node';
@ -26,6 +25,7 @@ import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './inte
import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view'; import {CONTEXT, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, RootContext, RootContextFlags, TVIEW} from './interfaces/view';
import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state'; import {enterView, getPreviousOrParentTNode, leaveView, resetComponentState, setCurrentDirectiveDef} from './state';
import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util'; import {defaultScheduler, getRootView, readPatchedLView, renderStringify} from './util';
import { registerPreOrderHooks, registerPostOrderHooks } from './hooks';
@ -240,8 +240,7 @@ export function LifecycleHooksFeature(component: any, def: ComponentDef<any>): v
registerPreOrderHooks(dirIndex, def, rootTView); registerPreOrderHooks(dirIndex, def, rootTView);
// TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on // TODO(misko): replace `as TNode` with createTNode call. (needs refactoring to lose dep on
// LNode). // LNode).
registerPostOrderHooks( registerPostOrderHooks(rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
rootTView, { directiveStart: dirIndex, directiveEnd: dirIndex + 1 } as TNode);
} }
/** /**

View File

@ -12,7 +12,6 @@ import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {TNode, TNodeFlags} from './interfaces/node'; import {TNode, TNodeFlags} from './interfaces/node';
import {RElement} from './interfaces/renderer'; import {RElement} from './interfaces/renderer';
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view'; import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util'; import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
@ -258,7 +257,7 @@ function findViaDirective(lView: LView, directiveInstance: {}): number {
const directiveIndexStart = tNode.directiveStart; const directiveIndexStart = tNode.directiveStart;
const directiveIndexEnd = tNode.directiveEnd; const directiveIndexEnd = tNode.directiveEnd;
for (let i = directiveIndexStart; i < directiveIndexEnd; i++) { for (let i = directiveIndexStart; i < directiveIndexEnd; i++) {
if (unwrapOnChangesDirectiveWrapper(lView[i]) === directiveInstance) { if (lView[i] === directiveInstance) {
return tNode.index; return tNode.index;
} }
} }

View File

@ -194,7 +194,7 @@ export function defineComponent<T>(componentDefinition: {
/** /**
* A list of optional features to apply. * A list of optional features to apply.
* *
* See: {@link ProvidersFeature} * See: {@link NgOnChangesFeature}, {@link ProvidersFeature}
*/ */
features?: ComponentDefFeature[]; features?: ComponentDefFeature[];
@ -256,7 +256,6 @@ export function defineComponent<T>(componentDefinition: {
inputs: null !, // assigned in noSideEffects inputs: null !, // assigned in noSideEffects
outputs: null !, // assigned in noSideEffects outputs: null !, // assigned in noSideEffects
exportAs: componentDefinition.exportAs || null, exportAs: componentDefinition.exportAs || null,
onChanges: typePrototype.ngOnChanges || null,
onInit: typePrototype.ngOnInit || null, onInit: typePrototype.ngOnInit || null,
doCheck: typePrototype.ngDoCheck || null, doCheck: typePrototype.ngDoCheck || null,
afterContentInit: typePrototype.ngAfterContentInit || null, afterContentInit: typePrototype.ngAfterContentInit || null,
@ -567,7 +566,7 @@ export const defineDirective = defineComponent as any as<T>(directiveDefinition:
/** /**
* A list of optional features to apply. * A list of optional features to apply.
* *
* See: {@link ProvidersFeature}, {@link InheritDefinitionFeature} * See: {@link NgOnChangesFeature}, {@link ProvidersFeature}, {@link InheritDefinitionFeature}
*/ */
features?: DirectiveDefFeature[]; features?: DirectiveDefFeature[];

View File

@ -20,7 +20,6 @@ import {NO_PARENT_INJECTOR, NodeInjectorFactory, PARENT_INJECTOR, RelativeInject
import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node'; import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType} from './interfaces/node';
import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view'; import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert'; import {assertNodeOfPossibleTypes} from './node_assert';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state';
import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, renderStringify} from './util'; import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, renderStringify} from './util';
@ -523,8 +522,6 @@ export function getNodeInjectable(
factory.resolving = false; factory.resolving = false;
setTNodeAndViewData(savePreviousOrParentTNode, saveLView); setTNodeAndViewData(savePreviousOrParentTNode, saveLView);
} }
} else {
value = unwrapOnChangesDirectiveWrapper(value);
} }
return value; return value;
} }

View File

@ -10,6 +10,7 @@ import {Type} from '../../interface/type';
import {fillProperties} from '../../util/property'; import {fillProperties} from '../../util/property';
import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty';
import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition'; import {ComponentDef, DirectiveDef, DirectiveDefFeature, RenderFlags} from '../interfaces/definition';
import { Component } from '../../metadata/directives';
@ -60,6 +61,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
} }
if (baseDef) { if (baseDef) {
// Merge inputs and outputs
fillProperties(definition.inputs, baseDef.inputs); fillProperties(definition.inputs, baseDef.inputs);
fillProperties(definition.declaredInputs, baseDef.declaredInputs); fillProperties(definition.declaredInputs, baseDef.declaredInputs);
fillProperties(definition.outputs, baseDef.outputs); fillProperties(definition.outputs, baseDef.outputs);
@ -124,6 +126,7 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
} }
} }
// Merge inputs and outputs // Merge inputs and outputs
fillProperties(definition.inputs, superDef.inputs); fillProperties(definition.inputs, superDef.inputs);
fillProperties(definition.declaredInputs, superDef.declaredInputs); fillProperties(definition.declaredInputs, superDef.declaredInputs);
@ -139,7 +142,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
definition.doCheck = definition.doCheck || superDef.doCheck; definition.doCheck = definition.doCheck || superDef.doCheck;
definition.onDestroy = definition.onDestroy || superDef.onDestroy; definition.onDestroy = definition.onDestroy || superDef.onDestroy;
definition.onInit = definition.onInit || superDef.onInit; definition.onInit = definition.onInit || superDef.onInit;
definition.onChanges = definition.onChanges || superDef.onChanges;
// Run parent features // Run parent features
const features = superDef.features; const features = superDef.features;
@ -166,7 +168,6 @@ export function InheritDefinitionFeature(definition: DirectiveDef<any>| Componen
definition.doCheck = definition.doCheck || superPrototype.ngDoCheck; definition.doCheck = definition.doCheck || superPrototype.ngDoCheck;
definition.onDestroy = definition.onDestroy || superPrototype.ngOnDestroy; definition.onDestroy = definition.onDestroy || superPrototype.ngOnDestroy;
definition.onInit = definition.onInit || superPrototype.ngOnInit; definition.onInit = definition.onInit || superPrototype.ngOnInit;
definition.onChanges = definition.onChanges || superPrototype.ngOnChanges;
} }
} }

View File

@ -0,0 +1,125 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChange} from '../../change_detection/change_detection_util';
import {SimpleChanges} from '../../interface/simple_change';
import {OnChanges} from '../../interface/lifecycle_hooks';
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
const PRIVATE_PREFIX = '__ngOnChanges_';
type OnChangesExpando = OnChanges & {
__ngOnChanges_: SimpleChanges|null|undefined;
// tslint:disable-next-line:no-any Can hold any value
[key: string]: any;
};
/**
* The NgOnChangesFeature decorates a component with support for the ngOnChanges
* lifecycle hook, so it should be included in any component that implements
* that hook.
*
* If the component or directive uses inheritance, the NgOnChangesFeature MUST
* be included as a feature AFTER {@link InheritDefinitionFeature}, otherwise
* inherited properties will not be propagated to the ngOnChanges lifecycle
* hook.
*
* Example usage:
*
* ```
* static ngComponentDef = defineComponent({
* ...
* inputs: {name: 'publicName'},
* features: [NgOnChangesFeature]
* });
* ```
*/
export function NgOnChangesFeature<T>(definition: DirectiveDef<T>): void {
const publicToDeclaredInputs = definition.declaredInputs;
const publicToMinifiedInputs = definition.inputs;
const proto = definition.type.prototype;
for (const publicName in publicToDeclaredInputs) {
if (publicToDeclaredInputs.hasOwnProperty(publicName)) {
const minifiedKey = publicToMinifiedInputs[publicName];
const declaredKey = publicToDeclaredInputs[publicName];
const privateMinKey = PRIVATE_PREFIX + minifiedKey;
// Walk the prototype chain to see if we find a property descriptor
// That way we can honor setters and getters that were inherited.
let originalProperty: PropertyDescriptor|undefined = undefined;
let checkProto = proto;
while (!originalProperty && checkProto &&
Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) {
originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey);
checkProto = Object.getPrototypeOf(checkProto);
}
const getter = originalProperty && originalProperty.get;
const setter = originalProperty && originalProperty.set;
// create a getter and setter for property
Object.defineProperty(proto, minifiedKey, {
get: getter ||
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }),
set<T>(this: OnChangesExpando, value: T) {
let simpleChanges = this[PRIVATE_PREFIX];
if (!simpleChanges) {
simpleChanges = {};
// Place where we will store SimpleChanges if there is a change
Object.defineProperty(this, PRIVATE_PREFIX, {value: simpleChanges, writable: true});
}
const isFirstChange = !this.hasOwnProperty(privateMinKey);
const currentChange = simpleChanges[declaredKey];
if (currentChange) {
currentChange.currentValue = value;
} else {
simpleChanges[declaredKey] =
new SimpleChange(this[privateMinKey], value, isFirstChange);
}
if (isFirstChange) {
// Create a place where the actual value will be stored and make it non-enumerable
Object.defineProperty(this, privateMinKey, {value, writable: true});
} else {
this[privateMinKey] = value;
}
if (setter) setter.call(this, value);
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode
});
}
}
// If an onInit hook is defined, it will need to wrap the ngOnChanges call
// so the call order is changes-init-check in creation mode. In subsequent
// change detection runs, only the check wrapper will be called.
if (definition.onInit != null) {
definition.onInit = onChangesWrapper(definition.onInit);
}
definition.doCheck = onChangesWrapper(definition.doCheck);
}
// This option ensures that the ngOnChanges lifecycle hook will be inherited
// from superclasses (in InheritDefinitionFeature).
(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
function onChangesWrapper(delegateHook: (() => void) | null) {
return function(this: OnChangesExpando) {
const simpleChanges = this[PRIVATE_PREFIX];
if (simpleChanges != null) {
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
if (delegateHook) delegateHook.apply(this);
};
}

View File

@ -12,7 +12,6 @@ import {assertEqual} from '../util/assert';
import {DirectiveDef} from './interfaces/definition'; import {DirectiveDef} from './interfaces/definition';
import {TNode} from './interfaces/node'; import {TNode} from './interfaces/node';
import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view'; import {FLAGS, HookData, LView, LViewFlags, TView} from './interfaces/view';
import {OnChangesDirectiveWrapper, unwrapOnChangesDirectiveWrapper} from './onchanges_util';
@ -35,12 +34,7 @@ export function registerPreOrderHooks(
ngDevMode && ngDevMode &&
assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass'); assertEqual(tView.firstTemplatePass, true, 'Should only be called on first template pass');
const {onChanges, onInit, doCheck} = directiveDef; const {onInit, doCheck} = directiveDef;
if (onChanges) {
(tView.initHooks || (tView.initHooks = [])).push(-directiveIndex, onChanges);
(tView.checkHooks || (tView.checkHooks = [])).push(-directiveIndex, onChanges);
}
if (onInit) { if (onInit) {
(tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit); (tView.initHooks || (tView.initHooks = [])).push(directiveIndex, onInit);
@ -148,31 +142,13 @@ export function executeHooks(
/** /**
* Calls lifecycle hooks with their contexts, skipping init hooks if it's not * Calls lifecycle hooks with their contexts, skipping init hooks if it's not
* the first LView pass, and skipping onChanges hooks if there are no changes present. * the first LView pass
* *
* @param currentView The current view * @param currentView The current view
* @param arr The array in which the hooks are found * @param arr The array in which the hooks are found
*/ */
export function callHooks(currentView: LView, arr: HookData): void { export function callHooks(currentView: LView, arr: HookData): void {
for (let i = 0; i < arr.length; i += 2) { for (let i = 0; i < arr.length; i += 2) {
const directiveIndex = arr[i] as number; (arr[i + 1] as() => void).call(currentView[arr[i] as number]);
const hook = arr[i + 1] as((() => void) | ((changes: SimpleChanges) => void));
// Negative indices signal that we're dealing with an `onChanges` hook.
const isOnChangesHook = directiveIndex < 0;
const directiveOrWrappedDirective =
currentView[isOnChangesHook ? -directiveIndex : directiveIndex];
const directive = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective);
if (isOnChangesHook) {
const onChanges: OnChangesDirectiveWrapper = directiveOrWrappedDirective;
const changes = onChanges.changes;
if (changes) {
onChanges.previous = changes;
onChanges.changes = null;
hook.call(onChanges.instance, changes);
}
} else {
hook.call(directive);
}
} }
} }

View File

@ -9,6 +9,7 @@ import {LifecycleHooksFeature, renderComponent, whenRendered} from './component'
import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition'; import {defineBase, defineComponent, defineDirective, defineNgModule, definePipe} from './definition';
import {getComponent, getDirectives, getHostElement, getRenderedText} from './discovery_utils'; import {getComponent, getDirectives, getHostElement, getRenderedText} from './discovery_utils';
import {InheritDefinitionFeature} from './features/inherit_definition_feature'; import {InheritDefinitionFeature} from './features/inherit_definition_feature';
import {NgOnChangesFeature} from './features/ng_onchanges_feature';
import {ProvidersFeature} from './features/providers_feature'; import {ProvidersFeature} from './features/providers_feature';
import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition'; import {BaseDef, ComponentDef, ComponentDefWithMeta, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, DirectiveDefWithMeta, DirectiveType, PipeDef, PipeDefWithMeta} from './interfaces/definition';
@ -158,6 +159,7 @@ export {
DirectiveDefFlags, DirectiveDefFlags,
DirectiveDefWithMeta, DirectiveDefWithMeta,
DirectiveType, DirectiveType,
NgOnChangesFeature,
InheritDefinitionFeature, InheritDefinitionFeature,
ProvidersFeature, ProvidersFeature,
PipeDef, PipeDef,

View File

@ -36,7 +36,6 @@ import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLA
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation'; import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {OnChangesDirectiveWrapper, isOnChangesDirectiveWrapper, recordChange, unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state';
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory'; import {BoundPlayerFactory} from './styling/player_factory';
@ -121,7 +120,7 @@ export function setHostBindings(tView: TView, viewData: LView): void {
if (instruction !== null) { if (instruction !== null) {
viewData[BINDING_INDEX] = bindingRootIndex; viewData[BINDING_INDEX] = bindingRootIndex;
instruction( instruction(
RenderFlags.Update, unwrapOnChangesDirectiveWrapper(viewData[currentDirectiveIndex]), RenderFlags.Update, readElementValue(viewData[currentDirectiveIndex]),
currentElementIndex); currentElementIndex);
} }
currentDirectiveIndex++; currentDirectiveIndex++;
@ -726,7 +725,6 @@ export function createTView(
expandoStartIndex: initialViewLength, expandoStartIndex: initialViewLength,
expandoInstructions: null, expandoInstructions: null,
firstTemplatePass: true, firstTemplatePass: true,
changesHooks: null,
initHooks: null, initHooks: null,
checkHooks: null, checkHooks: null,
contentHooks: null, contentHooks: null,
@ -961,18 +959,16 @@ function listenerInternal(
const propsLength = props.length; const propsLength = props.length;
if (propsLength) { if (propsLength) {
const lCleanup = getCleanup(lView); const lCleanup = getCleanup(lView);
// Subscribe to listeners for each output, and setup clean up for each. for (let i = 0; i < propsLength; i += 2) {
for (let i = 0; i < propsLength;) { const index = props[i] as number;
const directiveIndex = props[i++] as number; ngDevMode && assertDataInRange(lView, index);
const minifiedName = props[i++] as string; const minifiedName = props[i + 1];
const declaredName = props[i++] as string; const directiveInstance = lView[index];
ngDevMode && assertDataInRange(lView, directiveIndex as number); const output = directiveInstance[minifiedName];
const directive = unwrapOnChangesDirectiveWrapper(lView[directiveIndex]);
const output = directive[minifiedName];
if (ngDevMode && !isObservable(output)) { if (ngDevMode && !isObservable(output)) {
throw new Error( throw new Error(
`@Output ${minifiedName} not initialized in '${directive.constructor.name}'.`); `@Output ${minifiedName} not initialized in '${directiveInstance.constructor.name}'.`);
} }
const subscription = output.subscribe(listenerFn); const subscription = output.subscribe(listenerFn);
@ -1042,7 +1038,7 @@ export function elementEnd(): void {
if (hasClassInput(previousOrParentTNode)) { if (hasClassInput(previousOrParentTNode)) {
const stylingContext = getStylingContext(previousOrParentTNode.index, lView); const stylingContext = getStylingContext(previousOrParentTNode.index, lView);
setInputsForProperty( setInputsForProperty(
lView, previousOrParentTNode.inputs !, 'class', getInitialClassNameValue(stylingContext)); lView, previousOrParentTNode.inputs !['class'] !, getInitialClassNameValue(stylingContext));
} }
} }
@ -1137,7 +1133,7 @@ function elementPropertyInternal<T>(
let dataValue: PropertyAliasValue|undefined; let dataValue: PropertyAliasValue|undefined;
if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) && if (!nativeOnly && (inputData = initializeTNodeInputs(tNode)) &&
(dataValue = inputData[propName])) { (dataValue = inputData[propName])) {
setInputsForProperty(lView, inputData, propName, value); setInputsForProperty(lView, dataValue, value);
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
if (ngDevMode) { if (ngDevMode) {
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) { if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
@ -1215,30 +1211,21 @@ export function createTNode(
* @param lView the `LView` which contains the directives. * @param lView the `LView` which contains the directives.
* @param inputAliases mapping between the public "input" name and privately-known, * @param inputAliases mapping between the public "input" name and privately-known,
* possibly minified, property names to write to. * possibly minified, property names to write to.
* @param publicName public binding name. (This is the `<div [publicName]=value>`)
* @param value Value to set. * @param value Value to set.
*/ */
function setInputsForProperty( function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: any): void {
lView: LView, inputAliases: PropertyAliases, publicName: string, value: any): void { for (let i = 0; i < inputs.length; i += 2) {
const inputs = inputAliases[publicName]; ngDevMode && assertDataInRange(lView, inputs[i] as number);
for (let i = 0; i < inputs.length;) { lView[inputs[i] as number][inputs[i + 1]] = value;
const directiveIndex = inputs[i++] as number;
const privateName = inputs[i++] as string;
const declaredName = inputs[i++] as string;
ngDevMode && assertDataInRange(lView, directiveIndex);
recordChangeAndUpdateProperty(lView[directiveIndex], declaredName, privateName, value);
} }
} }
function setNgReflectProperties( function setNgReflectProperties(
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue, lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
value: any) { value: any) {
for (let i = 0; i < inputs.length;) { for (let i = 0; i < inputs.length; i += 2) {
const directiveIndex = inputs[i++] as number;
const privateName = inputs[i++] as string;
const declaredName = inputs[i++] as string;
const renderer = lView[RENDERER]; const renderer = lView[RENDERER];
const attrName = normalizeDebugBindingName(privateName); const attrName = normalizeDebugBindingName(inputs[i + 1] as string);
const debugValue = normalizeDebugBindingValue(value); const debugValue = normalizeDebugBindingValue(value);
if (type === TNodeType.Element) { if (type === TNodeType.Element) {
isProceduralRenderer(renderer) ? isProceduralRenderer(renderer) ?
@ -1274,20 +1261,15 @@ function generatePropertyAliases(tNode: TNode, direction: BindingDirection): Pro
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
const directiveDef = defs[i] as DirectiveDef<any>; const directiveDef = defs[i] as DirectiveDef<any>;
const publicToMinifiedNames: {[publicName: string]: string} = const propertyAliasMap: {[publicName: string]: string} =
isInput ? directiveDef.inputs : directiveDef.outputs; isInput ? directiveDef.inputs : directiveDef.outputs;
const publicToDeclaredNames: {[publicName: string]: string}|null = for (let publicName in propertyAliasMap) {
isInput ? directiveDef.declaredInputs : null; if (propertyAliasMap.hasOwnProperty(publicName)) {
for (let publicName in publicToMinifiedNames) {
if (publicToMinifiedNames.hasOwnProperty(publicName)) {
propStore = propStore || {}; propStore = propStore || {};
const minifiedName = publicToMinifiedNames[publicName]; const internalName = propertyAliasMap[publicName];
const declaredName = const hasProperty = propStore.hasOwnProperty(publicName);
publicToDeclaredNames ? publicToDeclaredNames[publicName] : minifiedName; hasProperty ? propStore[publicName].push(i, internalName) :
const aliases: PropertyAliasValue = propStore.hasOwnProperty(publicName) ? (propStore[publicName] = [i, internalName]);
propStore[publicName] :
propStore[publicName] = [];
aliases.push(i, minifiedName, declaredName);
} }
} }
} }
@ -1514,7 +1496,7 @@ export function elementStylingMap<T>(
const initialClasses = getInitialClassNameValue(stylingContext); const initialClasses = getInitialClassNameValue(stylingContext);
const classInputVal = const classInputVal =
(initialClasses.length ? (initialClasses + ' ') : '') + (classes as string); (initialClasses.length ? (initialClasses + ' ') : '') + (classes as string);
setInputsForProperty(lView, tNode.inputs !, 'class', classInputVal); setInputsForProperty(lView, tNode.inputs !['class'] !, classInputVal);
} else { } else {
updateStylingMap(stylingContext, classes, styles); updateStylingMap(stylingContext, classes, styles);
} }
@ -1647,7 +1629,6 @@ function instantiateAllDirectives(tView: TView, lView: LView, tNode: TNode) {
addComponentLogic(lView, tNode, def as ComponentDef<any>); addComponentLogic(lView, tNode, def as ComponentDef<any>);
} }
const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode); const directive = getNodeInjectable(tView.data, lView !, i, tNode as TElementNode);
postProcessDirective(lView, directive, def, i); postProcessDirective(lView, directive, def, i);
} }
} }
@ -1659,7 +1640,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod
const firstTemplatePass = getFirstTemplatePass(); const firstTemplatePass = getFirstTemplatePass();
for (let i = start; i < end; i++) { for (let i = start; i < end; i++) {
const def = tView.data[i] as DirectiveDef<any>; const def = tView.data[i] as DirectiveDef<any>;
const directive = unwrapOnChangesDirectiveWrapper(viewData[i]); const directive = viewData[i];
if (def.hostBindings) { if (def.hostBindings) {
const previousExpandoLength = expando.length; const previousExpandoLength = expando.length;
setCurrentDirectiveDef(def); setCurrentDirectiveDef(def);
@ -1716,17 +1697,12 @@ function prefillHostVars(tView: TView, lView: LView, totalHostVars: number): voi
* Process a directive on the current node after its creation. * Process a directive on the current node after its creation.
*/ */
function postProcessDirective<T>( function postProcessDirective<T>(
lView: LView, directive: T, def: DirectiveDef<T>, directiveDefIdx: number): void { viewData: LView, directive: T, def: DirectiveDef<T>, directiveDefIdx: number): void {
if (def.onChanges) {
// We have onChanges, wrap it so that we can track changes.
lView[directiveDefIdx] = new OnChangesDirectiveWrapper(lView[directiveDefIdx]);
}
const previousOrParentTNode = getPreviousOrParentTNode(); const previousOrParentTNode = getPreviousOrParentTNode();
postProcessBaseDirective(lView, previousOrParentTNode, directive, def); postProcessBaseDirective(viewData, previousOrParentTNode, directive, def);
ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode'); ngDevMode && assertDefined(previousOrParentTNode, 'previousOrParentTNode');
if (previousOrParentTNode && previousOrParentTNode.attrs) { if (previousOrParentTNode && previousOrParentTNode.attrs) {
setInputsFromAttrs(lView, directiveDefIdx, def, previousOrParentTNode); setInputsFromAttrs(directiveDefIdx, directive, def.inputs, previousOrParentTNode);
} }
if (def.contentQueries) { if (def.contentQueries) {
@ -1734,7 +1710,7 @@ function postProcessDirective<T>(
} }
if (isComponentDef(def)) { if (isComponentDef(def)) {
const componentView = getComponentViewByIndex(previousOrParentTNode.index, lView); const componentView = getComponentViewByIndex(previousOrParentTNode.index, viewData);
componentView[CONTEXT] = directive; componentView[CONTEXT] = directive;
} }
} }
@ -1927,53 +1903,20 @@ function addComponentLogic<T>(
* @param tNode The static data for this node * @param tNode The static data for this node
*/ */
function setInputsFromAttrs<T>( function setInputsFromAttrs<T>(
lView: LView, directiveIndex: number, def: DirectiveDef<any>, tNode: TNode): void { directiveIndex: number, instance: T, inputs: {[P in keyof T]: string;}, tNode: TNode): void {
let initialInputData = tNode.initialInputs as InitialInputData | undefined; let initialInputData = tNode.initialInputs as InitialInputData | undefined;
if (initialInputData === undefined || directiveIndex >= initialInputData.length) { if (initialInputData === undefined || directiveIndex >= initialInputData.length) {
initialInputData = generateInitialInputs(directiveIndex, def, tNode); initialInputData = generateInitialInputs(directiveIndex, inputs, tNode);
} }
const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; const initialInputs: InitialInputs|null = initialInputData[directiveIndex];
if (initialInputs) { if (initialInputs) {
const directiveOrWrappedDirective = lView[directiveIndex]; for (let i = 0; i < initialInputs.length; i += 2) {
(instance as any)[initialInputs[i]] = initialInputs[i + 1];
for (let i = 0; i < initialInputs.length;) {
const privateName = initialInputs[i++];
const declaredName = initialInputs[i++];
const attrValue = initialInputs[i++];
recordChangeAndUpdateProperty(
directiveOrWrappedDirective, declaredName, privateName, attrValue);
} }
} }
} }
/**
* Checks to see if the instanced passed as `directiveOrWrappedDirective` is wrapped in {@link
* OnChangesDirectiveWrapper} or not.
* If it is, it will update the related {@link SimpleChanges} object with the change to signal
* `ngOnChanges` hook
* should fire, then it will unwrap the instance. After that, it will set the property with the key
* provided
* in `privateName` on the instance with the passed value.
* @param directiveOrWrappedDirective The directive instance or a directive instance wrapped in
* {@link OnChangesDirectiveWrapper}
* @param declaredName The original, declared name of the property to update.
* @param privateName The private, possibly minified name of the property to update.
* @param value The value to update the property with.
*/
function recordChangeAndUpdateProperty<T, K extends keyof T>(
directiveOrWrappedDirective: OnChangesDirectiveWrapper<T>| T, declaredName: string,
privateName: K, value: any) {
let instance: T;
if (isOnChangesDirectiveWrapper(directiveOrWrappedDirective)) {
instance = unwrapOnChangesDirectiveWrapper(directiveOrWrappedDirective);
recordChange(directiveOrWrappedDirective, declaredName, value);
} else {
instance = directiveOrWrappedDirective;
}
instance[privateName] = value;
}
/** /**
* Generates initialInputData for a node and stores it in the template's static storage * Generates initialInputData for a node and stores it in the template's static storage
* so subsequent template invocations don't have to recalculate it. * so subsequent template invocations don't have to recalculate it.
@ -1990,7 +1933,7 @@ function recordChangeAndUpdateProperty<T, K extends keyof T>(
* @param tNode The static data on this node * @param tNode The static data on this node
*/ */
function generateInitialInputs( function generateInitialInputs(
directiveIndex: number, directiveDef: DirectiveDef<any>, tNode: TNode): InitialInputData { directiveIndex: number, inputs: {[key: string]: string}, tNode: TNode): InitialInputData {
const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []); const initialInputData: InitialInputData = tNode.initialInputs || (tNode.initialInputs = []);
initialInputData[directiveIndex] = null; initialInputData[directiveIndex] = null;
@ -2007,14 +1950,13 @@ function generateInitialInputs(
i += 4; i += 4;
continue; continue;
} }
const privateName = directiveDef.inputs[attrName]; const minifiedInputName = inputs[attrName];
const declaredName = directiveDef.declaredInputs[attrName];
const attrValue = attrs[i + 1]; const attrValue = attrs[i + 1];
if (privateName !== undefined) { if (minifiedInputName !== undefined) {
const inputsToStore: InitialInputs = const inputsToStore: InitialInputs =
initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []);
inputsToStore.push(privateName, declaredName, attrValue as string); inputsToStore.push(minifiedInputName, attrValue as string);
} }
i += 2; i += 2;

View File

@ -6,9 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {SimpleChanges, ViewEncapsulation} from '../../core'; import {ViewEncapsulation} from '../../core';
import {Type} from '../../interface/type'; import {Type} from '../../interface/type';
import {CssSelectorList} from './projection'; import {CssSelectorList} from './projection';
@ -143,7 +142,6 @@ export interface DirectiveDef<T> extends BaseDef<T> {
/* The following are lifecycle hooks for this component */ /* The following are lifecycle hooks for this component */
onInit: (() => void)|null; onInit: (() => void)|null;
doCheck: (() => void)|null; doCheck: (() => void)|null;
onChanges: ((changes: SimpleChanges) => void)|null;
afterContentInit: (() => void)|null; afterContentInit: (() => void)|null;
afterContentChecked: (() => void)|null; afterContentChecked: (() => void)|null;
afterViewInit: (() => void)|null; afterViewInit: (() => void)|null;

View File

@ -468,12 +468,10 @@ export type PropertyAliases = {
/** /**
* Store the runtime input or output names for all the directives. * Store the runtime input or output names for all the directives.
* *
* Values are stored in triplets: * - Even indices: directive index
* - i + 0: directive index * - Odd indices: minified / internal name
* - i + 1: minified / internal name
* - i + 2: declared name
* *
* e.g. [0, 'minifiedName', 'declaredPropertyName'] * e.g. [0, 'change-minified']
*/ */
export type PropertyAliasValue = (number | string)[]; export type PropertyAliasValue = (number | string)[];
@ -501,12 +499,10 @@ export type InitialInputData = (InitialInputs | null)[];
* Used by InitialInputData to store input properties * Used by InitialInputData to store input properties
* that should be set once from attributes. * that should be set once from attributes.
* *
* The inputs come in triplets of: * Even indices: minified/internal input name
* i + 0: minified/internal input name * Odd indices: initial value
* i + 1: declared input name (needed for OnChanges)
* i + 2: initial value
* *
* e.g. ['minifiedName', 'declaredName', 'value'] * e.g. ['role-min', 'button']
*/ */
export type InitialInputs = string[]; export type InitialInputs = string[];

View File

@ -534,7 +534,7 @@ export interface RootContext {
* Even indices: Directive index * Even indices: Directive index
* Odd indices: Hook function * Odd indices: Hook function
*/ */
export type HookData = (number | (() => void) | ((changes: SimpleChanges) => void))[]; export type HookData = (number | (() => void))[];
/** /**
* Static data that corresponds to the instance-specific data array on an LView. * Static data that corresponds to the instance-specific data array on an LView.

View File

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

View File

@ -31,6 +31,7 @@ export const angularCoreEnv: {[name: string]: Function} = {
'inject': inject, 'inject': inject,
'ɵinjectAttribute': r3.injectAttribute, 'ɵinjectAttribute': r3.injectAttribute,
'ɵtemplateRefExtractor': r3.templateRefExtractor, 'ɵtemplateRefExtractor': r3.templateRefExtractor,
'ɵNgOnChangesFeature': r3.NgOnChangesFeature,
'ɵProvidersFeature': r3.ProvidersFeature, 'ɵProvidersFeature': r3.ProvidersFeature,
'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature, 'ɵInheritDefinitionFeature': r3.InheritDefinitionFeature,
'ɵelementAttribute': r3.elementAttribute, 'ɵelementAttribute': r3.elementAttribute,

View File

@ -1,71 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChange, SimpleChanges} from '../interface/simple_change';
type Constructor<T> = new (...args: any[]) => T;
/**
* Checks an object to see if it's an exact instance of a particular type
* without traversing the inheritance hierarchy like `instanceof` does.
* @param obj The object to check
* @param type The type to check the object against
*/
export function isExactInstanceOf<T>(obj: any, type: Constructor<T>): obj is T {
return obj != null && typeof obj == 'object' && Object.getPrototypeOf(obj) == type.prototype;
}
/**
* Checks to see if an object is an instance of {@link OnChangesDirectiveWrapper}
* @param obj the object to check (generally from `LView`)
*/
export function isOnChangesDirectiveWrapper(obj: any): obj is OnChangesDirectiveWrapper<any> {
return isExactInstanceOf(obj, OnChangesDirectiveWrapper);
}
/**
* Removes the `OnChangesDirectiveWrapper` if present.
*
* @param obj to unwrap.
*/
export function unwrapOnChangesDirectiveWrapper<T>(obj: T | OnChangesDirectiveWrapper<T>): T {
return isOnChangesDirectiveWrapper(obj) ? obj.instance : obj;
}
/**
* A class that wraps directive instances for storage in LView when directives
* have onChanges hooks to deal with.
*/
export class OnChangesDirectiveWrapper<T = any> {
seenProps = new Set<string>();
previous: SimpleChanges = {};
changes: SimpleChanges|null = null;
constructor(public instance: T) {}
}
/**
* Updates the `changes` property on the `wrapper` instance, such that when it's
* checked in {@link callHooks} it will fire the related `onChanges` hook.
* @param wrapper the wrapper for the directive instance
* @param declaredName the declared name to be used in `SimpleChange`
* @param value The new value for the property
*/
export function recordChange(wrapper: OnChangesDirectiveWrapper, declaredName: string, value: any) {
const simpleChanges = wrapper.changes || (wrapper.changes = {});
const firstChange = !wrapper.seenProps.has(declaredName);
if (firstChange) {
wrapper.seenProps.add(declaredName);
}
const previous = wrapper.previous;
const previousValue: SimpleChange|undefined = previous[declaredName];
simpleChanges[declaredName] = new SimpleChange(
firstChange ? undefined : previousValue && previousValue.currentValue, value, firstChange);
}

View File

@ -17,7 +17,6 @@ import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './inte
import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer'; import {GlobalTargetName, GlobalTargetResolver, RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling'; import {StylingContext} from './interfaces/styling';
import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view'; import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view';
import {isOnChangesDirectiveWrapper} from './onchanges_util';
/** /**
@ -71,14 +70,9 @@ export function flatten(list: any[]): any[] {
/** Retrieves a value from any `LView` or `TData`. */ /** Retrieves a value from any `LView` or `TData`. */
export function loadInternal<T>(view: LView | TData, index: number): T { export function loadInternal<T>(view: LView | TData, index: number): T {
ngDevMode && assertDataInRange(view, index + HEADER_OFFSET); ngDevMode && assertDataInRange(view, index + HEADER_OFFSET);
const record = view[index + HEADER_OFFSET]; return view[index + HEADER_OFFSET];
// If we're storing an array because of a directive or component with ngOnChanges,
// return the directive or component instance.
return isOnChangesDirectiveWrapper(record) ? record.instance : record;
} }
/** /**
* Takes the value of a slot in `LView` and returns the element node. * Takes the value of a slot in `LView` and returns the element node.
* *
@ -297,4 +291,4 @@ export function resolveDocument(element: RElement & {ownerDocument: Document}) {
export function resolveBody(element: RElement & {ownerDocument: Document}) { export function resolveBody(element: RElement & {ownerDocument: Document}) {
return {name: 'body', target: element.ownerDocument.body}; return {name: 'body', target: element.ownerDocument.body};
} }

View File

@ -11,11 +11,10 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref'; import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref'; import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {checkNoChanges, checkNoChangesInRootView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions'; import {checkNoChanges, checkNoChangesInRootView, checkView, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn, viewAttached} from './instructions';
import {TNode, TNodeType, TViewNode} from './interfaces/node'; import {TNode, TNodeType, TViewNode} from './interfaces/node';
import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT} from './interfaces/view'; import {FLAGS, HOST, HOST_NODE, LView, LViewFlags, PARENT, RENDERER_FACTORY} from './interfaces/view';
import {destroyLView} from './node_manipulation'; import {destroyLView} from './node_manipulation';
import {unwrapOnChangesDirectiveWrapper} from './onchanges_util';
import {getNativeByTNode} from './util'; import {getNativeByTNode} from './util';
@ -272,8 +271,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
} }
private _lookUpContext(): T { private _lookUpContext(): T {
return this._context = return this._context = this._lView[PARENT] ![this._componentIndex] as T;
unwrapOnChangesDirectiveWrapper(this._lView[PARENT] ![this._componentIndex] as T);
} }
} }

View File

@ -80,21 +80,24 @@
{ {
"name": "NO_PARENT_INJECTOR" "name": "NO_PARENT_INJECTOR"
}, },
{
"name": "NgOnChangesFeature"
},
{ {
"name": "NodeInjectorFactory" "name": "NodeInjectorFactory"
}, },
{ {
"name": "ObjectUnsubscribedErrorImpl" "name": "ObjectUnsubscribedErrorImpl"
}, },
{
"name": "OnChangesDirectiveWrapper"
},
{ {
"name": "PARENT" "name": "PARENT"
}, },
{ {
"name": "PARENT_INJECTOR" "name": "PARENT_INJECTOR"
}, },
{
"name": "PRIVATE_PREFIX"
},
{ {
"name": "RENDERER" "name": "RENDERER"
}, },
@ -104,6 +107,9 @@
{ {
"name": "SANITIZER" "name": "SANITIZER"
}, },
{
"name": "SimpleChange"
},
{ {
"name": "TVIEW" "name": "TVIEW"
}, },
@ -326,15 +332,9 @@
{ {
"name": "isCreationMode" "name": "isCreationMode"
}, },
{
"name": "isExactInstanceOf"
},
{ {
"name": "isFactory" "name": "isFactory"
}, },
{
"name": "isOnChangesDirectiveWrapper"
},
{ {
"name": "isProceduralRenderer" "name": "isProceduralRenderer"
}, },
@ -368,6 +368,9 @@
{ {
"name": "noSideEffects" "name": "noSideEffects"
}, },
{
"name": "onChangesWrapper"
},
{ {
"name": "postProcessBaseDirective" "name": "postProcessBaseDirective"
}, },
@ -446,9 +449,6 @@
{ {
"name": "tickRootContext" "name": "tickRootContext"
}, },
{
"name": "unwrapOnChangesDirectiveWrapper"
},
{ {
"name": "updateViewQuery" "name": "updateViewQuery"
}, },

View File

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

View File

@ -143,6 +143,9 @@
{ {
"name": "NgModuleRef" "name": "NgModuleRef"
}, },
{
"name": "NgOnChangesFeature"
},
{ {
"name": "NodeInjector" "name": "NodeInjector"
}, },
@ -152,9 +155,6 @@
{ {
"name": "ObjectUnsubscribedErrorImpl" "name": "ObjectUnsubscribedErrorImpl"
}, },
{
"name": "OnChangesDirectiveWrapper"
},
{ {
"name": "Optional" "name": "Optional"
}, },
@ -167,6 +167,9 @@
{ {
"name": "PARENT_INJECTOR" "name": "PARENT_INJECTOR"
}, },
{
"name": "PRIVATE_PREFIX"
},
{ {
"name": "QUERIES" "name": "QUERIES"
}, },
@ -908,9 +911,6 @@
{ {
"name": "isDirty" "name": "isDirty"
}, },
{
"name": "isExactInstanceOf"
},
{ {
"name": "isFactory" "name": "isFactory"
}, },
@ -929,9 +929,6 @@
{ {
"name": "isNodeMatchingSelectorList" "name": "isNodeMatchingSelectorList"
}, },
{
"name": "isOnChangesDirectiveWrapper"
},
{ {
"name": "isPositive" "name": "isPositive"
}, },
@ -1019,6 +1016,9 @@
{ {
"name": "noSideEffects" "name": "noSideEffects"
}, },
{
"name": "onChangesWrapper"
},
{ {
"name": "pointers" "name": "pointers"
}, },
@ -1049,12 +1049,6 @@
{ {
"name": "readPatchedLView" "name": "readPatchedLView"
}, },
{
"name": "recordChange"
},
{
"name": "recordChangeAndUpdateProperty"
},
{ {
"name": "reference" "name": "reference"
}, },
@ -1232,9 +1226,6 @@
{ {
"name": "trackByIdentity" "name": "trackByIdentity"
}, },
{
"name": "unwrapOnChangesDirectiveWrapper"
},
{ {
"name": "updateClassProp" "name": "updateClassProp"
}, },

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import {ElementRef, QueryList, ViewContainerRef} from '@angular/core'; import {ElementRef, QueryList, ViewContainerRef} from '@angular/core';
import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature} from '../../src/render3/index'; import {AttributeMarker, defineComponent, template, defineDirective, InheritDefinitionFeature, ProvidersFeature, NgOnChangesFeature} from '../../src/render3/index';
import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions'; import {allocHostVars, bind, directiveInject, element, elementAttribute, elementEnd, elementProperty, elementStyleProp, elementStyling, elementStylingApply, elementStart, listener, load, text, textBinding, loadQueryList, registerContentQuery, elementHostAttrs} from '../../src/render3/instructions';
import {query, queryRefresh} from '../../src/render3/query'; import {query, queryRefresh} from '../../src/render3/query';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -357,6 +357,7 @@ describe('host bindings', () => {
template: (rf: RenderFlags, ctx: InitHookComp) => {}, template: (rf: RenderFlags, ctx: InitHookComp) => {},
consts: 0, consts: 0,
vars: 0, vars: 0,
features: [NgOnChangesFeature],
hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => { hostBindings: (rf: RenderFlags, ctx: InitHookComp, elIndex: number) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
allocHostVars(1); allocHostVars(1);

View File

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

View File

@ -14,6 +14,7 @@ import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def'; import {NgIf} from './common_with_def';
import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util'; import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util';
import { fixmeIvy } from '@angular/private/testing';
describe('lifecycles', () => { describe('lifecycles', () => {
@ -1940,6 +1941,7 @@ describe('lifecycles', () => {
}); });
fixmeIvy('FW-956: refactor onChanges').
describe('onChanges', () => { describe('onChanges', () => {
let events: ({type: string, name: string, [key: string]: any})[]; let events: ({type: string, name: string, [key: string]: any})[];
@ -2699,6 +2701,7 @@ describe('lifecycles', () => {
}); });
fixmeIvy('FW-956: refactor onChanges').
describe('hook order', () => { describe('hook order', () => {
let events: string[]; let events: string[];

View File

@ -0,0 +1,327 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DoCheck, OnChanges, SimpleChange, SimpleChanges} from '../../src/core';
import {InheritDefinitionFeature} from '../../src/render3/features/inherit_definition_feature';
import {DirectiveDef, NgOnChangesFeature, defineDirective} from '../../src/render3/index';
import { fixmeIvy } from '@angular/private/testing';
fixmeIvy('FW-956: refactor onChanges').
describe('NgOnChangesFeature', () => {
it('should patch class', () => {
class MyDirective implements OnChanges, DoCheck {
public log: Array<string|SimpleChange> = [];
public valA: string = 'initValue';
public set valB(value: string) { this.log.push(value); }
public get valB() { return 'works'; }
ngDoCheck(): void { this.log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
this.log.push('ngOnChanges');
this.log.push('valA', changes['valA']);
this.log.push('valB', changes['valB']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(myDir.log).toEqual(['second']);
expect(myDir.valB).toEqual('works');
myDir.log.length = 0;
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(myDir.log).toEqual(['ngOnChanges', 'valA', changeA, 'valB', changeB, 'ngDoCheck']);
});
it('should inherit the behavior from super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
set valB(value: string) { log.push(value); }
get valB() { return 'works'; }
ngDoCheck(): void { log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
log.push('ngOnChanges');
log.push('valA', changes['valA']);
log.push('valB', changes['valB']);
log.push('valC', changes['valC']);
}
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'},
});
}
class SubDirective extends SuperDirective {
valC = 'initValue';
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valC: 'valC'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(myDir.valB).toEqual('works');
myDir.valC = 'third';
expect(myDir.valC).toEqual('third');
log.length = 0;
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
const changeC = new SimpleChange(undefined, 'third', true);
expect(log).toEqual(
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
});
it('should not run the parent doCheck if it is not called explicitly on super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
ngDoCheck(): void { log.push('ERROR: Child overrides it without super call'); }
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA'},
});
}
class SubDirective extends SuperDirective implements DoCheck {
valB = 'initValue';
ngDoCheck(): void { log.push('sub ngDoCheck'); }
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valB: 'valB'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(log).toEqual([changeA, changeB, 'sub ngDoCheck']);
});
it('should run the parent doCheck if it is inherited from super class', () => {
const log: any[] = [];
class SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
ngDoCheck(): void { log.push('super ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void { log.push(changes.valA, changes.valB); }
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'superDir', '']],
factory: () => new SuperDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA'},
});
}
class SubDirective extends SuperDirective implements DoCheck {
valB = 'initValue';
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'subDir', '']],
factory: () => new SubDirective(),
features: [InheritDefinitionFeature],
inputs: {valB: 'valB'},
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
expect(log).toEqual([changeA, changeB, 'super ngDoCheck']);
});
it('should apply the feature to inherited properties if on sub class', () => {
const log: any[] = [];
class SuperDirective {
valC = 'initValue';
static ngDirectiveDef = defineDirective({
type: SuperDirective,
selectors: [['', 'subDir', '']],
factory: () => new SuperDirective(),
features: [],
inputs: {valC: 'valC'},
});
}
class SubDirective extends SuperDirective implements OnChanges, DoCheck {
valA = 'initValue';
set valB(value: string) { log.push(value); }
get valB() { return 'works'; }
ngDoCheck(): void { log.push('ngDoCheck'); }
ngOnChanges(changes: SimpleChanges): void {
log.push('ngOnChanges');
log.push('valA', changes['valA']);
log.push('valB', changes['valB']);
log.push('valC', changes['valC']);
}
static ngDirectiveDef = defineDirective({
type: SubDirective,
selectors: [['', 'superDir', '']],
factory: () => new SubDirective(),
// Inheritance must always be before OnChanges feature.
features: [
InheritDefinitionFeature,
NgOnChangesFeature,
],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).factory(null) as SubDirective;
myDir.valA = 'first';
expect(myDir.valA).toEqual('first');
myDir.valB = 'second';
expect(log).toEqual(['second']);
expect(myDir.valB).toEqual('works');
myDir.valC = 'third';
expect(myDir.valC).toEqual('third');
log.length = 0;
(SubDirective.ngDirectiveDef as DirectiveDef<SubDirective>).doCheck !.call(myDir);
const changeA = new SimpleChange(undefined, 'first', true);
const changeB = new SimpleChange(undefined, 'second', true);
const changeC = new SimpleChange(undefined, 'third', true);
expect(log).toEqual(
['ngOnChanges', 'valA', changeA, 'valB', changeB, 'valC', changeC, 'ngDoCheck']);
});
it('correctly computes firstChange', () => {
class MyDirective implements OnChanges {
public log: Array<string|SimpleChange|undefined> = [];
public valA: string = 'initValue';
// TODO(issue/24571): remove '!'.
public valB !: string;
ngOnChanges(changes: SimpleChanges): void {
this.log.push('valA', changes['valA']);
this.log.push('valB', changes['valB']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {valA: 'valA', valB: 'valB'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.valA = 'first';
myDir.valB = 'second';
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA1 = new SimpleChange(undefined, 'first', true);
const changeB1 = new SimpleChange(undefined, 'second', true);
expect(myDir.log).toEqual(['valA', changeA1, 'valB', changeB1]);
myDir.log.length = 0;
myDir.valA = 'third';
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeA2 = new SimpleChange('first', 'third', false);
expect(myDir.log).toEqual(['valA', changeA2, 'valB', undefined]);
});
it('should not create a getter when only a setter is originally defined', () => {
class MyDirective implements OnChanges {
public log: Array<string|SimpleChange> = [];
public set onlySetter(value: string) { this.log.push(value); }
ngOnChanges(changes: SimpleChanges): void {
this.log.push('ngOnChanges');
this.log.push('onlySetter', changes['onlySetter']);
}
static ngDirectiveDef = defineDirective({
type: MyDirective,
selectors: [['', 'myDir', '']],
factory: () => new MyDirective(),
features: [NgOnChangesFeature],
inputs: {onlySetter: 'onlySetter'}
});
}
const myDir =
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).factory(null) as MyDirective;
myDir.onlySetter = 'someValue';
expect(myDir.onlySetter).toBeUndefined();
(MyDirective.ngDirectiveDef as DirectiveDef<MyDirective>).doCheck !.call(myDir);
const changeSetter = new SimpleChange(undefined, 'someValue', true);
expect(myDir.log).toEqual(['someValue', 'ngOnChanges', 'onlySetter', changeSetter]);
});
});

View File

@ -8,7 +8,7 @@
import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core'; import {ChangeDetectorRef, Component as _Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, NgModuleRef, Pipe, PipeTransform, QueryList, RendererFactory2, TemplateRef, ViewContainerRef, createInjector, defineInjector, ɵAPP_ROOT as APP_ROOT, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {ViewEncapsulation} from '../../src/metadata'; import {ViewEncapsulation} from '../../src/metadata';
import {AttributeMarker, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index'; import {AttributeMarker, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, definePipe, injectComponentFactoryResolver, load, query, queryRefresh} from '../../src/render3/index';
import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding, elementHostAttrs} from '../../src/render3/instructions'; import {allocHostVars, bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation3, nextContext, projection, projectionDef, reference, template, text, textBinding, elementHostAttrs} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
@ -1631,6 +1631,7 @@ describe('ViewContainerRef', () => {
textBinding(0, interpolation1('', cmp.name, '')); textBinding(0, interpolation1('', cmp.name, ''));
} }
}, },
features: [NgOnChangesFeature],
inputs: {name: 'name'} inputs: {name: 'name'}
}); });
} }
@ -1795,13 +1796,12 @@ describe('ViewContainerRef', () => {
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>'); expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks></hooks><hooks>B</hooks>');
expect(log).toEqual([]); expect(log).toEqual([]);
// Below will *NOT* cause onChanges to fire, because only bindings trigger onChanges
componentRef.instance.name = 'D'; componentRef.instance.name = 'D';
log.length = 0; log.length = 0;
fixture.update(); fixture.update();
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>'); expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>');
expect(log).toEqual([ expect(log).toEqual([
'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D', 'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A', 'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A',
'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B' 'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
]); ]);

View File

@ -9,7 +9,6 @@
import {SecurityContext} from '@angular/core'; import {SecurityContext} from '@angular/core';
import {ArgumentType, BindingFlags, NodeCheckFn, NodeFlags, Services, ViewData, ViewFlags, ViewState, asElementData, directiveDef, elementDef, rootRenderNodes} from '@angular/core/src/view/index'; import {ArgumentType, BindingFlags, NodeCheckFn, NodeFlags, Services, ViewData, ViewFlags, ViewState, asElementData, directiveDef, elementDef, rootRenderNodes} from '@angular/core/src/view/index';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {fixmeIvy} from '@angular/private/testing';
import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper'; import {callMostRecentEventListenerHandler, compViewDef, createAndGetRootNodes, createRootView, isBrowser, recordNodeToRemove} from './helper';

View File

@ -11,7 +11,6 @@ import {getDebugContext} from '@angular/core/src/errors';
import {BindingFlags, NodeFlags, Services, ViewData, ViewDefinition, asElementData, elementDef} from '@angular/core/src/view/index'; import {BindingFlags, NodeFlags, Services, ViewData, ViewDefinition, asElementData, elementDef} from '@angular/core/src/view/index';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {fixmeIvy} from '@angular/private/testing';
import {ARG_TYPE_VALUES, callMostRecentEventListenerHandler, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes, isBrowser, recordNodeToRemove} from './helper'; import {ARG_TYPE_VALUES, callMostRecentEventListenerHandler, checkNodeInlineOrDynamic, compViewDef, createAndGetRootNodes, isBrowser, recordNodeToRemove} from './helper';
@ -185,7 +184,6 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
return result; return result;
} }
it('should listen to DOM events', () => { it('should listen to DOM events', () => {
const handleEventSpy = jasmine.createSpy('handleEvent'); const handleEventSpy = jasmine.createSpy('handleEvent');
const removeListenerSpy = const removeListenerSpy =
@ -253,7 +251,6 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
expect(removeListenerSpy).toHaveBeenCalled(); expect(removeListenerSpy).toHaveBeenCalled();
}); });
it('should preventDefault only if the handler returns false', () => { it('should preventDefault only if the handler returns false', () => {
let eventHandlerResult: any; let eventHandlerResult: any;
let preventDefaultSpy: jasmine.Spy = undefined !; let preventDefaultSpy: jasmine.Spy = undefined !;
@ -282,7 +279,6 @@ const removeEventListener = '__zone_symbol__removeEventListener' as 'removeEvent
expect(preventDefaultSpy).toHaveBeenCalled(); expect(preventDefaultSpy).toHaveBeenCalled();
}); });
it('should report debug info on event errors', () => { it('should report debug info on event errors', () => {
const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError'); const handleErrorSpy = spyOn(TestBed.get(ErrorHandler), 'handleError');
const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough(); const addListenerSpy = spyOn(HTMLElement.prototype, addEventListener).and.callThrough();

View File

@ -315,235 +315,239 @@ withEachNg1Version(() => {
}); });
})); }));
it('should bind properties, events', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); .it('should bind properties, events', async(() => {
const ng1Module = const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
angular.module('ng1', []).value($EXCEPTION_HANDLER, (err: any) => { throw err; }); const ng1Module = angular.module('ng1', []).value(
$EXCEPTION_HANDLER, (err: any) => { throw err; });
ng1Module.run(($rootScope: any) => { ng1Module.run(($rootScope: any) => {
$rootScope.name = 'world'; $rootScope.name = 'world';
$rootScope.dataA = 'A'; $rootScope.dataA = 'A';
$rootScope.dataB = 'B'; $rootScope.dataB = 'B';
$rootScope.modelA = 'initModelA'; $rootScope.modelA = 'initModelA';
$rootScope.modelB = 'initModelB'; $rootScope.modelB = 'initModelB';
$rootScope.eventA = '?'; $rootScope.eventA = '?';
$rootScope.eventB = '?'; $rootScope.eventB = '?';
}); });
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [ outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange',
], 'twoWayBEmitter: twoWayBChange'
template: 'ignore: {{ignore}}; ' + ],
'literal: {{literal}}; interpolate: {{interpolate}}; ' + template: 'ignore: {{ignore}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + 'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
}) 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
class Ng2 { })
ngOnChangesCount = 0; class Ng2 {
ignore = '-'; ngOnChangesCount = 0;
literal = '?'; ignore = '-';
interpolate = '?'; literal = '?';
oneWayA = '?'; interpolate = '?';
oneWayB = '?'; oneWayA = '?';
twoWayA = '?'; oneWayB = '?';
twoWayB = '?'; twoWayA = '?';
eventA = new EventEmitter(); twoWayB = '?';
eventB = new EventEmitter(); eventA = new EventEmitter();
twoWayAEmitter = new EventEmitter(); eventB = new EventEmitter();
twoWayBEmitter = new EventEmitter(); twoWayAEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { twoWayBEmitter = new EventEmitter();
const assert = (prop: string, value: any) => { ngOnChanges(changes: SimpleChanges) {
if ((this as any)[prop] != value) { const assert = (prop: string, value: any) => {
throw new Error( if ((this as any)[prop] != value) {
`Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`); throw new Error(
} `Expected: '${prop}' to be '${value}' but was '${(this as any)[prop]}'`);
}; }
};
const assertChange = (prop: string, value: any) => { const assertChange = (prop: string, value: any) => {
assert(prop, value); assert(prop, value);
if (!changes[prop]) { if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`); throw new Error(`Changes record for '${prop}' not found.`);
} }
const actValue = changes[prop].currentValue; const actValue = changes[prop].currentValue;
if (actValue != value) { if (actValue != value) {
throw new Error( throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`); `Expected changes record for'${prop}' to be '${value}' but was '${actValue}'`);
} }
}; };
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
assert('ignore', '-'); assert('ignore', '-');
assertChange('literal', 'Text'); assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world'); assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A'); assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B'); assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA'); assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB'); assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA'); this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB'); this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired'); this.eventA.emit('aFired');
this.eventB.emit('bFired'); this.eventB.emit('bFired');
break; break;
case 1: case 1:
assertChange('twoWayA', 'newA'); assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB'); assertChange('twoWayB', 'newB');
break; break;
case 2: case 2:
assertChange('interpolate', 'Hello everyone'); assertChange('interpolate', 'Hello everyone');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
@NgModule({ @NgModule({
declarations: [Ng2], declarations: [Ng2],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
} }
const element = html(`<div> const element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{name}}" <ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB" bind-one-way-a="dataA" [one-way-b]="dataB"
bindon-two-way-a="modelA" [(two-way-b)]="modelB" bindon-two-way-a="modelA" [(two-way-b)]="modelB"
on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2> on-event-a='eventA=$event' (event-b)="eventB=$event"></ng2>
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(document.body.textContent !)) expect(multiTrim(document.body.textContent !))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' + 'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.ng1RootScope.$apply('name = "everyone"'); ref.ng1RootScope.$apply('name = "everyone"');
expect(multiTrim(document.body.textContent !)) expect(multiTrim(document.body.textContent !))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' + 'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
ref.dispose(); ref.dispose();
}); });
})); }));
it('should support two-way binding and event listener', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); .it('should support two-way binding and event listener', async(() => {
const listenerSpy = jasmine.createSpy('$rootScope.listener'); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { const listenerSpy = jasmine.createSpy('$rootScope.listener');
$rootScope['value'] = 'world'; const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['listener'] = listenerSpy; $rootScope['value'] = 'world';
}); $rootScope['listener'] = listenerSpy;
});
@Component({selector: 'ng2', template: `model: {{model}};`}) @Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
@Input() model = '?'; @Input() model = '?';
@Output() modelChange = new EventEmitter(); @Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
expect(changes.model.currentValue).toBe('world'); expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC'); this.modelChange.emit('newC');
break; break;
case 1: case 1:
expect(changes.model.currentValue).toBe('newC'); expect(changes.model.currentValue).toBe('newC');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component));
@NgModule({declarations: [Ng2Component], imports: [BrowserModule]}) @NgModule({declarations: [Ng2Component], imports: [BrowserModule]})
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const element = html(` const element = html(`
<div> <div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2> <ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}} | value: {{value}}
</div> </div>
`); `);
adapter.bootstrap(element, ['ng1']).ready((ref) => { adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC'); expect(listenerSpy).toHaveBeenCalledWith('newC');
ref.dispose(); ref.dispose();
}); });
})); }));
it('should initialize inputs in time for `ngOnChanges`', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); .it('should initialize inputs in time for `ngOnChanges`', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: ` template: `
ngOnChangesCount: {{ ngOnChangesCount }} | ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} | firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}` initialValue: {{ initialValue }}`
}) })
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
firstChangesCount = 0; firstChangesCount = 0;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
initialValue !: string; initialValue !: string;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() foo !: string; @Input() foo !: string;
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++; this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) { if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo; this.initialValue = this.foo;
} }
if (changes['foo'] && changes['foo'].isFirstChange()) { if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++; this.firstChangesCount++;
} }
} }
} }
@NgModule({imports: [BrowserModule], declarations: [Ng2Component]}) @NgModule({imports: [BrowserModule], declarations: [Ng2Component]})
class Ng2Module { class Ng2Module {
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', []).directive(
'ng2', adapter.downgradeNg2Component(Ng2Component)); 'ng2', adapter.downgradeNg2Component(Ng2Component));
const element = html(` const element = html(`
<ng2 [foo]="'foo'"></ng2> <ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2> <ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2> <ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2> <ng2 foo="qux" ng-if="true"></ng2>
`); `);
adapter.bootstrap(element, ['ng1']).ready(ref => { adapter.bootstrap(element, ['ng1']).ready(ref => {
const nodes = element.querySelectorAll('ng2'); const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) => const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
ref.dispose(); ref.dispose();
}); });
})); }));
it('should bind to ng-model', async(() => { it('should bind to ng-model', async(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
@ -1868,6 +1872,7 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-956: refactor onChanges').
it('should call `$onChanges()` on binding destination', fakeAsync(() => { it('should call `$onChanges()` on binding destination', fakeAsync(() => {
const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module));
const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA'); const $onChangesControllerSpyA = jasmine.createSpy('$onChangesControllerA');

View File

@ -22,104 +22,106 @@ withEachNg1Version(() => {
beforeEach(() => destroyPlatform()); beforeEach(() => destroyPlatform());
afterEach(() => destroyPlatform()); afterEach(() => destroyPlatform());
it('should bind properties, events', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { .it('should bind properties, events', async(() => {
$rootScope['name'] = 'world'; const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['dataA'] = 'A'; $rootScope['name'] = 'world';
$rootScope['dataB'] = 'B'; $rootScope['dataA'] = 'A';
$rootScope['modelA'] = 'initModelA'; $rootScope['dataB'] = 'B';
$rootScope['modelB'] = 'initModelB'; $rootScope['modelA'] = 'initModelA';
$rootScope['eventA'] = '?'; $rootScope['modelB'] = 'initModelB';
$rootScope['eventB'] = '?'; $rootScope['eventA'] = '?';
}); $rootScope['eventB'] = '?';
});
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'], inputs: ['literal', 'interpolate', 'oneWayA', 'oneWayB', 'twoWayA', 'twoWayB'],
outputs: [ outputs: [
'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange', 'twoWayBEmitter: twoWayBChange' 'eventA', 'eventB', 'twoWayAEmitter: twoWayAChange',
], 'twoWayBEmitter: twoWayBChange'
template: 'ignore: {{ignore}}; ' + ],
'literal: {{literal}}; interpolate: {{interpolate}}; ' + template: 'ignore: {{ignore}}; ' +
'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' + 'literal: {{literal}}; interpolate: {{interpolate}}; ' +
'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})' 'oneWayA: {{oneWayA}}; oneWayB: {{oneWayB}}; ' +
}) 'twoWayA: {{twoWayA}}; twoWayB: {{twoWayB}}; ({{ngOnChangesCount}})'
class Ng2Component implements OnChanges { })
ngOnChangesCount = 0; class Ng2Component implements OnChanges {
ignore = '-'; ngOnChangesCount = 0;
literal = '?'; ignore = '-';
interpolate = '?'; literal = '?';
oneWayA = '?'; interpolate = '?';
oneWayB = '?'; oneWayA = '?';
twoWayA = '?'; oneWayB = '?';
twoWayB = '?'; twoWayA = '?';
eventA = new EventEmitter(); twoWayB = '?';
eventB = new EventEmitter(); eventA = new EventEmitter();
twoWayAEmitter = new EventEmitter(); eventB = new EventEmitter();
twoWayBEmitter = new EventEmitter(); twoWayAEmitter = new EventEmitter();
twoWayBEmitter = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
const assert = (prop: string, value: any) => { const assert = (prop: string, value: any) => {
const propVal = (this as any)[prop]; const propVal = (this as any)[prop];
if (propVal != value) { if (propVal != value) {
throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`); throw new Error(`Expected: '${prop}' to be '${value}' but was '${propVal}'`);
} }
}; };
const assertChange = (prop: string, value: any) => { const assertChange = (prop: string, value: any) => {
assert(prop, value); assert(prop, value);
if (!changes[prop]) { if (!changes[prop]) {
throw new Error(`Changes record for '${prop}' not found.`); throw new Error(`Changes record for '${prop}' not found.`);
} }
const actualValue = changes[prop].currentValue; const actualValue = changes[prop].currentValue;
if (actualValue != value) { if (actualValue != value) {
throw new Error( throw new Error(
`Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`); `Expected changes record for'${prop}' to be '${value}' but was '${actualValue}'`);
} }
}; };
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
assert('ignore', '-'); assert('ignore', '-');
assertChange('literal', 'Text'); assertChange('literal', 'Text');
assertChange('interpolate', 'Hello world'); assertChange('interpolate', 'Hello world');
assertChange('oneWayA', 'A'); assertChange('oneWayA', 'A');
assertChange('oneWayB', 'B'); assertChange('oneWayB', 'B');
assertChange('twoWayA', 'initModelA'); assertChange('twoWayA', 'initModelA');
assertChange('twoWayB', 'initModelB'); assertChange('twoWayB', 'initModelB');
this.twoWayAEmitter.emit('newA'); this.twoWayAEmitter.emit('newA');
this.twoWayBEmitter.emit('newB'); this.twoWayBEmitter.emit('newB');
this.eventA.emit('aFired'); this.eventA.emit('aFired');
this.eventB.emit('bFired'); this.eventB.emit('bFired');
break; break;
case 1: case 1:
assertChange('twoWayA', 'newA'); assertChange('twoWayA', 'newA');
assertChange('twoWayB', 'newB'); assertChange('twoWayB', 'newB');
break; break;
case 2: case 2:
assertChange('interpolate', 'Hello everyone'); assertChange('interpolate', 'Hello everyone');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', downgradeComponent({ ng1Module.directive('ng2', downgradeComponent({
component: Ng2Component, component: Ng2Component,
})); }));
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const element = html(` const element = html(`
<div> <div>
<ng2 literal="Text" interpolate="Hello {{name}}" <ng2 literal="Text" interpolate="Hello {{name}}"
bind-one-way-a="dataA" [one-way-b]="dataB" bind-one-way-a="dataA" [one-way-b]="dataB"
@ -128,23 +130,23 @@ withEachNg1Version(() => {
| modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}}; | modelA: {{modelA}}; modelB: {{modelB}}; eventA: {{eventA}}; eventB: {{eventB}};
</div>`); </div>`);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello world; ' + 'literal: Text; interpolate: Hello world; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (2) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
$apply(upgrade, 'name = "everyone"'); $apply(upgrade, 'name = "everyone"');
expect(multiTrim(document.body.textContent)) expect(multiTrim(document.body.textContent))
.toEqual( .toEqual(
'ignore: -; ' + 'ignore: -; ' +
'literal: Text; interpolate: Hello everyone; ' + 'literal: Text; interpolate: Hello everyone; ' +
'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' + 'oneWayA: A; oneWayB: B; twoWayA: newA; twoWayB: newB; (3) | ' +
'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;'); 'modelA: newA; modelB: newB; eventA: aFired; eventB: bFired;');
}); });
})); }));
it('should bind properties to onpush components', async(() => { it('should bind properties to onpush components', async(() => {
const ng1Module = angular.module('ng1', []).run( const ng1Module = angular.module('ng1', []).run(
@ -187,57 +189,58 @@ withEachNg1Version(() => {
}); });
})); }));
it('should support two-way binding and event listener', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
const listenerSpy = jasmine.createSpy('$rootScope.listener'); .it('should support two-way binding and event listener', async(() => {
const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => { const listenerSpy = jasmine.createSpy('$rootScope.listener');
$rootScope['value'] = 'world'; const ng1Module = angular.module('ng1', []).run(($rootScope: angular.IScope) => {
$rootScope['listener'] = listenerSpy; $rootScope['value'] = 'world';
}); $rootScope['listener'] = listenerSpy;
});
@Component({selector: 'ng2', template: `model: {{model}};`}) @Component({selector: 'ng2', template: `model: {{model}};`})
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
@Input() model = '?'; @Input() model = '?';
@Output() modelChange = new EventEmitter(); @Output() modelChange = new EventEmitter();
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
switch (this.ngOnChangesCount++) { switch (this.ngOnChangesCount++) {
case 0: case 0:
expect(changes.model.currentValue).toBe('world'); expect(changes.model.currentValue).toBe('world');
this.modelChange.emit('newC'); this.modelChange.emit('newC');
break; break;
case 1: case 1:
expect(changes.model.currentValue).toBe('newC'); expect(changes.model.currentValue).toBe('newC');
break; break;
default: default:
throw new Error('Called too many times! ' + JSON.stringify(changes)); throw new Error('Called too many times! ' + JSON.stringify(changes));
} }
} }
} }
ng1Module.directive('ng2', downgradeComponent({component: Ng2Component})); ng1Module.directive('ng2', downgradeComponent({component: Ng2Component}));
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule] imports: [BrowserModule, UpgradeModule]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const element = html(` const element = html(`
<div> <div>
<ng2 [(model)]="value" (model-change)="listener($event)"></ng2> <ng2 [(model)]="value" (model-change)="listener($event)"></ng2>
| value: {{value}} | value: {{value}}
</div> </div>
`); `);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then((upgrade) => {
expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC'); expect(multiTrim(element.textContent)).toEqual('model: newC; | value: newC');
expect(listenerSpy).toHaveBeenCalledWith('newC'); expect(listenerSpy).toHaveBeenCalledWith('newC');
}); });
})); }));
it('should run change-detection on every digest (by default)', async(() => { it('should run change-detection on every digest (by default)', async(() => {
let ng2Component: Ng2Component; let ng2Component: Ng2Component;
@ -401,65 +404,66 @@ withEachNg1Version(() => {
}); });
})); }));
it('should initialize inputs in time for `ngOnChanges`', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
@Component({ .it('should initialize inputs in time for `ngOnChanges`', async(() => {
selector: 'ng2', @Component({
template: ` selector: 'ng2',
template: `
ngOnChangesCount: {{ ngOnChangesCount }} | ngOnChangesCount: {{ ngOnChangesCount }} |
firstChangesCount: {{ firstChangesCount }} | firstChangesCount: {{ firstChangesCount }} |
initialValue: {{ initialValue }}` initialValue: {{ initialValue }}`
}) })
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
ngOnChangesCount = 0; ngOnChangesCount = 0;
firstChangesCount = 0; firstChangesCount = 0;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
initialValue !: string; initialValue !: string;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() foo !: string; @Input() foo !: string;
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this.ngOnChangesCount++; this.ngOnChangesCount++;
if (this.ngOnChangesCount === 1) { if (this.ngOnChangesCount === 1) {
this.initialValue = this.foo; this.initialValue = this.foo;
} }
if (changes['foo'] && changes['foo'].isFirstChange()) { if (changes['foo'] && changes['foo'].isFirstChange()) {
this.firstChangesCount++; this.firstChangesCount++;
} }
} }
} }
@NgModule({ @NgModule({
imports: [BrowserModule, UpgradeModule], imports: [BrowserModule, UpgradeModule],
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component] entryComponents: [Ng2Component]
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component})); 'ng2', downgradeComponent({component: Ng2Component}));
const element = html(` const element = html(`
<ng2 [foo]="'foo'"></ng2> <ng2 [foo]="'foo'"></ng2>
<ng2 foo="bar"></ng2> <ng2 foo="bar"></ng2>
<ng2 [foo]="'baz'" ng-if="true"></ng2> <ng2 [foo]="'baz'" ng-if="true"></ng2>
<ng2 foo="qux" ng-if="true"></ng2> <ng2 foo="qux" ng-if="true"></ng2>
`); `);
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const nodes = element.querySelectorAll('ng2'); const nodes = element.querySelectorAll('ng2');
const expectedTextWith = (value: string) => const expectedTextWith = (value: string) =>
`ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`; `ngOnChangesCount: 1 | firstChangesCount: 1 | initialValue: ${value}`;
expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo')); expect(multiTrim(nodes[0].textContent)).toBe(expectedTextWith('foo'));
expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar')); expect(multiTrim(nodes[1].textContent)).toBe(expectedTextWith('bar'));
expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz')); expect(multiTrim(nodes[2].textContent)).toBe(expectedTextWith('baz'));
expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux')); expect(multiTrim(nodes[3].textContent)).toBe(expectedTextWith('qux'));
}); });
})); }));
it('should bind to ng-model', async(() => { it('should bind to ng-model', async(() => {
const ng1Module = angular.module('ng1', []).run( const ng1Module = angular.module('ng1', []).run(

View File

@ -721,63 +721,66 @@ withEachNg1Version(() => {
}); });
})); }));
it('should propagate input changes inside the Angular zone', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
let ng2Component: Ng2Component; .it('should propagate input changes inside the Angular zone', async(() => {
let ng2Component: Ng2Component;
@Component({selector: 'ng2', template: ''}) @Component({selector: 'ng2', template: ''})
class Ng2Component implements OnChanges { class Ng2Component implements OnChanges {
@Input() attrInput = 'foo'; @Input() attrInput = 'foo';
@Input() propInput = 'foo'; @Input() propInput = 'foo';
constructor() { ng2Component = this; } constructor() { ng2Component = this; }
ngOnChanges() {} ngOnChanges() {}
} }
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const bootstrapFn = (extraProviders: StaticProvider[]) => const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module = const ng1Module =
angular.module('ng1', [lazyModuleName]) angular.module('ng1', [lazyModuleName])
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest})) .directive(
.run(($rootScope: angular.IRootScopeService) => { 'ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
$rootScope.attrVal = 'bar'; .run(($rootScope: angular.IRootScopeService) => {
$rootScope.propVal = 'bar'; $rootScope.attrVal = 'bar';
}); $rootScope.propVal = 'bar';
});
const element = html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>'); const element =
const $injector = angular.bootstrap(element, [ng1Module.name]); html('<ng2 attr-input="{{ attrVal }}" [prop-input]="propVal"></ng2>');
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; 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 the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true); const expectToBeInNgZone = () => expect(NgZone.isInAngularZone()).toBe(true);
const changesSpy = const changesSpy =
spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone); spyOn(ng2Component, 'ngOnChanges').and.callFake(expectToBeInNgZone);
expect(ng2Component.attrInput).toBe('bar'); expect(ng2Component.attrInput).toBe('bar');
expect(ng2Component.propInput).toBe('bar'); expect(ng2Component.propInput).toBe('bar');
$rootScope.$apply('attrVal = "baz"'); $rootScope.$apply('attrVal = "baz"');
expect(ng2Component.attrInput).toBe('baz'); expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('bar'); expect(ng2Component.propInput).toBe('bar');
expect(changesSpy).toHaveBeenCalledTimes(1); expect(changesSpy).toHaveBeenCalledTimes(1);
$rootScope.$apply('propVal = "qux"'); $rootScope.$apply('propVal = "qux"');
expect(ng2Component.attrInput).toBe('baz'); expect(ng2Component.attrInput).toBe('baz');
expect(ng2Component.propInput).toBe('qux'); expect(ng2Component.propInput).toBe('qux');
expect(changesSpy).toHaveBeenCalledTimes(2); expect(changesSpy).toHaveBeenCalledTimes(2);
}); });
}); });
})); }));
it('should create and destroy nested, asynchronously instantiated components inside the Angular zone', it('should create and destroy nested, asynchronously instantiated components inside the Angular zone',
async(() => { async(() => {
@ -940,165 +943,167 @@ withEachNg1Version(() => {
}); });
})); }));
it('should run the lifecycle hooks in the correct order', async(() => { fixmeIvy('FW-715: ngOnChanges being called a second time unexpectedly')
const logs: string[] = []; .it('should run the lifecycle hooks in the correct order', async(() => {
let rootScope: angular.IRootScopeService; const logs: string[] = [];
let rootScope: angular.IRootScopeService;
@Component({ @Component({
selector: 'ng2', selector: 'ng2',
template: ` template: `
{{ value }} {{ value }}
<button (click)="value = 'qux'"></button> <button (click)="value = 'qux'"></button>
<ng-content></ng-content> <ng-content></ng-content>
` `
}) })
class Ng2Component implements AfterContentChecked, class Ng2Component implements AfterContentChecked,
AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges,
OnInit { OnDestroy, OnInit {
@Input() value = 'foo'; @Input() value = 'foo';
ngAfterContentChecked() { this.log('AfterContentChecked'); } ngAfterContentChecked() { this.log('AfterContentChecked'); }
ngAfterContentInit() { this.log('AfterContentInit'); } ngAfterContentInit() { this.log('AfterContentInit'); }
ngAfterViewChecked() { this.log('AfterViewChecked'); } ngAfterViewChecked() { this.log('AfterViewChecked'); }
ngAfterViewInit() { this.log('AfterViewInit'); } ngAfterViewInit() { this.log('AfterViewInit'); }
ngDoCheck() { this.log('DoCheck'); } ngDoCheck() { this.log('DoCheck'); }
ngOnChanges() { this.log('OnChanges'); } ngOnChanges() { this.log('OnChanges'); }
ngOnDestroy() { this.log('OnDestroy'); } ngOnDestroy() { this.log('OnDestroy'); }
ngOnInit() { this.log('OnInit'); } ngOnInit() { this.log('OnInit'); }
private log(hook: string) { logs.push(`${hook}(${this.value})`); } private log(hook: string) { logs.push(`${hook}(${this.value})`); }
} }
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule], imports: [BrowserModule],
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const bootstrapFn = (extraProviders: StaticProvider[]) => const bootstrapFn = (extraProviders: StaticProvider[]) =>
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn); const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
const ng1Module = const ng1Module =
angular.module('ng1', [lazyModuleName]) angular.module('ng1', [lazyModuleName])
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest})) .directive(
.run(($rootScope: angular.IRootScopeService) => { 'ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
rootScope = $rootScope; .run(($rootScope: angular.IRootScopeService) => {
rootScope.value = 'bar'; rootScope = $rootScope;
}); rootScope.value = 'bar';
});
const element = const element =
html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>'); html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>');
angular.bootstrap(element, [ng1Module.name]); angular.bootstrap(element, [ng1Module.name]);
setTimeout(() => { // Wait for the module to be bootstrapped. setTimeout(() => { // Wait for the module to be bootstrapped.
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs. setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
const button = element.querySelector('button') !; const button = element.querySelector('button') !;
// Once initialized. // Once initialized.
expect(multiTrim(element.textContent)).toBe('bar Content'); expect(multiTrim(element.textContent)).toBe('bar Content');
expect(logs).toEqual([ expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges` // `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher. // $watcher.
'OnChanges(bar)', 'OnChanges(bar)',
// Initial CD triggered directly through the `detectChanges()` or // Initial CD triggered directly through the `detectChanges()` or
// `inputChanges` // `inputChanges`
// $watcher (for `propagateDigest` true/false respectively). // $watcher (for `propagateDigest` true/false respectively).
'OnInit(bar)', 'OnInit(bar)',
'DoCheck(bar)', 'DoCheck(bar)',
'AfterContentInit(bar)', 'AfterContentInit(bar)',
'AfterContentChecked(bar)', 'AfterContentChecked(bar)',
'AfterViewInit(bar)', 'AfterViewInit(bar)',
'AfterViewChecked(bar)', 'AfterViewChecked(bar)',
...(propagateDigest ? ...(propagateDigest ?
[ [
// CD triggered directly through the `detectChanges()` $watcher (2nd // CD triggered directly through the `detectChanges()` $watcher (2nd
// $digest). // $digest).
'DoCheck(bar)', 'DoCheck(bar)',
'AfterContentChecked(bar)', 'AfterContentChecked(bar)',
'AfterViewChecked(bar)', 'AfterViewChecked(bar)',
] : ] :
[]), []),
// CD triggered due to entering/leaving the NgZone (in `downgradeFn()`). // CD triggered due to entering/leaving the NgZone (in `downgradeFn()`).
'DoCheck(bar)', 'DoCheck(bar)',
'AfterContentChecked(bar)', 'AfterContentChecked(bar)',
'AfterViewChecked(bar)', 'AfterViewChecked(bar)',
]); ]);
logs.length = 0; logs.length = 0;
// Change inputs and run `$digest`. // Change inputs and run `$digest`.
rootScope.$apply('value = "baz"'); rootScope.$apply('value = "baz"');
expect(multiTrim(element.textContent)).toBe('baz Content'); expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual([ expect(logs).toEqual([
// `ngOnChanges()` call triggered directly through the `inputChanges` // `ngOnChanges()` call triggered directly through the `inputChanges`
// $watcher. // $watcher.
'OnChanges(baz)', 'OnChanges(baz)',
// `propagateDigest: true` (3 CD runs): // `propagateDigest: true` (3 CD runs):
// - CD triggered due to entering/leaving the NgZone (in `inputChanges` // - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher). // $watcher).
// - CD triggered directly through the `detectChanges()` $watcher. // - CD triggered directly through the `detectChanges()` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `detectChanges` // - CD triggered due to entering/leaving the NgZone (in `detectChanges`
// $watcher). // $watcher).
// `propagateDigest: false` (2 CD runs): // `propagateDigest: false` (2 CD runs):
// - CD triggered directly through the `inputChanges` $watcher. // - CD triggered directly through the `inputChanges` $watcher.
// - CD triggered due to entering/leaving the NgZone (in `inputChanges` // - CD triggered due to entering/leaving the NgZone (in `inputChanges`
// $watcher). // $watcher).
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
...(propagateDigest ? ...(propagateDigest ?
[ [
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
] : ] :
[]), []),
]); ]);
logs.length = 0; logs.length = 0;
// Run `$digest` (without changing inputs). // Run `$digest` (without changing inputs).
rootScope.$digest(); rootScope.$digest();
expect(multiTrim(element.textContent)).toBe('baz Content'); expect(multiTrim(element.textContent)).toBe('baz Content');
expect(logs).toEqual( expect(logs).toEqual(
propagateDigest ? propagateDigest ?
[ [
// CD triggered directly through the `detectChanges()` $watcher. // CD triggered directly through the `detectChanges()` $watcher.
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
// CD triggered due to entering/leaving the NgZone (in the above // CD triggered due to entering/leaving the NgZone (in the above
// $watcher). // $watcher).
'DoCheck(baz)', 'DoCheck(baz)',
'AfterContentChecked(baz)', 'AfterContentChecked(baz)',
'AfterViewChecked(baz)', 'AfterViewChecked(baz)',
] : ] :
[]); []);
logs.length = 0; logs.length = 0;
// Trigger change detection (without changing inputs). // Trigger change detection (without changing inputs).
button.click(); button.click();
expect(multiTrim(element.textContent)).toBe('qux Content'); expect(multiTrim(element.textContent)).toBe('qux Content');
expect(logs).toEqual([ expect(logs).toEqual([
'DoCheck(qux)', 'DoCheck(qux)',
'AfterContentChecked(qux)', 'AfterContentChecked(qux)',
'AfterViewChecked(qux)', 'AfterViewChecked(qux)',
]); ]);
logs.length = 0; logs.length = 0;
// Destroy the component. // Destroy the component.
rootScope.$apply('hideNg2 = true'); rootScope.$apply('hideNg2 = true');
expect(logs).toEqual([ expect(logs).toEqual([
'OnDestroy(qux)', 'OnDestroy(qux)',
]); ]);
logs.length = 0; logs.length = 0;
}); });
}); });
})); }));
it('should detach hostViews from the ApplicationRef once destroyed', async(() => { it('should detach hostViews from the ApplicationRef once destroyed', async(() => {
let ng2Component: Ng2Component; let ng2Component: Ng2Component;

View File

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