Angular scope doesn't update when removing an item - javascript

I have a simple application where I can add users to a list and remove them.
The form to add a new user binds to $scope.newPerson. On submitting the form, I add the newPerson object to $scope.people which is an array containing person objects.
I loop over the people array with an ng-repeat directive, to print out the people who are currently added to the scope. These rows all have a remove button (Jade snippet):
div.row(data-person, ng-repeat="person in people", ng-model="person")
button(ng-click="removePerson(person)") Remove
When I click the Remove button, I execute this function:
$scope.removePerson = function(person) {
var index = $scope.people.indexOf(person);
if (index > -1) {
$scope.people.splice(index, 1);
person = null;
}
}
This removes the row from the table, and sets the person scope to null. Batarang shows { This scope has no models } afterwards.
However, I have noticed that my people array doesn't update. When I check it's scope in Batarang, the person I just deleted is still in that array. When I start typing to add a new person, it updates. If I submit the whole page to my server without doing this, the array still contains the removed people.
If i put $scope.$apply() after person = null;, I get the expected behaviour, however it throws an error that an apply is in progress. I also read calling $apply() yourself is considered bad practice. (?)
I'm new to Angular and I can't seem to find a lot of information about solving this problem. How would I make it update my array when I remove a person? Thanks.

I did the following to fix this:
No more ng-model on the ng-repeat block:
div.row(data-person, ng-repeat="person in people")
Refactored the ng-click event for removePerson():
<button ng-click="removePerson($index)">
Remove
</button>
and changed the removePerson() code to this:
$scope.removePerson = function(index) {
$scope.people.splice(index, 1);
};
Not sure if this actually fixed anything compared to my previous code, because I noticed that this was also a Batarang issue. When I simply log {{ people }} to my HTML, or console.log($scope.people), I see the people array update. However, in Batarang, the array does not update.
Lesson learned: sometimes, logging stuff out yourself is better than relying on tools ;)

slice method do not update your array, but return New one

Related

angularJs form not adding object to array properly. 2 way binding not working

I have started a demo app and come across this problem where my parts that I am adding to the repairs are not being properly proccessed. This is my first angular that I am building.
To get the bug press update on one of the repair cards and then scroll down to the quantity and repair item section at the bottom of the form and try to add a new item. it doesnt regester what is in the form.
This is the codepen with the full view that doesnt work.
This is the function in the controller to pass the info the factory.
$scope.addPartsPerRepair = function() {
partFactory.addPartsPerRepair($scope.newPartsPerRepair);
$scope.newPartsPerRepair = {};
};
This is the function in the factory to add the new repair to the array partsPerRepair
factory.addPartsPerRepair = function(newPartsPerRepair) {
partsPerRepairs.push(newPartsPerRepair);
};
After I abstracted away the repair factory and simplified my view we are left with this
This is the codepen with the partial view that does work.
I have been over this code for the past 6 hours trying to fix this one probelm and I can not see my issue.
Secondly, This is more of an add-on question, Does anyone have any advice on how to save the partsPerRepair to the actual repair[Idx] instead of in one array called partsPerRepair that every repair accesses. I thought I might be able to add an array called parts to each repair and store quanity and item name in that array but that has proven to be more difficult then I can manage. I realize I am asking two different questions but since your here reading I figured you might be willing.
Thanks in advance for any help.
An updated CodePen.
So I've tried to resolve both your questions here, but all I have edited is the arra yof repairs and the add parts method, which means there is alot of cleaning up chanegs to be made like deleting the old partsPerRepairs and its references.
First thing newPartsPerRepair needed to be intialized this is why add parts was not working the function was passsing undefined(anything used as model in ng-model needs to be intialized), which is done in the controller - $scope.newPartsPerRepair = {};.
Second I moved the partsPerRepairs array into the repairs array and called it parts this is under each customer in the array, this is to answer your second question.
Third I changed the way the ng-repeats work to use these new parts arrays - ng-repeat="partsPerRepair in repairs[$index].parts track by $index"
Finally I moved and edited the addNewParts to the repairs factory so it used the new parts array and could access the repairs array.
Hope this all helps.

update, instead of replace, list used for ng-repeat

