Angularjs reuse calculated values - angularjs

I need to use a calculated value to hide elements and do other things in the template.
<button ng-hide="expensive()" ng-click="foo()">foo</button>
<button ng-show="expensive() && otherFunction()" ng-click="bar()">bar</button>
<span ng-show="expensive()">Bas</span>
This causes the excecution of expensive() multiple times for each $digest cycle.
I want to reuse its result, but it needs to be executed for each digest - just not multiple times per digest.
Is there any best practice for reuse function results that needs to be re-calculated each digest?
* update *
This function works with a huge object and its properties can be changed with a lot of input fields and sub-formulars on the page. It has multiple one to many relations. If I have to add events / ngChanges to each field and if I miss only one, this will not work correcly.

You didn't really give us enough information to give you the best option for your situation.
Very generally speaking, the best option is to have whatever interactions that cause the return value of expensive() to change update a $scope.property and just use that in your view.
In other words, don't use functions on scope in your view unless you're setting up a binding like ng-click or something. Instead, update properties on your scope whenever they need updating and just reference them directly.
Caution: This might tempt you to use $watch... don't do that. There are cheaper, more effective ways to trigger updates, like ngChange, or other such events.

You have a few options:
make expensive() a $q promise. Angular templates understand $q promises and resolve them accordingly
store the value when running the function, if the value exists return it first thing in the function. You can add a flush param to it in case you want to flush the cached value.
If the values truly never change once computed, you could put them in a resolve
If these values are the result of some DOM work, create a directive to do this and $emit actual changes to the parent scope.

Try this.
<button ng-hide="result" ng-click="foo()">foo</button>
<button ng-show="result && otherFunction()" ng-click="bar()">bar</button>
<span ng-show="expensive()">Bas</span>
Controller:
$scope.expensive = function() {
... do stuff ...
$scope.result = ...;
return $scope.result;
}

Related

Switching the data, using the same Controller AngularJS

I was wondering how to change the data, but keep the same controller in my angular app. Basically I will have a list of activities (restaurants, parks etc...) when I click on one of these activities, The view will display all the restaurant, and same thing for the parks. I know how to do that, but I would need to create a park_ctrl and a restaurant_ctrl, and since the data will be formatted the exact same way. I just wanted to know if I could use only one controller and just change the data that it receives when I click on those buttons.
I hope my question makes sense.
logic around retrieving data should be the responsibility of services, so I guess you'd just call a different service in the different cases, from the same controller
I think it's not a really good idea, but opinion based.
You can make a function :
function($scope){
$scope.changePage = function (type) {
if(type==="park"){
$scope.parks = asynLoadFunctionToGetParks();
}else{
if(type === "restaurants"){
/* same as below */
}
}
};
}
And changing the type in your view with :
<button ng-click="changePage('parks')">Parks</button>
<button ng-click="changePage('restaurants')">Restaurants</button>
<div ng-if="type==='park'">
{{parks}}
</div>
<div ng-if="type==='restaurants'">
{{restaurants}}
</div>
I think the issue here is that most of the Angular examples available are of the "hello world" variety and so they show retrieving data directly from the Controller. The problem is that AngularJS out of the box doesn't really have a business logic layer itself, and I think most people who have added such a layer are too busy to be putting up examples.
The way I'd do this is to create a "master" service that can get all of the different data types either up front in the Run block or lazily as the user navigates the app, depending on your needs. Then I'd supply a reference to the applicable sub-collection in the route resolution (resolve property) or the isolate scope in the case of a directive.
Alternatively, the controller can ask for the data by calling masterService.getCollection($scope.collectionName) or something like that, but if you do that you run into the issue that masterService may not yet have that particular collection yet and then you have to clutter up your controller with all the promise resolution stuff as if it were a Controller's responsibility to handle that.
You could avoid that by binding to masterService.collections[$scope.collectionName] in the View, which would leave the Controller only exposing the collection on the $scope or controllerAs variable and the masterService still responsible for retrieving the data and making it available.
Yes you can. Just use different service and a common variable in the scope.
if (something) {
$scope.data = restaurantsService.get();
} else {
$scope.data = parksService.get();
}

ui-bootstrap pagination with filter

