diff --git a/docs/content/error/$compile/infchng.ngdoc b/docs/content/error/$compile/infchng.ngdoc
new file mode 100644
index 000000000000..00d97061fe45
--- /dev/null
+++ b/docs/content/error/$compile/infchng.ngdoc
@@ -0,0 +1,30 @@
+@ngdoc error
+@name $compile:infchng
+@fullName Unstable `$onChanges` hooks
+@description
+
+This error occurs when the application's model becomes unstable because some `$onChanges` hooks are causing updates which then trigger
+further calls to `$onChanges` that can never complete.
+Angular detects this situation and prevents an infinite loop from causing the browser to become unresponsive.
+
+For example, the situation can occur by setting up a `$onChanges()` hook which triggers an event on the component, which subsequently
+triggers the component's bound inputs to be updated:
+
+```html
+
+```
+
+```js
+function Controller1() {}
+Controller1.$onChanges = function() {
+ this.onChange();
+};
+
+mod.component('c1', {
+ controller: Controller1,
+ bindings: {'prop': '<', onChange: '&'}
+}
+```
+
+The maximum number of allowed iterations of the `$onChanges` hooks is controlled via TTL setting which can be configured via
+{@link ng.$compileProvider#onChangesTtl `$compileProvider.onChangesTtl`}.
diff --git a/docs/content/guide/component.ngdoc b/docs/content/guide/component.ngdoc
index f2c5537b6055..55e22d137d6f 100644
--- a/docs/content/guide/component.ngdoc
+++ b/docs/content/guide/component.ngdoc
@@ -147,6 +147,30 @@ components should follow a few simple conventions:
}
```
+- **Components have a well-defined lifecycle**
+Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life
+of the component. The following hook methods can be implemented:
+
+ * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
+ had their bindings initialized (and before the pre & post linking functions for the directives on
+ 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
+ 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.
+ * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
+ function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
+ Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
+ they are waiting for their template to load asynchronously and their own compilation and linking has been
+ suspended until that occurs.
+ This hook can be considered analogous to the `ngAfterViewInit` and `ngAfterContentInit` hooks in Angular 2.
+ Since the compilation process is rather different in Angular 1 there is no direct mapping and care should
+ be taken when upgrading.
+
+By implementing these methods, you component can take part in its lifecycle.
+
- **An application is a tree of components:**
Ideally, the whole application should be a tree of components that implement clearly defined inputs
and outputs, and minimize two-way data binding. That way, it's easier to predict when data changes and what the state
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 057c4c8545fe..12b0a893626e 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -293,9 +293,23 @@
* `true` if the specified slot contains content (i.e. one or more DOM nodes).
*
* The controller can provide the following methods that act as life-cycle hooks:
- * * `$onInit` - Called on each controller after all the controllers on an element have been constructed and
+ * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
* had their bindings initialized (and before the pre & post linking functions for the directives on
* 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.
+ * * `$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
+ * components will have their `$onDestroy()` hook called before child components.
+ * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
+ * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
+ * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
+ * they are waiting for their template to load asynchronously and their own compilation and linking has been
+ * suspended until that occurs.
+ *
*
* #### `require`
* Require another directive and inject its controller as the fourth argument to the linking function. The
@@ -1207,6 +1221,36 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
+
+ var TTL = 10;
+ /**
+ * @ngdoc method
+ * @name $compileProvider#onChangesTtl
+ * @description
+ *
+ * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
+ * assuming that the model is unstable.
+ *
+ * The current default is 10 iterations.
+ *
+ * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
+ * in several iterations of calls to these hooks. However if an application needs more than the default 10
+ * iterations to stabilize then you should investigate what is causing the model to continuously change during
+ * the `$onChanges` hook execution.
+ *
+ * Increasing the TTL could have performance implications, so you should not change it without proper justification.
+ *
+ * @param {number} limit The number of `$onChanges` hook iterations.
+ * @returns {number|object} the current limit (or `this` if called as a setter for chaining)
+ */
+ this.onChangesTtl = function(value) {
+ if (arguments.length) {
+ TTL = value;
+ return this;
+ }
+ return TTL;
+ };
+
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
@@ -1215,6 +1259,36 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var SIMPLE_ATTR_NAME = /^\w/;
var specialAttrHolder = document.createElement('div');
+
+
+
+ var onChangesTtl = TTL;
+ // The onChanges hooks should all be run together in a single digest
+ // When changes occur, the call to trigger their hooks will be added to this queue
+ var onChangesQueue;
+
+ // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
+ function flushOnChangesQueue() {
+ try {
+ if (!(--onChangesTtl)) {
+ // We have hit the TTL limit so reset everything
+ onChangesQueue = undefined;
+ throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
+ }
+ // We must run this hook in an apply since the $$postDigest runs outside apply
+ $rootScope.$apply(function() {
+ for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
+ onChangesQueue[i]();
+ }
+ // Reset the queue to trigger a new schedule next time there is a change
+ onChangesQueue = undefined;
+ });
+ } finally {
+ onChangesTtl++;
+ }
+ }
+
+
function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
@@ -2360,10 +2434,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
- // Trigger the `$onInit` method on all controllers that have one
+ // Handle the init and destroy lifecycle hooks on all controllers that have them
forEach(elementControllers, function(controller) {
- if (isFunction(controller.instance.$onInit)) {
- controller.instance.$onInit();
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$onInit)) {
+ controllerInstance.$onInit();
+ }
+ if (isFunction(controllerInstance.$onDestroy)) {
+ controllerScope.$on('$destroy', function callOnDestroyHook() {
+ controllerInstance.$onDestroy();
+ });
}
});
@@ -2400,6 +2480,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
);
}
+ // Trigger $postLink lifecycle hooks
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$postLink)) {
+ controllerInstance.$postLink();
+ }
+ });
+
// This is the function that is injected as `$transclude`.
// Note: all arguments are optional!
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
@@ -2995,6 +3083,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// only occurs for isolate scopes and new scopes with controllerAs.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
+ var changes;
forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -3010,6 +3099,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
attrs.$observe(attrName, function(value) {
if (isString(value)) {
+ var oldValue = destination[scopeName];
+ recordChanges(scopeName, value, oldValue);
destination[scopeName] = value;
}
});
@@ -3081,6 +3172,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
destination[scopeName] = parentGet(scope);
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
+ var oldValue = destination[scopeName];
+ recordChanges(scopeName, newParentValue, oldValue);
destination[scopeName] = newParentValue;
}, parentGet.literal);
@@ -3101,6 +3194,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
+ function recordChanges(key, currentValue, previousValue) {
+ if (isFunction(destination.$onChanges) && currentValue !== previousValue) {
+ // If we have not already scheduled the top level onChangesQueue handler then do so now
+ if (!onChangesQueue) {
+ scope.$$postDigest(flushOnChangesQueue);
+ onChangesQueue = [];
+ }
+ // If we have not already queued a trigger of onChanges for this controller then do so now
+ if (!changes) {
+ changes = {};
+ onChangesQueue.push(triggerOnChangesHook);
+ }
+ // If the has been a change on this property already then we need to reuse the previous value
+ if (changes[key]) {
+ previousValue = changes[key].previousValue;
+ }
+ // Store this change
+ changes[key] = {previousValue: previousValue, currentValue: currentValue};
+ }
+ }
+
+ function triggerOnChangesHook() {
+ destination.$onChanges(changes);
+ // Now clear the changes so that we schedule onChanges when more changes arrive
+ changes = undefined;
+ }
+
return removeWatchCollection.length && function removeWatches() {
for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
removeWatchCollection[i]();
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index c282067213be..a9fd0069e666 100755
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -3515,6 +3515,391 @@ describe('$compile', function() {
});
});
+ describe('controller lifecycle hooks', function() {
+
+ describe('$onInit', function() {
+
+ it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() {
+
+ function check() {
+ /*jshint validthis:true */
+ expect(this.element.controller('d1').id).toEqual(1);
+ expect(this.element.controller('d2').id).toEqual(2);
+ }
+
+ function Controller1($element) { this.id = 1; this.element = $element; }
+ Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
+
+ function Controller2($element) { this.id = 2; this.element = $element; }
+ Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
+
+ angular.module('my', [])
+ .directive('d1', valueFn({ controller: Controller1 }))
+ .directive('d2', valueFn({ controller: Controller2 }));
+
+ module('my');
+ inject(function($compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce();
+ expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce();
+ });
+ });
+ });
+
+
+ describe('$onDestroy', function() {
+
+ it('should call `$onDestroy`, if provided, on the controller when its scope is destroyed', function() {
+
+ function TestController() { this.count = 0; }
+ TestController.prototype.$onDestroy = function() { this.count++; };
+
+ angular.module('my', [])
+ .directive('d1', valueFn({ scope: true, controller: TestController }))
+ .directive('d2', valueFn({ scope: {}, controller: TestController }))
+ .directive('d3', valueFn({ controller: TestController }));
+
+ module('my');
+ inject(function($compile, $rootScope) {
+
+ element = $compile('')($rootScope);
+
+ $rootScope.$apply('show = [true, true, true]');
+ var d1Controller = element.find('d1').controller('d1');
+ var d2Controller = element.find('d2').controller('d2');
+ var d3Controller = element.find('d3').controller('d3');
+
+ expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([0,0,0]);
+ $rootScope.$apply('show = [false, true, true]');
+ expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,0,0]);
+ $rootScope.$apply('show = [false, false, true]');
+ expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,0]);
+ $rootScope.$apply('show = [false, false, false]');
+ expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,1]);
+ });
+ });
+
+
+ it('should call `$onDestroy` top-down (the same as `scope.$broadcast`)', function() {
+ var log = [];
+ function ParentController() { log.push('parent created'); }
+ ParentController.prototype.$onDestroy = function() { log.push('parent destroyed'); };
+ function ChildController() { log.push('child created'); }
+ ChildController.prototype.$onDestroy = function() { log.push('child destroyed'); };
+ function GrandChildController() { log.push('grand child created'); }
+ GrandChildController.prototype.$onDestroy = function() { log.push('grand child destroyed'); };
+
+ angular.module('my', [])
+ .directive('parent', valueFn({ scope: true, controller: ParentController }))
+ .directive('child', valueFn({ scope: true, controller: ChildController }))
+ .directive('grandChild', valueFn({ scope: true, controller: GrandChildController }));
+
+ module('my');
+ inject(function($compile, $rootScope) {
+
+ element = $compile('')($rootScope);
+ $rootScope.$apply('show = true');
+ expect(log).toEqual(['parent created', 'child created', 'grand child created']);
+ log = [];
+ $rootScope.$apply('show = false');
+ expect(log).toEqual(['parent destroyed', 'child destroyed', 'grand child destroyed']);
+ });
+ });
+ });
+
+
+ describe('$postLink', function() {
+
+ it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() {
+
+ var log = [];
+
+ function Controller1() { }
+ Controller1.prototype.$postLink = function() { log.push('d1 view init'); };
+
+ function Controller2() { }
+ Controller2.prototype.$postLink = function() { log.push('d2 view init'); };
+
+ angular.module('my', [])
+ .directive('d1', valueFn({
+ controller: Controller1,
+ link: { pre: function(s, e) { log.push('d1 pre: ' + e.text()); }, post: function(s, e) { log.push('d1 post: ' + e.text()); } },
+ template: ''
+ }))
+ .directive('d2', valueFn({
+ controller: Controller2,
+ link: { pre: function(s, e) { log.push('d2 pre: ' + e.text()); }, post: function(s, e) { log.push('d2 post: ' + e.text()); } },
+ template: 'loaded'
+ }));
+
+ module('my');
+ inject(function($compile, $rootScope) {
+ element = $compile('')($rootScope);
+ expect(log).toEqual([
+ 'd1 pre: loaded',
+ 'd2 pre: loaded',
+ 'd2 post: loaded',
+ 'd2 view init',
+ 'd1 post: loaded',
+ 'd1 view init'
+ ]);
+ });
+ });
+ });
+
+
+ describe('$onChanges', function() {
+
+ it('should call `$onChanges`, if provided, when a one-way (`<`) or interpolation (`@`) bindings are updated', function() {
+ var log = [];
+ function TestController() { }
+ TestController.prototype.$onChanges = function(change) { log.push(change); };
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: TestController,
+ bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' }
+ });
+
+ module('my');
+ inject(function($compile, $rootScope) {
+ // Setup a watch to indicate some complicated updated logic
+ $rootScope.$watch('val', function(val, oldVal) { $rootScope.val2 = val * 2; });
+ // Setup the directive with two bindings
+ element = $compile('')($rootScope);
+
+ // There should be no changes initially
+ expect(log).toEqual([]);
+
+ // Update val to trigger the onChanges
+ $rootScope.$apply('val = 42');
+ // Now we should have a single changes entry in the log
+ expect(log).toEqual([
+ {
+ prop1: {previousValue: undefined, currentValue: 42},
+ prop2: {previousValue: undefined, currentValue: 84}
+ }
+ ]);
+
+ // Clear the log
+ log = [];
+
+ // Update val to trigger the onChanges
+ $rootScope.$apply('val = 17');
+ // Now we should have a single changes entry in the log
+ expect(log).toEqual([
+ {
+ prop1: {previousValue: 42, currentValue: 17},
+ prop2: {previousValue: 84, currentValue: 34}
+ }
+ ]);
+
+ // Clear the log
+ log = [];
+
+ // Update val3 to trigger the "other" two-way binding
+ $rootScope.$apply('val3 = 63');
+ // onChanges should not have been called
+ expect(log).toEqual([]);
+
+ // Update val4 to trigger the "attr" interpolation binding
+ $rootScope.$apply('val4 = 22');
+ // onChanges should not have been called
+ expect(log).toEqual([
+ {
+ attr: {previousValue: '', currentValue: '22'}
+ }
+ ]);
+ });
+ });
+
+
+ it('should pass the original value as `previousValue` even if there were multiple changes in a single digest', function() {
+ var log = [];
+ function TestController() { }
+ TestController.prototype.$onChanges = function(change) { log.push(change); };
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: TestController,
+ bindings: { 'prop': '<' }
+ });
+
+ module('my');
+ inject(function($compile, $rootScope) {
+ element = $compile('')($rootScope);
+
+ // We add this watch after the compilation to ensure that it will run after the binding watchers
+ // therefore triggering the thing that this test is hoping to enfore
+ $rootScope.$watch('a', function(val) { $rootScope.b = val * 2; });
+
+ // There should be no changes initially
+ expect(log).toEqual([]);
+
+ // 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}}]);
+
+ // 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}}]);
+ });
+ });
+
+
+ it('should only trigger one extra digest however many controllers have changes', function() {
+ var log = [];
+ function TestController1() { }
+ TestController1.prototype.$onChanges = function(change) { log.push(['TestController1', change]); };
+ function TestController2() { }
+ TestController2.prototype.$onChanges = function(change) { log.push(['TestController2', change]); };
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: TestController1,
+ bindings: {'prop': '<'}
+ })
+ .component('c2', {
+ controller: TestController2,
+ bindings: {'prop': '<'}
+ });
+
+ module('my');
+ inject(function($compile, $rootScope) {
+
+ // Create a watcher to count the number of digest cycles
+ var watchCount = 0;
+ $rootScope.$watch(function() { watchCount++; });
+
+ // Setup two sibling components with bindings that will change
+ element = $compile('
')($rootScope);
+
+ // There should be no changes initially
+ expect(log).toEqual([]);
+
+ // Update val to trigger the onChanges
+ $rootScope.$apply('val1 = 42; val2 = 17');
+
+ expect(log).toEqual([
+ ['TestController1', {prop: {previousValue: undefined, currentValue: 42}}],
+ ['TestController2', {prop: {previousValue: undefined, currentValue: 17}}]
+ ]);
+ // A single apply should only trigger three turns of the digest loop
+ expect(watchCount).toEqual(3);
+ });
+ });
+
+
+ it('should cope with changes occuring inside `$onChanges()` hooks', function() {
+ var log = [];
+ function OuterController() { }
+ OuterController.prototype.$onChanges = function(change) {
+ log.push(['OuterController', change]);
+ // Make a change to the inner component
+ this.b = 72;
+ };
+
+ function InnerController() { }
+ InnerController.prototype.$onChanges = function(change) { log.push(['InnerController', change]); };
+
+ angular.module('my', [])
+ .component('outer', {
+ controller: OuterController,
+ bindings: {'prop1': '<'},
+ template: ''
+ })
+ .component('inner', {
+ controller: InnerController,
+ bindings: {'prop2': '<'}
+ });
+
+ module('my');
+ inject(function($compile, $rootScope) {
+
+ // Setup the directive with two bindings
+ element = $compile('')($rootScope);
+
+ // There should be no changes initially
+ expect(log).toEqual([]);
+
+ // Update val to trigger the onChanges
+ $rootScope.$apply('a = 42');
+
+ expect(log).toEqual([
+ ['OuterController', {prop1: {previousValue: undefined, currentValue: 42}}],
+ ['InnerController', {prop2: {previousValue: undefined, currentValue: 72}}]
+ ]);
+ });
+ });
+
+
+ it('should throw an error if `$onChanges()` hooks are not stable', function() {
+ function TestController() {}
+ TestController.prototype.$onChanges = function(change) {
+ this.onChange();
+ };
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: TestController,
+ bindings: {'prop': '<', onChange: '&'}
+ });
+
+ module('my');
+ inject(function($compile, $rootScope) {
+
+ // Setup the directive with bindings that will keep updating the bound value forever
+ element = $compile('')($rootScope);
+
+ // Update val to trigger the unstable onChanges, which will result in an error
+ expect(function() {
+ $rootScope.$apply('a = 42');
+ }).toThrowMinErr('$compile', 'infchng');
+
+ dealoc(element);
+ element = $compile('')($rootScope);
+ $rootScope.$apply('b = 24');
+ $rootScope.$apply('b = 48');
+ });
+ });
+
+
+ it('should log an error if `$onChanges()` hooks are not stable', function() {
+ function TestController() {}
+ TestController.prototype.$onChanges = function(change) {
+ this.onChange();
+ };
+
+ angular.module('my', [])
+ .component('c1', {
+ controller: TestController,
+ bindings: {'prop': '<', onChange: '&'}
+ })
+ .config(function($exceptionHandlerProvider) {
+ // We need to test with the exceptionHandler not rethrowing...
+ $exceptionHandlerProvider.mode('log');
+ });
+
+ module('my');
+ inject(function($compile, $rootScope, $exceptionHandler) {
+
+ // Setup the directive with bindings that will keep updating the bound value forever
+ element = $compile('')($rootScope);
+
+ // Update val to trigger the unstable onChanges, which will result in an error
+ $rootScope.$apply('a = 42');
+ expect($exceptionHandler.errors.length).toEqual(1);
+ expect($exceptionHandler.errors[0].toString()).toContain('[$compile:infchng] 10 $onChanges() iterations reached.');
+ });
+ });
+ });
+ });
+
describe('isolated locals', function() {
var componentScope, regularScope;
@@ -5324,32 +5709,6 @@ describe('$compile', function() {
});
});
- it('should call `controller.$onInit`, if provided after all the controllers have been constructed', function() {
-
- function check() {
- /*jshint validthis:true */
- expect(this.element.controller('d1').id).toEqual(1);
- expect(this.element.controller('d2').id).toEqual(2);
- }
-
- function Controller1($element) { this.id = 1; this.element = $element; }
- Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
-
- function Controller2($element) { this.id = 2; this.element = $element; }
- Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
-
- angular.module('my', [])
- .directive('d1', valueFn({ controller: Controller1 }))
- .directive('d2', valueFn({ controller: Controller2 }));
-
- module('my');
- inject(function($compile, $rootScope) {
- element = $compile('')($rootScope);
- expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce();
- expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce();
- });
- });
-
describe('should not overwrite @-bound property each digest when not present', function() {
it('when creating new scope', function() {
module(function($compileProvider) {