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

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

View File

@ -34,14 +34,20 @@ import {Directive, EmbeddedViewRef, Input, OnChanges, SimpleChange, SimpleChange
*/
@Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges {
// TODO(issue/24571): remove '!'.
private _viewRef !: EmbeddedViewRef<any>;
private _viewRef: EmbeddedViewRef<any>|null = null;
// TODO(issue/24571): remove '!'.
@Input() public ngTemplateOutletContext !: Object;
/**
* A context object to attach to the {@link EmbeddedViewRef}. This should be an
* object, the object's keys will be available for binding by the local template `let`
* declarations.
* Using the key `$implicit` in the context object will set its value as default.
*/
@Input() public ngTemplateOutletContext: Object|null = null;
// TODO(issue/24571): remove '!'.
@Input() public ngTemplateOutlet !: TemplateRef<any>;
/**
* A string defining the template reference and optionally the context object for the template.
*/
@Input() public ngTemplateOutlet: TemplateRef<any>|null = null;
constructor(private _viewContainerRef: ViewContainerRef) {}
@ -97,7 +103,7 @@ export class NgTemplateOutlet implements OnChanges {
private _updateExistingContext(ctx: Object): void {
for (let propName of Object.keys(ctx)) {
(<any>this._viewRef.context)[propName] = (<any>this.ngTemplateOutletContext)[propName];
(<any>this._viewRef !.context)[propName] = (<any>this.ngTemplateOutletContext)[propName];
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {SimpleChange} from '../../change_detection/change_detection_util';
import {SimpleChanges} from '../../interface/simple_change';
import {OnChanges} from '../../interface/lifecycle_hooks';
import {SimpleChange, SimpleChanges} from '../../interface/simple_change';
import {EMPTY_OBJ} from '../empty';
import {DirectiveDef, DirectiveDefFeature} from '../interfaces/definition';
const PRIVATE_PREFIX = '__ngOnChanges_';
@ -40,86 +40,63 @@ type OnChangesExpando = OnChanges & {
* ```
*/
export function NgOnChangesFeature<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;
if (definition.type.prototype.ngOnChanges) {
definition.setInput = ngOnChangesSetInput;
// Walk the prototype chain to see if we find a property descriptor
// That way we can honor setters and getters that were inherited.
let originalProperty: PropertyDescriptor|undefined = undefined;
let checkProto = proto;
while (!originalProperty && checkProto &&
Object.getPrototypeOf(checkProto) !== Object.getPrototypeOf(Object.prototype)) {
originalProperty = Object.getOwnPropertyDescriptor(checkProto, minifiedKey);
checkProto = Object.getPrototypeOf(checkProto);
}
const prevDoCheck = definition.doCheck;
const prevOnInit = definition.onInit;
const getter = originalProperty && originalProperty.get;
const setter = originalProperty && originalProperty.set;
definition.onInit = wrapOnChanges(prevOnInit);
definition.doCheck = wrapOnChanges(prevDoCheck);
}
}
// create a getter and setter for property
Object.defineProperty(proto, minifiedKey, {
get: getter ||
(setter ? undefined : function(this: OnChangesExpando) { return this[privateMinKey]; }),
set<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});
}
function wrapOnChanges(hook: (() => void) | null) {
return function(this: OnChanges) {
const simpleChangesStore = getSimpleChangesStore(this);
const current = simpleChangesStore && simpleChangesStore.current;
const isFirstChange = !this.hasOwnProperty(privateMinKey);
const currentChange = simpleChanges[declaredKey];
if (currentChange) {
currentChange.currentValue = value;
} else {
simpleChanges[declaredKey] =
new SimpleChange(this[privateMinKey], value, isFirstChange);
}
if (isFirstChange) {
// Create a place where the actual value will be stored and make it non-enumerable
Object.defineProperty(this, privateMinKey, {value, writable: true});
} else {
this[privateMinKey] = value;
}
if (setter) setter.call(this, value);
},
// Make the property configurable in dev mode to allow overriding in tests
configurable: !!ngDevMode
});
if (current) {
simpleChangesStore !.previous = current;
simpleChangesStore !.current = null;
this.ngOnChanges(current);
}
}
// If an onInit hook is defined, it will need to wrap the ngOnChanges call
// so the call order is changes-init-check in creation mode. In subsequent
// change detection runs, only the check wrapper will be called.
if (definition.onInit != null) {
definition.onInit = onChangesWrapper(definition.onInit);
}
hook && hook.call(this);
};
}
definition.doCheck = onChangesWrapper(definition.doCheck);
function ngOnChangesSetInput<T>(
this: DirectiveDef<T>, instance: T, value: any, publicName: string, privateName: string): void {
const simpleChangesStore = getSimpleChangesStore(instance) ||
setSimpleChangesStore(instance, {previous: EMPTY_OBJ, current: null});
const current = simpleChangesStore.current || (simpleChangesStore.current = {});
const previous = simpleChangesStore.previous;
const declaredName = (this.declaredInputs as{[key: string]: string})[publicName];
const previousChange = previous[declaredName];
current[declaredName] = new SimpleChange(
previousChange && previousChange.currentValue, value, previous === EMPTY_OBJ);
(instance as any)[privateName] = value;
}
const SIMPLE_CHANGES_STORE = '__ngSimpleChanges__';
function getSimpleChangesStore(instance: any): null|NgSimpleChangesStore {
return instance[SIMPLE_CHANGES_STORE] || null;
}
function setSimpleChangesStore(instance: any, store: NgSimpleChangesStore): NgSimpleChangesStore {
return instance[SIMPLE_CHANGES_STORE] = store;
}
// This option ensures that the ngOnChanges lifecycle hook will be inherited
// from superclasses (in InheritDefinitionFeature).
(NgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
function onChangesWrapper(delegateHook: (() => void) | null) {
return function(this: OnChangesExpando) {
const simpleChanges = this[PRIVATE_PREFIX];
if (simpleChanges != null) {
this.ngOnChanges(simpleChanges);
this[PRIVATE_PREFIX] = null;
}
if (delegateHook) delegateHook.apply(this);
};
interface NgSimpleChangesStore {
previous: SimpleChanges;
current: SimpleChanges|null;
}

View File

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

View File

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

View File

@ -468,10 +468,11 @@ export type PropertyAliases = {
/**
* Store the runtime input or output names for all the directives.
*
* - Even indices: directive index
* - Odd indices: minified / internal name
* i+0: directive instance index
* i+1: publicName
* i+2: privateName
*
* e.g. [0, 'change-minified']
* e.g. [0, 'change', 'change-minified']
*/
export type PropertyAliasValue = (number | string)[];
@ -484,14 +485,15 @@ export type PropertyAliasValue = (number | string)[];
*
* Within each sub-array:
*
* Even indices: minified/internal input name
* Odd indices: initial value
* i+0: attribute name
* i+1: minified/internal input name
* i+2: initial value
*
* If a directive on a node does not have any input properties
* that should be set from attributes, its index is set to null
* to avoid a sparse array.
*
* e.g. [null, ['role-min', 'button']]
* e.g. [null, ['role-min', 'minified-input', 'button']]
*/
export type InitialInputData = (InitialInputs | null)[];
@ -499,10 +501,11 @@ export type InitialInputData = (InitialInputs | null)[];
* Used by InitialInputData to store input properties
* that should be set once from attributes.
*
* Even indices: minified/internal input name
* Odd indices: initial value
* i+0: attribute name
* i+1: minified/internal input name
* i+2: initial value
*
* e.g. ['role-min', 'button']
* e.g. ['role-min', 'minified-input', 'button']
*/
export type InitialInputs = string[];

View File

@ -8,7 +8,6 @@
import {InjectionToken} from '../../di/injection_token';
import {Injector} from '../../di/injector';
import {SimpleChanges} from '../../interface/simple_change';
import {Type} from '../../interface/type';
import {QueryList} from '../../linker';
import {Sanitizer} from '../../sanitization/security';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,14 +7,14 @@
*/
import {ComponentFactoryResolver, OnDestroy, SimpleChange, SimpleChanges, ViewContainerRef} from '../../src/core';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {AttributeMarker, ComponentTemplate, LifecycleHooksFeature, NO_CHANGE, NgOnChangesFeature, defineComponent, defineDirective, injectComponentFactoryResolver} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, directiveInject, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, markDirty, projection, projectionDef, store, template, text} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgIf} from './common_with_def';
import {ComponentFixture, containerEl, createComponent, renderComponent, renderToHtml, requestAnimationFrame} from './render_util';
import { fixmeIvy } from '@angular/private/testing';
import {fixmeIvy} from '@angular/private/testing';
describe('lifecycles', () => {
@ -1941,7 +1941,6 @@ describe('lifecycles', () => {
});
fixmeIvy('FW-956: refactor onChanges').
describe('onChanges', () => {
let events: ({type: string, name: string, [key: string]: any})[];
@ -2008,7 +2007,8 @@ describe('lifecycles', () => {
consts: consts,
vars: vars,
inputs: {a: 'val1', b: ['publicVal2', 'val2']}, template,
directives: directives
directives: directives,
features: [NgOnChangesFeature],
});
};
}
@ -2026,7 +2026,8 @@ describe('lifecycles', () => {
type: Directive,
selectors: [['', 'dir', '']],
factory: () => new Directive(),
inputs: {a: 'val1', b: ['publicVal2', 'val2']}
inputs: {a: 'val1', b: ['publicVal2', 'val2']},
features: [NgOnChangesFeature],
});
}
@ -2701,7 +2702,6 @@ describe('lifecycles', () => {
});
fixmeIvy('FW-956: refactor onChanges').
describe('hook order', () => {
let events: string[];
@ -2731,7 +2731,8 @@ describe('lifecycles', () => {
consts: consts,
vars: vars,
inputs: {val: 'val'}, template,
directives: directives
directives: directives,
features: [NgOnChangesFeature],
});
};
}

View File

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

View File

@ -1677,7 +1677,8 @@ describe('ViewContainerRef', () => {
elementProperty(3, 'name', bind('B'));
}
},
directives: [ComponentWithHooks, DirectiveWithVCRef]
directives: [ComponentWithHooks, DirectiveWithVCRef],
features: [NgOnChangesFeature],
});
}
@ -1769,7 +1770,8 @@ describe('ViewContainerRef', () => {
elementProperty(1, 'name', bind('B'));
}
},
directives: [ComponentWithHooks, DirectiveWithVCRef]
directives: [ComponentWithHooks, DirectiveWithVCRef],
features: [NgOnChangesFeature],
});
}
@ -1801,7 +1803,7 @@ describe('ViewContainerRef', () => {
fixture.update();
expect(fixture.html).toEqual('<hooks vcref="">A</hooks><hooks>D</hooks><hooks>B</hooks>');
expect(log).toEqual([
'doCheck-A', 'doCheck-B', 'onChanges-D', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'doCheck-A', 'doCheck-B', 'onInit-D', 'doCheck-D', 'afterContentInit-D',
'afterContentChecked-D', 'afterViewInit-D', 'afterViewChecked-D', 'afterContentChecked-A',
'afterContentChecked-B', 'afterViewChecked-A', 'afterViewChecked-B'
]);

View File

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

View File

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

View File

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