fix(upgrade): initialize all inputs in time for ngOnChanges()

Previously, non-bracketed inputs (e.g. `xyz="foo"`) on downgraded components
were initialized using `attrs.$observe()` (which uses `$evalAsync()` under the
hood), while bracketed inputs (e.g. `[xyz]="'foo'"`) were initialized using
`$watch()`. If the downgraded component was created during a `$digest` (e.g. by
an `ng-if` watcher), the non-bracketed inputs were not initialized in time for
the initial call to `ngOnChanges()` and `ngOnInit()`.

This commit fixes it by using `$watch()` to initialize all inputs. `$observe()`
is still used for subsequent updates on non-bracketed inputs, because it is more
performant.

Fixes #16212
This commit is contained in:
Georgios Kalpakas
2017-05-03 18:18:40 +03:00
committed by Matias Niemelä
parent 77b8a76f2e
commit b3e63c09ab
5 changed files with 143 additions and 10 deletions

View File

@ -11,7 +11,7 @@ import {ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injecto
import * as angular from './angular1';
import {PropertyBinding} from './component_info';
import {$SCOPE} from './constants';
import {getAttributesAsArray, getComponentName, hookupNgModel} from './util';
import {getAttributesAsArray, getComponentName, hookupNgModel, strictEquals} from './util';
const INITIAL_VALUE = {
__UNINITIALIZED__: true
@ -75,16 +75,28 @@ export class DowngradeComponentAdapter {
const observeFn = (prop => {
let prevValue = INITIAL_VALUE;
return (currValue: any) => {
if (prevValue === INITIAL_VALUE) {
// Initially, both `$observe()` and `$watch()` will call this function.
if (!strictEquals(prevValue, currValue)) {
if (prevValue === INITIAL_VALUE) {
prevValue = currValue;
}
this.updateInput(prop, prevValue, currValue);
prevValue = currValue;
}
this.updateInput(prop, prevValue, currValue);
prevValue = currValue;
};
})(input.prop);
attrs.$observe(input.attr, observeFn);
// Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time
// for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that
// `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback.
let unwatch: any = this.componentScope.$watch(() => {
unwatch();
unwatch = null;
observeFn((attrs as any)[input.attr]);
});
} else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = (attrs as any /** TODO #9100 */)[input.bindAttr];
} else if (attrs.hasOwnProperty(input.bracketAttr)) {

View File

@ -75,3 +75,10 @@ export function hookupNgModel(ngModel: angular.INgModelController, component: an
component.registerOnChange(ngModel.$setViewValue.bind(ngModel));
}
}
/**
* Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
*/
export function strictEquals(val1: any, val2: any): boolean {
return val1 === val2 || (val1 !== val1 && val2 !== val2);
}

View File

@ -10,7 +10,7 @@ import {Directive, DoCheck, ElementRef, EventEmitter, Inject, OnChanges, OnInit,
import * as angular from '../common/angular1';
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $SCOPE, $TEMPLATE_CACHE} from '../common/constants';
import {controllerKey} from '../common/util';
import {controllerKey, strictEquals} from '../common/util';
interface IBindingDestination {
@ -309,8 +309,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
checkProperties.forEach((propName, i) => {
const value = destinationObj ![propName];
const last = lastValues[i];
if (value !== last &&
(value === value || last === last)) { // ignore NaN values (NaN !== NaN)
if (!strictEquals(last, value)) {
const eventEmitter: EventEmitter<any> = (this as any)[propOuts[i]];
eventEmitter.emit(lastValues[i] = value);
}