directive scope variables not accessible in jasmine test - angularjs

I have a directive like below:
angular.module('buttonModule', []).directive('saveButton', [
function () {
function resetButton(element) {
element.removeClass('btn-primary');
}
return {
restrict: 'E',
replace: 'false',
scope: {
isSave: '='
},
template:
'<button class="btn" href="#" style="margin-right:10px;" ng-disabled="!isSave">' +
'</button>',
link: function (scope, element) {
console.log(scope.isSave);
scope.$watch('isSave', function () {
if (scope.isSave) {
resetButton(scope, element);
}
});
}
};
}
]);
and the jasmine test as below:
describe('Directives: saveButton', function() {
var scope, compile;
beforeEach(module('buttonModule'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
compile = $compile;
}));
function createDirective() {
var elem, compiledElem;
elem = angular.element('<save-button></save-button>');
compiledElem = compile(elem)(scope);
scope.$digest();
return compiledElem;
}
it('should set button clean', function() {
var el = createDirective();
el.scope().isSaving = true;
expect(el.hasClass('btn-primary')).toBeFalsy();
});
});
The issue is the value of isSaving is not getting reflected in the directive and hence resetButton function is never called. How do i access the directive scope in my spec and change the variable values. i tried with isolateScope but the same issue persists.

First note that you are calling the resetButton function with two arguments when it only accepts one. I fixed this in my example code. I also added the class btn-primary to the button element to make the passing of the test clearer.
Your directive is setting up two-way databinding between the outer scope and the isolated scope:
scope: {
isDirty: '=',
isSaving: '='
}
You should leverage this to modify the isSaving variable.
Add the is-saving attribute to your element:
elem = '<save-button is-saving="isSaving"></save-button>';
Then modify the isSaving property of the scope that was used when compiling (you also need to trigger the digest loop to make the watcher detect the change):
var el = createDirective();
scope.isSaving = true;
scope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
Demo: http://plnkr.co/edit/Fr08guUMIxTLYTY0wTW3?p=preview
If you don't want to add the is-saving attribute to your element and still want to modify the variable you need to retrieve the isolated scope:
var el = createDirective();
var isolatedScope = el.isolateScope();
isolatedScope.isSaving = true;
isolatedScope.$apply();
expect(el.hasClass('btn-primary')).toBeFalsy();
For this to work however you need to remove the two-way binding to isSaving:
scope: {
isDirty: '='
}
Otherwise it would try to bind to something non-existing as there is no is-saving attribute on the element and you would get the following error:
Expression 'undefined' used with directive 'saveButton' is
non-assignable!
(https://docs.angularjs.org/error/$compile/nonassign?p0=undefined&p1=saveButton)
Demo: http://plnkr.co/edit/Ud6nK2qYxzQMi6fXNw1t?p=preview

Related

Variable watch does not fire unless is an array

I have the following markup:
<div ng-controller="DataController as vm">
<div ng-repeat="name in vm.users track by $index">
{{name}}
</div>
<form name="form" validation="vm.errors">
<input validator ng-model="vm.name" name="vm.name" placeholder="name" type="text" />
Add
</form>
</div>
I have the following controller:
function DataController($scope) {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = 1;
vm.add = function(name) {
vm.errors++;
vm.users.push(name);
}
}
Every time I add a user I increase the value of errors.
I need to watch this variable inside a directive so I have:
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.errors = $scope.validation;
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
scope.$watch(function() {
return controller.errors;
}, function () {
console.log(controller.errors);
});
}
The console.log shows the initial value but not new values:
https://jsfiddle.net/qb8o006h/2/
If I change vm.errors to an array, add the values, and watch its length then it works fine:
https://jsfiddle.net/nprx63qa/2/
Why is my first example does not work?
I update your code, you can access to the property scope.vm.errors which is updated, if you debug the code, you will see that the property controller.errors is not updated (after each digest all the watches are called to re-evaluate them). If you access the property errors from the scope you can add the $scope.$watch and make it work. However I would not recommend to have a $scope.$watch inside a directive. But that's up to you :
var app = angular.module('app', []);
app.controller("DataController", DataController);
function DataController($scope) {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = 1;
vm.add = function(name) {
vm.errors++;
vm.users.push(name);
}
}
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.errors = $scope.validation;
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
scope.$watch(function() {
return scope.vm.errors
}, function () {
console.log(scope.vm.errors);
});
}
}
https://jsfiddle.net/kcvqn5kL/
In both of your examples inside validation directive controller you assign errors property a reference to $scope.validation value.
In the first example the value is numeric and thus immutable - 1 - the reference value cannot be modified. The vm.add modifies property value of the controller instance. The change is then propagated to validation directive $scope.validation but not to the validation directive controller instance $errors property.
In the second example the value is an array and thus mutable - [] - the reference value can be modified. The vm.add does not modify property value of the controller instance. Thus the validation directive controller instance errors property value is the very same Array instance - hence it's length changes.
One way to use a immutable value (as in your first example) is to $watch a controller function as in this example:
function link(scope, element, attributes, controller) {
scope.$watch(controller.errors, function (newValue) {
console.log(newValue);
});
}
Where controller.errors is defined as follows:
function controller($scope) {
this.errors = function(){ return $scope.validation; };
}
You can find the following answer(s) useful:
Why form undefined inside ng-include when checking $pristine or $setDirty()?

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.

