AngularJS/ng-grid - Updating array with splice doesn't updates UI - javascript

I am trying to update ng-grid with array splice.
I have a plunk here.
Add button adds new row. Update button updates last item in the array.
Select a row & press update button. Nothing happens.
Press add button. Now UI gets updated with new element & as well as the previously updated element.
Same behavior gets repeated again & again.
I tried $scope.$apply. I get:
“Error: $apply already in progress”
I even tried by placing $scope.$apply block inside a setTimeout call. Again the same error!
Any pointers!
Thanks!

That's because data $watcher in ng-grid (incorrectly) compares the data object for reference, instead on object equality. You might remedy this by setting the third parameter to true in data $watch function (line 3128):
$scope.$parent.$watch(options.data, dataWatcher, true);
Plunker

UPDATE (2015-04-10)
Angular has improved their code base (1.4.0), try the $scope.$watchCollection method first, and see if it works for you. (Link)
ANSWER
If you don't feel like hacking into a 3rd party library, you could add the hack in your code using:
$scope.updateData = function() {
var data = angular.copy($scope.myData);
data.splice(data.length - 1, 1, {name: 'UPDATED', age: '4'})
$scope.myData = data;
};
plunkr
As #Stewie mentions, the problem is that for performance reasons ngGrid compares the data object superficially, and in the case of arrays, this is by reference. ngGrid also compares by the array length, so if the array doesn't change it's length the grid wont' get updated.
This solution creates a copy of the array (different place in memory) so that when angularjs $watcher checks for changes it will find a different object and run the ngGrid update callback.
NOTE: Because this solution creates a copy of the data array on every call to updateData, it could lead to performance problems if your data is too big, also Javascript doesn't have a great garbage collection.
Old Incorrect Answer:
$timeout(angular.noop, 0);
This simply sets a timeout to trigger a $scope.$apply() after the current one is done. A way of forcing a dirty check.

I am using ui-grid v3.0.0 (from an April 2015 unstable build). I found this post and wanted to show others how I refreshed my grid after I removed a row from the grid data object using splice:
// Remove the row and refresh the grid.
$scope.myData.splice(rowIndex, 1);
$scope.gridApi.grid.refresh(true);
where my gridApi scope variable was set with this function:
$scope.gridOptions.onRegisterApi = function(gridApi){
$scope.gridApi = gridApi;
}

Related

binding updates in knockout.js

This may well be a very basic problem for anyone familiar with knockout.js, however it is causing me a problem.
I have a situation where I have a model containing an array of items that is dynamically added to and displayed in the view.
So far no problem, I can add entries into the model and the view is updated appropriately.
However. each item in the array itself has an array as a property, this is an array of object, and when I update the properties on these objects the view is not updated.
It is difficult to demonstrate this is a short code snippet so I have created a JsFiddle to show the problem:
https://jsfiddle.net/mikewardle/t0nvwqvL/1/
I have tries making the properties generated by calling
ko.observable()
rather than initializing them directly, but to no avail.
clicking the add button adds items to the array on the model itself.
either of the change... buttons alters the properties of the objects in the inner array.
As Ko2r stated your properties are not declared as observables and therefore updates will not be noticed by knockout.
To fix your changecolors() function you just need to change your linePusher function to create the color as an observable:
var linePusher = function (color, name) {
self.lines.push({ color: ko.observable(color), name: name, current:0 });
};
and then update usages of the color property to box/unbox the observable instead of replacing its value with the standard assignment operator, "="
for (i=0;i<counters.length;i++){
var lines = counters[i].lines();
for (j=0;j<lines.length;j++){
//lines[j].color = color;
lines[j].color(color); //sets the existing observable to the new value
}
}
Unfortunately I can't seem to make sense of your code enough to figure out what the increment() function is supposed to be doing so I can't tell you how to fix it, but hopefully the fixes to changecolors() put you on the right track.
You might want to read up on working with observables

KnockoutJS - UI not updating with built-in observableArray methods except push and pop

