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

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/guide/component.ngdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 15 additions & 3 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -846,6 +846,9 @@

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

function UNINITIALIZED_VALUE() {}
var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();

/**
* @ngdoc provider
* @name $compileProvider
Expand Down Expand Up @@ -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 '=':
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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.
Expand Down
63 changes: 52 additions & 11 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''})
}
]);

Expand All @@ -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})
}
]);

Expand All @@ -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'})
}
]);
});
Expand Down Expand Up @@ -3739,15 +3740,54 @@ 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 = [];

// 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);
});
});

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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})}]
]);
});
});
Expand Down