$watch does not work when changing scope variables inside a service - angularjs

I am using a service to change some of scope A's variables. When I try to $watch for changes on those variables nothing happens.
The basic setup is shown below. I would like to use $watch in my own custom directive that I add to the element. But if that is impossible I would at least like to be able to use $watch from inside the controller.
I made a plunkr here. As you can see $scope.someVar changes but $watch is never fired on either the controller or the directive.
app.factory("changerService", function() {
var $scope;
function change(to) {
$scope.someVar = to;
}
function setScope(scope) {
$scope = scope;
}
return {
Change: change,
SetScope: setScope
}
});
app.controller("bar", ["$scope", "changerService", function ($scope, changerService) {
$scope.someVar = true;
changerService.SetScope($scope);
$scope.showImg = function (val) {
changerService.Change(val);
$scope.state = $scope.someVar;
};
$scope.$watch($scope.someVar, function (newVal, oldVal) {
$scope.watchController = newVal ? newVal : "undefined";
});
}]);
app.directive("myDirective", function () {
return {
link: function (scope, element, attrs) {
scope.$watch(scope.someVar, function (newVal, oldVal) {
scope.watchDirective = newVal ? newVal : "undefined";
});
}
}
});

After examining your code i noticed that your watch expression is an object but it must be a string.
Example:
app.controller("bar", ["$scope", "changerService", function ($scope, changerService) {
$scope.someVar = true;
changerService.SetScope($scope);
$scope.showImg = function (val) {
changerService.Change(val);
$scope.state = $scope.someVar;
};
$scope.$watch("someVar", function (newVal, oldVal) {
$scope.watchController = newVal ? newVal : "undefined";
});
}]);
app.directive("myDirective", function () {
return {
link: function (scope, element, attrs) {
scope.$watch("someVar", function (newVal, oldVal) {
scope.watchDirective = newVal ? newVal : "undefined";
});
}
}
});

Related

Can I call a Service from a Directive?

I have the following service:
myapp.service('myService', function () {
var vm = this;
vm.returnChoreList = function () {
console.log('returnChoreList : ' + vm.chore.length);
return vm.chore;
}});
Here is my directive:
myapp.directive("applyplugin", function (myService) {
return {
link: function (scope, element, attr) {
scope.$watch(function () {
$(element).bootstrapSwitch();
});
$(element).bind("switchChange.bootstrapSwitch", function (event, state) {
if (state == true) {
var clistArr = myService.returnChoreList;
console.log(clistArr.chore.length);
}
})
}
}
});
I want to be able to use the chore array in my directive. But I'm having difficulties getting it.
How can I get this array so I can use it in the directive?
You're missing the parentheses on the function call. Try: var clistArr = myService.returnChoreList();

how to pass a value from attrs.$observe in an angular directive

Currently the console.log(value) outputs the value of 'lat' but if I try and access that value elsewhere in my directive it will not work. I have tried to set a variable equal to attrs.$observe as well as putting the attrs.$observe within a function. In addition for some reason I cannot return the value of data. Any thoughts
angular.module('Ski').directive('mapCanvas', function() {
function link (scope, element, attrs) {
attrs.$observe('lat', function(data) {
console.log(data);
});
}
}
Simply you can do this by making some global variable, using scope or var
CODE
angular.module('Ski').directive('mapCanvas', function() {
function link (scope, element, attrs) {
scope.lat = ''; //or it can be var lat = '';
attrs.$observe('lat', function(data) {
scope.lat = data; //or it can be var scope.lat = '';
console.log(data);
});
scope.test = function(){
//when it gets called
console.log(scope.lat);
}
}
}
Hope this could help you. Thanks.

Unit test angular directive that uses ngModel

I am trying to unit test a directive that uses ngModel and having difficulties. It seems that the link function of my directive is never being called...
Here is my directive code:
coreModule.directive('coreUnit', ['$timeout', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
template: "{{output}}",
link: function (scope, elem, attrs, ngModelCtrl) {
ngModelCtrl.$render = function () {
render(ngModelCtrl.$modelValue);
};
console.log("called");
function render(unit) {
if (unit) {
var output = '(' +
unit.numerator +
(unit.denominator == '' ? '' : '/') +
unit.denominator +
(unit.rate == 'NONE' || unit.rate == '' ? '' : '/' + unit.rate) +
')';
scope.output = output == '()' ? '' : output;
}
}
}
}
}]);
Here is my test spec:
describe('core', function () {
describe('coreUnitDirective', function () {
beforeEach(module('core'));
var scope,
elem;
var tpl = '<core-unit ng-model="myUnit"></core-unit>';
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.myUnit = {};
elem = $compile(tpl)(scope);
scope.$digest();
}));
it('the unit should be empty', function () {
expect(elem.html()).toBe('');
});
it('should show (boe)', function () {
scope.myUnit = {
numerator: 'boe',
denominator: "",
rate: ""
};
scope.$digest();
expect(elem.html()).toContain('(boe)');
});
});
});
The console log output "called" is never occurring and obviously the elem in my test spec is never updating.
What am I doing wrong??
Turns out that I wasn't including the directive in my karma.config file :S. Adding it in resolved all of my issues.
You can try out two things.
First, instead of using just a string tpl, try angular.element().
var tpl = angular.element('<core-unit ng-model="myUnit"></core-unit>');
Second, place the tpl in the beforeEach block. So the result should look like this:
beforeEach(inject(function ($rootScope, $compile) {
var tpl = angular.element('<core-unit ng-model="myUnit"></core-unit>');
scope = $rootScope.$new();
scope.myUnit = {};
elem = $compile(tpl)(scope);
scope.$digest();
}));

