How to 'unwatch' an expression - angularjs

Say I have an ng-repeat with a big array.
When ng-repeat runs, it adds every element of that array to an isolated scope, as well as having the array itself in a scope. That means that $digest checks the entire array for changes, and on top of that, it checks every individual element in that array for changes.
See this plunker as an example of what I'm talking about.
In my use case, I never change a single element of my array so I don't need to have them watched. I will only ever change the entire array, in which case ng-repeat would re-render the table in it's entirety. (If I'm wrong about this please let me know..)
In an array of (say) 1000 rows, that's 1000 more expressions that I don't need evaluated.
How can I deregister each element from the watcher while still watching the main array?
Perhaps instead of deregistering I could have more control of my $digest and somehow skip each individual row?
This specific case is actually an example of a more general issue. I know that $watch returns a 'deregisteration' function, but that doesn't help when a directive is registering the watches, which is most of the time.

To have a repeater with a large array that you don't watch to watch every item.
You'll need to create a custom directive that takes one argument, and expression to your array, then in the linking function you'd just watch that array, and you'd have the linking function programmatically refresh the HTML (rather than using an ng-repeat)
something like (psuedo-code):
app.directive('leanRepeat', function() {
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attr) {
scope.$watch('data', function(value) {
elem.empty(); //assuming jquery here.
angular.forEach(scope.data, function(d) {
//write it however you're going to write it out here.
elem.append('<div>' + d + '</div>');
});
});
}
};
});
... which seems like a pain in the butt.
Alternate hackish method
You might be able to loop through $scope.$$watchers and examine $scope.$$watchers[0].exp.exp to see if it matches the expression you'd like to remove, then remove it with a simple splice() call. The PITA here, is that things like Blah {{whatever}} Blah between tags will be the expression, and will even include carriage returns.
On the upside, you might be able to just loop through the $scope of your ng-repeat and just remove everything, then explicitly add the watch you want... I don't know.
Either way, it seems like a hack.
To remove a watcher made by $scope.$watch
You can unregister a $watch with the function returned by the $watch call:
For example, to have a $watch only fire once:
var unregister = $scope.$watch('whatever', function(){
alert('once!');
unregister();
});
You can, of course call the unregister function any time you want... that was just an example.
Conclusion: There isn't really a great way to do exactly what you're asking
But one thing to consider: Is it even worth worrying about? Furthermore is it truly a good idea to have thousands of records loaded into dozens of DOMElements each? Food for thought.
EDIT 2 (removed bad idea)

$watch returns a function that unbinds the $watch when called. So this is all you need for "watchOnce":
var unwatchValue = scope.$watch('value', function(newValue, oldValue) {
// Do your thing
unwatchValue();
});

Edit: see the other answer I posted.
I've gone and implemented blesh's idea in a seperable way. My ngOnce directive just destroys the child scope that ngRepeat creates on each item. This means the scope doesn't get reached from its parents' scope.$digest and the watchers are never executed.
Source and example on JSFiddle
The directive itself:
angular.module('transclude', [])
.directive('ngOnce', ['$timeout', function($timeout){
return {
restrict: 'EA',
priority: 500,
transclude: true,
template: '<div ng-transclude></div>',
compile: function (tElement, tAttrs, transclude) {
return function postLink(scope, iElement, iAttrs, controller) {
$timeout(scope.$destroy.bind(scope), 0);
}
}
};
}]);
Using it:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
Note ng-once doesn't create its own scope which means it can affect sibling elements. These all do the same thing:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
<li ng-repeat="item in contents">
<ng-once>
{{item.title}}: {{item.text}}
</ng-once>
</li>
<li ng-repeat="item in contents">
{{item.title}}: {{item.text}} <ng-once></ng-once>
</li>
Note this may be a bad idea

You can add the bindonce directive to your ng-repeat. You'll need to download it from https://github.com/pasvaz/bindonce.
edit: a few caveats:
If you're using {{}} interpolation in your template, you need to replace it with <span bo-text>.
If you're using ng- directives, you need to replace them with the right bo- directives.
Also, if you're putting bindonce and ng-repeat on the same element, you should try either moving the bindonce to a parent element (see https://github.com/Pasvaz/bindonce/issues/25#issuecomment-25457970 ) or adding track by to your ng-repeat.

