Angularjs function call issue with nested elements - angularjs

I have an angulars setup as follows, trying to mimic some excel functionality where I have a controller nested inside an ng-repeat.
<tr ng-repeat="lw in lw_list" my-lw ng-model="lw"
<td>
<!-- next two elements act as an excel cell, one for inputing data, they other for displaying calcualtion result -->
<div ng-controller="MyCellCtrl">
<input type="text" class="inputdiv" ng-model="lw.library.name" >in</input>
<div class="output" ng-bind="getCellValue(lw.library.name)" syle="postion:absolute" contenteditable="True" >out</div>
</div>
<div ng-controller="MyCellCtrl">
more input / div pairs to act as a new cell
.....
</div>
</td>
I have the stylesheets set up so that input and output are in the same position, and get hidden / unhidden, so that they act like an excel cell (you type a formula, then when you leave focus, it updates the content).
Anyway, when I put a console.log() inside the getCellValue() function, to show what instance of the controller is being called, then typing in one particular cell, I can see that getCellValue() is being called on every cell.
Is there some way to call getCellValue() when the input is updated without calling the method on every instance?
(I based this code on the code from this tutorial:
https://github.com/graunked/spreadsheet
you can see the same behaviour by putting a console.log in the compute function. If you increase the arrays to 20 x 20 elements, it starts to get slow when you type anything.)

Is there some way to call getCellValue() when the input is updated without calling the method on every instance?
<div class="output" ng-bind="foo">
then use $watch:
function MyCellCtrl($scope)
{
$scope.foo = $scope.lw.library.name;
$scope.$watch('foo', function(newValue) {
$scope.foo = getCellValue($scope.foo);
});
}
or use viewChangeListeners as an alternative:
function MyCellCtrl($scope)
{
$scope.foo = $scope.lw.library.name;
this.$viewChangeListeners.push(function(newValue) {
$scope.foo = getCellValue($scope.foo);
});
}
References
Effective Strategies for avoiding watches in AngularJS
Compile, Pre, and Post Linking in AngularJS

Related

Filter a directive by using a div wrapper or within the directive tag

