AngularJS unit testing directive with ngModel - angularjs

I have made a directive which uses ngModel:
.directive('datetimepicker', function () {
return {
restrict: 'E',
require: ['datetimepicker', '?^ngModel'],
controller: 'DateTimePickerController',
replace: true,
templateUrl: ...,
scope: {
model: '=ngModel'
},
link: function (scope, element, attributes, controllers) {
var pickerController = controllers[0];
var modelController = controllers[1];
if (modelController) {
pickerController.init(modelController);
}
}
}
});
But when testing...
var scope, element;
beforeEach(module('appDateTimePicker'));
beforeEach(module('templates'));
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile;
scope = $rootScope;
scope.model = new Date();
element = compile(angular.element('<datetimepicker ng-model="model"></datetimepicker>'))(scope);
scope.$digest();
}));
I can't anyhow set value to ng-model.
Fo example, here scope.model is a date, so scope.year and scope.month should be date and year of that model, but it is undefined.
As seen in the directive's code, I'm using this.init on the controller to initialise all the process.
What am I missing?
EDIT
Example of test:
it('should test', function () {
expect(scope.model).toBe(undefined);
expect(scope.year).toBe(undefined);
});
EDIT
This helped to solve the problem: http://jsfiddle.net/pTv49/3/

The '?^ngModel' mean you are asking for the ng-model on parent elements, but the html in your test has the ng-model on the same element as datetimepicker directive.
If the ng-model really have to be on parent elements, you have to change the html in the test, for example:
element = compile(angular.element('<div ng-model="model"><datetimepicker></datetimepicker></div>'))(scope);
But if it should be on the same element, just remove the ^ symbol in the require:
require: ['datetimepicker', '?ngModel'],

The directive has a scope: {} block so it creates an isolate scope. I assume, in the tests scope refers to the outer scope, while element.isolateScope() should be used to reference the inner scope instead.

Related

Get element scope from attribute directive

I'm trying to extend functionality of any directive by simply attaching an attribute directive, but I'm having trouble getting the scope of the element on which the attribute is defined.
For example, I have this template:
<div class="flex-item-grow flex-item flex-column report-area">
<sv-report sv-reloadable id="reportId"></sv-report>
</div>
Here, sv-reloadable has some implicit understanding of sv-report, but sv-report has no idea about sv-reloadable.
I've defined sv-reloadable as:
angular
.module( 'sv-reloadable', [
'sv.services',
])
.directive('svReloadable', function(reportServices, $timeout) {
return {
restrict: 'A',
controller: function($scope, $timeout) {
$scope.$on('parameter-changed', function(evt, payload) {
evt.stopPropagation();
$scope.viewModel = getNewViewModel(payload);/* hit the server to retrieve new data */
});
}
};
});
Now, $scope in sv-reloadable is the parent scope of sv-report. I'm wanting sv-reloadable to be able to attach a listener to sv-report's scope, and swap out properties of that scope. I understand that it's possible to grab the sibling scopes, but that causes problems when trying to figure out exactly which element it's attached to.
I attempted the following:
link: function(scope, element, attrs) {
ele = element;
var actualScopyThingy = element.scope();
},
Which I had assumed would give me the scope of the element the attribute was defined on, but alas, it still returns the parent scope of the element.
If it's important, sv-report is defined as the following, but I'd like to be able to keep it the same (since sv-reloadable is going to be attached to many different elements, all of which must have viewModel defined on their scope)
return {
restrict: 'E',
replace: true,
templateUrl: 'sv-report/sv-report.tpl.html',
scope: {
id: '=',
reportParameters: '='
},
controller: function ($scope, svAnalytics) {
/* unrelated code here */
},
link: function(scope, element, attrs) {
initialLoadReport(scope);
}
};
After a bit of digging around, isolateScope() is what I was after (rather than scope()). sv-reloadable's directive becomes:
return {
restrict: 'A',
link: function(scope, element, attrs) {
var elementScope = element.isolateScope();
elementScope.$on('parameter-changed', function(evt, payload) {
...
});
}
};

unit-test invoking of ngModelCtrl.$render in Directive

Trying to unit-test the invokement on ngModelCtrl.$render. Now by $setViewValue but actually I want to set a value on ng-model on the directive and test if $render is invoked.
The Directive:
angular.module('inputValidationBelow', []).directive('inputValidationBelow', function ($compile) {
return {
scope: {
dataModel: '=ngModel',
placeHolder: '#',
identifier: '#',
feedbackMessage: '#',
type: '#'
},
templateUrl: 'scripts/directives/inputValidationBelow/inputValidationBelow.html',
restrict: 'E',
transclude: true,
replace: false,
require: ['ngModel', '^form'],
link: function (scope, element, attrs, controllers) {
scope.type = attrs.type;
var ngModelController = controllers[0];
ngModelController.$render = function () {
if (typeof ngModelController.$viewValue !== 'undefined') {
scope.dataModel = ngModelController.$viewValue;
}
};
}
};
});
The unit test
it('should invoke ngModelCtrl.$render after changing value on ngModel from the outside (the view)', function () {
var scope,
element,
ngModelCtrl,
modelValue;
scope = $rootScope.$new();
element = angular.element('<form name="loginForm"><input-validation-below place-holder="foo" feedback-message="bar" identifier="username" ng-model="login"></input-validation-below></form>');
element = $compile(element)(scope);
scope.$digest();
var ngModelCtrl = element.find('input-validation-below').controller('ngModel');
modelValue = {username: 'SJV'};
ngModelCtrl.$setViewValue(modelValue);
scope.$digest();
expect(scope.dataModel).toEqual(modelValue);
});
Test fails: Expected undefined to equal { username : 'SJV' }
I read this in the Angular docs:
Since ng-model does not do a deep watch, $render() is only invoked if the values of $modelValue and $viewValue are actually different from their previous value. If $modelValue or $viewValue are objects (rather than a string or number) then $render() will not be invoked if you only change a property on the objects.
But this confuses me a bit.
However, I need $render to be invoked ng-model being changed from the outside to make it as real life as possible AND for the code coverage its code-covered.
Question: could use some pointers on this.

