refactor(ivy): Add newer, smaller NgOnChangesFeature (#28187)
PR Close #28187
This commit is contained in:
parent
5552661fd7
commit
a95e81978b
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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>');
|
||||
|
|
|
@ -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!']);
|
||||
|
|
|
@ -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],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
]);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue