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:...}
Related
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>
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...
}
});
I'm trying to call a service function within another function of the same service, but seeing some strange behavior. Hopefully this is some obvious mistake I am overlooking. Here's the relevant part of my service:
app.factory('Data', ['$http', function($http) {
var Data = this;
var theProduct = {};
var seletedSku = {};
var uniqueItem = {};
return {
product: function(){
return theProduct;
},
getProduct: function(ext_id){
console.log(Data.product);
console.log(this.product);
},
}
}]);
As you can see, within the getProduct() function, I am simply trying to log the product function just to confirm the reference is working correctly. When getProduct() is called, the first line logs undefined, and the second line logs what I would expect, the product function:
function (){
return theProduct;
}
Why is my reference not working? You can see towards the top of the service I save this to the Data variable. Any ideas?
I'm pasting the full service code below just for reference in case it helps:
app.factory('Data', ['$http', function($http) {
var Data = this;
var theProduct = {};
var seletedSku = {};
var uniqueItem = {};
return {
//return the current product being used in the app
product: function(){
return theProduct;
},
//get a products data from the server and set it as the current product being used by the app
getProduct: function(ext_id){
console.log(Data.product);
console.log(this.product);
return $http.post('get_product', {product_id: ext_id}).success(function(data){
theProduct = data;
//when a product is fetched, set the app's unique item
if(theProduct.unique_item){
Data.setUniqueItem(theProduct.unique_item);
}
else{
Data.setUniqueItem({});
}
});
},
//change the currently selected sku for the app
setSku: function(sku){
if(sku){
selectedSku = sku;
}
else{
//null was passed, meaning, the -- Selected SKU -- option
//was chosen, so reset selectedSku back to an empty object
selectedSku = {};
}
//when sku is set, also need to set current unique item
if(selectedSku.unique_item){
Data.setUniqueItem(selectedSku.unique_item);
}
else{
Data.setUniqueItem({});
}
return selectedSku;
},
//get the currently selected sku for the app
sku: function(){
return selectedSku;
},
//set the current unique item
setUniqueItem: function(item){
//before set a unique item, we need to check if the unique
//item we are setting is the same as the current unique item.
//if it is, we need to set them equal so the new item reflects
//current state of it since it's not repulling the item from the database
if(item.id != uniqueItem.id){
//only change the unique item if they are not the same
//if they are the same, just leave unique item as is
uniqueItem = item;
}
console.log(uniqueItem);
return uniqueItem;
},
//get the current unque item
uniqueItem: function(){
return uniqueItem;
}
}
}]);
Because at the time of the reference, this has no context to itself as an object literal.
I have some search functionality that I am working on, every time a user types into a text input I filter a collection, here is the code,
userSearch: function() {
var that = this;
var letters = $('.js-user-search').val();
this.filteredCollection.reset(that.filterUsers( that.collection, letters));
var resultsList = new app.SearchUserResults({
collection: this.filteredCollection
});
resultsList.render();
},
filterUsers: function( collection, filterValue) {
var filteredCollection;
if (filterValue === "") {
return collection.toJSON();
}
return filteredCollection = collection.filter(function(data) {
return _.some(_.values(data.toJSON()), function(value) {
if( value != undefined ) {
value = (!isNaN(value) ? value.toString() : value);
//var re = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return value.indexOf(filterValue) >= 0;
}
});
});
}
As you can see from the code above, I pass a collection (users) and the search parameters to filterUsers(), that then returns a collection of matching models. I am then trying to render that into a list of search results ( links ), but the events on those links run several times (dependent on the length of the search string).
How can I build a list of results from the return collection? I have tried adding,
this.filteredCollection.on('reset', this.doSomething); however this never seems to get run, I have tried initialising my results view in the initialise function also, but I cannot pass the collection to that view as it is empty what is the best way to go?
you have to be careful with views in backbone. You keep adding a new searchresults view without removing the old one. Always keep a reference to views you add multiple times so that you can remove the previous one. I think this part will help you out:
var myCurrentSearchList = null;
userSearch: function() {
var that = this;
var letters = $('.js-user-search').val();
this.filteredCollection.reset(that.filterUsers( that.collection, letters));
if (myCurrentSearchList) {
myCurrentSearchList.remove();
}
var resultsList = new app.SearchUserResults({
collection: this.filteredCollection
});
myCurrentSearchList = resultsList;
resultsList.render();
},
http://backbonejs.org/#View-remove
I am quite new to knockout.js, and I am enjoying learning how to make interfaces with it. But I have a bit of a wall while trying to make my interface more efficient. What I am trying to achieve is remove only the elements selected by $('.document_checkbox').serializeArray(), which contains the revision_id. I will then re-add the entries to the view model with a modified call to self.getDocument(), passing only the modified records which will be re-added. Can anyone help me how to remove the entries from the arrays based on the 'revision_id' values of $('.document_checkbox').serializeArray()
?
function Document(data) {
this.line_id = data.line_id
this.revision_id = ko.observable(data.revision_id);
this.status_id = ko.observable(data.status_id);
}
function DocumentViewModel() {
var self = this;
self.documents = ko.observableArray([]);
self.getDocument = function(){
//Reset arrays
self.documents.removeAll();
//Dynamically build section arrays
$.getJSON("/Documentation/Get-Section", function(allData) {
$.map(allData, function(item) {
var section = { name: item.array_name, display_name: item.display_name, documents: ko.observableArray([])};
self.documents.push(section);
})
//Add document objects to the arrays
$.getJSON("/Documentation/Get-Document", function(allData){
$.map(allData, function(item) {
var section = ko.utils.arrayFirst(self.documents(), function(documentSection) {
return documentSection.name === item.array_name;
});
section.documents.push(new Document(item));
});
});
});
}
self.updateStatusBatch = function(data,event){
$.post('/Documentation/Update-Status-Batch',
{
revision_id : $('.document_checkbox').serializeArray(),
status_id : event.currentTarget.value
}).done(
function(){
//This is where I get confused.
});
}
}
You should modify the /Documentation/Update-Status-Batch in order that it returns the deleted item id. So you will be able to remove it on the client side.
Try this "done" function:
function(removedItemId) {
self.documents.remove(function(doc){
return doc.status_id == removedItemId;
})
}
Take a look at the remove function.
I hope it helps.