If you are using angularjs 1.3 or above, you can use the single bind syntax as
<li ng-repeat="item in ::contents">{{item}}</li>
This will bind the value and will remove the watchers once the first digest cycle is run and the value changes from undefined to defined for the first time.
A very helpful BLOG on this.

Related

Using Two-Way Binding with Custom Directive and ng-repeat

Starting this off by saying that I know a common answer for this is to put the ng-repeat inside the content of the directive, but in this case I can't figure out how that will work for this.
Basic Problem
As the charCounter increases throughout the letter spans, I need to access the offsetTop value of each span in the ng-repeat in order to do some things, (When the charCoutner gets to a new line (has offsetTop > 0), adjust the begin variable that is in the limitTo in the ng-repeat).
Struggling Point
I am not able to update a variable in the custom directive and make it accessible to the ng-repeat.
If I have the custom directive outside of the ng-repeat I have no access to the offsetTop of each span (but the begin variable updates).
<p><span shell counter="charCounter" begin="begin"><span ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></span></p>
What I'm thinking that needs to happen is this, but how do I get the begin variable to be updated?
<p><span positioner counter="charCounter" begin="begin" ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></p>
Code Pen Sample
I've been playing around with this on Code Pen, You can see this problem in code pen here:
UPDATE
I've taken the example one step further and also integrated r0m4n's feedback about upping the priority of this and #Amy Blankenship's about further clarifying what I am trying to do. Here is the updated CodePen. While this technically works, I'm not thinking I'd even need to do a custom directive now, since I'm accessing the element manually rather than using the element from the directive. I still have a delicate understanding of all this.
Your scope binding is having some priority issues. Take a look at using the priority property. Per the docs:
"When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied"
So you could do something like this:
myApp.directive('positioner', function($timeout) {
return {
restrict: 'A',
priority: 1001,
scope: {
letter: '=',
begin: '=',
counter: '='
},
link: function($scope, element, attr) {
$timeout(function() {
$scope.begin += 5;
console.info('begin', $scope.begin);
}, 1000);
}
};
});
https://docs.angularjs.org/api/ng/service/$compile

AngularJS - Getting the next sibling scope in ng-repeat

Given this very simplistic markup:
<div ng-repeat="item in items" item>
And the directive:
app.directive('item', function()
{
return function(scope, element, attrs)
{
}
})
Say on a click event, I want to get the next item's scope and do something. I can either use scope.$$nextSibling() or element.next().scope().
Is there an advantage/disadvantage of using either? Or is there a better way of getting the next sibling's scope?
you should do like this
in the scope, call $rootScope.$broadcast('SOME_CHANGE_IN_SCOPE',...);
in sibling scope, call $scope.$on('SOME_CHANGE_IN_SCOPE',...)
you can of course wrap data in the event, check angularjs doc for more details
Not sure what you're trying to do, but you should never use any angular variables that begin with $$. They are meant for internal implementation and are not guaranteed to stay around from release to release.

How to find a watch in scope.$$watchers

