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

Commit a6a4b23

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

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
@@ -3115,6 +3118,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
31153118
// the value to boolean rather than a string, so we special case this situation
31163119
destination[scopeName] = lastValue;
31173120
}
3121+
recordChanges(scopeName, destination[scopeName], _UNINITIALIZED_VALUE);
31183122
break;
31193123

31203124
case '=':
@@ -3170,6 +3174,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
31703174
parentGet = $parse(attrs[attrName]);
31713175

31723176
destination[scopeName] = parentGet(scope);
3177+
recordChanges(scopeName, destination[scopeName], _UNINITIALIZED_VALUE);
31733178

31743179
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
31753180
var oldValue = destination[scopeName];
@@ -3211,7 +3216,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
32113216
previousValue = changes[key].previousValue;
32123217
}
32133218
// Store this change
3214-
changes[key] = {previousValue: previousValue, currentValue: currentValue};
3219+
changes[key] = new SimpleChange(previousValue, currentValue);
32153220
}
32163221
}
32173222

@@ -3230,6 +3235,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
32303235
}];
32313236
}
32323237

3238+
function SimpleChange(previous, current) {
3239+
this.previousValue = previous;
3240+
this.currentValue = current;
3241+
}
3242+
SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
3243+
3244+
32333245
var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
32343246
/**
32353247
* 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
@@ -3676,8 +3676,9 @@ describe('$compile', function() {
36763676
// Now we should have a single changes entry in the log
36773677
expect(log).toEqual([
36783678
{
3679-
prop1: {previousValue: undefined, currentValue: 42},
3680-
prop2: {previousValue: undefined, currentValue: 84}
3679+
prop1: jasmine.objectContaining({currentValue: 42}),
3680+
prop2: jasmine.objectContaining({currentValue: 84}),
3681+
attr: jasmine.objectContaining({currentValue: ''})
36813682
}
36823683
]);
36833684

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

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

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

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

@@ -3786,8 +3826,8 @@ describe('$compile', function() {
37863826
$rootScope.$apply('val1 = 42; val2 = 17');
37873827

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

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

0 commit comments

Comments
 (0)