Adding new object properties in ng-repeat for each object - angularjs

I'm fairly new with AngularJS so please excuse my ignorance and improper terminology.
I have a Controller called activitiesController that returns an array of activity objects. Each activity object has some properties which I can display in the view using ng-repeat="activity in activities".
One of the properties is a big string that I need to hack apart and essentially add to new properties of the activity object (I know this is horrible but it's from a legacy system).
Do I run a function in ng-repeat like ng-repeat="activity in getNewProperties(activities)" where I would loop through each activity and return the array back to ng-repeat.
OR
Do I just make a function in the scope that will return each property that I'm looking for.
Ideally in the view I would just use activity.newDetails.NewValue which makes me beielve i need to run my function on all of the activity objects before they are passed to ng-repeat in the view.
Sorry I know this isn't very details I can try to add more shortly.

Do I run a function in ng-repeat like ng-repeat="activity in getNewProperties(activities)" where I would loop through each activity and return the array back to ng-repeat.
Let's say you did this and the function looks something like this:
$scope.getNewProperties = function(activities) {
var modifiedActivities = [];
for (var i = 0; i < activities.length; i++) {
var modifiedActivity = activities[i];
modifiedActivity.foo = "bar";
modifiedActivities.push(modifiedActivity);
}
return modifiedActivities;
};
This function will run in every $digest cycle. Besides from being resource consuming, it also means that if you make changes to modified properties (in this case foo), the next time this function runs, the property will be overwritten by this function.
What you want to do is modify $scope.activities as soon as you get the data. This way it will only occur once unless you refresh the data. e.g.
$http.get("http://somewebsite/api").then(function(response) {
$scope.activities = response.data;
for (var i = 0; i < $scope.activities.length; i++) {
// modify properties
}
});

Related

AngularJS Filter for ng-repeat, how to get access to the filtered data

I have tried to make the title as descriptive as possible.
I have a set of data which I use ng-repeat to display in a table. There is also a filter applied so the user can type in a search bar and it will filter the results of the ng-repeat, there is nothing special about the filter (it is the default filter.
This obviously filters the data and shows it correctly.
The issue is, I also have a dropdown allowing the user to specify what sort of report they want.
The tablature data is raw, but they can select bar or line and it will show a graph. The data for the graph is created when they load the page. It takes the raw data and transforms it into labels and series.
What I would like to happen, is that when a filter is applied, the function transforms the filtered data, but I have no idea how I can actually get access to that data.
Does anyone know how I might go about it?
Update
I tried adding the filter to the controller and then doing my conversion in that function like this:
// Apply our filter
self.applyFilter = function () {
var filteredList = $filter('filter')(self.list, self.filter);
self.list = filteredList;
self.chartData = provider.getOverdueData(filteredList);
};
The problem with that, is that it is invoked many times and causes a $rootScope:infdig Infinite $digest Loop error.
Ok, so the error was caused by the new array which was being created each time applyFilter was invoked. I am not sure why an actual filter works (i.e. in the view) rather than in the function because they are doing the same thing ?!
But I was able to get around this issue by using angular.equals. First when I get the data, I had to store the result into a separate array, then when I invoke my filter I can use the unmodified array.
// Get our report
provider.overdueCollections().then(function (response) {
self.models = response;
self.list = response;
self.predicate = 'center';
self.chartData = provider.getOverdueData(response);
});
Once that was done, the table used self.list as it's datasource, but the applyFilter used the self.models to apply the filter to. It now looks like this:
// Apply our filter
self.applyFilter = function () {
// Create our filtered list
var filteredList = $filter('filter')(self.models, self.filter);
// Check to see if we are the same as the current list
var isSame = angular.equals(self.list, filteredList)
// If we are not the same
if (!isSame) {
// Update our scope
self.list = filteredList;
self.chartData = provider.getOverdueData(filteredList);
}
};
This meant that the data was only applied to the scope if it changed.

"10 $digest() iterations reached" when ng-repeat a list from a directive's controller function

I'm trying to create a directive that will display a given data. Displaying the data involves a simple parsing that I put in the directive's controller functions. When running this, I got the infamous 10 $digest() iteration reached error but can't understand why.
I've isolated everything into this very simple plnkr:
http://plnkr.co/edit/D8X9AmfDPdbvQDr4ENBR?p=preview
Can someone tell:
Why does it only fail when I use ng-repeat to iterate the getList() function results but not when I just print it?
Why does it only fail when I return an Array from getList() but not when I return an Object?
Why does it only fail when the list is returned from getList() but the same list, just static in the controller, works?
I must say I'm confused here...
Doing return [...]; creates a new array every time. Then ng-repeat thinks the watched expression has changed and trigges another digest cycle... which calls getList() again, returning a new array, triggering and so on.
I thought using track by in the ng-repeat expression would help, but it doesn't. It seems that track by can relate newer version of objects inside the array, but not changes to the array itself.
The only way is to make sure you return the same array reference every time. E.g.:
controller: function($scope) {
var list = [];
$scope.getList = function() {
return list;
};
$scope.fetchList = function() {
// You would need a way to fill the list. Can of course be done in
// the initializer, i.e. `var list = [{x:1, y:2}]`, but this is a trivial
// case; you probably want to call a service and fill the list.
...
};
$scope.removeFromList = function(item) {
// also remember *not* to change the reference when manipulating the list,
// e.g. removing items: do it in place with `splice()`, `push()` etc
var index = list.indexOf(item);
if( index >= 0 ) {
list.splice(index, 1);
}
};
}
Using track by has its own merits anyway, so consider it, but it is irrelevant to this problem.

Linking MVC In AngularJS

I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).

Sharing observable array reference between controllers in AngularJS

I am writing an AngularJS application in which one array (a list of active work orders) is used in many different controllers. This array is refreshed periodically using an $http call to the server. In order to share this information I've placed the array in a service.
The array in the service starts out empty (or, for debugging purposes, with dummy values ["A", "B", "C"]) and then queries the server to initialize the list. Unfortunately, all my controllers seem to be bound to the pre-queried version of the array -- in my application all I see are the dummy values I initialized the array with.
My goal is to bind the array into my controllers in such a way that Angular will realize that the array has been updated and cause my view to re-render when the array changes without my having to introduce a $watch or force my $http call to wait for results.
Question: How do I bind the wo_list array from my service to my controller so that Angular treats it like a regular observable part of the model?
I have:
A service that contains the array and a refresh function used to initialize and periodically update the array from the server (I know this is working by the console.log() message). For debugging purposes I'm initializing the array with the placeholders "A", "B", and "C" -- the real work orders are five digit strings.
angular.module('activeplant', []).
service('workOrderService', function($http) {
wo_list = ["A", "B", "C"]; //Dummy data, but this is what's bound in the controllers.
refreshList = function() {
$http.get('work_orders').success(function(data) {
wo_list = data;
console.log(wo_list) // shows wo_list correctly populated.
})
}
refreshList();
return {
wonums: wo_list, // I want to return an observable array here.
refresh: function() {
refreshList();
}
}
})
A controller that should bind to the array in workOrderService so that I can show a list of work orders. I'm binding both the service and the array returned by the service in two different attempts to get this to work.
function PlantCtrl($scope, $http, workOrderService) {
$scope.depts = []
$scope.lastUpdate = null
$scope.workorders = workOrderService
$scope.wonums = workOrderService.wonums // I want this to be observable
$scope.refresh = function() {
$scope.workorders.refresh()
$http.get('plant_status').success(function(data) {
$scope.depts = data;
$scope.lastUpdate = new Date()
});
}
$scope.refresh()
}
A view template that iterates over the bound array in the plant controller to print a list of work orders. I'm making two attempts to get this to work, the final version will only have the ul element once.
<div ng-controller="PlantCtrl">
<div style='float:left;background-color:red' width="20%">
<h2>Work Orders</h2>
<ul>
<li ng-repeat="wonum in workorders.wonums"> {{wonum}} </li>
</ul>
<ul>
<li ng-repeat="wonum in wonums"> {{wonum}} </li>
</ul>
</div>
</div>
Several other view / controller pairs, not shown here, that also bind and iterate over the array of work orders.
See if this solves your problem:
Instead of wo_list = data, populate the same array. When you assign a new data array to wo_list, you lose the bindings to the old array -- i.e., your controllers are probably still bound to the previous data array.
wo_list.length = 0
for(var i = 0; i < data.length; i++){
wo_list.push(data[i]);
}
See https://stackoverflow.com/a/12287364/215945 for more info.
Update: angular.copy() can/should be used instead:
angular.copy(data, wo_list);
When a destination is supplied to the copy() method, it will first delete destination's elements, and then copy in the new ones from source.
You can put the array which all the controllers use in a parent controller. Since scopes inherit from higher scopes, this'll mean that all the controllers have the same copy of the array. Since the array is a model of the higher scope, changing it will update the view, whenever it's used.
Example:
<div ng-controller="MainControllerWithYourArray">
<div ng-controller="PlantCtrl">
Wonums length: {{wonums.length}}
</div>
<div ng-controller="SecondCtrl">
Wonums length in other controller: {{wonums.length}}
</div>
</div>
You only need to define the wonums array in the MainController:
function MainControllerWithYourArray($scope, workOrderService){
$scope.wonums = workOrderService.wonums;
}
Hope this works for you!

Backbone.js behind the scenes

I read several articles about Backbone.js with sample apps but I can't find an explanation or example on how Backbone knows when a widget in a view is clicked and to which model it is bound.
Is it handled by internal assignment of IDs or something?
For example if you want to delete a div with id="123" could remove it from the DOM with jQuery or javascript functions. In backbone this div could be without the id but could be removed without knowing it, right?
If anybody knows a good article or could improve my understanding on that it would be great.
The way the view "knows" the model to which it's bound is done through the _configure method shown below:
_configure: function(options) {
if (this.options) options = _.extend({}, this.options, options);
for (var i = 0, l = viewOptions.length; i < l; i++) {
var attr = viewOptions[i];
if (options[attr]) this[attr] = options[attr];
}
this.options = options;
}
The import block to note is:
for (var i = 0, l = viewOptions.length; i < l; i++) {
var attr = viewOptions[i];
if (options[attr]) this[attr] = options[attr];
}
viewOptions is an array of keys that have "special" meaning to a view. Here's the array:
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
This loop is the "glue" between view and model or view and collection. If they're present in the options, they're assigned automatically.
All this is in the annotated source code.
Check http://www.joezimjs.com/javascript/introduction-to-backbone-js-part-1-models-video-tutorial/.
Even if it looks complicated, there's so little to learn, trust me.
If you ask more specifically I could try to help.
Reading the source is probably your best bet for improving your understanding. The Backbone function you want to look at is called delegateEvents. But the short version is that it uses the jQuery delegate() function. The root element is the View's element (the el property), and it's filtered by whatever selector you provided.
jQuery doesn't actually bind a handler to each element that you're listening to. Instead it lets the events bubble up to the root element and inspects them there. Since there's nothing attached to each individual element you can delete them freely without causing any problems. However some methods of deleting the View's element (eg, by setting innerHTML on a parent element) might cause a memory leak. I'm not 100% sure about that, but it's probably best to just not do that anyway.

Resources