When I do a push or pop operation on my observable array, it is reflected in the ui. However other operations on the array won't change anything in the UI. Here's an example of my case:
<ul data-bind="foreach: addresses">
<!-- ko template: {name: 'AddressItemTemplate', data: {address: $data, page: 'update-page'} }-->
<!-- /ko -->
</ul>
I use my template in two different pages and thats the reason I am using the template data like that.
<script type="text/html" id="AddressItemTemplate">
<p data-bind="text: (page == 'update-page') ? 'updating' : 'declined'"</p>
<p data-bind="text: address.title"></p>
</script>
Now on js side, ofc I declared the addresses as an observable array
this.addresses = ko.observableArray([addresObject1, addressObject2, ...])
Somewhere on the page, I edit the address values. To have UI reflecting the changes, I do the following:
//suppose we know that the first address is being edited
var tmp_addresses = addresses();
tmp_addresses[0].title = 'blabla';
addresses(tmp_addresses);
And there it is, in the viewModel, I can see that the content of the addresses has been updated, but not in the UI??
addresses.push(someAddressObject);
or
addresses.pop();
works (updates the UI with the new/removed element). But addresses.splice(0, 1, newAddressObject) does not do anything in the UI again.
What am I missing here? How can push pop work and not the others??
Am I experiencing a bug in knockout framework?
UPDATE
I found out a way to do it, but there's something wrong. I'll come to that but first:
I am well aware that if I use observable objects in the observable array, the changes would be reflected in UI. However that is exactly the thing I want to avoid. It is an overkill.
Observable properties should be required in cases where properties are really exposed to user interaction. For example, if you have a UI for setting each of the fields of an object, then yes, observable property would be the right call.
However in my case, I dont even have a UI for updating the address field. Moreover, I dont need tinkering and constantly watching all the properties of all the addresses. In my case, every now and then an update occurs from the server and that changes only a single field in a single address field.
On another perspective the way I suggest should work. I simply update the whole array at once, not every element individually. It's the exactly the same logic with:
someObservableObject({newObject: withNewFields, ...});
Thats why I dont need my objects as observables. I simply want to re-declare the array and be done with the change. For example, it is advised that if you are going to make lots of pushes into the observable array, dont use array.push(...) multiple times, instead re-declare the larger array on to the observable array variable in a similar way I do it in my question. Otherwise, I am telling knockout to track every single object and every single field in them, which is hardly what I want.
Now, I finally got it working but the way I do suggests that there is a cleaner way to do it.
I found out that, the items in the observable array are somehow tracked and not updated when you re-declare the array with them. For example the code I gave in the question would not work. However the code below works:
var tmp_addresses = addresses();
var tmp_addr = tmp_addresses[0];
var new_addr = {};
Object.keys(tmp_addr).forEach(function(key){
new_addr[key] = tmp_addr[key];
});
new_addr.title = 'Hey this is something new!'
addresses.splice(0, 1, new_addr);
Not satisfied? The code below is going to work as well, because we are re-defining the array:
var newAddressObject1 = {...}, newAddressObject2 = {...};
addresses([newAddressObject1, newAddressObject2]);
But the following would not work!
var tmp_addresses = addresses();
var tmp_addr = tmp_addresses[0];
tmp_addr.title = 'Hey this address wont update';
addresses.splice(0, 1, tmp_addr);
How come? I think knockout adds an internal property to his items in observableArrays and when I try to reinsert one, it will not update.
My problem has now morphed into creating a new object with the same properties of the desired item in the observable array. The way I coded above is simply very dirty-looking. There's gotta be a better way to do that
You are wrongly assigning value to observable title that is the reason why UI not reflecting its changes (2 way binding broken).
Thumb rule is always use () notation while assigning a value to observable (keeps two way binding intact)
viewModel:
var ViewModel = function () {
var self = this;
self.addresses = ko.observableArray([{
'title': ko.observable('one')
}, {
'title': ko.observable('two')
}])
setTimeout(function () {
var tmp_addresses = self.addresses();
tmp_addresses[0].title('blabla'); //assigning data to observable
self.addresses(tmp_addresses);
}, 2000)
};
ko.applyBindings(new ViewModel());
working sample here
PS: Don't get deceived by seeing the value change in viewModel the moment you done assigning using = two binding is broken UI wont reflect VM'S changes .
when you splice up your observableArray UI takes it changes check here
The problem was exactly as #jason9187 pointed out in the comments: The references of the objects in the observable array does not change when I edit a field of them. Therefore, KO would not interpret my array as changed. If the observableArray had contained simple data types, then the way I suggested could work without a problem. However, I have an Object in the array, therefore although I edit the Object, it's reference (pointer) remains the same, and KO thinks that all Objects are the same as before.
In order to achieve what I wanted, we have to solve the deep cloning problem in javascript like in this post.
Now there's a trade-off there, deep cloning is very simple in vanilla if you don't have a circular architecture or functions in your objects. In my case, there's nothing like that. The data comes from a restful API. If anybody in the future gets hold of this problem, they need to deep-clone their 'hard-to-clone' objects.
Here's my solution:
var tmp_addresses = JSON.parse(JSON.stringify(addresses())); //Creates a new array with new references and data
tmp_addresses[0].title = 'my new title';
addresses(tmp_addresses);
Or, if you can create address objects, following will work as well:
var tmp_addresses = addresses();
tmp_addresses[0] = new randomAddressObject();
addresses(tmp_addresses);
Here is a fiddle that I demonstrate both of the methods in a single example