I'm trying to go with the best approach and avoid unnecessary rendering/processing time in my AngularJS app when choosing between 2 directives to be displayed in the page inside an ngRepeat loop, want to know which is the best way:
If by setting the ng-if directly in the directive html element, like:
<div ng-repeat="element in list">
<my-directive-a ng-if="someFunction(element)"></my-directive-a>
<my-directive-b ng-if="!someFunction(element)"></my-directive-b>
</div>
Or by moving out the first <div> from the directive's template and use it as a wrapper for each directive. For instance:
<div ng-repeat="element in list">
<div ng-if="someFunction(element)">
<my-directive-a></my-directive-a>
</div>
<div ng-if="!someFunction(element)">
<my-directive-b></my-directive-b>
</div>
</div>
NOTE: The starting <div> element on each directive could be modified behave the same so I will basically take that out of the directive's html and moving it outside the directive declaration in order to place the ng-if there
What would be the best approach for this case? Are there any performance implications from doing it one way or another? Or is it just the same thing? Consider that the number of elements in the list could get really big.
They are quite the same, but you can improve performance with one-time binding, but only when element does not change at runtime (for example, let's say that it has property name, and your someFunction is like return element.name === 'John'). Angular just stop observing this function when it returns value, and watches will be deleted. There are 2 prerequisites to use this solution:
Elements properties in list does not change (if you rely on them in someFunction), for example if you rely on name property name must not change, because watcher on someFunction is note available.
When list changes or its elements properties change, you reload all list (for example, you fetch it from server again if you know that change occurred)
What you get with this? There is no watches after my-directives are drawn on ng-ifs, and when something changes, new reference is bound to list (for example, it comes from server) and everything will be redrawn, ng-ifs will run again and when will become stable (function returns value) then will be unbound. How it looks like? Like this:
<div ng-repeat="element in list">
<div ng-if="::(someFunction(element))">
<my-directive-a></my-directive-a>
</div>
<div ng-if="::(!someFunction(element))">
<my-directive-b></my-directive-b>
</div>
</div>
Two colons before expression. But be aware, that with one-time binding it's easy to mess up - you need to be sure that you test your code enough to be sure it works.

angularjs bind function call to span without triggering event

I use ng-repeat to populate my table. One of the columns in the table should be dynamically populated again by a different function call.
Below is my code snippet.
<tr ng-repeat="item in ctrl.items">
<td><span ng-bind="item.name"></span></td>
<td><span ng-bind="getItemDetails(item.id)"></span></td>
</tr>
I have array of items. I need to display those items in a table. Item name will be present in the item object, however, item details will be populated by another function call which needs item id.
On using ng-bind (like in the code above) I face 2 issues.
Multiple calls to function even if array has 1 items. Sometimes it goes on thereby freezing my browser and server out of memory issue
The item id doesn't get passed to function always. Sometimes it is undefined.
I am not sure if ng-bind is the right directive to be used. ng-model doesn't work though. Is there any other directive or other way to do it?
How can I achieve this?
EDIT:
Here is the jsfiddle url: https://jsfiddle.net/grubxaur/
If you check browser console, you can see the function is called twice. I guess it is called N no. of times where N is no. of columns in the table.
I have tweaked my implementation a bit to get rid of this issue. Rather than calling a function within ng-repeat, I modified the items array within the controller using angular.forEach before ng-repeat is invoked.
Something like code below.
angular.forEach(self.items, function(item){
item.details = $scope.getItemDetails(item.id);
});

How do I ask AngularJS to recalculate the value of a complex function?

I have an AngularJS controller with the following function:
$scope.getExampleValue = function(exampleId) {
// calculate a value using underscore's _.where()
// clause against two JSON arrays on the $scope
// and the exampleId parameter passed in
return computedValue;
}
The function's parameter (exampleId) is rendered from the server, so the resulting HTML looks like this:
<div ng-controller="ExampleController">
...
<span>{{ getExampleValue(3) }}</span>
<span>{{ getExampleValue(4) }}</span>
<span>{{ getExampleValue(5) }}</span>
...
</div>
The problem I have is that AngularJS doesn't know to call getExampleValue() again when the JSON arrays used in the function have changed: they're 2 simple JSON arrays, new JSON items can be added or removed, or properties of existing JSON items in either array can be modified which affect the result.
I've looked at $scope.watch() and $scope.watchCollection() but I'm unsure how I can use them without changing my approach to bind against already computed values rather than against the function I prefer.
Essentially I think I'm asking how to notify AngularJS that a complicated bound value has changed, then I could wrap that notification up in a $scope.watch()..
Thanks for your help.
You are looking for $scope.$apply:
When you change the JSON asyncly, run:
$scope.$apply(function () {
// update the properties on $scope
});

How can I change text elsewhere using AngularJS function with parameters?

Alright so I found out what might be my solution for a task I am trying to accomplish: to use some sort of toggle feature which I have seen represented a couple of different ways in other questions here on Stack Overflow. However, mine is a bit of a special case that I can't seem to figure out how to adjust to get it to work.
Here is the accordion where the text needs to appear:
<div ng-controller="Ctrl">
<accordion id="myID">
<accordion-group heading="My Heading">
{{toggleText}}
</accordion-group>
</accordion>
</div>
The text that needs to appear depends on what is clicked however:
<area ng-repeat="x in Object" alt="{{x.name}}" title="" ng-click="thisClick(x.name,x.address);toggle = !toggle" shape="poly" coords="{{x.htmlcoords}}" />
I have an image that has hot spots on it. I used ng-click="thisClick(x.name,x.address)" to easily capture the data from my Object and I was able to alert it in my thisClick(name,address) function. This part of the HTML is in a div that separately calls the same controller as the one above, I don't know if that would be relevant. I couldn't get my code working before trying this toggle stuff unless I kept the controller where it was and just called it again. Anyway, now to apply the toggle feature I tried changing the ng-click to what is shown above and the function to:
$scope.thisClick = function(name,address){
$scope.toggle = true;
$scope.$watch('toggle',function(){
$scope.toggleText = $scope.toggle ? '' : name+address;
});
};
Ultimately the name and address won't be squished together but this is for testing purposes only.
When I run the code, simply put nothing happens.
Either there is a way to clean this up or a way to approach this entirely differently? I hope I provided enough information.
I wish it were as simple as:
<area ng-repeat="x in building" alt="{{x.name}}" title="" ng-click="thisBuilding = x.name+x.address" shape="poly" coords="{{x.htmlcoords}}" />
$scope.buildingName = name;
$scope.buildingAddress = address;
$scope.thisBuilding = function(){
return $scope.buildingName + " " + $scope.buildingAddress;
};
};
:
{{thisBuilding()}}
This sounds quite a lot like a scope issue. As ng-repeat creates a new child scope for each item, most likely you end up setting the $scope.toggleText property in "wrong" scope, i.e. not in the scope of accordion-group where you are trying to display the property.
You might want to play around a bit with some Angular inspector tool, for example ng-inspector to verify this.
If the problem indeed is with toggleText property ending up in the wrong child scope, one possible workaround could be introducing a container object for the property in the root scope. The reason why this could work is that objects are passed as references for child scopes, not as copies like primitive properties are. A whole lot more on this topic can be read from Understanding Scopes article in AngularJS wiki.
So, something along these lines, starting from controller Ctrl:
// inside the controller code:
// initialize the toggleContainer object
$scope.toggleContainer = {};
Then in html template:
<div ng-controller="Ctrl">
<accordion id="myID">
<accordion-group heading="My Heading">
{{toggleContainer.text}}
</accordion-group>
</accordion>
</div>
And finally in the thisClick function, store the value you want to property toggleContainer.text instead of simple toggleText:
$scope.thisClick = function(name,address){
$scope.toggle = true;
$scope.$watch('toggle',function(){
$scope.toggleContainer.text = $scope.toggle ? '' : name+address;
});
};