I'm using angularjs and need to find the watch of the ng-repeat, because I need ng-repeat to stop working from a specific point. this my code:
<ul>
<li ng-repeat="item in items">
<div>{{item.name}}</div>
</li>
</ul>
I need to find the watcher of the ng-repeat. If I go to scope.$parent.$$watchers[x] and perform splice the watch is removed, but how can I find the specific watch?
It's not possible to find the watch (see explanation below), but you can achieve what you wish by use the following directive
app.directive('repeatStatic',
function factory() {
var directiveDefinitionObject = {
restrict : 'A',
scope : true, // this isolates the scope from the parent
priority : 1001, // ng-repeat has 1000
compile : function compile() {
return {
pre : function preLink(tElement, tAttrs) {
},
post : function postLink(scope, iElement, iAttrs, controller,
transcludeFn) {
scope.$apply(); // make the $watcher work at least once
scope.$$watchers = null; // remove the $watchers
},
};
}
};
return directiveDefinitionObject;
}
);
and its usage is
<ul>
<li repeat-static ng-repeat="item in items">
{{ item }}
</li>
</ul>
See http://plnkr.co/k9BTSk as a working example.
The rational behind is that
the angular directive ng-repeat directive uses internal function $watchCollection to add a self created listener that watchs the items object. Since the listener is a function created during the process, and is not keep anywhere as reference, there is no good way to correctly identify which function to remove from the $$watchers list.
However a new scope can be forced into the ng-repeat by using an attribute directive, in this way the $$watchers added by ng-repeat are isolated from the parent scope. Thus, we obtain full control of the scope.$$watchers. Immediate after the ng-repeat uses the function that fills the value, the $$watchers are safe to be removed.
This solution uses hassassin's idea of cleaning the $$watchers list.
I have a fork of Angular that lets you keep the watch in the $$watchers but skip it most of the time. Unlike writing a custom directive that compiles your HTML it lets you use normal Angular templates on the inside, the difference is that once the inside is fully rendered the watches will not get checked any more.
Don't use it unless you really genuinely need the extra performance because it can have surprising behaviour.
Here it is:
https://github.com/r3m0t/angular.js/tree/digest_limit
Here's the directive you can use with it (new-once):
https://gist.github.com/r3m0t/9271790
If you want the page to never update you can use new-once=1, if you want it to sometimes update you can use new-once=updateCount and then in your controller run $scope.updateCount++; to trigger an update.
More information: https://github.com/Pasvaz/bindonce/issues/42#issuecomment-36354087
The way that I have dealt with this in the past is that I created a custom directive that copies the logic of the built in ngRepeat directive but never sets up the watches. You can see an example of this here, which was created from the ngRepeat code from version 1.1.5.
The other way, as you mentioned was to remove it from $$watchers of a scope, which is a little stranger since it accesses a private variable.
How this could be done is that you create a custom directive on the repeat to remove the watch that is the repeat. I created a fiddle that does this. It basically just on the last element of the repeat clears the parent watch (which is the one on data)
if (scope.$last) {
// Parent should only be the ng-repeat parent with the main watch
scope.$parent.$$watchers = null;
}
This can be modified to fit your specific case.
Hope this helps!
It sounds like you want to put the bindonce directive on your ng-repeat.
https://github.com/Pasvaz/bindonce
If you don't need angular dual-binding, have you tried a custom directive with a complie function where you construct HTML yourself by creating DOM from scratch without any angular dual-binded mechanisms ?

How can I pass an ng-repeated object to my AngularJS directive?