Angular Isolate Scope breaks down?

I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview

In Angular JS, click event bound on a directive, does not make the directive interpolate certain parameter

I am learning directives and I am not sure why this directive code is not working:
define([
'appConfig'
], function() {
angular.module('appDirectives').directive('workspaceCanvas', function() {
return {
restrict: 'A',
scope: {},
template: '<h1>Hello there, I am {{name}}</h1>',
link: function(scope, element, attrs) {
scope.name = 'Dany';
var self = scope;
element.bind('click', function() {
console.log('clicked');
self.name = 'Ted';
console.log(self.name)
});
}
};
});
});
When I add the directive to the DOM, I can see the name "Dany" being interpolated.
But when I click on the element, even if I capture the scope and assign the value Ted to scope's name property, the change does not interpolate on the view.
Any idea why?
element.bind('click', function() {
console.log('clicked')
scope.$apply(function () {
self.name = "Ted";
});
console.log(self.name)
});
You need to call the digest loop. Inside the click event add the last statment scope.$apply();
or even better
scope.$apply(function() {
scope.name = "Ted";
});

Unit testing an AngularJS directive that watches an an attribute with isolate scope

I have a directive that uses an isolate scope to pass in data to a directive that changes over time. It watches for changes on that value and does some computation on each change. When I try to unit test the directive, I can not get the watch to trigger (trimmed for brevity, but the basic concept is shown below):
Directive:
angular.module('directives.file', [])
.directive('file', function() {
return {
restrict: 'E',
scope: {
data: '=',
filename: '#',
},
link: function(scope, element, attrs) {
console.log('in link');
var convertToCSV = function(newItem) { ... };
scope.$watch('data', function(newItem) {
console.log('in watch');
var csv_obj = convertToCSV(newItem);
var blob = new Blob([csv_obj], {type:'text/plain'});
var link = window.webkitURL.createObjectURL(blob);
element.html('<a href=' + link + ' download=' + attrs.filename +'>Export to CSV</a>');
}, true);
}
};
});
Test:
describe('Unit: File export', function() {
var scope;
beforeEach(module('directives.file'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
};
it('should create a CSV', function() {
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
//I've also tried below but that does not help
scope.$apply(function() { scope.input = {}; });
});
What can I do to trigger the watch so my "In watch" debugging statement is triggered? My "In link" gets triggered when I compile.
For a $watch to get triggered, a digest cycle must occur on the scope it is defined or on its parent. Since your directive creates an isolate scope, it doesn't inherit from the parent scope and thus its watchers won't get processed until you call $apply on the proper scope.
You can access the directive scope by calling scope() on the element returned by the $compile service:
scope.input = someData;
var e = $compile('<file data="input" filename="filename.csv"></file>')(scope);
e.isolateScope().$apply();
This jsFiddler exemplifies that.

Resources