AngularJS view doesn't update when assigning a $http response to $scope - javascript

I have a ng-repeat loop for a number of values. While looping, I fetch a variable from another array of values and use that:
<div ng-repeat="i in [1,2,3]">
<div ng-init="obj = getObject(i)">
<pre>{{ obj }} </pre>
</div>
</div>
My goal is now to change a property of that variable and do a POST request containing the updated variable. The response for that from the server then contains all values, which I bind to $scope in order to update the view.
<a ng-click="change(obj, 5)">Set property to 5</a>
$scope.change = function(o, value) {
o.prop = value;
// save() sends a POST requests and returns a JSON with all values
$scope.values = save(o);
}
This works, but only the first time I do it. All other changes will be reflected in the $scope.variables, but not in the {{ obj }} variables in the template. $scope.$apply() has no effect, either.
I've created a JS Fiddle to show my problem, which only mocks the HTTP requests. However, I have found that even when I run this code against my REST backend, the first time everything works fine but every time after that reflects no changes at all.

I think the issue is caused because you are using ng-init, which probably sets a non-changing value since you are calling a function. It will work once you change {{ obj }} to {{ getObject(i) }}. The only issue is that your variables are also being referenced and modified in the script allTwo and allThree are being modified since you directly assign them. I fixed that by cloning the objects, but it will probably not be an issue when you are using AJAX.
Here is an updated version of your fiddle: http://jsfiddle.net/0ps2d7Lp/6/

I have made changes to your fiddle.
<div ng-repeat="i in [1,2,3]">
<div>
<pre>{{ getObject(i) }} </pre>
</div>
</div>
Controller changes:
$scope.changeType = function(ids, type) {
angular.forEach($scope.objects, function(o) {
if (ids.indexOf(o.id) > -1) {
o.type = type;
var response = (type === 2) ? allTwo : allThree
$scope.objects = angular.copy(response);
}
});
};
Link to your updated fiddle is here

In your case, getObject() is necessary, but I excluded it in my answer for simplicity sake. I understand that you need to perform a PUT/POST request to update the objects on the server-side, but I don't believe it's necessary at all to re-bind the view to the server's response. Fundamentally, a PUT doesn't require a response other than 200 OK in most cases. The point is you're telling the server to update objects, not create them. Thus, no primary keys change, so, you don't need to rebind the objects. Changes are already resident in memory.
HTML
<div class="example" ng-controller="MyCtrl">
<div ng-repeat="obj in objects">
<div><pre>{{ obj }}</pre></div>
</div>
Change all to Type 2
Change all to Type 3
</div>
JavaScript
var myApp = angular.module('myApp',[]);
function MyCtrl($scope) {
// initial objects
$scope.objects = [
{ id: 1, type: 1 },
{ id: 2, type: 2 },
{ id: 3, type: 3 }
];
$scope.changeType = function(ids, type) {
angular.forEach($scope.objects, function(o) {
if (ids.indexOf(o.id) > -1) {
o.type = type;
// Perform your PUT/POST request here for each
// updated object to update it on the server-side.
// There is no need to bind to a server response.
}
});
};
}

Related

AngularJS custom filter called twice and delete input data on second call

Here are the codes.
var app = angular.module("nameApp", ["ngRoute"]);
app.controller("ctrlname", function ($scope, $http, $filter, apiKey, apiUrl) {
$scope.data = {};
$scope.currentPage = 1;
$scope.pageSize = 5;
});
The $scope.data will contain an array of data from an HTTP GET request.
The following is a code for a custom filter for the purpose of pagination of results. Basically, this will limit the results to only 5. Buttons for pagination will update $scope.currentPage's value.
app.filter("limitResults", function ($filter, $log) {
return function (data, page, size) {
if (angular.isArray(data) & angular.isNumber(page) && angular.isNumber(size)) {
var startPage = (page - 1) * size;
if (data.length < startPage) {
return [];
} else {
$log.info(data);
$log.info(page);
$log.info(size);
$log.info(startPage);
return $filter("limitTo")(data.splice(startPage), size);
}
} else {
return data;
}
}
});
This is the HTML page that will render the data.
<div class="row resultItems" ng-repeat="video in data.videos | limitResults:currentPage:pageSize">
<div class="col-sm-3 testing">
<img ng-src="{{video.snippet.thumbnails.default.url}}">
</div>
<div class="col-sm-9 testing">
<h5>
{{video.snippet.title}}
</h5>
<p>
{{video.snippet.channelTitle}}
</p>
<p>
{{video.snippet.description}}
</p>
</div>
</div>
I put a few lines of $log.info code in the custom filter in order to see what really happens when the filter is applied. The filter runs twice, which is a normal behaviour.
What I find confusing is that when the custom filter runs for the first time, $log.info(data) logs the original data received from a HTTP GET call to the console. However, when the custom filter runs for the second time, $log.info(data) logs an empty array to the console.
Given the fact that "$log.info(data); $log.info(page); $log.info(size);" get logged to the console, it is obvious that the second IF statement (if (data.length < startPage)) is evaluated to TRUE and the filter (return $filter("limitTo")(data.splice(startPage), size);) is applied.
I just don't understand why the array, which is the data passed to the custom filter, gets emptied when the filter runs the second time.
The reason you are seeing empty array is because of the splice method.
$filter("limitTo")(data.splice(startPage), size);
Splice method syntax
array.splice(start, deleteCount[, item1[, item2[, ...]]])
If splice method is called without second parameter, that means if deleteCount is not passed, deleteCount will be treated as [arr.length - start]. In your case, when the first time filter executes, the entire array becomes empty.
See this doc for splice method