Take a look at my code here...
Controller ("tasks" is an array of JSON objects resolved in my Routes.js):
app.controller('testCtrl', function(tasks){
$scope.tasks = tasks.data;
};
HTML:
<div class="client-wrapper">
<div class="task-wrapper" ng-repeat="taskData in tasks">
<div task="taskData">
</div>
</div>
</div>
Directive:
app.directive('task', function(){
return {
restrict: 'A',
scope: {taskData: '=taskData'},
templateUrl: "/templates/directives/task.html",
link: function(scope, element, attribute) {
console.log(scope.taskData);
}
};
});
For some reason, I seem incapable of figuring out how to pass the current object being looped through in the tasks array to this directive so that I can manipulate it therein.
I've tried numerous solutions, as seen below:
how to pass a json as a string param to a directive <--- which tells me to output {{ object }} inside if an HTML attribute, and then $eval that back to JSON in the directive...
That's a very gross way of doing it, and I definitely don't want to do it that way (nor do I think this will allow it to two-way-bind back to the actual object in the tasks array in the controllers scope. This method just converts the JSON to string --> evals back to JSON + makes a copy of that string inside the directives scope).
https://groups.google.com/forum/#!msg/angular/RyywTP64sNs/Y_KQLbbwzzIJ <-- Same as above, they're saying to output the JSON as a string in an attribute, and then $eval it back to JSON... won't work for me for the same reasons as the first link.
http://jsfiddle.net/joshdmiller/FHVD9/ <-- Copying his exact syntax isn't possible because the data I want to pass to my directive is the current index of the tasks array while being ng-repeated... This is close, but doesn't work within the constraints of ng-repeat apparently?
Angularjs pass multiple arguments to directive ng-repeat <-- This syntax isn't working for me -- if I attempt to pass the taskData object (the current representation object in the array being looped through) in a parameter, it passes the literal string "taskData" and not the object itself? At this point I'm really scratching my head.
What I'm trying to accomplish (since I might be going about this in the wrong way entirely and feel I should explain the problem as a whole):
I have a bunch of data called tasks. They have a few properties, such as:
completed: true || false
description: a string
tags: an array of strings
Etc, etc.
I am outputting a big table of rows for all of these tasks. Each row will be a directive, with some buttons you can press on that row in order to change the data pertaining to the task on this row.
I want to have the functions to manipulate the data of each individual task inside the link function of the directive. So like markAsCompleted() would be a function within the directive, that would update the completed boolean of whichever task was being clicked on.
I am doing it this way, because as I see it I have two options:
A function in the controller where I pass in the task to modify as a parameter, and then perform the data manipulation
Or a function in the angular directive that just manipulates the data of the object attached to this particular directive (and my current issue is my apparent inability to two-way bind an object to this particular directive)
I want to be able to accomplish the second option in order to make my directive modular and stand-alone.
So yeah. I'm confused as to how to go about doing this and would greatly appreciate some insight as to where I'm going wrong :)
scope: {taskData: '=taskData'} means Angular expects an attribute called task-data with the value to be passed in. So give this a try...
<div class="client-wrapper">
<div class="task-wrapper" ng-repeat="taskData in tasks">
<div task task-data="taskData">
</div>
</div>
</div>
In your current attempt your directive is expecting an attribute called task-data.
This should fix your problem:
app.directive('task', function(){
return {
restrict: 'A',
scope: {task: '='},
templateUrl: "/templates/directives/task.html",
link: function(scope, element, attribute) {
console.log(scope.taskData);
}
};
});
Notice i changed the name of your property in the directive's scope from task-data to task

Same directive with different attributes

I have same directive used on two different places in one .html file. I want to be able to use these two directives with different options. Here's the code that should make this requirement more clear:
<ul multisortable="{ stop: 'updateFirstColumn', receive: 'receiveFirstColumn' }"
class='connected'>
<li ng-repeat='task in tasks.firstcolumn> {{task.title}} </li>
</ul>
<ul multisortable="{ stop: 'updateSecondColumn', receive: 'receiveSecondColumn' }"
class='connected'>
<li ng-repeat='task in tasks.secondcolumn> {{task.title}} </li>
</ul>
I would like to create directive, which is able to recognize that, if it is called by element from first column, it should work with options defined for that element. So I should be able to distinguish it, and fetch updateFirstColumn, or updateSecondColumn, e.g, depending on whether it is called by moving items from first or second column.
In my directive, in link function I did something like this:
link: function postLink(scope, iElement, iAttrs) {
$timeout(function (){
$scope.$watch('tasks', function(newValue, oldValue) {
if (newValue){
angular.element($element).multisortable({
selectedClass: 'ui-selected',
connectWith: '.product-backlog-column'
})
}
}, true);
}, 0);
}
And it looks like my directive always uses only options that I pass from second element.
I can't get it to work with options from first, or from second element, depending on which one is being dragged.
Does anyone have an idea, why this is happening?
Maybe I should define separate scope within directive definition, or something like that?
I tried many things but with no luck.
Thanks.
UPDATE:
If I remove $watch and $timeout, options passed with directive work fine, but jQuery call that I'm using isn't working fine. I placed jquery plugin inside controller part in directive.
Your jquery plugin is probably doing something outside of angulars knowledge. You will have to do a $apply() to let angular know to update.
To read up on how the $apply() method works, you can find it in the docs: http://docs.angularjs.org/api/ng.$rootScope.Scope#$apply

Resources