How to access the parent scope within a directive link

So on the angular documentation website, you can define Tobias and Jeff
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: 'my-dialog.html',
link: function (scope, element) {
scope.name = 'Jeff';
}
};
});
If you do The name is {{name}} it'll say
The name is Tobias
I'm trying to access Tobias in the link function. From inside the link function, how would I get the $scope.name value equal to Tobias?
Since the scope is isolated scope: {}, directive creates a new child scope. In this case the only way to directly access parent scope property is to use scope $parent, reference to parent scope object. For example:
link: function(scope, element) {
scope.name = 'Jeff';
scope.parentName = scope.$parent.name; // Tobias
}
However this is not ideal. This is why you may want to consider more flexible approach:
<my-dialog name="name"></my-dialog>
and define a scope configuration as:
scope: {
parentName: '=name'
}
You will have to use $parent property of the the scope:
scope.$parent.name = 'Jeff';
you can get it through $parent like this:
link: function (scope, element) {
scope.name = 'Jeff';
console.log(scope.name);
console.log(scope.$parent.name);
}
As you have used transclude:true, you can omit scope:{} if you do not have any local variables. Putting scope:{} does not make sense.
so the declaration would be like following
angular.module('docsTransclusionExample', [])
.controller('Controller', ['$scope', function($scope) {
$scope.name = 'Tobias';
}])
.directive('myDialog', function() {
return {
restrict: 'E',
transclude: true,
templateUrl: 'my-dialog.html',
link: function (scope, element) {
// scope.name = 'Jeff';
// if name is in your parent scope, you should be able to get it here
console.log(scope.name);
}
};
});
If you look at the template you will see ng-transclude directive has been used, this means where in template the parent scope's variables will be used there. Hope it makes sense.
I'm just wondering why would you want something like this.
This way you're creating a deppendency between the controller and the directive that shouldn't exist.
If you need input data on your directive, declare it explicitly.

Get angular parent directive scope from child directive

I'm trying to access the scope of the parent directive from a child directive.
Please see jsbin
http://jsbin.com/tipekajegage/3/edit
when I click the button I would like to the get value of prod.
var cpApp = angular.module('cpApp', []);
cpApp.directive('cpProduct', function(){
var link = function (scope, element, attr) {
console.log('scope.prod '+ scope.prod);
};
return {
link: link,
restrict: 'EA',
scope: {
prod: '='
}
};
});
cpApp.directive('mediaButtons', [function(){
var template =
'<button ng-click="addToFavoriteList()">get prod from parent</div>' +
'</button>';
var controller = function($scope){
var vm = this;
$scope.addToFavoriteList = function(event){
console.log($scope.$parent.prod);
//GET PROD?
};
};
return {
template: template,
restrict: 'E',
controller: controller,
scope: true
};
}]);
You need to pass in the parent function as a require and assign it to local scope:
var linkFunction = function (scope, element, attrs, cpProductCtrl) {
scope.prod=cpProductCtrl.prod;
You also need to define a controller on the parent function like so:
controller: function($scope){
this.prod=$scope.prod;
}
Then you can call your clickHandler method like so:
console.log($scope.prod);
Here is the JS Bin In case I missed describing anything.
I restructured your code a bit, created a controller in your cpProduct directive and required it in the mediaButton to access its scope.
See this plunker

How to create a new isolated scope with an existing object?

I want to create a new scope with this object:
$scope.model = {
itemA: "First item",
itemB: "Second item"
};
// I know, this is wrong, but I want to show you, what I would like to do.
var newScope = $scope.$new($scope.model);
The new scope I want to access in the ngTransclude-Part of my directive:
link: function (scope, element, attrs, ctrl, transclude) {
transclude(scope.model, function (clone, scope) {
element.find('section').html("").append(clone);
});
And in the template:
<p>{{itemA}} - {{itemB}}
But this doesn´t work
I have the idea from: http://angular-tips.com/blog/2014/03/transclusion-and-scopes/
but I don´t want to work in the scope of the directive, but in a new scope.
AFAIK when you are creating directive usually it inherits the scope. The idea is to create isolated scope and this is done by doing.
.directive('directiveName', function ($compile) {
return {
restrict: "AE",
scope:{
yourModelName:"=";
}
link: function (scope, element) {
var el = angular.element('<div>Here you put your template scope.yourModelName</div>');
$compile(el)(scope);
element.append(el);
}
};
})
Will be copied from the upper scope but you can change it in the directive without changing it in the upper scope
his is my solution I found to a similar problem. Slightly long-winded but it works!
Create a new, 'empty' class directive with its own scope.
Add this directive as a class attribute to your DOM element. It automatically takes the scope of the new directive.
In your case, you would use it on your p tag. You would then select this element in your link function and call scope() on it:
1. <p id="ID" class="my-empty-directive">{{itemA}} - {{itemB}}
2. create your new directive:
angular.module('sgComponents').directive('panelData', [function () {
return {
restrict: 'C', // a class Directive
scope: true // with its own scope
};
}]);
2. link: function (scope, element, attrs, ctrl, transclude) {
var pTag = jQuery('#ID');
var angularElement = angular.element(pTag);
var newScope = angularElement.scope(); // THIS WILL BE THE NEW EMPTY DIRECTIVE'S SCOPE

Resources