How to set a boolean flag to collapse/expand a row with ng-repeat

I have this plunker code.
What I'm trying to do, is to display the gray box one time per row.
To achieve this, I thought to modify the partition filter in order to return a JSON to add it a new property by row to know if the gray box is expanded or not.
But, I could Not successfully return a JSON.
Do you know how to modify the filter to return a JSON or a better way to show the gray box by row?
Related questions:
Push down a series of divs when another div is shown
Update 1
The issue could be easily resolved by using the correct scope for the ng-repeat for the row without modifying the filter, thanks to #m59.
http://plnkr.co/edit/eEMfI1lv6z1MlG7sND6g?p=preview
Update 2
Live Demo
If I try to modify the item, it seems the ng-repeat would be called again losing the props values.
<div ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}">
<div ng-repeat="item in friendRow"
ng-click="collapse(item)"
ng-class="{myArrow: showArrow}">
{{item.name}} {{item.age}} years old.
<div>{{item.name}}</div>
</div>
<div collapse="!props.isExpanded">
some content
<br/>
<input type="text" ng-model="currentItem.name">
</div>
</div>
js
$scope.collapse = function(item){
this.props.isExpanded = !this.props.isExpanded;
this.showArrow = !this.showArrow;
$scope.currentItem = item;
};
This causes the gray box to collapse each time the item is modified. Any clue?
I've updated my code/answer regarding partitioning data. It's important to fully understand all of that before deciding on an approach to your project.
The problem you have in your plnkr demo is that you're modifying the parent $scope and not the scope of the ng-repeat for that row.
Just set a flag on the row and toggle it when clicked:
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="isExpanded = false"
ng-click="isExpanded = !isExpanded"
>
<div ng-repeat="item in friendRow">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!isExpanded">
some content
</div>
</div>
To access the correct scope within a function in the controller, you can use the this keyword instead of $scope. this will refer to the scope the function is called from, whereas $scope refers to the scope attached to the element with ng-controller (a parent of the ng-repeat scopes you want to target).
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-click="collapse()"
>
JS:
$scope.collapse = function() {
this.isExpanded = !this.isExpanded;
};
If you want to keep the ng-click directive on the item element instead of putting it on the row element as I have done, then you're dealing with another child scope because of that inner ng-repeat. Therefore, you will need to follow the "dot" rule so that the child scope can update the parent scope where the collapse directive is. This means you need to nest isExpanded in an object. In this example, I use ng-init="props = {}", and then use props.isExpanded. The dot rule works because the children share the same object reference to props, so the properties are shared rather than just copied, just like in normal JavaScript object references.
Live Demo
<div
class="row"
ng-repeat="friendRow in friends | partition:2"
ng-init="props = {}"
>
<div ng-repeat="item in friendRow" ng-click="collapse()">
{{item.name}} {{item.age}} years old.
</div>
<div collapse="!props.isExpanded">
some content
</div>
</div>
JS:
$scope.collapse = function(){
this.props.isExpanded = !this.props.isExpanded;
};
Update
We keep going through more and more issues with your project. You really just need to experiment/research and understand everything that's going on on a deeper level, or it will just be one question after another. I'll give it one last effort to get you on the right track, but you need to try in the basic concepts and go from there.
You could get past the issue of props reinitializing by putting $scope.expandedStates and then passing the $index of the current ng-repeat to your function (or just using it in the view) and setting a property of expandedStates like $scope.expandedStates[$index] = !$scope.expandedStates[$index]. With the nested ng-repeat as it is, you'll need to do $parent.$index so that you're associating the state with the row rather than the item.
However, you'll then have another problem with the filter: Using my old partition code, the inputs inside the partitions are going to lose focus every time you type a character. Using the new code, the view updates, but the underlying model will not. You could use the partition filter from this answer to solve this, but from my understanding of that code, it could have some unexpected behavior down the road and it also requires passing in this as an argument to the filter. I don't recommend you do this.
Filters are meant to be idempotent, so stabilizing them via some kind of memoization is technically a hack. Some argue you should never do this at all, but I think it's fine. However, you definitely should ONLY do this when it is for display purposes and not for user input! Because you are accepting user input within the partitioned view, I suggest partitioning the data in the controller, then joining it back together either with a watch (continuous) or when you need to submit it.
$scope.partitionedFriends = partitionFilter($scope.friends, 2);
$scope.$watch('partitionedFriends', function(val) {
$scope.friends = [].concat.apply([], val);
}, true); // deep watch

Resources