Re-evaluating controller functions from within ng-repeat - angularjs

I have the following angular code:
<tr ng-repeat="vm in ...">
<span ng-if="myLookupFunc(vm)"> {{myLookupFunc(vm)).label}}, {{myLookupFunc(vm).uuid}}
<span ng-if="!myLookupFunc(vm)">-</span>
</tr>
As you can see myLookupFunc is called 4 times for a single item.
How can this be optimized so that it is called only once for a given 'vm' instance?
I did try to use ng-init at 'tr' level but it doesn't re-evalute after properties of 'vm' change -- and it is expected, according to the documentation ng-init should not be used for such cases.
So what is a proper way in angularjs to accomplish this?

Please check the below code for better optimization.
<tr ng-repeat="vm in ..." ng-init="lookupData=myLookupFunc(vm)">
<span ng-if="lookupData"> {{lookupData.label}}, {{lookupData.uuid}}
<span ng-if="!lookupData">-</span>
</tr>

ng-init will run once on the start of the ng-repeat. That's why it doesn't change for you.
You have to use the controller to fetch the variable data, but you can improve it.
One way is to do this on your controller:
function lookup(vm, firstRun) {
if (firstRun) {
$scope.lookupVar = myLookupFunc(vm);
}
else {
return $scope.lookupVar;
}
}
and then you can keep your html code almost the same:
<tr ng-repeat="vm in ...">
<span ng-if="lookup(vm, true)"> {{lookup(vm)).label}}, {{lookup(vm).uuid}}
<span ng-if="!lookup(vm)">-</span>
</tr>
A better solution would be to just keep one span in the HTML and then do:
<tr ng-repeat="vm in ...">
<span>{{getVmText(vm)}}</span>
</tr>
And define a function getVmText on the controller that checks for VM value and returns the text. I believe this is the preferable way.

Use a data model and update it whenever necessary (even when "external events" occur). This way you don't have to bother with "re-evaluating controller functions", it's just pure angular data binding.
Example:
$scope.vms = ["id1", "id2", "id3"];
// this var will hold your data model (it could also be on the $scope, but for
// demo let's leave it like this)
var data = {
"id1": {
uuid: "123-123",
label: "label 1"
},
"id2": {
uuid: "456-456",
label: "label 2"
},
"id3": {
uuid: "abc-abc",
label: "label 3"
}
};
$scope.myLookupFunc = function(id) {
return data[id];
};
And then you can use it like this:
<div ng-repeat="vm in vms" ng-init="lookupData=myLookupFunc(vm)">
<span ng-if="lookupData"> {{lookupData.label}}, {{lookupData.uuid}}</span>
<span ng-if="!lookupData">-</span>
</div>
Plunker

OK, after considering the suggestions above, I found what I think is the easiest solution.
We just need to run
{{lookupData = myLookupFunc(vm)}}
and after that we can reference 'lookupData' variable. If we just run the code above inline, it will also evaluate and show the result (as JSON stringified text) inline, which is not what we want. So I ended up creating a dedicated directive which is a noop directive:
app.directive("ngAssign", function () {
return {
restrict: 'A'
};
});
Then one can just say:
<tr ng-repeat="vm in ..." ng-assign={{lookupData = myLookupFunc(vm)}}>
<span ng-if="lookupData"> {{lookupData.label}}, {{lookupData.uuid}}
<span ng-if="!lookupData">-</span>
</tr>
Full plunker: http://plnkr.co/edit/34DwCGR7Po8zg2mnCAtB?p=preview

Related

Change vm variable value after clicking anywhere apart from a specific element

