Angular repeat scope calculations - angularjs

Hi i'm building a form doing a lot of calculations, such as summarizing keys in objects
[
{ bar: 5, .. },
{ bar: 6, .. },
...
]
I use this expression in currently 35 places in my HTML. Sometimes connected to further calculations and with different keys
<div>
{{ sumKeys('bar') + foobar }}
</div>
The function i use is declared as
app.controller('someCtrl', function($scope){
$scope.sumKeys= function(key){
return (calculated sum);
}
}
My problem is, that if i write a single letter into any input field the sum function is called about 400 times. I know there is a rule, that calls the function if a scope is changed up to 10 times, but isn't there a more efficient way?
Can i output the result without changing the scope? Or force this calculation just to be done once? I know the results do not change any involved scope. So the results should be the same after 2 iterations.
I also implemented the function as a filter with same results.

What you're looking for is a SINGLETON service. You can make one in angular like this: (one of my real examples)
angular.module('ocFileUpload', [])
.service('ocFileUpload', ['$http', '$rootScope', function ($http, $rootScope) {
// Will house our files
this.files = [];
// This fn appends files to our array
this.addFile = function(files){
for (var i = 0; i < files.length; i++){
this.files.push(files[i]);
}
};
this.getFileDetails = function(ele){
// push files into an array.
for (var i = 0; i < ele.files.length; i++) {
this.files.push(ele.files[i])
}
};
this.removeFile = function(fileToRemove){
var indexOfFile = this.files.indexOf(fileToRemove);
this.files.splice(indexOfFile, 1);
};
this.clearFiles = function(){
this.files = [];
};
// Return files array
this.getFiles = function(){
return this.files;
};
}]);
Then just simply use it in your controller/directive:
.controller(['ocFileUpload', function(ocFileUpload){
var ref = ocFileUpload.getFiles();
}]

Related

Update value in controller when factory changes

I have a factory service to control a shopping cart and I'm will problems to sync the data with controller when I'm still configurating the shopping cart.
the process consists of analyzing whether or not the user is logged. If he is not, then spCart = 0, if he is logged, then I'll have to make other verifications. Let's say I have 2 more verifications to do:
If user has a valid address,
If the address is within a range, so the shipping cost will be 0;
If user doesn't have valid address, then spCart = 1;
If he has an address and is whitin the range,
spCart = [
'items':[], //add items here
cost: '0'
];
Or if isn't in the range:
spCart = [
'items':[], //add items here
cost: '9.99'
];
The problem is, some of these verifications needs to wait for a $http, this way I'm not able to sync the spCart value from the factory, with the controller. This is what I did:
controller:
function CartController(factCart) {
var sp = this;
factCart.initialLoad();
sp.cart = factCart.getCart();
};
factory:
function factCart (localStorageService,factMain) {
var spCart = 0;
var service = {
initialLoad: _initialLoad,
getCart: _getCart
};
return service;
function _initialLoad() {
var userData = localStorageService.cookie.get(ngApp);
if (userData) {
var func = 'load_address';
factMain.methodGet(func).then(function(res){ //$http from main factory
if(res==0) {
return spCart = [1];
} else {
_checkRange(res);
}
});
} else if (!userData) {
return spCart;
};
};
function _checkRange(data) {
spCart = [];
spCart['items'] = [];
/*
logic here
*/
if (inRange) {
spCart['costs'] = {
cost: 0;
};
return spCart;
} else {
spCart['costs'] = {
cost: 9.99;
};
return spCart;
};
};
function _getCart() {
return spCart;
};
};
The problem is, the spCart value is always 0, it doesn't change when I update it, and I don't know what I can do to solve this issue.
Note: I had this problem when adding items to the shopping cart, the view didn't updated. I solved that issue by using $apply(). But in this case, i couldn't use $apply, it said 'Cannot read property...'
I managed to make it work. I don't know if it's the best solution, so I'll keep the question opened.
But I had to keep the initial array just like I want it to be when all the verifications are OK and added an extra field to control everything.
So I'll have an array like this:
spCart = [];
spCart['address'] = {
verification: 0,
address: 'name of address',
//..etc..
};
spCart['items']= [];
//..Rest of array - for checkout only
This way the data inside the verification is being updated. It's not the best way (or at least not how i expected) because I'm exposing the whole array, instead of a single value.
So, if there is a better solution, it will be very welcome.

Angular.copy inside promise with angularjs

I need to make a copy of an object, but this object is inside a promise.
When I modify the object a, the object b is modified too.
I know that it can be due to async mode with promise, but I can not figure out it.
Simplifying my code, I have something like this:
The promise:
$scope.search = function () {
DocumentFactory.getDocuments(dataParams).then(function (data) {
makeFacets(data);
},
function (data) {
$scope.errorMessages.search = true;
});
};
It will search into many topics and will return some of them.
Then I create an array with that topics:
var makeFacets = function(data) {
$scope.topics=[];
$scope.topics[0] ={father: "General", label:""};
$scope.topics[1] ={father: "International", label:""};
$scope.topics[2] ={father: "Crime", label:""};
$scope.topics[3] ={father: "NonLegal", label:""};
[...]
};
Once I have the object, I use it to show that topics and also to filter inside that topics:
<div class="filter-box">
<input type="input" class="form-control" ng-model="filter.topic" ng-change="test1()">
</div>
<div class="filter-name" ng-repeat="data in topics">
<span>{{data.label}}</span>
</div>
My last step is try to filter inside that topics, to do that, I need to make a copy of the topics.
$scope.allTopics = [];
$scope.test1 = function(){
if($scope.allTopics.length === 0){
angular.copy($scope.topics, $scope.allTopics);
}
$scope.topics = $scope.allTopics;
var filter = $scope.filter.topic;
if(filter.length>=3){
for(var i = 0; i<$scope.topics.length; i++){
if($scope.topics[i].children !== undefined){
for(var j = 0; j<$scope.topics[i].children.length; j++){
if($scope.topics[i].children[j].label.toLowerCase().indexOf(filter.toLowerCase()) === -1){
$scope.topics[i].children.splice(j, 1);
}
}
}
}
}
};
This is not working, of course. I have tried a lot of things more but no one is working.
Also I have tried to add the copy here:
$scope.search = function () {
DocumentFactory.getDocuments(dataParams).then(function (data) {
makeFacets(data);
$scope.allTopics = [];
angular.copy($scope.topics, $scope.allTopics);
},
function (data) {
$scope.errorMessages.search = true;
});
};
All time I modify the topics object, the all Topics object is also modified.
The issue is that you are doing an assignment after calling copy. Thus, the two variables reference the same object leading to the problem you observe. Just leave out the assignment after you have copied it or do a copy instead of an assignment.
if($scope.allTopics.length === 0){
angular.copy($scope.topics, $scope.allTopics);
}
$scope.topics = $scope.allTopics; // <-- this assignment is wrong
Note you can also use copy in a more assignment-like fashion:
$scope.topics = angular.copy($scope.allTopics);

dynamic multidimensional arrays looping

I need to create a multidimensional array using intervals. I have 5 users who's data grows every 5 seconds. Each user needs array to hold this data that can be accessed for each user later.
currently $rootScope.BHR looks like this:
[1,2,3,4,5,6,7,8,9,10...] <--after 10 secs/ 2 intervals
I want this <--after 10 sec/2 intervals
[1,6..] //key0
[2,7..] //key1
[3,8..] //key2
[4,9..] //key3
[5,10..]//key5
//CODE
var liveDataCtrl = angular.module("xo").controller("liveDataCtrl", ["$scope", "$rootScope", "$http", "$interval", "lodash",
function ($scope, $rootScope, $http, $interval, lodash) {
$rootScope.BHR = [];
function shout() {
$http.get("URL")
.then(function (live) {
$scope.live = live.data;
var key;
for (key in $scope.live) {
console.log($scope.live[key].personnelID, "keys");
var getId = $scope.live[key].personnelID;
$http.get(URL + getId)
.then(function (all) {
var data = all.data[0].HR;
console.log(all.data[0].HR);
$rootScope.BHR.push(data);
})
}
})
}
$interval(shout, 5000);
function hrLive() {
console.log($rootScope.BHR, "SHOUT");
}
$interval(hrLive, 5000);
}]);
So, there are no true multi-dimensional arrays per se, what JavaScript has are Objects, which are more or less the same thing (but since labels matter, we'll call it what they are).
To declare a new object, use the syntax
var myObj = {};
Then, to append a new property you can use either dot or bracket notation:
var a = 'foo';
myObj[a] = [2,3,4];
myObj.bar = [5,6,7];
/**
* myObj = {
* foo: [2, 3, 4],
* bar: [5, 6, 7]
* };
*/
What you'll want to do is declare a new object when your code starts running, and then declare new attributes on that object as arrays. I created a plunk that demonstrates how to accomplish what I believe you are trying to do.

Converting string to array using filter and using it in ng-repeat

I have a string which is in "1200:2,1300:3,1400:2" format. I need this to be printed like
<p>1200</p><p>2</p>
<p>1300</p><p>3</p>
<p>1400</p><p>2</p>
I tried using filter,
return function (input) {
//Validate the input
if (!input) {
return '';
}
var hoaArray = [];
var inputArray = input.split(',');
for (var i = 0; i < inputArray.length; i++) {
var adminTimeArray = inputArray[i].split(':');
hoaArray.push({ 'adminTime': adminTimeArray[0], 'dose': adminTimeArray[1]?adminTimeArray[1]:'' });
}
return hoaArray;
};
and inside html like
<p ng-repeat="timing in timing_list | formatter">{{timing.}}</p>{{timing .adminTime}}</div>
I am getting the following error,
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: regularInterceptedExpression","newVal":36,"oldVal":34}],[{"msg":"fn: regularInterceptedExpression","newVal":38,"oldVal":36}],[{"msg":"fn: regularInterceptedExpression","newVal":40,"oldVal":38}],[{"msg":"fn: regularInterceptedExpression","newVal":42,"oldVal":40}],[{"msg":"fn: regularInterceptedExpression","newVal":44,"oldVal":42}]]
Could anyone please help me understand what am I doing wrong?
Regards,
Raaj
In the IndexController.js file:
var inputString = "1200:2,1330:3,1400:4,1500:3";
var formatInputString = function (input) {
//Validate the input
if (!input) {
return '';
}
var hoaArray = [];
var inputArray = input.split(',');
for (var i = 0; i < inputArray.length; i++) {
var adminTimeArray = inputArray[i].split(':');
hoaArray.push({ 'adminTime': adminTimeArray[0], 'dose': adminTimeArray[1] ? adminTimeArray[1] : '' });
}
return hoaArray;
};
$scope.inputString = inputString;
$scope.formattedString = formatInputString(inputString);
In the HTML file:
<div ng-repeat="timing in formattedString" >
{{timing.adminTime}}
{{timing.dose}}
</div>
The issue here - possibly a limitation or a bug in Angular - is that your filter creates new array objects every time it runs. ng-repeat uses under the covers $scope.$watchCollection to watch for the expression "timing_list | formatter" - this watcher always trips up because, in trying to detect a change in a values in the collection, it compares objects with a simple "!==" - and the objects are always new and different objects.
In short, this is another way to reproduce:
$scope.items = [1, 2, 3];
$scope.$watchCollection("items | foo", function(){
});
where foo is a filter that operates on each element in the array creating a new object:
.filter("foo", function(){
return function(inputArray){
return inputArray.map(function(item){
return {a: item};
});
};
});
So, to answer your question - you cannot with Angular v1.3.15 use a filter that returns an array with objects (without some funky object caching) with $watchCollection, and by extension, with ng-repeat.
The best way is to create the array first (with ng-init or in the controller), and then use it in the ng-repeat.

angularfire: Having trouble getting a Firebase array from my factory with $asArray()

I've got a factory that gets my data from Firebase, and I want my controller to be able to access it. However, when I console.log the data in my controller, it isn't the Array[10] that I would expect it to be, but rather an Array with keys 0,1,2,..10, $$added, $$error, $$moved,... and so on. However, when I skip out on using the factory, and use $asArray() method on my firebase ref directly in my controller it shows up nicely as an Array[10]
In my factory, this is what it looks like..
var listingsref = new Firebase("https://something.firebaseio.com");
var sync2 = $firebase(listingsref);
var products = sync2.$asArray();
factory.getProducts = function(){
return products;
};
Controller
$scope.products = marketFactory.getProducts();
console.log($scope.products) in my controller should be Array[10], but instead it's an Array with the data + a lot more $$ methods. Anyone know what's going on? Thanks
EDIT: Full Factory File
(function(){
var marketFactory = function($firebase){
var listingsref = new Firebase("https://something.firebaseio.com");
var sync2 = $firebase(listingsref);
var products = sync2.$asArray();
var factory = {};
factory.getProducts = function(){
console.log(products);
return products;
};
factory.getProduct = function(productId){
for(var x = 0; x<products.length ;x++){
if(productId == products[x].id){
return {
product:products[x],
dataPlace:x
};
}
}
return {};
};
factory.getNextProduct = function(productId, e){
var currentProductPlace = factory.getProduct(productId).dataPlace;
if (e=="next" && currentProductPlace<products.length){
return products[currentProductPlace+1];
}
else if(e=="prev" && currentProductPlace>0){
return products[currentProductPlace-1];
}
else{
return {};
}
};
factory.componentToHex = function(c){
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
};
factory.rgbToHex = function(r,g,b){
return "#" + factory.componentToHex(r) + factory.componentToHex(g) + factory.componentToHex(b);
};
factory.hexToRgb = function(hex) {
if(hex.charAt(0)==="#"){
hex = hex.substr(1);
}
var bigint = parseInt(hex, 16);
var r = (bigint >> 16) & 255;
var g = (bigint >> 8) & 255;
var b = bigint & 255;
return r + ", " + g + ", " + b;
};
factory.parseRgb = function(rgb){
rgb = rgb.replace(/\s/g, '');
var red = parseInt(rgb.split(',')[0]);
var green = parseInt(rgb.split(',')[1]);
var blue = parseInt(rgb.split(',')[2]);
return {
r:red,
g:green,
b:blue
};
};
return factory;
};
marketFactory.$inject = ['$firebase'];
angular.module('marketApp').factory('marketFactory', marketFactory);
}());
This snippet gets a synchronized AngulareFire array of products:
var products = sync2.$asArray();
The AngularFire documentation is a bit off on this point: what you get back from $asArray() is not an array, but the promise of an array. At some point in the future your products variable will contain an array. This is done because it may take (quite) some time for your array data to be downloaded from Firebase. Instead of blocking your code/browser while the data is downloading, it returns a wrapper object (called a promise) and just continues.
Such a promise is good enough for AngularJS: if you simply bind products to the scope and ng-repeat over it, your view will show all products just fine. This is because AngularFire behind the scenes lets AngularJS know when the data is available and Angular then redraws the view.
But you said:
console.log($scope.products) in my controller should be Array[10]
That is where you're mistaken. While AngularFire ensures that its $asArray() promise works fine with AngularJS, it doesn't do the same for console.log. So your console.log code runs before the data has been downloaded from Firebase.
If you really must log the products, you should wait until the promise is resolved. You this this with the following construct:
products.$loaded().then(function(products) {
console.log(products);
});
When you code it like this snippet, the data for your products will have been downloaded by the time console.log runs.
Note that the object will still have extra helper methods on it, such as $add. That is normal and also valid on an array. See the documentation for FirebaseArray for more information on what the methods are, what they're for an how to use them.
So I edited the code in the plnkr at http://plnkr.co/M4PqojtgRhDqU475NoRY.
The main differences are the following:
// Add $FirebaseArray so we can extend the factory
var marketFactory = function($firebase, $FirebaseArray){
var listingsref = new Firebase("https://something.firebaseio.com");
// Actually extend the AngularFire factory and return the array
var MarketFactory = $FirebaseArray.$extendFactory(factory);
return function() {
var sync = $firebase(listingsref, {arrayFactory: factory});
return sync.$asArray();
};
Check out https://www.firebase.com/docs/web/libraries/angular/guide.html#section-extending-factories for more information on extending AngularFire entries. You will likely need to make some adjustments to the rest of the factory code.

Resources