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 ) ;
@@ -2360,10 +2434,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
23602434 }
23612435 } ) ;
23622436
2363- // Trigger the `$onInit` method on all controllers that have one
2437+ // Handle the init and destroy lifecycle hooks on all controllers that have them
23642438 forEach ( elementControllers , function ( controller ) {
2365- if ( isFunction ( controller . instance . $onInit ) ) {
2366- controller . instance . $onInit ( ) ;
2439+ var controllerInstance = controller . instance ;
2440+ if ( isFunction ( controllerInstance . $onInit ) ) {
2441+ controllerInstance . $onInit ( ) ;
2442+ }
2443+ if ( isFunction ( controllerInstance . $onDestroy ) ) {
2444+ controllerScope . $on ( '$destroy' , function callOnDestroyHook ( ) {
2445+ controllerInstance . $onDestroy ( ) ;
2446+ } ) ;
23672447 }
23682448 } ) ;
23692449
@@ -2400,6 +2480,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
24002480 ) ;
24012481 }
24022482
2483+ // Trigger $postLink lifecycle hooks
2484+ forEach ( elementControllers , function ( controller ) {
2485+ var controllerInstance = controller . instance ;
2486+ if ( isFunction ( controllerInstance . $postLink ) ) {
2487+ controllerInstance . $postLink ( ) ;
2488+ }
2489+ } ) ;
2490+
24032491 // This is the function that is injected as `$transclude`.
24042492 // Note: all arguments are optional!
24052493 function controllersBoundTransclude ( scope , cloneAttachFn , futureParentElement , slotName ) {
@@ -2995,6 +3083,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
29953083 // only occurs for isolate scopes and new scopes with controllerAs.
29963084 function initializeDirectiveBindings ( scope , attrs , destination , bindings , directive ) {
29973085 var removeWatchCollection = [ ] ;
3086+ var changes ;
29983087 forEach ( bindings , function initializeBinding ( definition , scopeName ) {
29993088 var attrName = definition . attrName ,
30003089 optional = definition . optional ,
@@ -3010,6 +3099,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
30103099 }
30113100 attrs . $observe ( attrName , function ( value ) {
30123101 if ( isString ( value ) ) {
3102+ var oldValue = destination [ scopeName ] ;
3103+ recordChanges ( scopeName , value , oldValue ) ;
30133104 destination [ scopeName ] = value ;
30143105 }
30153106 } ) ;
@@ -3081,6 +3172,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
30813172 destination [ scopeName ] = parentGet ( scope ) ;
30823173
30833174 removeWatch = scope . $watch ( parentGet , function parentValueWatchAction ( newParentValue ) {
3175+ var oldValue = destination [ scopeName ] ;
3176+ recordChanges ( scopeName , newParentValue , oldValue ) ;
30843177 destination [ scopeName ] = newParentValue ;
30853178 } , parentGet . literal ) ;
30863179
@@ -3101,6 +3194,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
31013194 }
31023195 } ) ;
31033196
3197+ function recordChanges ( key , currentValue , previousValue ) {
3198+ if ( isFunction ( destination . $onChanges ) && currentValue !== previousValue ) {
3199+ // If we have not already scheduled the top level onChangesQueue handler then do so now
3200+ if ( ! onChangesQueue ) {
3201+ scope . $$postDigest ( flushOnChangesQueue ) ;
3202+ onChangesQueue = [ ] ;
3203+ }
3204+ // If we have not already queued a trigger of onChanges for this controller then do so now
3205+ if ( ! changes ) {
3206+ changes = { } ;
3207+ onChangesQueue . push ( triggerOnChangesHook ) ;
3208+ }
3209+ // If the has been a change on this property already then we need to reuse the previous value
3210+ if ( changes [ key ] ) {
3211+ previousValue = changes [ key ] . previousValue ;
3212+ }
3213+ // Store this change
3214+ changes [ key ] = { previousValue : previousValue , currentValue : currentValue } ;
3215+ }
3216+ }
3217+
3218+ function triggerOnChangesHook ( ) {
3219+ destination . $onChanges ( changes ) ;
3220+ // Now clear the changes so that we schedule onChanges when more changes arrive
3221+ changes = undefined ;
3222+ }
3223+
31043224 return removeWatchCollection . length && function removeWatches ( ) {
31053225 for ( var i = 0 , ii = removeWatchCollection . length ; i < ii ; ++ i ) {
31063226 removeWatchCollection [ i ] ( ) ;
0 commit comments