When I click anywhere in the page apart from ul element (where countries are listed) and the suggestion-text input element (where I type country name), vm.suggested in controller should be set to null. As a result ul element will be closed automatically. How can I do this?
I've seen Click everywhere but here event and AngularJS dropdown directive hide when clicking outside where custom directive is discussed but I couldn't work out how to adapt it to my example.
Markup
<div>
<div id="suggestion-cover">
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()">
<ul id="suggest" ng-if="vm.suggested">
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country)">{{ country }}</li>
</ul>
</div>
<table class="table table-hover">
<tr>
<th>Teams</th>
</tr>
<tr ng-if="vm.teams">
<td><div ng-repeat="team in vm.teams">{{ team }}</div></td>
</tr>
</table>
<!-- There are many more elements here onwards -->
</div>
Controller
'use strict';
angular
.module('myApp')
.controller('readController', readController);
function readController() {
var vm = this;
vm.countryNameChanged = countryNameChanged;
vm.select = select;
vm.teams = {.....};
vm.countryName = null;
vm.suggested = null;
function countryNameChanged() {
// I have a logic here
}
function select(country) {
// I have a logic here
}
}
I solved the issue by calling controller function from within the directive so when user clicks outside (anywhere in the page) of the element, controller function gets triggered by directive.
View
<ul ng-if="vm.suggested" close-suggestion="vm.closeSuggestion()">
Controller
function closeSuggestion() {
vm.suggested = null;
}
Directive
angular.module('myApp').directive('closeSuggestion', [
'$document',
function (
$document
) {
return {
restrict: 'A',
scope: {
closeSuggestion: '&'
},
link: function (scope, element, attributes) {
$document.on('click', function (e) {
if (element !== e.target && !element[0].contains(e.target)) {
scope.$apply(function () {
scope.closeSuggestion();
});
}
});
}
}
}
]);
This is just an example but you can simply put ng-click on body that will reset your list to undefined.
Here's example:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
You will need on li elements:
$event.stopPropagation();
so your html:
<li ng-repeat="country in vm.suggested" ng-click="vm.select(country); $event.stopPropagation()">{{ country }}</li>
and your body tag:
<body ng-app="myWebApp" ng-controller="Controller01 as vm" ng-click="vm.suggested=undefined;">
UPDATE:
As I said it's only an example, you could potentially put it on body and then capture click there, and broadcast 'closeEvent' event throughout the app. You could then listen on your controller for that event - and close all. That would be one way to work around your problem, and I find it pretty decent solution.
Updated plunker showing communication between 2 controllers here:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
LAST UPDATE:
Ok, last try - create a directive or just a div doesn't really matter, and put it as an overlay when <li> elements are open, and on click close it down. Currently it's invisible - you can put some background color to visualize it.
Updated plunker:
http://plnkr.co/edit/iSw4Fqqg4VoUCSJ00tX4?p=preview
And finally totally different approach
After some giving it some thought I actually saw that we're looking at problem from the totally wrong perspective so final and in my opinion best solution for this problem would be to use ng-blur and put small timeout on function just enough so click is taken in case someone chose country:
on controller:
this.close = function () {
$timeout(()=>{
this.suggested = undefined;
}, 200);
}
on html:
<input id="suggestion-text" type="text" ng-model="vm.countryName" ng-change="vm.countryNameChanged()" ng-blur="vm.close()">
This way you won't have to do it jQuery way (your solution) which I was actually trying to avoid in all of my previous solutions.
Here is plnker: http://plnkr.co/edit/w5ETNCYsTHySyMW46WvO?p=preview

Angular $watch just parent object instead of its multiple children

