I have a simple table of data, created using AngularJS. One of the columns of the table is calculated from a function on the controller.
I have a button on the page that opens a new modal. When I open a modal using UI bootstrap, I get a new isolated scope (child of the root scope), as expected. If, however, I have an input text in the modal any key-presses in this text field automatically invoke functions on the parent scope - even though I can verify that the scope is isolated.
Here is a plunkr of the behavior: http://plnkr.co/edit/JzhxSDcSefDe04Psxq0w
As shown in the example, the third column of the table is calculated with a function called "ageNextYear". When the table is being rendered, this function is called many times as expected (and can be verified in the console log). If however, I open the modal and type some text in to the field, the "ageNextYear" function on the parent scope still gets called (type some text in the input field and watch the console log output).
I'm not sure whether this is intended behavior, or whether I'm doing something wrong. I have tried using dot notation on both scopes, and explicitly passing a new scope to $modal.open, but with no joy.
I can get around the problem (by creating a watchCollection on "people" and updating the table that way - which may be a better way of doing this overall) but wanted to validate whether others have seen this behavior also.
The issue you are experiencing is not related to the scope of the Modal Dialog. The issue is related to the use of a function within an ng-repeat expression. In general, using functions within expressions is a performance issue, but it's a much larger problem within an ng-repeat. according to This excellent article regarding common pitfalls of using scopes,
When using expressions in views or watchers, you should always remember that an expression is called every time AngularJS thinks it is needed. You will not get the best performance using functions, you might even miss some change events.
That means an expression…
within a ng-repeat will be called for each item separately. Additionally, this is used by the repeat directive to determine data changes.
Can be evaluated multiple times in one digest. This can happen when you're using multiple directives or additional scope watchers.
Can be evaluated even if the direct scope seems to be unchanged.
Containing a function will not be evaluated if the return value of the function changes, but only if the function definition has changed.
Your example causes 3 of these 4 to occur.
You repeat the function call for each object in your scope, 3 items = 3 calls to the function.
You add an additional watcher indirectly by calling the Modal Dialog.
Changes to the data in the Modal Dialog's scope causes evaluation of the scope of the controller containing the ng-repeat, even though the data within the ng-repeat didn't change (no way for it to know if the data changed until the $digest is called). Each change to the Modal causes the $digest, which causes another trip through the ng-repeat, and another call for each item in the ng-repeat.
In your case, the logic does not need to run every time the expression will be evaluated. It is better to compute and write the logic into the scope when the logic result has changed. This decouples the logic from the object and the view.
in summary,
Best practices:
DO NOT use functions in expressions.
DO NOT use other data besides the scope in an expression.
DO use $scope.$apply() when applying external data changes.
Simon,
I liked your question and I added watch on the scope and saw the digest cycle is getting called
$scope.$watch(function watchMe(scope) { console.log('Digest watched me!'); });
The following is the fork with the digest.
http://plnkr.co/edit/5PTO1uPFvmLrg7K9vzTm?p=preview
I donot know this is the reason but I think expressions inside the ng-repeat are calling the digest as it tries to evaluate expression on any event on that item.
I think we should evaluate expressions in the model and give the updated model to the ng-repeat to solve the issue.
Related
I have written a directive which has a two way binding to bring in the text specification for a flow chart which is used to generate the actual objects (steps and connections) inside the directive. I have a $watch set up inside the directive to allow the controller to send in a new specification (e.g. loading a new chart), but I also want to have the directive make changes to the specification in response to user action (e.g. deleting a connection) so that the controller can save the changed specification.
I have a function in the directive which converts the chart objects back into a text specification, but if I simply replace the specification scope variable with the updated value, the original watch sees a change (which it thinks might have come from the controller) and so reloads the chart from the specification. This has the effect of breaking things like dragging elements around because the elements are being removed from the DOM and replaced by new ones.
What I would like to be able to do is temporarily suspend the $watch while I make my internal changes, or in some other way avoid the watch from triggering when the directive makes changes to it, only going off when the controller makes changes from outside. I tried to unbind and rebind the watch around making the change, but because the actual checking happens elsewhere in the cycle that does not work. An alternative solution I could use is to have two variables passed between the controller and directive, one going each way, but that is somewhat inelegant. Any better suggestions would be welcome.
When I do this, usually my $watch looks something like this:
$scope.$watch('MyVar',function(newval,oldval) {
if (oldval == newval) return;
if (newval == $scope.internalval) return;
// process here
})
For precisely the reason you outline. I don't believe you can turn the $watch off, so before I make an internal update to the watched variable, I update a tracking version of the variable to make sure I don't get infinitely-recursing changes. It seems like a pain, but the watched variables are finite and I always use a setter function so the code updating the internal value is only written once
I have an issue that only occurs when I load directives as part of a angular-ui-router state. When I add the directives directly to index.html everything works as expected.
Here's a plunker that demonstrates the issue I'm having:
http://plnkr.co/edit/EzvTOiSzaf6jjCsBKM1e?p=preview
I have a Resource personResource that returns an array of objects.
The Controller personController queries the Resource, and saves the results into $scope.people.
This populates a template list-people.html which is loaded in a directive listPeople.
What I'm having problems with is another directive filterPeople contains radio buttons with options to filter the list.
The expected result is that if I select an option, it will only display those rows where the row's value matches the option value. What is actually happening is that all of the data is displayed no matter what I select
It's a scope issue, your templates have their own child scopes that can't see values in each other, so when you set the personFilter.color in one template, your template using it as a filter doesn't know about it. You can use angularjs batarang or just insert a simple binding to debug these issues (plnkr):
</tbody></table>personFilter json: {{personFilter|json}
To fix the problem you can set the personFilter object on the parent scope and both children will use the inherited object. Just realize that if you set the object on a child scope instead of just a property, it will only set that value on the one child scope and the other scopes will continue to use the inherited object. You should probably put the controller on a parent. Your controller is being instantiated for both the content and sidebar, so you are querying your people twice even though the data isn't used in your sidebar...
I've got a small problem. I have a bootstrap tabset within an angularjs app. The tabs are partially generated from ng-repeat and contain data, that is bound to the scope.
Basically, within the controller:
$scope.data = { ... } // Loaded from a factory
Now, when I reload this data (by replacing the old one with the new one) the tabset will be rebuilt by ng-repeat and the view will automatically switch to the first tab.
Is there a way to replace the data in scope without rebuilding everything in the view?
You aren't explicit about the specific data and which of it is used in what ways.
So this will have to be a general answer:
Try to update only the bits that actually changed.
For example do not replace whole objects or arrays, but only updated properties or indices that actually changed. That way only the relevant parts of the GUI will update.
If you must replace objects, you can still help angular keep the connection between objects in the model and dom elements for ng-repeat if you use "track by" in the expression (which is possible only if the element has some unique id that you can use for that).
Another option: Use one time binding for the parts that should change only exactly once when the data is first loaded: See the section "One-time binding" in https://docs.angularjs.org/guide/expression
We are facing some weird behaviour in the execution of link functions of 2 nested custom AngularJS directives:
1/ The first time (or each time after a hard page refresh) the linking function of the outer directive is executed before the linking function of the inner directive --> this is how we expect/want it, since parameters are passed from the outer to the inner directive.
2/ But then, each time the directive is used again, eg. after navigating away from the view and returning to it (without ever refreshing the page), the execution of the linking functions gets reversed: i.e. the linking function of the inner directive is executed before the linking function of the outer directive.
This, obviously, results in errors, since params passed from the outer directive to the inner directive (which are used in the link function) are not yet existing.
We are puzzled by this problem for quite some time now, so we hope there are some bright minds out there that may be able to help us out here :-) Tx in advance!
Have you tried setting the priority value of the directives?
It is easily described in the priority chapter of this Article
I'm still running into the same problem, filters and functions inside ng-repeat being called all the damn time.
Example here, http://plnkr.co/edit/G8INkfGZxMgTvPAftJ91?p=preview, anytime you change something on a single row, someFilter filter is called 1000 times.
Apparently it's because any change on a child scope bubbles up to its parent, causing $digest to run, causing all filters to run(https://stackoverflow.com/a/15936362/301596). Is that right? How can I prevent it from happening in my particular case?
How can I make it run only on the item that has changed?
In my actual use case the filter is called even when the change is not even on the items of ng-repeat, it's so pointless and it is actually causing performance problems..
// edit cleared all the unnecessary stuff from the plunker
http://plnkr.co/edit/G8INkfGZxMgTvPAftJ91?p=preview
This is just how Angular's dirty checking works. If you have an array of 500 items and the array changes, the filter must be reapplied to the entire array. And now you're wondering "why twice"?
From another answer:
This is normal, angularjs uses a 'dirty-check' approach, so it need to call all the filters to see if exists any change. After this it detect that have a change on one variable(the one that you typed) and then it execute all filters again to detect if has other changes.
And the answer it references: How does data binding work in AngularJS?
Edit: If you're really noticing sluggishness (which I'm not on an older Core 2 Duo PC), there are probably a number of creative ways you can get around it depending on what your UI is going to be.
You could put the row into edit mode while the user is editing the data to isolate the changes, and sync the model back up when the user gets out of edit mode
You could only update the model onblur instead of onkeypress using a directive, like this: http://jsfiddle.net/langdonx/djtQR/1/