Scope property which depends on another scope property? - angularjs

I'm just starting out with Angular and could use a hand re: binding to scopes.
I'm trying to load in data which populates a property.
I then want to update other properties based off this property, is there any way to trigger this easily?
At the moment I'm just doing:
Load JSON data
Set mythings
Trigger calculation of mythings_processed
I feel like I should be able to trigger (3) with (2)?
Example
Javascript
app.controller('MyCtrl', function($scope, $http) {
$scope.loadData = function () {
$http.get('/api/orgs/gocardless/issues.json').success(function(data) {
$scope.mythings = data.things;
$scope.generateCalculatedItems() // Surely this isn't necessary?
});
};
$scope.generateCalculatedItems = function () {
mythings_processed = {}; // Generated using mythings
$scope.mythings_processed = mythings_processed;
};
};
HTML
<div ng-controller="IssuesCtrl" ng-init="loadData()">
<ul>
<li ng-repeat="thing in things">{{thing.id}}</li>
</ul>
<ul>
<li ng-repeat="processed_thing in mythings_processed">{{processed_thing.id}}</li>
</ul>
</div>
I'm sure I'm doing something horrible to Angular here so I apologise!
I did feel like I should be able to do:
$scope.mythingsProcessed = function () {
mythings_processed = {}; // Generated using mythings
return mythings_processed;
};
<ul>
<li ng-repeat="processed_thing in mythingsProcessed()">{{processed_thing.id}}</li>
</ul>
But when I did, it gave me the error: 10 $digest() iterations reached. Aborting!
Any ideas?
UPDATE: Example data/use case
I have a list of items fetched via ajax:
{
items: [
{name: 'pete', value: 1},
{name: 'john', value: 2},
{name: 'pete', value: 3},
{name: 'steve', value: 2},
{name: 'john', value: 1}
]
}
I want to display all of these, but I also want to process (sum) them into:
{
processed_items: [
{ name: 'pete', value: 4 }
{ name: 'john', value: 3 }
{ name: 'steve', value: 2 }
}
and then display these in a list as well.

I am not 100% sure what you are trying to acheive. But to trigger (3) with (2) you could use the watch function of the $scope object:
app.controller('MyCtrl', function($scope, $http) {
$scope.loadData = function () {
$http.get('/api/orgs/gocardless/issues.json').success(function(data) {
$scope.mythings = data.things;
});
};
scope.$watch('mythings', function(newValue, oldValue) {
$scope.generateCalculatedItems();
}
$scope.generateCalculatedItems = function () {
mythings_processed = {}; // Generated using mythings
$scope.mythings_processed = mythings_processed;
};
};
To bind mythings to mythings_processed you have to do the binding manually via the $watch-function in AngularJS.

With your updated example I think the focus of your question shifted from the problem with the binding to how you could process the incoming data. Is that correct?
I made a plunker with a possible solution:
http://plnkr.co/edit/BMNmE5
I would process the data as soon as the success method is called. And not do a watch in this case.
$scope.loadData = function() {
$http.get('data.json').success(function(data) {
$scope.mythings = data;
$scope.mythingsProcessed = processThings(data);
});
};
Then the function processThings makes the sum as you want them (it uses the underscore.js library):
var processThings = function(things) {
var result = _.reduce(
things,
function(result, obj) {
var storedObject = _.findWhere(result, {'name': obj.name});
if(storedObject) {
storedObject.value += obj.value;
return result;
}
else {
return _.union(result, _.clone(obj));
}
},
[]);
return result;
};

Related

how to test inner controller which loads data for select control in angular-formly

I have a ui-select field
{
key: 'data_id',
type: 'ui-select',
templateOptions: {
required: true,
label: 'Select label',
options: [],
valueProp: 'id',
labelProp: 'name'
},
controller: function($scope, DataService) {
DataService.getSelectData().then(function(response) {
$scope.to.options = response.data;
});
}
}
How can I access that inner controller in my unit tests and check that data loading for the select field actually works ?
UPDATE:
An example of a test could be as such:
var initializePageController = function() {
return $controller('PageCtrl', {
'$state': $state,
'$stateParams': $stateParams
});
};
var initializeSelectController = function(selectElement) {
return $controller(selectElement.controller, {
'$scope': $scope
});
};
Then test case looks like:
it('should be able to get list of data....', function() {
$scope.to = {};
var vm = initializePageController();
$httpBackend.expectGET(/\/api\/v1\/data...../).respond([
{id: 1, name: 'Data 1'},
{id: 2, name: 'Data 2'}
]);
initializeSelectController(vm.fields[1]);
$httpBackend.flush();
expect($scope.to.options.length).to.equal(2);
});
You could do it a few ways. One option would be to test the controller that contains this configuration. So, if you have the field configuration set to $scope.fields like so:
$scope.fields = [ { /* your field config you have above */ } ];
Then in your test you could do something like:
$controller($scope.fields[0].controller, { mockScope, mockDataService });
Then do your assertions.
I recently wrote some test for a type that uses ui-select. I actually create a formly-form and then run the tests there. I use the following helpers
function compileFormlyForm(){
var html = '<formly-form model="model" fields="fields"></formly-form>';
var element = compile(html)(scope, function (clonedElement) {
sandboxEl.html(clonedElement);
});
scope.$digest();
timeout.flush();
return element;
}
function getSelectController(fieldElement){
return fieldElement.find('.ui-select-container').controller('uiSelect');
}
function getSelectMultipleController(fieldElement){
return fieldElement.find('.ui-select-container').scope().$selectMultiple;
}
function triggerEntry(selectController, inputStr) {
selectController.search = inputStr;
scope.$digest();
try {
timeout.flush();
} catch(exception){
// there is no way to flush and not throw errors if there is nothing to flush.
}
}
// accepts either an element or a select controller
function triggerShowOptions(select){
var selectController = select;
if(angular.isElement(select)){
selectController = getSelectController(select);
}
selectController.activate();
scope.$digest();
}
An example of one of the tests
it('should call typeaheadMethod when the input value changes', function(){
scope.fields = [
{
key: 'selectOneThing',
type: 'singleSelect'
},
{
key: 'selectManyThings',
type: 'multipleSelect'
}
];
scope.model = {};
var formlyForm = compileFormlyForm();
var selects = formlyForm.find('.formly-field');
var singleSelectCtrl = getSelectController(selects.eq(0));
triggerEntry(singleSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(1);
var multiSelectCtrl = getSelectController(selects.eq(1));
triggerEntry(multiSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(2);
});

Issue with modifying objects that are added by Angular modal controller

I'm having issue with modifying objects that are adding through angular modal controller
I have
.controller("viewController", function($scope, $modal) {
$scope.allPosts = [
{
id: 1,
owner: "Owner 2",
profile: "images/profile.png",
title: "Book title 1",
image: null,
price: 25,
reply: 2,
fav: 1,
isFaved: false,
content: "test"
},
{
id: 2,
owner: "Owner",
profile: "images/profile2.png",
title: "Ken Follett",
image: "images/book1.jpg",
price: 20,
reply: 12,
fav: 3,
isFaved: true,
content: "The book is in nice"
}
];
$scope.addFav = function(id) {
_.each($scope.allPosts, function(post) {
if(post.id === id) {
post.isFaved = !post.isFaved;
if(post.isFaved) {
post.fav++;
$scope.myFavs.push(post);
} else {
post.fav--;
$scope.myFavs = _.reject($scope.myFavs, function(post) {
return post.id === id;
});
}
}
});
};
$scope.addPost = function() {
var modalInstance = $modal.open({
templateUrl: 'myModalContent.html',
controller: 'ModalInstanceCtrl',
resolve: {
allPosts: function(){
return $scope.allPosts;
}
}
});
};
)
.controller('ModalInstanceCtrl', function ($scope, $modalInstance, allPosts) {
$scope.postId = 50;
$scope.ok = function () {
var temp = {};
temp.id = $scope.postId;
temp.profile = "images/profile.png";
temp.title = $scope.title;
temp.type = $scope.type;
temp.price = $scope.price;
temp.reply = 0;
temp.fav = 0;
temp.isFaved = false;
temp.content = $scope.description;
$scope.allPosts.push(temp);
$scope.postId++;
$modalInstance.close();
};
});
$scope.addFav(id) function works fine with existing $scope.allPosts. However, when I add new object by using the ModalInstanceCtrl, the $scope.allPosts is updated but when it goes to $scope.addFav(id), I can not modified the new object that is pushed in to $scope.allPosts from ModalInstanceCtrl. for example I try to update the fav property in post by using
post.fav++; // console.log(post) shows the fav property is not updated. it remains at 0.
As you don't show the markup I suspect that the ModalInstanceController must be nested within the scope of the viewController. This would explain how the same allPosts is available in both controllers. However the postId will be different on each scope due to the way that javascript's prototypical inheritance works. To overcome this you could define an object on scope something like this:
$scope.posts = {
postId: 0,
allPosts: []
}
Alternatively, and even better imho, define a Posts service that encapsulates all the post behaviours and inject that into both controllers. You are then insulated from any changes to the markup that could muck up the controller inheritance.

ng-repeat with checkboxes and capture value

Still new to angular and js. I have a list:
<ul ng-repeat="rate in resources">
<li> <input type="checkbox" ng-model="isSelected" ng-change="appendList(rate)"> </li>
</ul>
When a user clicks on them, i'd like the value to be pushed to a list:
$scope.rateValues = {
'rates': {
'fastrates': {
'value': '',
}
}
};
But I'd like to append a key, value pair to the fastrates like this:
$scope.rateValues = {
'rates': {
'fastrates': {
'value': '100',
'value': '200',
'value': '300',
'value': '400',
}
}
};
I guess i'm stuck on my ng-change function in my controller to handle this. I know this is far from complete but at least the value changes. I need to make it check if it has been added and if so then remove it from the key, value from the object. If it's unchecked and they check it. It needs to create a new 'value': rate pair in the fastrates obj.
$scope.appendList = function(rate){
$scope.rateValues.rates.fastrates.value = rate
}
Here's a plnkr I started http://plnkr.co/edit/MhqEp7skAdejdBRpXx2n?p=info. Any scripting advice on how to accomplish this?
You can't use same key multiple times. You can use array of object.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.resources = {value:['100','200','300', '400']}
$scope.rateValues = {
'rates': {
'fastrates': []
}
};
$scope.appendList = function(rate){
$scope.rateValues.rates.fastrates.push({ value: rate });
}
});
Don't forget to remove value when you uncheck the checkbox.
http://plnkr.co/edit/MhqEp7skAdejdBRpXx2n?p=preview removing value from array when you uncheck checkbox
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.resources = {
value: ['100', '200', '300', '400']
}
$scope.rateValues = {
'rates': {
'fastrates': {
'value': [],
}
}
};
$scope.appendList = function(rate) {
var index = $scope.rateValues.rates.fastrates.value.indexOf(rate);
if (index < 0) {
$scope.rateValues.rates.fastrates.value.push(rate);
} else {
$scope.rateValues.rates.fastrates.value.splice(index, 1);
}
}
});

$watch not finding changes on property in array of objects on new item in angular

I have an array of objects like this...
[{ name: 'foo', price: 9.99, qty: 1 }]
The UI allows for new items to be added to this array. I'm trying to listen for those new items to be added AND for changes on the qty property of each item.
$scope.order = [];
$scope.totalItems = 0;
$scope.$watchCollection('order', function() {
$scope.totalItems = $scope.order.reduce(function(memo, o) {
return memo + o.qty;
}, 0);
});
$scope.addItem = function() {
$scope.order.push({
name: $scope.item.name,
qty: 1,
options: []
});
};
As you can see from this... http://d.pr/v/MJzP The function fires when a new item is added, but NOT when the qty changes on that new item.
Maybe you should attach an ngChange to your quantity input box? Like this
<input ng-model="item.qty" ng-change="calculateTotal()" type="number"/>
Then in your controller:
$scope.calculateTotal = function() {
$scope.totalItems = $scope.order.reduce(function(memo, o) {
return memo + o.qty;
}, 0);
};
Then at the end of your add item function:
$scope.addItem = function() {
// Your logic
$scope.calculateTotal();
};
EDIT: After thinking about this more, the ngChange might automatically be invoked when the addItem function is called so calling it from addItem might not be entirely necessary

Single Controller for multiple html section and data from ajax request angularjs

I'm trying to show two section of my html page with same json data, i don't want to wrap both in same controller as it is positioned in different areas. I have implemented that concept successfully by using local json data in "angular service" see the demo
<div ng-app="testApp">
<div ng-controller="nameCtrl">
Add New
Remove First
<ul id="first" class="navigation">
<li ng-repeat="myname in mynames">{{myname.name}}</li>
</ul>
</div>
<div>
Lot of things in between
</div>
<ul id="second" class="popup" ng-controller="nameCtrl">
<li ng-repeat="myname in mynames">{{myname.name}}</li>
</ul>
JS
var testApp = angular.module('testApp', []);
testApp.service('nameService', function($http) {
var me = this;
me.mynames = [
{
"name": "Funny1"
},
{
"name": "Funny2"
},
{
"name": "Funny3"
},
{
"name": "Funny4"
}
];
//How to do
/*this.getNavTools = function(){
return $http.get('http://localhost/data/name.json').then(function(result) {
me.mynames = result.mynames;
return result.data;
});
};*/
this.addName = function() {
me.mynames.push({
"name": "New Name"
});
};
this.removeName = function() {
me.mynames.pop();
};
});
testApp.controller('nameCtrl', function ($scope, nameService) {
$scope.mynames = nameService.mynames;
$scope.$watch(
function(){ return nameService },
function(newVal) {
$scope.mynames = newVal.mynames;
}
)
$scope.addName = function() {
nameService.addName();
}
$scope.removeName = function() {
nameService.removeName();
}
});
jsfiddle
Next thing i want to do is to make a http request to json file and load my two section with data, and if i add or remove it should reflect in both areas.
Any pointers or exisisitng demo will be much helpful.
Thanks
The reason why only one ngRepeat is updating is because they are bound to two different arrays.
How could it happen? It's because that you have called getNavTools() twice, and in each call, you have replaced mynames with a new array! Eventually, the addName() and removeName() are working on the last assigned array of mynames, so you're seeing the problem.
I have the fix for you:
testApp.service('nameService', function($http) {
var me = this;
me.mynames = []; // me.mynames should not be replaced by new result
this.getNavTools = function(){
return $http.post('/echo/json/', { data: data }).then(function(result) {
var myname_json = JSON.parse(result.config.data.data.json);
angular.copy(myname_json, me.mynames); // update mynames, not replace it
return me.mynames;
});
};
this.addName = function() {
me.mynames.push({
"name": "New Name"
});
};
this.removeName = function() {
me.mynames.pop();
};
});
testApp.controller('nameCtrl', function ($scope, nameService) {
// $scope.mynames = nameService.mynames; // remove, not needed
nameService.getNavTools().then(function() {
$scope.mynames = nameService.mynames;
});
/* Remove, not needed
$scope.$watch(
function(){ return nameService },
function(newVal) {
$scope.mynames = newVal.mynames;
}
);
*/
$scope.addName = function() {
nameService.addName();
};
$scope.removeName = function() {
nameService.removeName();
};
});
http://jsfiddle.net/z6fEf/9/
What you can do is to put the data in a parent scope (maybe in $rootScope) it will trigger the both views ,And you don't need to $watch here..
$rootScope.mynames = nameService.mynames;
See the jsFiddle

Resources