I am attempting to pass a test for my angularjs directive using jasmine.
My directive simply takes what is passed into the attribute (as a one way binding) and replaces the elements text with that value. My directive also watches for changes in the value and updates the elements text accordingly:
return {
restrict: 'A',
scope: {
test: '&'
},
link: function (scope, element, attrs) {
element.text(scope.test());
scope.$watch('test', function (value) {
console.log('WATCH FIRED!');
element.text(scope.test());
});
}
};
My jasmine test attempts to set the value passed into the directive, then change it, and then checks to see if the elements text is different after changing the value:
it('should update when the model updates', function () {
$scope.model = 'hello';
$scope.$digest();
var before = element.text();
$scope.model = 'world';
$scope.$digest();
expect(element.text()).not.toEqual(before);
});
But the test fails everytime. In fact, the elements text is always 'hello' and never gets changed to 'world'.
Plunker: http://plnkr.co/edit/UeUGE88LuQRgeo7I412p?p=preview
This is beacuse you are using one way binding in your directive (&) so changes to the model are not tracked. Try using two way binding = and it should work as expected.
The & binding allows a directive to trigger evaluation of an expression in the context of the original scope, at a specific time. Any legal expression is allowed, including an expression which contains a function call. Because of this, & bindings are ideal for binding callback functions to directive behaviors.
...
return {
restrict: 'A',
scope: {
test: '='
},
link: function (scope, element, attrs) {
element.text(scope.text);
scope.$watch('test', function (value) {
console.log('WATCH FIRED!');
element.text(scope.test);
});
}
};
Plnkr
Or you would need to do:-
scope.$watch(function(){
return scope.test();
}, function (value) {
console.log('WATCH FIRED!');
element.text(scope.test);
});
Related
What I would like to be able to do is "wrap" the behavior of an ng-hide for a "permissions" directive... so I can do the following
Hide me
All is fine if I decide to simply "remove" the element from the dom; however, if I try to add an ng-hide and then recompile the element. Unfortunately, this causes an infinite loop
angular.module('my.permissions', []).
directive 'permit', ($compile) ->
priority: 1500
terminal: true
link: (scope, element, attrs) ->
element.attr 'ng-hide', 'true' # ultimately set based on the user's permissions
$compile(element)(scope)
OR
angular.module('my.permissions', []).directive('permit', function($compile) {
return {
priority: 1500,
terminal: true,
link: function(scope, element, attrs) {
element.attr('ng-hide', 'true'); // ultimately set based on the user's permissions
return $compile(element)(scope);
}
};
});
I've tried it without the priority or terminal to no avail. I've tried numerous other permutations (including removing the 'permit' attribute to prevent it from continually recompiling, but what it seems to come down to is this: there doesn't seem to be a way to modify an element's attributes and recompile inline through a directive.
I'm sure there's something I'm missing.
This solution assumes that you want to watch the changes of the permit attribute if it changes and hide the element as if it was using the ng-hide directive. One way to do this is to watch the permit attribute changes and then supply the appropriate logic if you need to hide or show the element. In order to hide and show the element, you can replicate how angular does it in the ng-hide directive in their source code.
directive('permit', ['$animate', function($animate) {
return {
restrict: 'A',
multiElement: true,
link: function(scope, element, attr) {
scope.$watch(attr.permit, function (value){
// do your logic here
var condition = true;
// this variable here should be manipulated in order to hide=true or show=false the element.
// You can use the value parameter as the value passed in the permit directive to determine
// if you want to hide the element or not.
$animate[condition ? 'addClass' : 'removeClass'](element, 'ng-hide');
// if you don't want to add any animation, you can simply remove the animation service
// and do this instead:
// element[condition? 'addClass': 'removeClass']('ng-hide');
});
}
};
}]);
angular.module('my.permissions', []).directive('permit', function($compile) {
return {
priority: 1500,
terminal: true,
link: function(scope, element, attrs) {
scope.$watch(function(){
var method = scope.$eval(attrs.permit) ? 'show' : 'hide';
element[method]();
});
}
};
});
I'm using this directive. This works like ng-if but it checks for permissions.
appModule.directive("ifPermission", ['$animate', function ($animate) {
return {
transclude: 'element',
priority: 600,
terminal: true,
restrict: 'A',
$$tlb: true,
link: function ($scope, $element, $attr, ctrl, $transclude) {
var block, childScope;
var requiredPermission = eval($attr.ifPermission);
// i'm using global object you can use factory or provider
if (window.currentUserPermissions.indexOf(requiredPermission) != -1) {
childScope = $scope.$new();
$transclude(childScope, function (clone) {
clone[clone.length++] = document.createComment(' end ifPermission: ' + $attr.ngIf + ' ');
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when it's template arrives.
block = {
clone: clone
};
$animate.enter(clone, $element.parent(), $element);
});
}
}
};
}]);
usage:
<div if-permission="requiredPermission">Authorized content</div>
I'm using AngularJS 1.2
I'm passing an array via ng-model into a custom directive.
The data is in the directive when it is loaded, but when changes are made to the model in the main controller, the model in the directive isn't updating.
This is the relative partial my controller uses. (not the directive controller) ui.pct is an array of percentages.
<dprogress ng-model="ui.pct"></dprogress>
Here's the directive:
```
angular.module('dprogress', [])
.directive('dprogress', function () {
function makeChart(data) {
var chart = d3.select('#chart')
.append("div").attr("class", "chart")
.selectAll('div')
.data(data)
.enter()
.append("div")
.style("width", function(d) { return d + "%"; })
.text(function(d) { return d + "%"; });
}
return {
restrict: 'E',
scope: { data: "=ngModel" },
template: '<div id="chart"></div>',
link: function (scope, element, attrs) {
makeChart(scope.data);
}
};
});
```
I've added scope.$watch('data') in the link, but it would not trigger when the data was being changed from outside the directive.
How can I update the data in my directive when it is changed from outside?
Your data is probably an array and you need to deepwatch it. A simple watch will check if the reference is changed bu no the content.
link: function (scope, element, attrs) {
makeChart(scope.data);
scope.$watch('data', makeChart, true); // last arg is for deepwatch
}
When objectEquality == true, inequality of the watchExpression is determined according to the angular.equals function. To save the value of the object for later comparison, the angular.copy function is used. This therefore means that watching complex objects will have adverse memory and performance implications.
scope.$watch('data', yourCallback, true);
i wrote a function called getDepth(object) that gives me the depth of an object and then returns a string, for example if the depth of the object was 3 it will return the string "sub-sub-sub", and in my directive template i want to call that function from ng-class such as
'<span ng-class="getDepth(item)">{{item.content}}</span>'
but i am not sure where to put that function in my directive, should it just be inside the link function?
This is typically the job for a controller. So you can create an anonymous controller in your directive and place it there, or in the scope of the parent controller looking after this section of code - that is assuming of course one exists.
Maybe though you would like to reuse this functionality later in the app, so I recommend placing it high on the controller tree to allow others to inherit its function.
The linker functions job is strictly DOM manipulation, and this is not DOM manipulation this is a function returning a string and the ng-class directive in turn does the DOM manipulation.
If you check the docs:
Directives that want to modify the DOM typically use the link option. link takes a function with the following signature, function link(scope, element, attrs) { ... } where:
.directive('myCurrentTime', function($interval, dateFilter) {
function link(scope, element, attrs) {
var format,
timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}...
As you can see the link function is changing the DOM.
So to answer the question, this is how the directive should be structured.
app.directive('myDirective', function() {
var controller = function($scope) {
$scope.getDepth = function (item) {
return "text-success";
};
}
return {
template: '<span ng-class="getDepth(item)">{{item.content}}</span>',
scope: {item: '='},
controller: controller // or this can be the name of an outside reference controller as well - which i prefer for unit testing and reusability purposes.
}
};
}
]);
if you want to let this control to your directive instead controller you can put it in the link function, an example directive should be like this...
app.directive('myDirective', ['$compile',
function($compile) {
return {
template: '<span ng-class="getDepth(item)">{{item.content}}</span>',
scope: {item: '=item'},
link: function(scope, element, attrs) {
scope.getDepth = function (item) {
return "text-success";
};
}
};
}
]);
here is PLUNKER
I have a simple directive
angular.module('myApp')
.directive('myDirective', function () {
return {
template: '<p ng-transclude></p>',
restrict: 'A',
transclude: true,
link: function postLink(scope, element, attrs) {
}
}
}
);
I am trying to run code each time the transclusion content changes and the directive is rendered - I need the transcluded content.
Example algorithm I would like to run in this case is:
count words of transcluded content.
I have tried scope.$watch in multiple forms but to no avail.
We can use the jqlite included within Angular inside a watch expression function to accomplish this. Below is code that watches the length of the transcluded element using jqLite (element.text().length). The watch fires whenever the length of the element that this directive is attached to changes.
And the new length is passed in as newValue to the second function within the watch (since we return it from the first watch function).
myApp.directive('myDirective', function () {
return {
template: '<p ng-transclude></p>',
restrict: 'A',
transclude: true,
replace: true,
link: function (scope, element, attrs) {
scope.$watch(function () {
return element.text().length;
},
function (newValue, oldValue) {
console.log('New Length ', newValue);
});
}
}
});
I've got a working jsfiddle here:
http://jsfiddle.net/2erbF/6/
This addresses the word/letter count scenario. But you could write a test on the element.text() itself if you needed it to fire on any changes- not just a length change.
I'm writing a custom directive to validate some value in the scope. It should work like the required attribute, but instead of validating the input text, it's going to validate a value in the scope. My problem is that this value is set in a $scope.$watch function and this function runs after my directive. So when my directive tries to validate the value it has not been set yet. Is it possible to run the $watch code before running my custom directive?
Here is the code:
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
var keys = {
a: {},
b: {}
};
$scope.data = {};
// I need to execute this before the directive below
$scope.$watch('data.objectId', function(newValue) {
$scope.data.object = keys[newValue];
});
});
app.directive('requiredAttribute', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
var requiredAttribute = attr.requiredAttribute;
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', scope[attr.requiredAttribute] != null);
return value;
});
}
};
});
<input type="text" name="objectId" ng-model="data.objectId" required-attribute="object" />
<span class="invalid" ng-show="myForm.objectId.$error.requiredAttribute">Key "{{data.objectId}}" not found</span>
And here is a plunker: http://plnkr.co/edit/S2NrYj2AbxPqDrl5C8kQ?p=preview
Thanks.
You can schedule the $watch to happen before the directive link function directly. You need to change your link function.
link: function(scope, elem, attr, ngModel) {
var unwatch = scope.$watch(attr.requiredAttribute, function(requiredAttrValue) {
if (requiredAttribute=== undefined) return;
unwatch();
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', requiredAttrValue != null);
return value;
});
});
}
This approach will activate the $watch function inside the directive only once and will remove the watcher the first time your required scope variable is set.
There is also another approach where you parse the value and check it this way:
link: function(scope, elem, attr, ngModel) {
var parsedAttr = $parse(attr.requiredAttribute);
ngModel.$parsers.unshift(function (value) {
ngModel.$setValidity('requiredAttribute', parsedAttr(scope) != null);
return value;
});
}
Here you will need to use $parse AngularJS service. The difference here is that this will mark the input field as invalid without waiting for first value set on the required scope variable.
Both variants allow you to pass an expression instead of a simple variable name. This makes it possible to write something as required-attribute="object.var1.var2".
It really depends on what you need.