293293 * `true` if the specified slot contains content (i.e. one or more DOM nodes).
294294 *
295295 * The controller can provide the following methods that act as life-cycle hooks:
296- * * `$onInit` - Called on each controller after all the controllers on an element have been constructed and
296+ * * `$onInit() ` - Called on each controller after all the controllers on an element have been constructed and
297297 * had their bindings initialized (and before the pre & post linking functions for the directives on
298298 * this element). This is a good place to put initialization code for your controller.
299+ * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
300+ * `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.
303+ * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
304+ * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
305+ * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
306+ * components will have their `$onDestroy()` hook called before child components.
307+ * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
308+ * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
309+ * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
310+ * they are waiting for their template to load asynchronously and their own compilation and linking has been
311+ * suspended until that occurs.
312+ *
299313 *
300314 * #### `require`
301315 * Require another directive and inject its controller as the fourth argument to the linking function. The
@@ -1207,6 +1221,36 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
12071221 return debugInfoEnabled ;
12081222 } ;
12091223
1224+
1225+ var TTL = 10 ;
1226+ /**
1227+ * @ngdoc method
1228+ * @name $compileProvider#onChangesTtl
1229+ * @description
1230+ *
1231+ * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
1232+ * assuming that the model is unstable.
1233+ *
1234+ * The current default is 10 iterations.
1235+ *
1236+ * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
1237+ * in several iterations of calls to these hooks. However if an application needs more than the default 10
1238+ * iterations to stabilize then you should investigate what is causing the model to continuously change during
1239+ * the `$onChanges` hook execution.
1240+ *
1241+ * Increasing the TTL could have performance implications, so you should not change it without proper justification.
1242+ *
1243+ * @param {number } limit The number of `$onChanges` hook iterations.
1244+ * @returns {number|object } the current limit (or `this` if called as a setter for chaining)
1245+ */
1246+ this . onChangesTtl = function ( value ) {
1247+ if ( arguments . length ) {
1248+ TTL = value ;
1249+ return this ;
1250+ }
1251+ return TTL ;
1252+ } ;
1253+
12101254 this . $get = [
12111255 '$injector' , '$interpolate' , '$exceptionHandler' , '$templateRequest' , '$parse' ,
12121256 '$controller' , '$rootScope' , '$sce' , '$animate' , '$$sanitizeUri' ,
@@ -1215,6 +1259,36 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
12151259
12161260 var SIMPLE_ATTR_NAME = / ^ \w / ;
12171261 var specialAttrHolder = document . createElement ( 'div' ) ;
1262+
1263+
1264+
1265+ var onChangesTtl = TTL ;
1266+ // The onChanges hooks should all be run together in a single digest
1267+ // When changes occur, the call to trigger their hooks will be added to this queue
1268+ var onChangesQueue ;
1269+
1270+ // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
1271+ function flushOnChangesQueue ( ) {
1272+ try {
1273+ if ( ! ( -- onChangesTtl ) ) {
1274+ // We have hit the TTL limit so reset everything
1275+ onChangesQueue = undefined ;
1276+ throw $compileMinErr ( 'infchng' , '{0} $onChanges() iterations reached. Aborting!\n' , TTL ) ;
1277+ }
1278+ // We must run this hook in an apply since the $$postDigest runs outside apply
1279+ $rootScope . $apply ( function ( ) {
1280+ for ( var i = 0 , ii = onChangesQueue . length ; i < ii ; ++ i ) {
1281+ onChangesQueue [ i ] ( ) ;
1282+ }
1283+ // Reset the queue to trigger a new schedule next time there is a change
1284+ onChangesQueue = undefined ;
1285+ } ) ;
1286+ } finally {
1287+ onChangesTtl ++ ;
1288+ }
1289+ }
1290+
1291+
12181292 function Attributes ( element , attributesToCopy ) {
12191293 if ( attributesToCopy ) {
12201294 var keys = Object . keys ( attributesToCopy ) ;
@@ -2373,10 +2447,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
23732447 }
23742448 } ) ;
23752449
2376- // Trigger the `$onInit` method on all controllers that have one
2450+ // Handle the init and destroy lifecycle hooks on all controllers that have them
23772451 forEach ( elementControllers , function ( controller ) {
2378- if ( isFunction ( controller . instance . $onInit ) ) {
2379- controller . instance . $onInit ( ) ;
2452+ var controllerInstance = controller . instance ;
2453+ if ( isFunction ( controllerInstance . $onInit ) ) {
2454+ controllerInstance . $onInit ( ) ;
2455+ }
2456+ if ( isFunction ( controllerInstance . $onDestroy ) ) {
2457+ controllerScope . $on ( '$destroy' , function callOnDestroyHook ( ) {
2458+ controllerInstance . $onDestroy ( ) ;
2459+ } ) ;
23802460 }
23812461 } ) ;
23822462
@@ -2413,6 +2493,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
24132493 ) ;
24142494 }
24152495
2496+ // Trigger $postLink lifecycle hooks
2497+ forEach ( elementControllers , function ( controller ) {
2498+ var controllerInstance = controller . instance ;
2499+ if ( isFunction ( controllerInstance . $postLink ) ) {
2500+ controllerInstance . $postLink ( ) ;
2501+ }
2502+ } ) ;
2503+
24162504 // This is the function that is injected as `$transclude`.
24172505 // Note: all arguments are optional!
24182506 function controllersBoundTransclude ( scope , cloneAttachFn , futureParentElement , slotName ) {
@@ -3008,6 +3096,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
30083096 // only occurs for isolate scopes and new scopes with controllerAs.
30093097 function initializeDirectiveBindings ( scope , attrs , destination , bindings , directive ) {
30103098 var removeWatchCollection = [ ] ;
3099+ var changes ;
30113100 forEach ( bindings , function initializeBinding ( definition , scopeName ) {
30123101 var attrName = definition . attrName ,
30133102 optional = definition . optional ,
@@ -3023,6 +3112,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
30233112 }
30243113 attrs . $observe ( attrName , function ( value ) {
30253114 if ( isString ( value ) ) {
3115+ var oldValue = destination [ scopeName ] ;
3116+ recordChanges ( scopeName , value , oldValue ) ;
30263117 destination [ scopeName ] = value ;
30273118 }
30283119 } ) ;
@@ -3094,6 +3185,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
30943185 destination [ scopeName ] = parentGet ( scope ) ;
30953186
30963187 removeWatch = scope . $watch ( parentGet , function parentValueWatchAction ( newParentValue ) {
3188+ var oldValue = destination [ scopeName ] ;
3189+ recordChanges ( scopeName , newParentValue , oldValue ) ;
30973190 destination [ scopeName ] = newParentValue ;
30983191 } , parentGet . literal ) ;
30993192
@@ -3114,6 +3207,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
31143207 }
31153208 } ) ;
31163209
3210+ function recordChanges ( key , currentValue , previousValue ) {
3211+ if ( isFunction ( destination . $onChanges ) && currentValue !== previousValue ) {
3212+ // If we have not already scheduled the top level onChangesQueue handler then do so now
3213+ if ( ! onChangesQueue ) {
3214+ scope . $$postDigest ( flushOnChangesQueue ) ;
3215+ onChangesQueue = [ ] ;
3216+ }
3217+ // If we have not already queued a trigger of onChanges for this controller then do so now
3218+ if ( ! changes ) {
3219+ changes = { } ;
3220+ onChangesQueue . push ( triggerOnChangesHook ) ;
3221+ }
3222+ // If the has been a change on this property already then we need to reuse the previous value
3223+ if ( changes [ key ] ) {
3224+ previousValue = changes [ key ] . previousValue ;
3225+ }
3226+ // Store this change
3227+ changes [ key ] = { previousValue : previousValue , currentValue : currentValue } ;
3228+ }
3229+ }
3230+
3231+ function triggerOnChangesHook ( ) {
3232+ destination . $onChanges ( changes ) ;
3233+ // Now clear the changes so that we schedule onChanges when more changes arrive
3234+ changes = undefined ;
3235+ }
3236+
31173237 return removeWatchCollection . length && function removeWatches ( ) {
31183238 for ( var i = 0 , ii = removeWatchCollection . length ; i < ii ; ++ i ) {
31193239 removeWatchCollection [ i ] ( ) ;
0 commit comments