feat($compile): add isFirstChange() method to onChanges object

Closes #14318
Closes #14323
This commit is contained in:
Peter Bacon Darwin
2016-03-26 15:50:30 +00:00
parent bd0915c400
commit a6a4b23517
3 changed files with 68 additions and 15 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ of the component. The following hook methods can be implemented:
this element). This is a good place to put initialization code for your controller.
* `$onChanges(changesObj)` - Called whenever one-way bindings are updated. The `changesObj` is a hash whose keys
are the names of the bound properties that have changed, and the values are an object of the form
`{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component such as
`{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a component such as
cloning the bound value to prevent accidental mutation of the outer value.
* `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
external resources, watches and event handlers.
+15 -3
View File
@@ -298,8 +298,8 @@
* this element). This is a good place to put initialization code for your controller.
* * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
* `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
* object of the form `{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component
* such as cloning the bound value to prevent accidental mutation of the outer value.
* object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a
* component such as cloning the bound value to prevent accidental mutation of the outer value.
* * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
* external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
* the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
@@ -846,6 +846,9 @@
var $compileMinErr = minErr('$compile');
function UNINITIALIZED_VALUE() {}
var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
/**
* @ngdoc provider
* @name $compileProvider
@@ -3115,6 +3118,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// the value to boolean rather than a string, so we special case this situation
destination[scopeName] = lastValue;
}
recordChanges(scopeName, destination[scopeName], _UNINITIALIZED_VALUE);
break;
case '=':
@@ -3170,6 +3174,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parentGet = $parse(attrs[attrName]);
destination[scopeName] = parentGet(scope);
recordChanges(scopeName, destination[scopeName], _UNINITIALIZED_VALUE);
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
var oldValue = destination[scopeName];
@@ -3211,7 +3216,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
previousValue = changes[key].previousValue;
}
// Store this change
changes[key] = {previousValue: previousValue, currentValue: currentValue};
changes[key] = new SimpleChange(previousValue, currentValue);
}
}
@@ -3230,6 +3235,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}];
}
function SimpleChange(previous, current) {
this.previousValue = previous;
this.currentValue = current;
}
SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
/**
* Converts all accepted directives format into proper directive name.
+52 -11
View File
@@ -3676,8 +3676,9 @@ describe('$compile', function() {
// Now we should have a single changes entry in the log
expect(log).toEqual([
{
prop1: {previousValue: undefined, currentValue: 42},
prop2: {previousValue: undefined, currentValue: 84}
prop1: jasmine.objectContaining({currentValue: 42}),
prop2: jasmine.objectContaining({currentValue: 84}),
attr: jasmine.objectContaining({currentValue: ''})
}
]);
@@ -3689,8 +3690,8 @@ describe('$compile', function() {
// Now we should have a single changes entry in the log
expect(log).toEqual([
{
prop1: {previousValue: 42, currentValue: 17},
prop2: {previousValue: 84, currentValue: 34}
prop1: jasmine.objectContaining({previousValue: 42, currentValue: 17}),
prop2: jasmine.objectContaining({previousValue: 84, currentValue: 34})
}
]);
@@ -3707,7 +3708,7 @@ describe('$compile', function() {
// onChanges should not have been called
expect(log).toEqual([
{
attr: {previousValue: '', currentValue: '22'}
attr: jasmine.objectContaining({previousValue: '', currentValue: '22'})
}
]);
});
@@ -3739,7 +3740,7 @@ describe('$compile', function() {
// Update val to trigger the onChanges
$rootScope.$apply('a = 42');
// Now the change should have the real previous value (undefined), not the intermediate one (42)
expect(log).toEqual([{prop: {previousValue: undefined, currentValue: 126}}]);
expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: 126})}]);
// Clear the log
log = [];
@@ -3747,7 +3748,46 @@ describe('$compile', function() {
// Update val to trigger the onChanges
$rootScope.$apply('a = 7');
// Now the change should have the real previous value (126), not the intermediate one, (91)
expect(log).toEqual([{ prop: {previousValue: 126, currentValue: 21}}]);
expect(log).toEqual([{prop: jasmine.objectContaining({previousValue: 126, currentValue: 21})}]);
});
});
it('should trigger an initial onChanges call for each binding with the `isFirstChange()` returning true', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop': '<', attr: '@' }
});
module('my');
inject(function($compile, $rootScope) {
element = $compile('<c1 prop="a" attr="{{a}}"></c1>')($rootScope);
expect(log).toEqual([]);
$rootScope.$apply('a = 7');
expect(log).toEqual([
{
prop: jasmine.objectContaining({currentValue: 7}),
attr: jasmine.objectContaining({currentValue: '7'})
}
]);
expect(log[0].prop.isFirstChange()).toEqual(true);
expect(log[0].attr.isFirstChange()).toEqual(true);
log = [];
$rootScope.$apply('a = 9');
expect(log).toEqual([
{
prop: jasmine.objectContaining({previousValue: 7, currentValue: 9}),
attr: jasmine.objectContaining({previousValue: '7', currentValue: '9'})
}
]);
expect(log[0].prop.isFirstChange()).toEqual(false);
expect(log[0].attr.isFirstChange()).toEqual(false);
});
});
@@ -3786,8 +3826,8 @@ describe('$compile', function() {
$rootScope.$apply('val1 = 42; val2 = 17');
expect(log).toEqual([
['TestController1', {prop: {previousValue: undefined, currentValue: 42}}],
['TestController2', {prop: {previousValue: undefined, currentValue: 17}}]
['TestController1', {prop: jasmine.objectContaining({currentValue: 42})}],
['TestController2', {prop: jasmine.objectContaining({currentValue: 17})}]
]);
// A single apply should only trigger three turns of the digest loop
expect(watchCount).toEqual(3);
@@ -3831,8 +3871,9 @@ describe('$compile', function() {
$rootScope.$apply('a = 42');
expect(log).toEqual([
['OuterController', {prop1: {previousValue: undefined, currentValue: 42}}],
['InnerController', {prop2: {previousValue: undefined, currentValue: 72}}]
['OuterController', {prop1: jasmine.objectContaining({currentValue: 42})}],
['InnerController', {prop2: jasmine.objectContaining({currentValue: undefined})}],
['InnerController', {prop2: jasmine.objectContaining({currentValue: 72})}]
]);
});
});