diff --git a/docs/content/guide/component.ngdoc b/docs/content/guide/component.ngdoc index 55e22d137d6..f2aaec04423 100644 --- a/docs/content/guide/component.ngdoc +++ b/docs/content/guide/component.ngdoc @@ -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. diff --git a/src/ng/compile.js b/src/ng/compile.js index 9d444ddb7bf..74cd3259ca4 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -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. diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index a9fd0069e66..4f37dc345ec 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -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('')($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})}] ]); }); });