after some research and study of examples I implemented a pagniation with a filter function.
Im very new to angular, so I need your help if this application is ok or it has some bugs/logical errors.
The target is to select a collection (in this application load1 or load2) and create new objects, manipulate existing, or delete some of them. On every update of the data, it has to be checked if the pagination is synchronous to the collection size.
If the user enters something into the search field, a watcher in the controller is fired for updating the filtered data:
$scope.$watch('search.name', function (newVal, oldVal) {
$scope.filtered = filterFilter($scope.items, {name: newVal});
}, true);
I would be very happy if some of you angular pros can look into this code and give me some feedback. I want to use this in a productive system, so every answer would be great!
Here is a working plunkr: http://plnkr.co/edit/j9DVahEm7y1j5MfsRk1F?p=preview
Thank you!
Watchers are heavy if you use them explicitly throughout your large application.
Use ng-change instead. Also, by passing true to that watcher means you're deep watching which is really a bad thing to do, since it will check each property of the object in the array which is performance intensive.
Since I can't see that you need old and new value for a reason, you can simply use $scope.search.name. Whenever you type in something, $scope.search.name has the updated value. Just need to call a function on ng-change.
DEMO: http://plnkr.co/edit/TWjEoM3oPdfrHfcru7LH?p=preview
Remove watch and use:
$scope.updateSearch = function () {
$scope.filtered = filterFilter($scope.items, {name: $scope.search.name});
};
In HTML:
<label>Search:</label> <input type="text" ng-model="search.name" placeholder="Search" ng-change="updateSearch()" />
Previous answer is still the correct, but you will have to make sure to replace the "page" inside the pagination tag and change it to ng-model.
From the changelog (https://github.com/angular-ui/bootstrap/blob/master/CHANGELOG.md)
Since 0.11.0:
Both pagination and pager are now integrated with ngModelController.
page is replaced from ng-model.

AngularJS: Force ng-repeat update when property of an element changes

I'm wrestling with the way angular watches arrays. I have the following markup:
<map name="theMap" center="{{mapInfo.center.toUrlValue()}}" zoom="{{mapInfo.zoom}}" on-dragend="dragEnd()" on-zoom_changed="zoomChanged()">
<marker ng-repeat="pin in pins() track by pin.iconKey()" position="{{pin.latitude}}, {{pin.longitude}}" title="{{pin.streetAddress}}" pinindex="{{$index}}" on-click="click()"
icon="{{pin.icon()}}"></marker>
</map>
Each individual pin returned by pins() has a number of properties, sub-properties, etc. One of those sub-properties controls the marker color. When that subproperty changes I want the UI to update.
Because ng-repeat appears to $watch based on simply changes to the collection it's not obvious how to achieve that. I thought that my tracking function, iconKey(), would do it because it returns a different value depending upon the subproperty's value. But that didn't work.
One other thing: the subproperty gets changed in the callback from an $interval that runs under a directive. I mention this because, in an earlier post, someone thought that there might be a context/scope problem.
However, even when I make the change in an event listener within the UI's controller (where the event is raised within the "success" clause of the $interval callback) it still doesn't work.
That's why I think the problem is just angular not noticing the change in iconKey(); it's acting like all it $watches for ng-repeat is the array's length. Which doesn't change when the subproperty changes.
Update
I've created a plunker to demonstrate the issue I'm facing. You can find it at http://plnkr.co/edit/50idft4qaxqw1CduYkOd
It's a cut down version of the app I'm building, but it contains the essential elements (e.g., a data context service to hold information about the map pins and an $interval service to toggle the subproperty of one of the pin array elements).
You start the update cycle by clicking Start in the menu bar (you may want to drag the map down slightly to put both pins into full view). It should toggle the color of each pin, alternatively, 5 times each, once every 2 seconds. It does this by toggling the value of the isDirty property of the pin object in a listener defined in the controller. The event is raised by the $interval service.
If you break on line 22 during the test cycle you'll see the pin's icon being returned. So something within angular is calling for the information...but the pin color doesn't change.
I look forward to someone quickly pointing out the bone-headed mistake that has nothing to do with any of my theories :). Apologies in advance for whatever blinders I'm wearing.
Update 2
After checking out the code snippet in the response I simplified my plnkr and demonstrated that angular is, in fact, updating the UI when a subproperty changes. So this appears to be a limitation or bug in ng-map.
What you are missing here is the concept of array and function your function pins() passes an array and that array is been bound with ng-repeat. But the brutal fact here is that no matter what that array is never changed, because you do not have ANY reference to that array hence the rg-repeat will always remain as is...
I'll suggest to try get the array be referenced two ways to do that
ng-init="pinsArray = pins()"
and second inside controller
$scope.pinsArray = $scope.pins()
then make changes to $scope.pinsArray inside controller
ng-repeat will be changed to
ng-repeat="pin in pinsArray"
also read about filters I am guessing that's what you where trying to do with "track by"
hope this helps..
Edit: different story with ngMap markers
seems like it doesn't watch sub-property.
so here's a work around
add following statement to you update the pinsArray after making changes to its properties.
pinsArray = angular.copy(pinsArray);
the solved plnkr example:
http://plnkr.co/edit/EnW1RjE9v47nDpynAZLK?p=preview