list an object in (Angular.js)

I am generating a list to search for the key "name" and "type".
results.push({ name: item.beast, type: 'color of animal' });
but I see this error to find an element that is contained in the array $scope.data:
Error: [$ rootScope: infdig] $ 10 digest () iterations reached. Aborting! Watchers fired in the last five iterations.
This is the code that I have:
http://plnkr.co/edit/EDd578?p=preview
The problem here is that you're using a set of data to filter against but trying to display a resulting data set from that filtering process that's in a different format. I'd advocate using ng-change on the input and using a new data set to fill the repeated items.
controller
$scope.matches = [];
$scope.findMatches = function(items, searchText) {
var results = [];
if (searchText) {
angular.forEach(items, function(item) {
if (item.beast.indexOf(searchText) === 0) {
results.push({
name: item.beast,
type: 'animal'
});
}
if (item.color.indexOf(searchText) === 0) {
results.push({
name: item.color,
type: 'color of animal'
});
}
});
}
return results;
}
html
<input type='text' ng-model='search' id='search' ng-change="matches = findMatches(data, search)">
<hr/>
<ul>
<li ng-repeat="item in matches track by $index">{{item.name}} and {{item.type}}</li>
</ul>
plunkr - http://plnkr.co/edit/hkMXPP?p=preview
You are creating a new array everytime your filter is run, and returning that. This makes angular think you've changed the array everytime (it doesn't check for item equality, rather, reference equality by ===).
Have a look at this for more details.
A solution is to modify the items array inplace, and return it, so the reference remains the same.

Angular: $scope.$watch a nested collection

In my Angular app, I have a checkbox list which is generated via a nested ng-repeat, like so:
<div ng-repeat="type in boundaryPartners">
<div class="row">
<div class="col-xs-12 highlight top-pad">
<div ng-repeat="partner in type.partners" class="highlight">
<label class="checkbox-inline">
<input type="checkbox" value="partner"
ng-model="ids[$parent.$index][$index]"
ng-true-value="{{partner}}"
ng-false-value="{{undefined}}">
<p><span ></span>{{partner.name}}<p>
</label>
</div>
</div>
</div>
</div>
and in my controller:
$scope.ids = [];
$scope.$watchCollection('ids', function(newVal) {
for (var i = 0, j = newVal.length; i < j; i++) {
// Create new participatingPatners tier if it doesn't exist
if(!$scope.report.participatingPartners[i]) $scope.report.participatingPartners[i] = {};
// Give it an id
$scope.report.participatingPartners[i].id = i + 1;
// Map our values to it
$scope.report.participatingPartners[i].entities = $.map(newVal[i], function(value, index) {
return [value];
});
}
});
The problem is, this $scope.$watchCollection stops watching once I've added one of each top-level ids, so if I add a given number of inputs from the first nested list, then another from the second list, My $scope.report.participatingPartners object never gets updated.
How can I $watch for changes within ids[$parent.$index][$index], making sure updated my object whenever a checkbox gets ticket or unticked?
You are creating an array of arrays:
$scope.ids = [
[],
[],
//...
]
But use $watchCollection to watch for changes in the outer array, i.e. of $scope.ids. This will only identify changes when nested arrays become different objects (or created the first time).
You could use $scope.$watch("ids", function(){}, true) - with true standing for "deep-watch", but that would be very wasteful, since it's an expensive check that would be performed on every digest cycle, whether a checkbox was clicked or not.
Instead, use ng-change to trigger the handler:
<input type="checkbox" value="partner"
ng-model="ids[$parent.$index][$index]"
ng-change="handleCheckboxChanged()">
$scope.handleCheckboxChanged = function(){
// whatever you wanted to do before in the handler of $watchCollection
}
$watchCollection is similar to $watch in that it checks the physical object reference, but goes one step further; it also goes one level deep and does a reference check on those properties.
You'll need to use $watch, but set the objectEquality flag to true. This will tell $watch to perform deep reference checking. Depending on the depth of the item being watched this can hurt performance significantly.
$watch(watchExpression, listener, [objectEquality]);
Can you try to watch for object equality :
$scope.$watchCollection('ids', function(newVal) {
}, true);

