Angular directive, detect change with ng-model - angularjs

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);

Related

Watch Number of Children Under Attribute Directive Element

All,
I have an attribute directive called scrolly. The directive looks like this:
.directive('scrolly', function () {
return {
restrict: 'A',
scope: {
scrollFunction: '&scrolly'
},
link: function (scope, element, attrs) {
var raw = element[0];
scope.$watch(element[0].children.length, function(){
if(raw.clientHeight <= raw.scrollHeight){
scope.scrollFunction();
}
});
element.bind('scroll', function () {
if (raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
scope.scrollFunction();
}
});
}
};
})
The objective is to perform an infinite scroll. The scroll part works fine. The part that isn't working is the scope.$watch part. Basically, the objective for the watch portion is to execute the paging function until the element becomes scrollable at which point the scroll binding would take over. Although the purpose of my directive is paging, I do not want to get hung up on that. The root question is how to watch an attribute of an element.
scope.$watch(angular.bind(element, function(){
return element.children.length;
}), function(oldValue, newValue) {
if(raw.clientHeight <= raw.scrollHeight){
scope.scrollFunction();
}
});
In a $watch you can use a function as the watch parameter to watch specific properties on things.
scope.$watch(function() {
return element[0].children.length;
},
function() {
// do your thang on change
})

How to use directive-defined events to update my model

I am creating drag and drop functionality by creating a <dragItem> directive and a <droptTraget> directive, but I don't understand yet how to work with the inner and out scope in this way.
Here are my directives. The events triggers the functions properly, I just want the on dragstart event to store a value of the drag element and the drop event to trigger the function testAddSet() which adds the drag value to my model.
drag
angular.module('app.directives.dragItem', [])
.directive('dragItem', function(){
return { // this object is the directive
restrict: 'E',
scope: {
excercise: '='
},
templateUrl: "templates/dragTile.html",
link: function(scope, element, attrs){
element.on('dragstart', function (event) {
var dataVar = element.innerText;
// It's here that I want to send a dataVar to the $scope
});
}
};
});
drop
angular.module('app.directives.dropTarget', [])
.directive('dropTarget', function(){
return { // this object is the directive
restrict: 'E',
scope: {
day: '='
},
templateUrl: "templates/calDay.html",
link: function(scope, element, attrs){
element.on('drop', function (event) {
event.preventDefault();
// It's here that I'd like to take the value from the drag item and update my model
testAddSet() // doesn't work
$parent.testAddSet() // doesn't work
});
element.on('dragover', function (event) {
event.preventDefault();
});
}
};
});
Since you are using isolate scope, you need to define an attribute for the function binding.
angular.module('app.directives.dropTarget', [])
.directive('dropTarget', function(){
return { // this object is the directive
restrict: 'E',
scope: {
day: '=',
//Add binding here
testAddSet: '&'
},
templateUrl: "templates/calDay.html",
link: function(scope, element, attrs){
element.on('drop', function (event) {
event.preventDefault();
//Invoke the function here
scope.testAddSet({arg: value, $event: event});
});
element.on('dragover', function (event) {
event.preventDefault();
});
}
};
});
In your template, connect the function using the directive attribute.
<drop-target test-add-set="fn(arg, $event)" day="x"></drop-target>
For more information on isolate scope binding, see AngularJS $compile Service API Reference - scope.
I recommend that the event object be exposed as $event since that is customary with AngularJS event directives.
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
I think the easiest way to get your cross-directive communication is to make a scope variable on the host page and then pass it double-bound ('=') to both directives. That way, they both have access to it as it changes.

angularjs - is it not possible to add ng- attributes on a directive?

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>

Test for directive watch fails; only fired first time

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);
});

angular - Listening on transcluded change

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.

Resources