Need explanation on a angular direcive load

I just want to understand why in the following jsFiddle 'here is a lo' is printed three times.
http://jsfiddle.net/wg385a1h/5/
$scope.getLog = function () {
console.log('here is a log');
}
Can someone explain me why ? What should I change to have only one log "here is a log" (that's what I would like this fiddle do). Thanks a lot.
Angular uses digest cycles/iterations to determine when state has changed and needs to update the UI. If it finds any change on one of it's cycles, it keeps rerunning cycles until the data stabilizes itself. If it's done 10 cycles and the data is still changing, you'll see a rather know message: "angularjs 10 iterations reached. aborting".
Therefor, The fact that you are seeing the message displayed 3 times is because you have a simple interface. In fact, you can get up to many more such messages in the log, due to the fact that your directive uses {{getLog()}}. Angular keeps evaluating the expression to see if it changed.
To avoid such problems, under normal circumstances, you should store the value returned by the function you want called only once in the $scope object inside the controller and use that variable (not the function call) in the UI.
So in the controller you'd have $scope.log = getLog() [assuming it returns something, and not just writing to the console] and in the directive use the template {{log}}. This way, you'll get the value only once, per controller instance.
Hope I was clear enough.

Having a set of checkboxes map to a nested array

I am working on a SPA that pulls in customer data from one $resource call, and gets some generic preference data from another $resource call.
The preference data is sent as an array, which I want to use to populate a series of checkboxes, like so:
<div ng-repeat="pref in fieldMappings.mealPrefs">
<input type="checkbox"
id="pref_{{$index}}"
ng-model="customer.mealPrefs"
ng-true-value="{{pref.name}}" />
<label class="checkbox-label">{{pref.name}}</label>
</div>
When a user clicks one or more checkboxes, I want the values represented in that array of checkboxes to be mapped to an array nested inside a customer object, like so:
.controller( 'AppCtrl', function ( $scope, titleService, AccountDataService ) {
// this is actually loaded via $resource call in real app
$scope.customer = {
"name": "Bob",
"mealPrefs":["1", "3"]
};
// this is actually loaded via $resource call in real app
$scope.fieldMappings.mealPrefs = [
{'id':"1", 'name':"Meat"},
{'id':"2", 'name':"Veggies"},
{'id':"3", 'name':"Fruit"},
{'id':"4", 'name':"None"}
];
});
I have tried setting up ng-click events to kick off functions in the controller to manually handle the logic of filling the correct part of the customer object model, and $watches to do the same. While I have had some success there, I have around 2 dozen different checkbox groups that need to be handled somehow (the actual SPA is huge), and I would love to implement this functionality in a way that is very clean and repeatable, without duplicating lots of click handlers and setting up lots of $watches on temporary arrays of values. Anyone in the community already solved this in a way that they feel is pretty 'best practice'?
I apologize if this is a repeat - I've looked at about a dozen or more SO answers around angular checkboxes, and have not found one that is pulling values from one object model, and stuffing them in another. Any help would be appreciated.
On a side-note, I'm very new to plunkr (http://plnkr.co/edit/xDjkY3i0pI010Em0Fi1L?p=preview) - I tried setting up an example to make it easier for folks answer my question, but can't get that working. If anyone wants to weigh in on that, I'll set up a second question and I'll accept that answer as well! :)
Here is a JSFiddle I put together that shows what you want to do. http://jsfiddle.net/zargyle/t7kr8/
It uses a directive, and a copy of the object to display if changes were made.
I would use a directive for the checkbox. You can set the customer.mealPrefs from the directive. In the checkbox directive's link function, bind to the "change" event and call a function that iterates over the customer's mealPrefs array and either adds or removes the id of the checkbox that is being changed.
I took your code and wrote this example: http://plnkr.co/edit/nV4fQq?p=preview

Resources