Testing AngularJS directive that modifies other scope in Karma / Jasmine

Reference: Unit Testing AngularJS Directives: scopes not updating?
Case
I have a directive called editable that take an ng-model and creates a toggleable/editable field. The directive works and the parent scope is updated correctly, there are no problems in the actual function of the directive. I cannot seem to write a test that supports this though. It took me a long time to get the directive working properly with all the caveats so I really want to get some tests in place to make sure it continues to work in all of the different cases.
The Directive (which works)
I can't be sure which pieces will be relevant so i included the whole thing.
app.directive('editable',
['$templateCache', '$compile',
function ($templateCache, $compile) {
return {
restrict: 'A',
transclude: true,
templateUrl: 'template/directives/editable.html',
replace: true,
require: 'ngModel',
scope: true,
compile: function(element, attrs, transcludeFn) {
return function (scope, iElement, iAttrs, ctrl) {
var validityId = 'editable-' + scope.$id;
scope.lastSavedValue = iElement.find('input').val();
scope.storeValue = function() {
scope.lastSavedValue = iElement.find('input').val();
};
scope.edit = function() {
scope.storeValue();
scope.editing = true;
$('input', iElement).focus().select();
ctrl.$setValidity(validityId, true);
};
scope.ok = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(inputCtrl.$valid === true) {
scope.editing = false;
scope.value = inputCtrl.$viewValue;
ctrl.$setValidity(validityId, false);
ctrl.$setViewValue(inputCtrl.$viewValue); // Not sure (why) this is needed
}
};
scope['delete'] = function() {
scope.deleted = true;
scope.editing = false;
};
scope.undo = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(scope.lastSavedValue) {
inputCtrl.$setViewValue(scope.lastSavedValue);
scope.value = scope.lastSavedValue;
ctrl.$setViewValue(scope.lastSavedValue);
}
iElement.find('input').val(scope.value);
scope.editing = false;
scope.deleted = false;
ctrl.$setValidity(validityId, false);
};
transcludeFn(scope, function(clone) {
var $editingReplacement = $(clone).filter('[editing]');
var $viewingReplacement = $(clone).filter('[viewing]');
var $deletedReplacement = $(clone).filter('[deleted]');
var $viewingControls = $('.editable-view-container .controls', iElement);
var $editingControls = $('.editable-input-container .controls', iElement);
var $deletedControls = $('.editable-delete-container .controls', iElement);
if($editingReplacement.length) {
$('.editable-input-container', iElement).html($editingReplacement.html());
$('.editable-input-container', iElement).append($editingControls);
$compile($('.editable-input-container', iElement))(scope);
} else {
$('.editable-input-container', iElement).find('input').attr('ng-model', iAttrs['ngModel']);
$compile($('.editable-input-container', iElement))(scope);
}
if($viewingReplacement.length) {
$('.editable-view-container', iElement).html($viewingReplacement.html());
$('.editable-view-container', iElement).append($viewingControls);
$compile($('.editable-view-container', iElement))(scope);
}
if($deletedReplacement.length) {
$('.editable-delete-container', iElement).html($deletedReplacement.html());
$('.editable-delete-container', iElement).append($deletedControls);
}
});
/**
* Deleted (Isolated Scope)
*
* Tracks if the user has clicked the delete button
*
* #type {Boolean}
*/
scope.deleted = false;
/**
* Editing (Isolated Scope)
*
* Tracks the state of the view
*
* #type {Boolean}
*/
scope.editing = false;
/**
* Initial Loader
*
* Run once after ctrl is loaded
*
* #return {[type]} [description]
*/
var unbindWatcher = scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
if(typeof ctrl.$modelValue !== 'undefined') {
scope.value = ctrl.$modelValue;
scope.editing = ctrl.$modelValue ? false : true;
unbindWatcher();
}
});
};
}
};
}
]);
Spec
Fails at the end
describe('Editable Directive', function() {
// Keep references to element and scope so that they are available to all tests
var element, scope, ctrl;
beforeEach(module('ltAccountApp'));
beforeEach(module('templates'));
beforeEach(inject(function ($rootScope, $compile) {
var linkFn, el;
// scope = $rootScope;
scope = $rootScope.$new();
scope.testValue = 'xxx';
el = angular.element('\
<div editable="" ng-model="testValue"></div>\
');
// The $compile method returns the directive's link function
linkFn = $compile(el);
// The link function returns the resulting DOM object
element = linkFn(scope);
element.scope().$apply();
ctrl = element.controller('ngModel');
}));
it('should assign input value to scope value', function() {
expect(element.find('input').val()).toEqual(scope.testValue);
});
it('should have access to parent scope variable passed into directive', function() {
expect(ctrl.$viewValue).toEqual(scope.testValue);
expect(ctrl.$modelValue).toEqual(scope.testValue);
});
it('should manage state editing correctly', function() {
expect(element.scope().editing).toBe(false);
element.scope().edit();
expect(element.scope().editing).toBe(true);
});
it('should manage state deleted correctly', function() {
expect(element.scope().deleted).toBe(false);
element.scope()['delete']();
expect(element.scope().deleted).toBe(true);
});
it('should be able to modify parent scope variable passed into directive', function() {
// Not sure what this does, added from referenced SO question
// spyOn(scope, '$apply').andCallThrough();
var newValue = 'yyy';
element.scope().edit();
element.find('input').val(newValue);
element.find('input').trigger('input');
element.scope().ok();
expect(ctrl.$viewValue).toEqual(newValue);
expect(ctrl.$modelValue).toEqual(newValue);
expect(element.scope().value).toEqual(newValue);
expect(scope.$apply).toHaveBeenCalled();
expect(scope.testValue).toEqual(newValue); // <-fails
});
});
So...
Everything seems to be working until I actually expect the parent scope to have the changed value.
I know there is a lot here, I appreciate any guidance you can provide.
Plunk of Angular + Jasmine with directive (work in progress)
Lets say you have to update the value of input field to 'kasrak'. Trying doing it like this:
var elm = element.find("input");
elm.val('kasrak');
elm.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest()
Key is the third line with $sniffer service. I found this in angular-ui's bootstrap tests.
Here is the link to the function in test spec: https://github.com/angular-ui/bootstrap/blob/master/src/typeahead/test/typeahead.spec.js#L31