Modify source data for ng-repeat

I have an similar app as in the following example and I can't figure out why the source data is not updating. More info is in the example comments. I'm sure this is some trivial issue that I've overlooked.
Controller
$scope.items = [
{ id: 1, color: 'red', title: 'car' },
{ id: 2, color: 'blue', title: 'sky' },
{ id: 3, color: 'transparent', title: 'nothing' }
]
$scope.favoriteIds = [1, 2, 3]
$scope.getItem = function(id) { /* returns an item with given id */ }
Finally, there are two methods to modify $scope.items, but only the first one works, because the new item gets not-already-known id.
$scope.changeData1 = function() {
$scope.items = [{ id: 666, color: 'ugly', title: 'face' }]
$scope.favoriteIds = [666]
}
$scope.changeData2 = function() {
$scope.items = [{ id: 1, color: 'ugly', title: 'face' }]
$scope.favoriteIds = [1]
}
View
<h1>Favourite items</h1>
<ul ng-repeat="id in favoriteIds" data-ng-init="item = getItem(id)">
<li>I like my {{ item.color }} {{ item.title }}</li>
</ul>
<button ng-click="changeData1()">Modify data</button>
<!-- prints: I like my ugly face -->
<button ng-click="changeData2()">Modify nothing</button>
<!-- prints: I like my red car -->
The problem is, that I need to use this second way to modify data.
http://jsfiddle.net/4pEpN/7/
I'm relatively new to Angular as well, so if there's a simple way to do this, I don't know what it is (unfortunately, Angular documentation is atrocious). Regardless, you can avoid this by rethinking the structure of your code (and you'll end up with a better program too).
In your view, you're using ng-init to call getItem on the id during each iteration of your ng-repeat loop. This is what's causing your problem, and it's (apparently) due to an Angular performance feature (more at this question).
Basically, don't use ng-init except to execute something when your app starts. Otherwise, you'll end up with what you've got now: logic in the view (calling getItem(id)) rather than the model, where it belongs.
Instead, use ng-repeat to repeat over the exact data you want to display. Like I said before, this means some code rearrangement. For example, you could use a function to generate the user's current list of items on the fly. Check out this fiddle: http://jsfiddle.net/4pEpN/19/
See my comments in that code for all the changes I made, but the most relevant one is:
$scope.favoriteItems = function() {
var favObjs = [];
for (var i = 0; i < favoriteIds.length; ++i) {
favObjs.push(getItem(favoriteIds[i]));
}
return favObjs;
};
then in your view: <ul ng-repeat="item in favoriteItems()">
There are also lots of other approaches you could use. For instance, you could have an update function, which handles anything that might need to be done after any user input (including updating the user's custom array of items). Then you could call this in your changeData functions.
I don't think ng-init is appropriate since it only affects template initialization.
So how about just calling your getItem(id) for fetching each attribute, like this:
http://jsfiddle.net/JrvbD/1/

Multiple <select /> chaining with KnockoutJS with dependencies stored in a database

I want to convert a solution of <select /> box chaining I've already built to use KnockoutJS. Here's how it works now:
I have a database that is full of products that have attributes and those have values which in turn have a dependency on another selected value.
product > attributes > values > dependency
bench > length > 42" > (height == 16")
In my database we also store what values are dependent on other values. e.g. length can only be 42" if the height is 16" or something like that.
This comes from the server to a JSON object on the client that contains all of the attributes, values and dependencies for the product in a tree like form.
var product =
{
attributes:
[
values:
[
value:
{
dependencies: [{dependencyOp}]
}
]
]
};
I'll loop through each value and its dependency for the entire object and build an expression like "16 == 14 && 4 == 4" which would evaluate to false of course (14 being the selected value from another attribute). in that expression the && would be the dependencyOp in the dependencies array.
Now in my attempt I used KnockoutJS mapping plugin to get the object to be my view model but my problem is when I make a dependentObservable that needs to know what its dependant on. So now I would have to loop through every single array/object in my product variable?
If I understood your question, you're trying to get data from your server, and use it determine if the user's input is valid. If that's the case, put your data structure into a field in your viewModel, and make your dependentObservable dependent on that field.
function ViewModel() {
this.data = ko.observable();
this.input = ko.observable();
this.isValid = ko.dependentObservable(function() {
// evaluate input() against data() to determine it is valid; return a boolean
return ...;
}, this);
this.loadData = function() {
$.ajax('/data/get', {
success: function(dataFromServer) {
this.data(dataFromServer);
});
}
}
var vm = new ViewModel();
ko.applyBindings(vm);
vm.loadData();
now you can refer to this dependentObservable in a data-bind attribute like this
<input type="text" data-bind="value: input, valueupdate='afterkeydown'" />
<div data-bind="visible: isValid">shown only when data is valid...</div>

Categories

Resources