Sharing observable array reference between controllers in AngularJS - 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!

Related

Adding new object properties in ng-repeat for each object

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
}
});

Initializing an array at return of an ajax request

Trying to learn angular I got the following situation.
My application returns product objects from a complex and slow database using ASP.NET Web Api. Because of the slow speed i wanna page the objects returned from the server. I created a viewmodel containing an array of products, the current page and the last page of the products.
For displaying a paging widget I wanna fill my $scope object with an array of pagenumbers for use with the ng-repeat directive.
I tried filling an array using a function:
(in pseudo)
$scope.pages = function(){
var pageNumberArray = [];
pagecounter = 1;
while (pagecounter <= maxPage){
pageNumberArray.push(pageCounter);
pageCounter++;
}
};
with in my view an ng-repeat using this function:
<ul>
<li ng-repeat="pageNr in pages()">{{pageNr}}</li>
</ul>
It seems to work for displaying but my developer tools console shows errors which seem legit: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
How can I initialize the $scope array (field) without calling a $scope function from the view.
p.s. the array should reinitialize when an ajax request returns a new page (not all page numbers are shown (5 beneath, 5 above the current page).
I think the two-way binding is disturbed by using a function for your ng-repeat.
I would say $scope.pages should be a variable in your scope. If you later update this variable, your view will be automatically updated by the grace of two-way binding.
So something like this:
in controller:
$scope.pages = []
for (var i; i < maxPage; i += 1) {
$scope.pages.push(i);
}
$http.get(...).success(data){
// update $scope.pages
}
In view:
<div ng-repeat="page in pages">{{page}}</div>

Angular ng-repeat with polling data provider

I am just reading about Angular now.
Let's say ng-repeat binding is in effect, and child records appear as <li> elements beneath the <ul>.
Now let's say new objects are pushed onto a watched array (watched with watchCollection) in the model every 60 seconds by a polling data provider (i.e. the existing data in the array don't change).
Will Angular append new counterpart <li> elements to the <ul> or completely recreate the <ul> when it detects the array has additional elements?
EDIT: #Blackhole has asked, "Why ask...?", so I'll elaborate with a more complicated hypothetical that matches pretty well my actual app, which I'm thinking of rewriting to learn Angular.
Imagine you have to fetch the manifest for an immensely long freight train and display for your users the contents of each freight car. You don't want the user to wait and wait and wait, and then suddenly see the entire freight train manifest. Instead, you ask the server for a list of Car Ids and fetch each car's manifest in turn, and in the success callback, you pop another CarId off the array, and fetch the next car's manifest, until the array of Car Ids is empty. You expect the UI to add the current car's manifest to the page immediately, as the next car's manifest is being fetched.
After a quick test, I can confirm that when pushing new items to a bound array, the former DOM elements are not recreated.
To prove it, I created a simple array and bound it to a ng-repeat, then I created a Jquery method that changes the DOM elements, note that when you push a new element to array (using angular) the elements that were changed using the jquery method:
app.controller('MainCtrl', function($scope) {
$scope.items = [];
$scope.pushItems = function(){
var newItem = {description: 'Item description ' + $scope.items.length};
$scope.items.push(newItem);
}
$scope.newArray = function(){
var newArray = [{description: 'First item of new array'}]
$scope.items = newArray;
}
$scope.reassign = function(){
var newArray = angular.copy($scope.items);
$scope.items = newArray;
}
});
$( document ).ready(function() {
$("#domchanger").click(function(){
$(".mylist li").html('changed');
});
});
Here is the plunkr with that demonstration: http://plnkr.co/edit/2ScX4ZJJdOrlqWGtivar?p=preview

AngularJS InfDig error (infinite loop) with ng-repeat function that returns array of objects

Here's my code:
<h1 ng-repeat="item in func()">something</h1>
$scope.func = function(){
return [{"property" : "value1"},{"property": "value2"}];
}
In Angular.js v. 1.1.1 there's no mistake. In Angular.JS v 1.2.1 I get an infDig mistake.
Fiddle of v.1.1.1
Fiddle of v.1.2.1
Could you explain this situation? Thanks a lot.
As of AngularJS 1.2: The "track by" expression was added to ng-repeat and more appropriately addresses this issue as demonstrated
in the following code.
<h1 ng-repeat="item in func() track by $index">something</h1>
$scope.func = function(){
return [{"property" : "value1"},{"property": "value2"}];
}
The following article helps understand the expression in more detail and why it is so useful, particularly when dealing with $$haskey Using Track-By With ngRepeat In AngularJS 1.2 by Ben Nadal.
The problem is that you're creating a new array each time, so it's something new that angular needs to track. As far as I can tell, ng-repeat runs, then immediately checks its collection again to see if anything changed in that cycle. Because the function returns a new array, that is perceived as a change.
Check this out: http://jsfiddle.net/kL5YZ/.
If you look in the console.log and click the button, you will see that the $$hashKey property of the objects is being changed each time ng-repeat runs.
The change occurs starting at version 1.1.4, but the changelog doesn't give any clues as to why the behavior is different. The new behavior does make more sense to me.
Here's a great post I found explaining the current behavior in depth: How to Loop through items returned by a function with ng-repeat?
If you make sure to return the same object/array each time, you won't have the error. You could have the function cache anything it creates based on the arguments and always return the same array/object when those arguments are passed in. So, myFunc('foo') will always return the same array, not a new one that looks the same. See the notes in my code below. Live demo (click).
<div ng-repeat="foo in foos">
<div ng-repeat="bar in barFunc(foo)">{{bar.text}}</div>
<div ng-repeat="bar in barFunc('test')">{{bar.text}}</div>
</div>
JavaScript:
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, myService) {
$scope.foos = [
'a','b','c'
];
//I put this into a service to avoid cluttering the controller
$scope.barFunc = myService.getObj;
});
app.factory('myService', function() {
/*
* anything created will be stored in this cache object,
* based on the arguments passed to `getObj`.
* If you need multiple arguments, you could form them into a string,
* and use that as the cache key
* since there's only one argument here, I'll just use that
*/
var cache = {};
var myService = {
getObj : function(val) {
//if we haven't created an array with this argument before
if (!cache[val]) {
//create one and store it in the cache with that argument as the key
cache[val] = [
{text:val}
];
}
//return the cached array
return cache[val];
}
};
return myService;
});

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).

Resources