Replacing a row in ngGrid with JavaScript 'splice' method - angularjs, nggrid

I am trying to replace an item in ngGrid with a different item. Splicing a single item works. Using splice to insert an item:
$scope.myData.splice(1, 0, object);
also works. However, splice(index,1,object) does not update grid. How can I show updates to myData on ngGrid? I have replicated the problem here.
I think that should work, I'd report it as a bug to the ng-grid team. If I change $watch to $watchCollection in their source it works fine (lines 3295, 3296 in ng-grid.js) (plnkr):
$scope.$on('$destroy', $scope.$parent.$watchCollection(options.data, dataWatcher));
$scope.$on('$destroy', $scope.$parent.$watchCollection(options.data + '.length', function() {
As an alternative you could use angular.copy to change the properties on the existing object instead of splicing the new object in:
angular.copy(obj, $scope.myData[1]);

If a $scope model changes, but remains the same length, why does Angular-UI calendar not update?

I'm writing an event calendar in AngularUI with some filtering.
<div
id="calendar"
ui-calendar="{ height: 450, editable: false, defaultView: 'month' }"
class="span9 pull-right calendar"
ng-model="events"
</div>
When the filter dropdowns are changed, a function is fired, which should do some voodoo and update $scope.events.
(Shortened)
// Clear $scope.events
$scope.events = [];
// Initialise new events
var new_events = all_events;
if($scope.events_filter.type != 'all') {
// Do filter
new_events = json_service.filter(
new_events,
'type',
$scope.events_filter.type
);
}
// Update $scope.events
$scope.events = new_events;
Let's start with 6 events. If there are two 'types,' with - say - 3 events each, and lets call one type A and one type B, you can change from All to A, and three events will be correctly shown. If you change back to 'all,' there will be 6 visible.
If you change from A to B, the calendar does not update, however, examining $scope.events DOES show the data has changed.
The filtering works, and $scope.events is being updated.
The problem seems to be that if $scope.events length doesn't change - it doesn't see fit to redraw the calendar.
You are correct. If the length of the events array does not change the calendar will not update. This is because angular will not detect changes in the whole array object,(without throwing a digest error) rather it needs to watch a more focused variable set or watch a function that returns one event source to work properly.
Two edits solved this specific problem. Aadding getOptions() to the watch method value, and either watching the first event source or watching all event sources and adding an equalsTracker to the directive's attributes.
Here is a link to a calendar that switches out 2 arrays of events from a filter service.
(this does not solve your issue however) http://plnkr.co/edit/VbYDNK?p=preview
The calendar is now watching the first event array in eventSources. So now any events that you want to control with angular on the scope can be added to the first array, and then if you need to pull in any sources you can call fullcalendar's addSource method at any given time or any other method of your choice.
This is a much more open ended calendar that creates many angles of attack.
Here is a link to a calendar that is watching the first array itself and not its length. This works as well, so maybe we should go with this one.
(this solves your problem) http://plnkr.co/edit/AU6KNZ?p=preview
I have read that it can cause performance issues to watch large arrays.
Edit:
I have went back to the drawing board and found a way to solve all of these problems in one. Now we no longer need to watch the first array in event sources, rather we can just watch a tracker variable. The tracker variable will watch the length of all events in eventSources plus the length of eventSources itself. This will allow for any array to be watched inside of eventSources and still let angular to do its magic.
Because of this specific use case an equalsTracker attribute has been added to the calendar. This equalsTracker attr must be a number and should be updated when a filter service is ran and the resulting array of events has the same length as the current scope.events array being filtered.
I found a solution! With joshkurz help, I realised that it was only checking for the length changing.
Simple solution:-
Before filtering check length of $scope.events.
After filtering check length of $scope.events.
var length = $scope.events.length;
$scope.events = [];
var new_events = all_events;
... filter new_events a little bit ...
$scope.events = new_events;
if($scope.events.length == length) {
$scope.events.push({});
}
The length isn't different any more ;)
Thanks josh :)

knockout.mapping.js, Is it possible to update only a part of mapped variable?

In my JS I have the following ajax call, that bind the resulting json using knockout mapping plugin.
$.getJSON("/Game/GetRack", function (data) {
game.rack = ko.mapping.fromJS(data);
ko.applyBindings(game.rack, $('.rack')[0]);
});
/Get/GetRack returns:
[{"Color":3,"Letter":"a","Points":5},null,null]
As you can see, there is only one object in the array. The other two are nulls.
Now using knockout mapping I can do:
ko.mapping.fromJS([null, { Color: 55, Letter: "b", Points: 88 }, null], game.rack);
Doing so correctly updates my view and now I see only a letter B on a second position. The other two are nulls.
My questions is: Can I update a value at a particular position without using mapping.fromJS?
So assuming I have a letter A at index 0, I want to change the second null to
{ Color: 55, Letter: "b", Points: 88 }
and make my UI automatically update to resemble this change. How can this be done?
Edit:
I decided to go with the example given by John Earles. Unfortunately I still have a problem, because my array is two dimenstional.
You have a sample here:
http://jsfiddle.net/wgZ59/29/
(it's very similar to John Earles' example, but includes two dimensional array).
Can someone point out why clicking on change button does not change the values of elements? Is it also possible to change their values without calling HasMutated()?
And the last one (only if the previous two are solved). Is it possible to create html table statically (because e.g. I know that it will always be 3x3, so I want two print table with 3 rows and 3 columns and then bind each individual cell to my matrix cells. I tried that already and I had problems, because knockout didn't have the values for the cells...
EDIT2:
I managed to answer my above questions myself, fiddle example is here:
http://jsfiddle.net/wgZ59/44/
So, I can make a static table and bind individual cells, when I declare the array this way:
self.matrix = ko.observableArray([[0,0,0],[0,0,0],[0,0,0]]);
or
self.matrix = ko.observableArray([[,,],[,,],[,,]]);.
I can update the values and it works for a static table, but it does not work for a dynamic table (created dynamically by knockout). You can see the behavior on the fiddle page (link at the beginning of this edit). Do you know why pressing "change" button does not update the values in a dynamically created table?
I don't know exactly what you are trying to do, but if game.rack is an observableArray then you can manipulate it using JavaScript.
Here is the documentation page on observableArrays:
http://knockoutjs.com/documentation/observableArrays.html
That page shows the available "helper" methods that are available on the observableArray itself. Additionally you can manipulate the underlying array, but then you have to call 'valueHasMutated()' on the observableArray to let any registered listeners know about the changes.
Here is a simple JSFiddle showing manipulation:
http://jsfiddle.net/jearles/wgZ59/8/

Categories

Resources