The problem
I am unit testing a directive has no controller or template, only a link function. The directive requires ngModel and calls one of its functions in the link function. I want to spy on ngModel in my unit tests to ensure the right function is being called.
The code
Directive:
angular.module('some-module').directive('someDirective', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, controller) {
controller.doSomething(); //Calls some random function on the ngModel controller
}
};
});
What I've tried
I've tried to inject a spy ngModel like this:
beforeEach(module(function($provide) {
$provide.factory('ngModelDirective', function() {
return {};
});
$provide.factory('ngModelController', function() {
return function() {};
});
}));
As I discovered on this question, trying to override built-in properties causes an error to be thrown, and is bad practice.
So then I tried to test the directive the way the Angular docs say to:
var $scope = $rootScope.$new();
var element = $compile('<div some-directive></div>')($scope);
And spy on NgModelController like this:
var ngModelControllerSpyDoSomething = sinon.spy(element.controller('ngModel'), 'doSomething');
But this doesn't work, because one $compile is run, it executes the link function, so I'm not spying on it until it's too late (the spy is coming back as never having been called). This is also the case if I put $scope.$digest(); before or after creating the spy.
You will have to add your spy to the $scope you are injecting into the $compile-Function and then link it within the actual directives HTMLngModel`, so:
var $scope = $rootScope.$new();
$scope.mySpy = // create a stub function with sinon here
var element = $compile('<div some-directive ng-model="mySpy"></div>')($scope);
Related
To test angular 1.5 components, the docs recommend you use ngMock's $componentController instead of using $compile if you don't need to test any of the DOM.
However, my component uses ngModel which I need to pass into the locals for $componentController, but there is no way to programmatically get the ngModelController; the only way to test it is to actually $compile an element with it on it, as this issue is still open: https://github.com/angular/angular.js/issues/7720.
Is there any way to test my components controller without resorting to $compiling it? I also don't want to have to mock the ngModelController myself as its behavior is somewhat extensive and if my tests rely on a fake one rather than the real thing there is a chance newer versions of Angular could break it (though that probably isn't an issue given Angular 1 is being phased out).
tl;dr: Solution is in the third code block.
but there is no way to programmatically get the ngModelController
Not with that attitude. ;)
You can get it programmatically, just a little roundabout. The method of doing so is in the code for ngMock's $componentController service (paraphrased here); use $injector.get('ngModelDirective') to look it up, and the controller function will be attached to it as the controller property:
this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) {
return function $componentController(componentName, locals, bindings, ident) {
// get all directives associated to the component name
var directives = $injector.get(componentName + 'Directive');
// look for those directives that are components
var candidateDirectives = directives.filter(function(directiveInfo) {
// ...
});
// ...
// get the info of the component
var directiveInfo = candidateDirectives[0];
// create a scope if needed
locals = locals || {};
locals.$scope = locals.$scope || $rootScope.$new(true);
return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs);
};
}];
Though you need to supply the ngModelController locals for $element and $attrs when you instantiate it. The test spec for ngModel demonstrates exactly how to do this in its beforeEach call:
beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias', ngModel: 'value'};
parentFormCtrl = {
$$setPending: jasmine.createSpy('$$setPending'),
$setValidity: jasmine.createSpy('$setValidity'),
$setDirty: jasmine.createSpy('$setDirty'),
$$clearControlValidity: noop
};
element = jqLite('<form><input></form>');
scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, {
$scope: scope,
$element: element.find('input'),
$attrs: attrs
});
//Assign the mocked parentFormCtrl to the model controller
ctrl.$$parentForm = parentFormCtrl;
}));
So, adapting that to what we need, we get a spec like this:
describe('Unit: myComponent', function () {
var $componentController,
$controller,
$injector,
$rootScope;
beforeEach(inject(function (_$componentController_, _$controller_, _$injector_, _$rootScope_) {
$componentController = _$componentController_;
$controller = _$controller_;
$injector = _$injector_;
$rootScope = _$rootScope_;
}));
it('should update its ngModel value accordingly', function () {
var ngModelController,
locals
ngModelInstance,
$ctrl;
locals = {
$scope: $rootScope.$new(),
//think this could be any element, honestly, but matching the component looks better
$element: angular.element('<my-component></my-component>'),
//the value of $attrs.ngModel is exactly what you'd put for ng-model in a template
$attrs: { ngModel: 'value' }
};
locals.$scope.value = null; //this is what'd get passed to ng-model in templates
ngModelController = $injector.get('ngModelDirective')[0].controller;
ngModelInstance = $controller(ngModelController, locals);
$ctrl = $componentController('myComponent', null, { ngModel: ngModelInstance });
$ctrl.doAThingToUpdateTheModel();
scope.$digest();
//Check against both the scope value and the $modelValue, use toBe and toEqual as needed.
expect(ngModelInstance.$modelValue).toBe('some expected value goes here');
expect(locals.$scope.value).toBe('some expected value goes here');
});
});
ADDENDUM: You can also simplify it further by instead injecting ngModelDirective in the beforeEach and setting a var in the describe block to contain the controller function, like you do with services like $controller.
describe('...', function () {
var ngModelController;
beforeEach(inject(function(_ngModelDirective_) {
ngModelController = _ngModelDirective_[0].controller;
}));
});
I am trying to use $emit in my code for a directive.
I have
(function () {
angular
.module('myApp')
.directive('testDirective', testDirective);
function testDirective() {
var directive = {
link: link,
restrict: 'A',
controller: testCtrl,
controllerAs: 'vm'
};
return directive;
function link(scope, element, attrs) {
}
}
function testCtrl() {
var vm = this;
// do something
vm.$emit('someEvent', {'id': '123'})
}
})();
However, I am getting 'TypeError: vm.$emit is not a function'. I am not sure how to fix this. Can anyone help? Thanks a lot!
controllerAs just means that scoped variables are attached directly to the controller -- it doesn't mean that the controller is an instance of an angular scope itself. In this case, you'll need to inject the scope into the controller and then emit the event from on the scope:
function testCtrl($scope) {
// do something
$scope.$emit('someEvent', {'id': '123'})
}
Also beware, the normal injection rules apply -- If you're going to minify this, you'll probably need something like:
testCtrl['$inject'] = ['$scope'];
Problem: The attribute I pass to my directive's controller is not evaluated. For example, I get {{ attribute.value }} instead of 5.
Desired Outcome: My directive's controller has access to a primary key contained in an object from a parent controller. I need it to make API calls like MyResource.save({id: attribute});.
Code Snippets:
Calling directive from HTML
<div ng-controller="BoatDetailController as boatCtrl">
<div class="row">
<booking-widget boat-id="{{ boatCtrl.boat.id }}"></booking-widget>
</div>
Directive
(function () {
'use strict';
angular.
module('trips').
directive('bookingWidget', bookingWidget);
bookingWidget.$inject = [];
function bookingWidget() {
return {
restrict: 'E',
scope: {
boatId: '#'
},
templateUrl: "/static/app/trips/trips.bookingwidget.template.html",
controller: 'BookingWidgetController as bookingCtrl'
}
}
})();
Controller
(function () {
'use strict';
angular.
module('trips').
controller('BookingWidgetController', BookingWidgetController);
BookingWidgetController.$inject = ['Trip', 'Booking', 'Messages', '$scope', '$attrs'];
function BookingWidgetController(Trip, Booking, Messages, $scope, $attrs) {
var vm = this;
vm.boatId = $attrs.boatId;
...
activate();
//////////////////////////////
function activate() {
console.log(vm.boatId);
//
}
Console Results:
With $scope.boatId: (logs a blank line)
With $attrs.boatId: {{ boatCtrl.boat.id }} (a string)
Recap: The boat-id attribute of my directive is not resolving. Can you help me figure out how to fix it?
You can actually create a custom directive like this:
function bookingWidget() {
return {
restrict: 'E',
scope: {
boatId: '#'
},
templateUrl: "/static/app/trips/trips.bookingwidget.template.html",
controller: 'BookingWidgetController as bookingCtrl',
link : function(scope, element, attrs, controller){
console.log(attrs.boatId);
scope.boatId = attrs.boatId;
}
}
}
The link function actually allows you to have an access to the element, the scope of the directive, the attributes associated to the directive and the controller of the directive. The function is called after everything associated to the directive has been performed. In other words, this is the last stage.
The same scope would be shareable between the link function and controller.
Now, to make the API call, you may actually add a function in your controller that accepts the boatID, makes a call to the API and accepts the response onto the controller object. After that, add a watcher within the link function that watches over "vm.boatId", within which you may call that function which makes the API call. So, even if the controller has initialized before the link function, you would still be able to perform what you wish to. So, it would be a "link-based activation".
You may give this solution a try. Hope it helps.
You can pass a function and call it. Need to use & then.
https://thinkster.io/egghead/isolate-scope-am
JQuery to Directive
I want to call a method from the scope of this directive but can't seem to work it out (if possible).
$("my-directive").first().scope().loadData();
Directive Looks Something Like This
I would like to call the loadData function from the directive code below.
app.directive("myDirective", function () {
return {
restrict: "E",
templateUrl: "..."
scope: {},
controller: function ($scope, $element, $attrs) {
var self = this;
$scope.loadData = function () {
...
};
}
};
});
Scope is accessible inside the directive
You can get any child of the element of which directive is applied and get scope of it.
$('my-directive').first().children(":first").scope().loadData()
Strajk's answer is correct!
When Code is Added Dynamically setTimeout Needed
In the following example detail row has a nested directive (random-testees). I get the scope from that to dynamically compile the directive (dynamic and late-bound). The setTimeout is needed because it seems to take a bit before the
var detailRow = e.detailRow;
// Compile the directive for dyanmic content.
angular.element(document).injector().invoke(function ($compile) {
var scope = angular.element(detailRow).scope();
$compile(detailRow)(scope);
});
// Get some data from directive.
var testId = detailRow.find("random-testees").attr("testid");
// Wait, and call method on the directive.
setTimeout(function () {
var randomTesteesScope = $("random-testees").first().children(":first").scope();
randomTesteesScope.loadTestees(this);
}.bind(testId),200);
Not Very Confident About This
This seems a little brittle since I was getting mixed results with a 100 ms timeout sometimes and error when the randomTesteesScope returned undefined.
I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk