$digest error on $watch for changes in model - javascript

My application receive updates on items via SSE (server sent events) from the API.
What I have is a main controller that looks for this changes:
if (!!window.EventSource) {
var source = new EventSource('/items/updates');
} else {
console.log('SSE not supported');
}
source.addEventListener('items', function (e) {
var data = JSON.parse(e.data);
Items.updateOneItem(data);
$scope.$digest();
}, false);
Items is a service factory that handle the changes:
App.factory('Items', function () {
var items = {};
// previous and updated item
var previousItemState = null;
var updatedItem = null;
items.list = [];
items.getUpdatedItem = function () {
return {
previous: previousItemState,
updated: updatedItem
};
};
items.updateOneItem = function (item) {
var i = $.map(items.list, function (e, i) {
if (e !== null) {
if (e.id === item.id) {
return i;
}
}
});
previousItemState = items.list[i];
items.list[i] = item;
updatedItem = item;
};
return items;
});
Basically in this service I store the items and I'm trying to check if an item has been updated and what exactly is changed in the item model.
In my controller I'm watching this and doing my controls, or maybe I'm trying to do that:
$scope.$watch(Items.getUpdatedItem, function (item) {
if (item.previous !== null && item.updated !== null) {
// do my controls on previous and updated item...
}
});
What happen is that I have an error like this:
Uncaught Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting! [...]
I tried, from my Items service, to return just a single value and it works fine but I receive just the updated item and I don't know how to check in what the item is changed:
items.getUpdatedItem = function () {
return updatedItem;
};
My questions are:
Why I can not return an object from my service method?
What is in this case the best practice to have the previous and updated item in order to see changes?
Cheers

Firstly, you are not supposed to run $digest() manually. Wrap the code you need to digest in $timeout.
Second thing, it is correct to pass your object like this:
items.getUpdatedItem = function () {
return updatedItem;
};
Problem you are having with accessing old and new version in $watch is easily solved. Second parameter you are passing to $watch is a function that actually use two arguments:
$scope.$watch(Items.getUpdatedItem, function (newItem, oldItem) {
if (oldItem !== newItem) {
// do my controls on previous and updated item...
}
});

Related

Ensure Observable Array is populated before Filter runs

Problem: When My filter method is executing that is used to display an Html element, my observable array has not been populated yet by my web-api call resulting in nothing to filter from.
My Solution :
If I place an alert right before my filter method execution, everything seems to be working.
Length of my observable array is 0 right before my alert call whereas it has a value after my alert.
Question: How can I ensure my array is populated without placing an alert.
This issue is occurring on multiple Pages, placing an alert before my Html is rendered makes everything works fine.
I have a simple Observable Array and a Filter Method on it.
Part of my Knockout Code :
self.currentVendorSupport = ko.observable(new VendorContact());
//Populates Observable Array - allManufactures
self.allManufacturers = ko.observableArray([]);
$.getJSON(serviceRoot + '/api/Manufacturer', function (data) {
var mappedManufacturers = $.map(data, function (item) {
return new Manufacturer(manID = item.manID, name = item.name);
});
self.allManufacturers(mappedManufacturers);
});
//Filters allManufacturers
self.GetCurrentVendor = function () {
alert('allManufacturerLength value again:' + allManufacturerLength);
return ko.utils.arrayFirst(self.allManufacturers(), function (item) {
return item.manID === self.currentVendorSupport().manID();
});
}
It seems to be working.
It is not working with arrayFilter though, is it because of return type difference between the two, wrong syntax or something else?
self.GetCurrentManufacturer = ko.computed(function () {
if (self.allManufacturers().length > 0)
{
return ko.utils.arrayFilter(self.allManufacturers(), function (item)
{
return item.manufacturerID ===
self.currentVendorSupport().manufacturerID() });
}
else return new Manufacturer(0, '...');
}, self);
Html Code:
<label class="control-label readOnlyLabel" data-bind="text: GetCurrentVendor().name"></label>
You can simply make GetCurrentVendor a computedObservable instead so that you can conditionally show a value based on the observable array length. Since it is computed it would react on changes made to the array and update its value.
You can even make it pureComputed so it is only ever activated/computed when called.
For example the computedObservable currentVendor would show "..." when the array is empty and the filtered name when the array is populated.
Computed:
self.currentVendor = ko.computed(function () {
if(this.allManufacturers().length > 0) {
return ko.utils.arrayFirst(this.allManufacturers(), function (item) {
return item.manID === this.currentVendorSupport().manID();
}).name;
} else {
return '...'
}
}, this)
HTML:
<label class="control-label readOnlyLabel" data-bind="text: currentVendor"></label>
Right now, your code is written such that GetCurrentVendor is called only once, by the HTML. And obviously it's called too soon and doesn't get updated afterwards. This is exactly what an observable is for, so that the HTML gets updated when the JS values get updated. So try this:
JS
self.currentVendorSupport = ko.observable(new VendorContact());
//Populates Observable Array - allManufactures
self.allManufacturers = ko.observableArray([]);
//New observable, initially empty
self.currentVendorName = ko.observable();
$.getJSON(serviceRoot + '/api/Manufacturer', function (data) {
var mappedManufacturers = $.map(data, function (item) {
return new Manufacturer(manID = item.manID, name = item.name);
});
self.allManufacturers(mappedManufacturers);
//updated value after api call is complete
self.currentVendorName(self.GetCurrentVendor().name);
});
//Filters allManufacturers
self.GetCurrentVendor = function () {
//alert('allManufacturerLength value again:' + allManufacturerLength);
return ko.utils.arrayFirst(self.allManufacturers(), function (item) {
return item.manID === self.currentVendorSupport().manID();
});
}
HTML
//this automatically updates when a new value is available
<label class="control-label readOnlyLabel" data-bind="text: currentVendorName"></label>

Save key=>value style with ngStorage/localstorage

In my Ionic app I've added the plugin 'ngStorage' and it comes with a little demo code:
var add = function (thing) {
$localStorage.things.push(thing);
}
This works exactly as told. I add("foo") it, and do getAll() and the value is there. I remove the add(), but keep the getAll(), I still have the value "foo" (as expected).
This isn't very usefull for me, I want to access it with keys, so I've made the following:
var addByKey = function (key, value) {
$localStorage.things[key] = value;
// Or, I've also tried:
$localStorage.things.key = value;
}
When I do the addByKey("foo","bar") and then the getAll() I get the values exactly as I want. When I remove the addByKey() and reload, I expect it to still remember the set information, but it doesn't exist. However, the first attempt via the add() function still exists, "foo" is still there (meaning the array doesnt reset).
How do I make a key->value type of structure?
In case it's usefull:
.factory ('StorageService', function ($localStorage) {
$localStorage = $localStorage.$default({
things: []
});
var _getAll = function () {
return $localStorage.things;
};
var _add = function (thing) {
$localStorage.things.push(thing);
}
var _addByKey = function (thing, value) {
$localStorage.things[key] = value;
// Or, I've also tried:
$localStorage.things.key = value;
}
return {
getAll: _getAll,
add: _add,
addByKey: _addByKey
};
})
Assuming that you want a key value storage system you can simply use an object instead of an array so that every key can be set as a property of this object.
.factory('StorageService', function($localStorage) {
$localStorage = $localStorage.$default({
things: {}
});
var _getAll = function() {
return $localStorage.things;
};
var _addByKey = function(thing, value) {
$localStorage.things[thing] = value;
}
return {
getAll: _getAll,
addByKey: _addByKey
};
})
However, assuming that you want to keep a reference of all values on the main collection and access them through keys, you can consider using an object to store the things intead of an array. So that you can use a property to store all items (you can store in a different place as well) and use this object to store your keys by referencing the to a desired value on your collection.
You may need to implement the deletion logic to maintain the consistence between the collection and the dictionary.
Your factory would look like this:
.factory('StorageService', function($localStorage) {
$localStorage = $localStorage.$default({
things: {
items: []
}
});
var _getAll = function() {
return $localStorage.things.items;
};
var _add = function(thing) {
$localStorage.things.items.push(thing);
}
var _addByKey = function(thing, value) {
var i = $localStorage.things.items.push(value) - 1;
$localStorage.things[thing] = $localStorage.things.items[i];
}
return {
getAll: _getAll,
add: _add,
addByKey: _addByKey
};
})

How to call a function in my controller which is being used as scope for view also

I am calling the fetchData function on click of an button and it fetches the complete set of data perfectly, what I need to pass form view is the item value,
I want to reuse this function and when I call this function and pass this item value it does not call the function at all, how can I reuse this function.
$scope.fetchData = function(item) {
$scope.selectedData = item;
var entry = ServiceName.ServiceItem.query({
item: item,
}, function() {
$scope.item_array = entry;
});
}
if(some_condition)
{//$stateParams.item_valis used to fetch the data
$scope.item_array = $scope.fetchData($stateParams.item_val);
/* this call works perfectly
var entry = ServiceName.ServiceItem.query({
item: $stateParams.item_val,
}, function() {
$scope.item_array = entry;
}); */
}
$scope.fetchData = function fetchData (item) {
$scope.selectedData = item;
var entry = ServiceName.ServiceItem.query({
item: item,
}, function() {
$scope.item_array = entry;
});
}
It does return undefined (by default) because you don't return anything and you're assigning (on condition):
$scope.item_array = $scope.fetchData($stateParams.item_val);

how to use $scope.$watchCollection insted of angular.forEach()

I have this block of code when user do delete or undelete it will update deleted property on model. my problem I'm using for angular.forEach() just want to see if it possible to change angular.forEach to use $scope.$watchCollection() ?
$scope.deleteForm = function (form) {
var result = confirm('Want to delete the form ?');
if (result){
var splitDeleteHref = form.deleteHref.split('/');
var formName = splitDeleteHref[1];
var formId = splitDeleteHref[2];
homeService.deleteForm(formName, formId);
$scope.$watchCollection('forms',)
//angular.forEach($scope.forms, function(value, key) {
// if (value.recordId === form.recordId) {
// $scope.forms[key].deleted = true;
// }
//});
}
};
$scope.undeleteForm = function (form) {
var result = confirm('Want to undelete the form ?');
if (result) {
homeService.undeleteForm(form).then(function () {
//angular.forEach($scope.forms, function (value, key) {
// if (value.recordId === form.recordId) {
// $scope.forms[key].deleted = false;
// }
//});
});
}
};
$scope.$watch and .$watchCollection are for watching for changes to an object or a collection of objects. These methods cannot be used to iterate over an array, which is what angular.forEach is for. Your usage of .forEach appears to be correct, so I recommend leaving it as is.

What is wrong with angular service

I have a view that displays a list of items. To get those items I use a service with the following public interface
return {
items: _items,
getItems: _getItems,
getItemById: _getItemById,
item: _item
};
The service local variables are:
var _items = [];
var _item;
items array is filled when I call getItems().
However, when someone selects an item from the list. I use the getItemById(id) to fetch and init _item variable. (The item is found and appears to init the _item.
But there is something wrong cause when I try to access the item from my controller it appears as undefined. itemsService.item
var _getItemById = function (id) {
_item = null;
$.each(_items, function (i, item) {
if (item.id == id) {
_item = item;
console.log(_item); //this works
return false;
}
});
}
I want to keep track of the selected item, as I have pages like "ItemsList", "ItemDetail", "ItemExtra" etc. This is why i decided to keep track of the item in my service. maybe there is a better practise ?
From comments
change your service to return {items : return _items,getItems:...}

Categories