Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 8d43d8b

Browse files
feat($compile): add isFirstChange() method to onChanges object
Closes #14318 Closes #14323
1 parent d08f5c6 commit 8d43d8b

File tree

3 files changed

+68
-15
lines changed

3 files changed

+68
-15
lines changed

docs/content/guide/component.ngdoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ of the component. The following hook methods can be implemented:
156156
this element). This is a good place to put initialization code for your controller.
157157
* `$onChanges(changesObj)` - Called whenever one-way bindings are updated. The `changesObj` is a hash whose keys
158158
are the names of the bound properties that have changed, and the values are an object of the form
159-
`{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component such as
159+
`{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a component such as
160160
cloning the bound value to prevent accidental mutation of the outer value.
161161
* `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
162162
external resources, watches and event handlers.

src/ng/compile.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,8 @@
298298
* this element). This is a good place to put initialization code for your controller.
299299
* * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
300300
* `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
301-
* object of the form `{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component
302-
* such as cloning the bound value to prevent accidental mutation of the outer value.
301+
* object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a
302+
* component such as cloning the bound value to prevent accidental mutation of the outer value.
303303
* * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
304304
* external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
305305
* the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
@@ -846,6 +846,9 @@
846846

847847
var $compileMinErr = minErr('$compile');
848848

849+
function UNINITIALIZED_VALUE() {}
850+
var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
851+
849852
/**
850853
* @ngdoc provider
851854
* @name $compileProvider
@@ -3128,6 +3131,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
31283131
// the value to boolean rather than a string, so we special case this situation
31293132
destination[scopeName] = lastValue;
31303133
}
3134+
recordChanges(scopeName, destination[scopeName], _UNINITIALIZED_VALUE);
31313135
break;
31323136

31333137
case '=':
@@ -3183,6 +3187,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
31833187
parentGet = $parse(attrs[attrName]);
31843188

31853189
destination[scopeName] = parentGet(scope);
3190+
recordChanges(scopeName, destination[scopeName], _UNINITIALIZED_VALUE);
31863191

31873192
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
31883193
var oldValue = destination[scopeName];
@@ -3224,7 +3229,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
32243229
previousValue = changes[key].previousValue;
32253230
}
32263231
// Store this change
3227-
changes[key] = {previousValue: previousValue, currentValue: currentValue};
3232+
changes[key] = new SimpleChange(previousValue, currentValue);
32283233
}
32293234
}
32303235

@@ -3243,6 +3248,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
32433248
}];
32443249
}
32453250

3251+
function SimpleChange(previous, current) {
3252+
this.previousValue = previous;
3253+
this.currentValue = current;
3254+
}
3255+
SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
3256+
3257+
32463258
var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
32473259
/**
32483260
* Converts all accepted directives format into proper directive name.

test/ng/compileSpec.js

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3675,8 +3675,9 @@ describe('$compile', function() {
36753675
// Now we should have a single changes entry in the log
36763676
expect(log).toEqual([
36773677
{
3678-
prop1: {previousValue: undefined, currentValue: 42},
3679-
prop2: {previousValue: undefined, currentValue: 84}
3678+
prop1: jasmine.objectContaining({currentValue: 42}),
3679+
prop2: jasmine.objectContaining({currentValue: 84}),
3680+
attr: jasmine.objectContaining({currentValue: ''})
36803681
}
36813682
]);
36823683

@@ -3688,8 +3689,8 @@ describe('$compile', function() {
36883689
// Now we should have a single changes entry in the log
36893690
expect(log).toEqual([
36903691
{
3691-
prop1: {previousValue: 42, currentValue: 17},
3692-
prop2: {previousValue: 84, currentValue: 34}
3692+
prop1: jasmine.objectContaining({previousValue: 42, currentValue: 17}),
3693+
prop2: jasmine.objectContaining({previousValue: 84, currentValue: 34})
36933694
}
36943695
]);
36953696

@@ -3706,7 +3707,7 @@ describe('$compile', function() {
37063707
// onChanges should not have been called
37073708
expect(log).toEqual([
37083709
{
3709-
attr: {previousValue: '', currentValue: '22'}
3710+
attr: jasmine.objectContaining({previousValue: '', currentValue: '22'})
37103711
}
37113712
]);
37123713
});
@@ -3738,15 +3739,54 @@ describe('$compile', function() {
37383739
// Update val to trigger the onChanges
37393740
$rootScope.$apply('a = 42');
37403741
// Now the change should have the real previous value (undefined), not the intermediate one (42)
3741-
expect(log).toEqual([{prop: {previousValue: undefined, currentValue: 126}}]);
3742+
expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: 126})}]);
37423743

37433744
// Clear the log
37443745
log = [];
37453746

37463747
// Update val to trigger the onChanges
37473748
$rootScope.$apply('a = 7');
37483749
// Now the change should have the real previous value (126), not the intermediate one, (91)
3749-
expect(log).toEqual([{ prop: {previousValue: 126, currentValue: 21}}]);
3750+
expect(log).toEqual([{prop: jasmine.objectContaining({previousValue: 126, currentValue: 21})}]);
3751+
});
3752+
});
3753+
3754+
3755+
it('should trigger an initial onChanges call for each binding with the `isFirstChange()` returning true', function() {
3756+
var log = [];
3757+
function TestController() { }
3758+
TestController.prototype.$onChanges = function(change) { log.push(change); };
3759+
3760+
angular.module('my', [])
3761+
.component('c1', {
3762+
controller: TestController,
3763+
bindings: { 'prop': '<', attr: '@' }
3764+
});
3765+
3766+
module('my');
3767+
inject(function($compile, $rootScope) {
3768+
element = $compile('<c1 prop="a" attr="{{a}}"></c1>')($rootScope);
3769+
expect(log).toEqual([]);
3770+
$rootScope.$apply('a = 7');
3771+
expect(log).toEqual([
3772+
{
3773+
prop: jasmine.objectContaining({currentValue: 7}),
3774+
attr: jasmine.objectContaining({currentValue: '7'})
3775+
}
3776+
]);
3777+
expect(log[0].prop.isFirstChange()).toEqual(true);
3778+
expect(log[0].attr.isFirstChange()).toEqual(true);
3779+
3780+
log = [];
3781+
$rootScope.$apply('a = 9');
3782+
expect(log).toEqual([
3783+
{
3784+
prop: jasmine.objectContaining({previousValue: 7, currentValue: 9}),
3785+
attr: jasmine.objectContaining({previousValue: '7', currentValue: '9'})
3786+
}
3787+
]);
3788+
expect(log[0].prop.isFirstChange()).toEqual(false);
3789+
expect(log[0].attr.isFirstChange()).toEqual(false);
37503790
});
37513791
});
37523792

@@ -3785,8 +3825,8 @@ describe('$compile', function() {
37853825
$rootScope.$apply('val1 = 42; val2 = 17');
37863826

37873827
expect(log).toEqual([
3788-
['TestController1', {prop: {previousValue: undefined, currentValue: 42}}],
3789-
['TestController2', {prop: {previousValue: undefined, currentValue: 17}}]
3828+
['TestController1', {prop: jasmine.objectContaining({currentValue: 42})}],
3829+
['TestController2', {prop: jasmine.objectContaining({currentValue: 17})}]
37903830
]);
37913831
// A single apply should only trigger three turns of the digest loop
37923832
expect(watchCount).toEqual(3);
@@ -3830,8 +3870,9 @@ describe('$compile', function() {
38303870
$rootScope.$apply('a = 42');
38313871

38323872
expect(log).toEqual([
3833-
['OuterController', {prop1: {previousValue: undefined, currentValue: 42}}],
3834-
['InnerController', {prop2: {previousValue: undefined, currentValue: 72}}]
3873+
['OuterController', {prop1: jasmine.objectContaining({currentValue: 42})}],
3874+
['InnerController', {prop2: jasmine.objectContaining({currentValue: undefined})}],
3875+
['InnerController', {prop2: jasmine.objectContaining({currentValue: 72})}]
38353876
]);
38363877
});
38373878
});

0 commit comments

Comments
 (0)