I have a div, listing properties of the object POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}. The object POI si owned by a Service. The directive's controller has the function getPoi() that gets the POI from the service, and returns it to the directive.
My current HTML is something like this:
<table ng-controller="Controller as ctrl">
<tr> <!-- address -->
<td>{{ctrl.getPoi().address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{ctrl.getPoi().phone}}</td>
</tr>
<tr> <!-- website -->
<td>
<a ng-href="{{ctrl.getPoi().website}}">
{{ctrl.getPoi().website}}
</a>
</td>
</tr>
</table>
The controller
.controller('Controller', function(CurrentPoiService)
{
this.getPoi = function()
{ return CurrentPoiService.POI; }
}
The service
.service('CurrentPoiService', function()
{
this.POI = {"address":"Martinsicuro (TE), Italy", "phone":"+39 333 45657", "website':'http://mysite.it"}
}
In this way I am adding 3 watchers. Is there a way to add just 1 watcher, since it's the same parent object? Here it is a JSFiddle
Thank you
[UPDATE 1]
This is the (still not working) JSFiddle using the solution proposed by #Charlie
[UPDATE 2]
This is the working JSFiddle
As Claies has mentioned in a comment, you should never call your data from
the view through a function this way.
In this scenario you can create a watch for the POI object with the objectEquality argument true to watch the properties of the object in a single $watch. Then find your elements inside the listener and change the value in the way you want.
$scope.$watch('POI', function(){
//Assume that $scope.propertyIndex is the current property to display
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.address);
angular.element($document[0].querySelector("#myTd" + $scope.propertyIndex)).html(POI.phone);
//etc...
}, true);
You have a better control this way. But please keep in mind that this method is not suitable if POI is a complex object.
UPDATE:
Here is a working example of showing a random number every second using a watch and a factory. You should be able to learn from this and apply it to your project.
myApp.controller('myController', function($scope, dataSource) {
$scope.poi = {rand: 0};
$scope.$watch('poi', function() {
$('#rand').html($scope.poi.rand);
}, true);
dataSource.open($scope);
});
myApp.factory('dataSource', function($interval) {
return {
open: function(scope){
$interval(function() {
scope.poi.rand = Math.random();
}, 1000);
}
}
});
Try inside your controller :
$scope.POI = ctrl.getPoi();
HTML :
<tr> <!-- address -->
<td>{{POI.address}}</td>
</tr>
<tr> <!-- phone -->
<td>{{POI.phone}}</td>
</tr>

Angular "bind twice"

I'm trying to keep my watches down by using one-time binding (::) in most places.
However, I've run into the situation where I need to wait for one property of an object to arrive from our server.
Is there someway I can make Angular bind twice (first to a placeholder and second to the actual value)?
I tried accomplishing this using bindonce but it did not seem to work (I am guessing this is because bindonce wants to watch an entire object, not a single property).
Another solution would be if I could somehow remove a watch from the templates after the value comes in, if that is possible.
My objects look something like this:
{
name: 'Name',
id: 'Placeholder'
}
And my template:
<div ng-repeat="object in objects">
{{::object.name}}
{{::object.id}}
</div>
Id will change once and only once in the application life time, having a watch forever for a value that will only change once feels wasteful as we'll have many of these objects in the list.
I think this is what you are looking for! Plunkr
I just wrote a bind-twice directive and if I did not completely missed the question it should solve your problem:
directive("bindTwice", function($interpolate) {
return {
restrict: "A",
scope: false,
link: function(scope, iElement, iAttrs) {
var changeCount = 0;
var cancelFn = scope.$watch(iAttrs.bindTwice, function(value) {
iElement.text(value === undefined ? '' : value);
changeCount++;
if (changeCount === 3) {
cancelFn();
}
});
}
}
});
What I do is, I add a watcher on the scope element we need to watch and update the content just like ng-bind does. But when changeCount hit the limit I simply cancel $watch effectively cleaning it from watchlist.
Usage:
<body ng-controller="c1">
<div ng-repeat="t in test">
<p>{{ ::t.binding }}</p>
<p bind-twice="t.binding"></p>
<p>{{ t.binding }}</p>
</div>
</body>
Please see Plunkr for working example.

Multiple ng-repeat on single element

Is this possible to achieve a code like this:-
<tr ng-repeat="data in dataArray,value in valueArray">
{{data}} {{value}}
</tr>
I am having two arrays I want to show them in single row.
PS: I am not asking for syntax. I am looking for logic to achieve this
Thanks
Like :- "http://jsfiddle.net/6ob5bkcx/1/"
You should be doing this in the controller, not in the view. Map the dataValues into a key/value pair object and reference the values array using an index. This assumes that each data key has a corresponding value key.
Controller:
var dataArray = [];
var valueArray = [];
this.repeatData = dataArray.map(function(value, index) {
return {
data: value,
value: valueArray[index]
}
});
View:
<tr ng-repeat="data in repeatData">
{{data.data}} {{data.value}}
</tr>
Does this suits your need
http://jsfiddle.net/jmo65wyn/
Your data, value array as object array
this.obj = [{data: 'a', value :true}, {data: 'b', value:true}];
And you loop like this
<div ng:repeat="o in obj">
{{o.data}} and {{o.value}}
<input type="checkbox" ng:model="o.value">
</div>
Angular ng-repeat does not support it but still you can write your own custom directive according to your requirements.
Update Section
var traverseCollection = angular.module("CollectionTraverse", []);
traverseCollection.directive("repeatCollection", function(){
return {
restrict: "A",
scope: {
model: "="
},
controller: controller: function($scope) {
var collectionList = $scope.model;
angular.forEach(collectionList, function(obj, index) {
angular.forEach(obj, function(data, index) {
});
});
}
}
});
Your scope should contains the list of your collection objects : $scope.collectionList = [dataArray, valueArray];
Usage in HTML:
-------------------------------------------
<div repeat_collection model="collectionList"></div>
This directive will be generic to traverse list of collections and yes in the above code there can be some syntactical errors because i did not run it. Its your luck.
If you have, for any reason, two arrays with the same length and where their contents are corresponding (array1[0] correspond to array2[0], ..., array1[n] correspond to array2[n]), you can use AngularJS's track by (which was introduced for the 1st time in the version 1.1.4) like this for example :
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.4/angular.min.js"></script>
<table ng-app="" ng-init="names=['Bill','Billa','Billy']; ages=['10', '20', '30']">
<tr ng-repeat="name in names track by $index">
<td>{{ name }} is {{ ages[$index] }} years old.</td>
</tr>
</table>
Hope that can help.
if you want something like a list with two or more items in the same row:
in html file:
<li ng-repeat="item in items">
<i class="material-icons">{{navItem[1]}}</i>{{navItem[0]}}</li>
in js file:
$scope.items = [
["Home", "home"],
["Favorite", "favorite"]
]

AngularJS: $watch from controller for changes in properties created with ng-repeat

How do you listen in an angular controller to changes in a child properties? And from that controller know which child was changed?
HTML
<div ng-app ng-init='people = [{name: "bill", age: "11"}, {name: "jim", age: ""}, {name: "ryan", age: ""}]'>
<h1> msg: {{ msg }} </h1>
<table>
<tr ng-repeat='person in people'>
<td>
{{ person.name }}
<input ng-model='person.name'>
</td>
</tr>
</table>
</div>
edit
For instance, if I wanted to add .highlight class to every <td> that has a value for age how would I do that? I am new to Angular, but I think $watch would be the right way to do this? Because the value might be changed in the UI but changes could come from elsewhere.
The parent controller would need the value changed, and pinpoint which one changed and add the class.
when deep $watch is needed, but not for the entire object, you can strip out irrelevant data, this way you make the comparison much faster.
Example: (solution by Karl Seamon)
$scope.$watch(function($scope) {
return $scope.people.
map(function(obj) {
return obj.name
});
}, function (newVal) {
$scope.msg = 'person name was changed';
}, true);
Live working example: http://jsfiddle.net/choroshin/uhrQ4/
For more information take a look at my blog post.
You can easily apply a class based on the person's age as follows:
<td ng-class ='{highlight : person.age}'>
Angular will do the magic for you, no need to manually watch for changes.
See http://jsfiddle.net/GAQvM/1/
EDIT: Old answer on $scope.$watchCollection:
It does work when you spell out the actual items to watch:
$scope.$watchCollection('[people[0].name, people[1].name, people[2].name]',
changeHappened);
http://jsfiddle.net/rCPvD/
I realise that this is not exactly feasible for large collections, but I think it points out that the watch is shallow, only one level deep.
The documentation for $watchCollection says (my emphasis):
Shallow watches the properties of an object and fires whenever any of
the properties change (for arrays, this implies watching the array
items; for object maps, this implies watching the properties)

Resources