How it is
I have an array of objects called vm.queued_messages (vm is set to this in my controller), and vm.queued_messages is used in ng-repeat to display a list of div's.
When I make an API call which changes the underlying model in the database, I have the API call return a fresh list of queued messages, and in my controller I set the variable vm.queued_messages to that new value, that fresh list of queued messages.
vm.queued_messages = data; // data is the full list of new message objects
The problem
This "full replacement" of vm.queued_messages worked exactly as I wanted, at first. But what I didn't think about was the fact that even objects in that list which had no changes to any properties were leaving and new objects were taking their place. This made no different to the display because the new objects had identical keys and values, they were technically different objects, and thus the div's were secretly leaving and entering every time. THIS MEANS THERE ARE MANY UNWANTED .ng-enter's AND .ng-leave's OCCURRING, which came to my attention when I tried to apply an animation to these div's when they entered or left. I would expect a single div to do the .ng-leave animation on some click, but suddenly a bunch of them did!
My solution attempt
I made a function softRefreshObjectList which updates the keys and values (as well as any entirely new objects, or now absent objects) of an existing list to match those of a new list, WITHOUT REPLACING THE OBJECTS, AS TO MAINTAIN THEIR IDENTITY. I matched objects between the new list and old list by their _id field.
softRefreshObjectList: function(oldObjs, newObjs) {
var resultingObjList = [];
var oldObjsIdMap = {};
_.each(oldObjs, function(obj) {
oldObjsIdMap[obj._id] = obj;
});
_.each(newObjs, function(newObj) {
var correspondingOldObj = oldObjsIdMap[newObj._id];
if (correspondingOldObj) {
// clear out the old obj and put in the keys/values from the new obj
for (var key in correspondingOldObj) delete correspondingOldObj[key];
for (var key in newObj) correspondingOldObj[key] = newObj[key];
resultingObjList.push(correspondingOldObj);
} else {
resultingObjList.push(newObj);
};
});
return resultingObjList;
}
which works for certain things, but with other ng-repeat lists I get odd behavior, I believe because of the delete's and values of the objects being references to other controller variables. Before continuing down this rabbit hole, I want to make this post in case I'm thinking about this wrong, or there's something I'm missing.
My question
Is there a more appropriate way to handle this case, which would either make it easier to handle, or bypass my issue altogether?
Perhaps a way to signal to Angular that these objects are identified by their _id instead of their reference, so that it doesn't make them leave and enter as long as the _id doesn't change.
Or perhaps a better softRefreshObjectList function which iterates through the objects differently, if there's something fishy about how I'm doing it.
Thanks to Petr's comment, I now know about track by for ng-repeat. It's where you can specify a field in your elements that "identifies" that element, so that angular can know when that element really is leaving or entering. In my case, that field was _id, and adding track by message._id to my ng-repeat (ng-repeat="message in ctrl.queued_messages track by message._id") solved my issue perfectly.
Docs here. Search for track by.

Remove all elements within a ng-repeat in Angular

I'm working with Angular and part of my page utilizes ng-repeat to display some bug tracker tickets. As part of the site, I also want to provide the ability to search tickets. I'm able to get that part working as I want, and if I'm just appending new tickets they show up fine.
However I would like to be able to, if a user searches, delete all of the currently visible ticket divs and replace them with the search results.
My initial thinking, since I have the ng-repeat set as item in tickets track by item.id, was to just set $scope.tickets equal to the new data. However, this didn't cause Angular to update the DOM.
So, I tried setting $scope.tickets equal to an empty array and then setting it equal to the new data:
$scope.$apply(function() {
$scope.tickets = [];
$scope.tickets = data;
});
Still no update to the DOM, even though console.log($scope.tickets) shows the correct objects.
I'm aware of the method of
$scope.$apply(function() {
array.splice(index, 1);
});
to remove individual elements, but I'm not sure how I would apply that removing all of the elements.
I'll try and get a Plunkr or JSBin added to the Q soon.
What would be the proper way for me to make Angular replace all of the current elements with the new elements created from the data?
try setting array.length = 0
this deletes all elements, while not removing the reference to the array, which actually seems to be the problem in your case.
but another way would be to have a additional data bag.
for example have $scope.data.tickets then you can reasign tickets as usual. important thing is, you have to reference your items using item in data.tickets
Did you test $watch?
$scope.$watch('tickets', function() {
// update data HERE
});

KnockoutJS: Stop a particular property from setting dirty flag