In AngularJS, why is a boolean parameter evaluated as a string?

In AngularJS I would like to test a boolean value inside a directive, but the value is returned as a string.
Here is the code:
angular.module('TestApp', ['TestApp.services', 'TestApp.controllers', 'TestApp.directives']);
angular.module('TestApp.services', ['ngResource']).
factory('Obj', function($resource){
return $resource('datas.json');
});
angular.module('TestApp.controllers', []).
controller('TestCtrl', ['$scope', 'Obj', function($scope, Obj) {
$scope.objs = Obj.query();
}]);
angular.module('TestApp.directives', []).
directive('requiredStatus', function() {
return function(scope, elm, attrs) {
attrs.$observe('v', function(av) {
if (attrs.completed) {
scope.val= true;
} else {
scope.val= false;
}
scope.type = typeof attrs.completed;
});
};
});
http://plnkr.co/edit/DvIvySFRCYaz4SddEvJk
What should I do to have a typeof "boolean" inside the directive?
Use $watch, which will evaluate the observed attribute expression against the scope:
scope.$watch(attrs.completed, function(completed) {
scope.val = completed;
scope.type = typeof completed;
});
or use scope.$eval:
scope.val = scope.$eval(attrs.completed);
scope.type = typeof scope.val;
DEMO PLUNKER

Resources