With some help from StackOverflow community I was able to get my dirty flag implementation to work, based on this example: http://www.knockmeout.net/2011/05/creating-smart-dirty-flag-in-knockoutjs.html
It does exactly what I want, except for a single use case that I don't know how to solve.
Basically I have a select menu that gets automatically populated from the database. This select menu also has an option to make an Ajax call to my back end and have the list of options refreshed, database updated and return the result. This is where things get hairy for me.
First method works fine, however, it has to re-index and re-apply my entire viewModel and takes about 2-3 seconds, running on a local machine with 16gigs of ram and SSD.
jsondata.component.available_tags = result.available_tags;
ko.mapping.fromJS(jsondata, viewModel);
Second method also works, and pretty much instantaneous, however, it sets of isDirty() flag, which I would like to avoid, because this data is already coming from the database and I wont need to save it. I can not use isDirty.reset() method either, because if isDirty was set by something else before I clicked an menu option to update available_tags, it will reset that too. Which I would also like to avoid.
viewModel().component.available_tags(result.available_tags);
My question is: With the first method, can I force UI refresh with ko.mapping.fromJS() on a particular element and not entire dataset? Or, with a second method, can I avoid setting isDirty flag set when available_tags are updated? The twist is that I still need to keep available_tags as an observable, so the select menu is automatically generate/updated.
UPDATE: I was able to update mapping for that one single element with
ko.mapping.fromJS(result.available_tags, {}, viewModel().component.available_tags);
but that immediately set off isDirty flag... Argh
In addition to Tomalak's suggestions, which I totally agree with, maybe the toJSON method can help you out in similar cases where you don't want to split the model. If your dirty flag implementation uses ko.toJSON as a hash function, as Ryan Niemeyer's does, you can give your model (on which the dirty flag is active) a toJSON method, where you do something like this:
function MyObjectConstructor() {
this.someProperty = ko.observable();
this.somePropertyNotUsedInDirtyFlag = ko.observable();
}
MyObjectConstructor.prototype.toJSON = function () {
var result = ko.toJS(this);
delete result.somePropertyNotUsedInDirtyFlag;
return result;
};
Please be aware that this is also used to serialize the object in some other occassions, such as ajax calls. It's generally a handy function for removing computeds and such from your objects before using them in a different context.

KnockoutJS: Update/Insert data to a viewModel using mapping

I've been trying to figure this out for quite some time now. I couldn't find anything that addresses this problem, but please correct me if I'm wrong.
The problem:
I have data from a JSON API comming in, with an nested array/object structure. I use mapping to initially fill the model with my data. To update this, I want to extend the model if new data arrives, or update the existing data.
As far as I found out, the mapping option key, should do this trick for me, but I might have misunderstood the functionality of the mapping options.
I've boiled down the problem to be represented by this example:
var userMapping = {
key: function(item) {
return ko.utils.unwrapObservable(item.id);
}
};
// JSON call replaced with values
var viewModel = {
users: ko.mapping.fromJS([], userMapping)
};
// Should insert new - new ID?
ko.mapping.fromJS([{"id":1,"name":"Foo"}, {"id":2,"name":"Bar"}], userMapping, viewModel.users);
// Should only update ID#1 - same ID?
ko.mapping.fromJS([{"id":1,"name":"Bat"}], userMapping, viewModel.users);
// Should insert new - New ID?
ko.mapping.fromJS([{"id":3,"name":"New"}, {"id":4,"name":"New"}], userMapping, viewModel.users);
ko.applyBindings(viewModel);​
Fiddle: http://jsfiddle.net/mikaelbr/gDjA7/
As you can see, the first line inserts the data. All good. But when I try to update, it replaces the content. The same for the third mapping; it replaces the content, instead of extening it.
Am I using it wrong? Should I try to extend the content "manually" before using mapping?
Edit Solution:
I solved this case by having a second helper array storing all current models. On new data i extended this array, and updated the view model to contain the accumulated items.
On update (In my case a WebSocket message), I looped through the models, changed the contents of the item in question, and used method valueHasMutated() to give notice of changed value to the Knockout lib.
From looking at your example code the mapping plugin is behaving exactly as I would expect it to. When you call fromJS on a collection you are effectively telling the mapping plugin this is the new contents of that collection. For example:
On the second line, How could it know whether you were updating or whether you had simply removed id:2?
I can't find any mention of a suitable method that treats the data as simply an update, although you could add one. Mapped arrays come with some helpful methods such as mappedIndexOf to help you find particular items. If you receive an update data set simply loop through it, find the item and update it with a mapping.fromJS call to that particular item. This can easily be generalized into reusable method.
You can use ko.mapping.updateFromJS() to update existing values. However, it does not add new values so that would be a problem in your instance. Take a look at the link below for more details.
Using updateFromJS is replacing values when it should be adding them
Yes, you should first collect all data into a list or array and then apply the mapping to that list. Otherwise you are going to overwrite the values in your viewModel.

Categories