I have a value defined as ko.observableArray([]) and use ko.mapping.fromJS to append data from ajax into it. However, when i use ko.mapping.fromJS again, the new data fetch from ajax call replace self.SampleArray instead of append new data. I would like to retain the previous data. How could it be done?
function ViewModel() {
var self = this;
self.SampleArray = ko.observableArray([]);
$.ajax({
..
..
success: function() {
ko.mapping.fromJS(data, {}, self.sampleArray());
}
})
}
Try pushing it into observableArray rather replacing it
viewModel:
function ViewModel() {
var self = this;
self.sampleArray = ko.observableArray([{
'Hours': 0.5
}])
setTimeout(function () {
alert('Mock of ajax call')
var newData = ko.mapping.fromJS(data1)();
self.sampleArray.push.apply(self.sampleArray,newData)
}, 2000);
}
ko.applyBindings(new ViewModel())
Advantage of push.apply over traditional loop statements:
If your array / collection has multiple items and if you add
(array.push(item) one by one then the subscribers will be notified for
each and every push / add operation. Then the UI will have that number
of refresh. That will hurt the UI page performances.
But if you use array.push.appy , then you can still add multiple
items, but subscribers will be notified only once.
That is the difference and the advantage / usage of this
array.push.apply function.
sample working fiddle here
sample working fiddle with utils.forEach here
Related
This is my viewmodel:
function PitchViewModel() {
var self = this;
self.selectedPitch = ko.observable();
self.pitches = ko.computed(function () {
return $.getJSON("/api/Pitch", function (data) {
var obs = ko.mapping.fromJS([])
ko.mapping.fromJS(data, {}, obs) ;
// seems to work, but somehow observables are changed back into objects after binding
return obs();
})}, this).extend({ asyncArray: [{ Id: 1, PitchNumber: 1, Length: 0, Width: 0, HasElectricity: false, Name: "Test" }] });
// behaviours
self.selectPitch = function () {
console.log("inside selectPitch");
self.selectedPitch(this);
}
}
I'm using an async extender as shown here: asynchronous computed observables
adapted a little bit for observablearrays like so (in line 3):
var plainObservable = ko.observableArray(initialValue), currentDeferred;
In my view i do this:
var domNode = $('#content')[0];
var pitchViewModel = new PitchViewModel();
ko.applyBindings(pitchViewModel, domNode);
It seems to work fine. The binding happens asynchronously. Pretty cool so far.
However!
When (in Chrome) I put a breakpoint on
return obs();
the obs() function is an observableArray and has objects with observable properties.
But when I break on
console.log("inside selectPitch");
and inspect self.pitches() it has become a 'normal' array with objects the have 'normal' (not observable) properties.
What am I missing here?
BTW: I have tried using a an observableArray for self.pitches instead of the computable. But then the ko.applybindings happens before the initialization of the observable array, leading to binding errors.
Thanks for your help.
Frans
I have not tried to run your code but what I SUSPECT happens is that the extender uses the result of the ajax call returned as a Deferred/Promise. Your processing of the results happens in the callback function and is not used for anything afterwards.
You should not mix Deferreds and callbacks like this. Try the following instead:
self.pitches = ko.computed(function () {
return $.getJSON("/api/Pitch")
.then(function(data) {
return ko.mapping.fromJS(data);
});
}).extend({ asyncArray: [...] });
I'm looking for a way to sort an array inside an Angular service, and still retain the correct bindings in the controller.
If I skip the sorting, the bindings work great, but the array isn't ordered as I need it to be.
Whenever I perform the sort using Lodash's _.sortBy or angular's $filter('orderBy') service, one of two things happens:
The array in the service is sorted correctly, but the binding to the controller is severed due to it no longer referencing the same array anymore.
If I attempt to fix this by using Lodash's _.cloneDeep or angular's angular.copy, the browser freezes due to circular references (?).
Service.js
angular.module('exampleapp')
.factory('ClientFeedService', function($filter, $firebase, FIREBASE_URL, FeedItemService) {
return function(clientId) {
var ClientFeedService = this;
var ref = new Firebase(FIREBASE_URL + 'feeds/clients/' + clientId);
var initialDataLoaded = false;
ClientFeedService.feedArray = [];
ClientFeedService.sortItems = function() {
// Sorting logic here
};
/**
* Bind to the initial payload from Firebase
*/
ref.once('value', function() {
// Sort items after initial payload
ClientFeedService.sortItems();
initialDataLoaded = true;
});
/**
* Bind to new items being added to Firebase
*/
ref.on('child_added', function(feedItemSnap) {
console.log('child_added');
ClientFeedService.feedArray.unshift(FeedItemService.find(feedItemSnap.name(), feedItemSnap.val()));
// Sort after new item if initial payload loaded
if (initialDataLoaded) {
ClientFeedService.sortItems();
}
});
ClientFeedService.getFeedItems = function() {
return ClientFeedService.feedArray;
};
return ClientFeedService;
};
});
Controller.js
app.controller('ClientsFeedCtrl', function($scope, $stateParams, ClientFeedService) {
var clientId = $stateParams.clientId;
$scope.clientFeed = new ClientFeedService(clientId).getFeedItems();
});
There are a couple of ways that you can solve this. First, let's look at what is happening.
You are assigning the initial array to $scope.cliendFeed. After this, as data is added, a new Array is being generated and stored in the Service, but you still have a reference to the original Array. So ultimately, what you want to do is find a way to keep $scope.clientFeed in sync with your service.
The simplest solution is probably to use a getter method instead of storing a reference to the array in your scope.
In order to do this, you would have to add something like this:
var service = new ClientFeedService(clientId);
$scope.getClientFeed = function () {
return service.getFeedItems();
};
And make sure your ng-repeat called this function:
<li ng-repeat="item in getClientFeed()">...</li>
Hope that helps!
You can push the new data returned from API to the same array in the controller and then apply the $filter
Here is example
function getData(){
$scope.array.push(returnData);
sortArrayList($scope.orderByField, $scope.reverseSort);
}
function sortArrayList(orderByField, reverseSort){
$scope.array = $filter('orderBy')($scope.array, orderByField, reverseSort);
}
I have an json object which I am responding from servlet to knockout js. I want to initialize this data in my view model for that I am writing this code.
success: function (data)
{
var jsondata = data['jsonObj'];
self.PopulateStates = ko.computed(function(){
ko.utils.arrayForEach(jsondata, function(item){
self.States.push(new State(item));
});
});
},
error: function (exception)
{
alert( "fail" );
}
});
My json object as string looks like this
{data:[{"id":"5345345","name":"dsfsdf","ssc":"","bic":"dgffdgfdg"},{"id":"123456","name":"SBI","ssc":"654321","bic":"vxvxc"}]}
js fiddle link is demo
What is my mistake ? Or do I need to do it by mapping plugin of knockout js?
I use this knockout extension, declared before use.
ko.observableArray.fn.map = function (data, Constructor) {
var mappedData = ko.utils.forEach(data, function () {
return new Constructor(data);
});
this(mappedData);
return this;
}
Then in my $.ajax request I do this:
success: function (data)
{
var jsondata = data['jsonObj'];
self.PopulateStates = ko.observableArray().map(data, State);
});
You had the results in a computed observable which isn't what you need.
Another thing I have noticed is that your jsondata is set using the data that gets returned from the GET. You are asking that data for the field jsonObj however, looking at your JSON it seems you don't have this field. I think I am correct in saying you have data as the field with the list of items being returned.
If in your view model you have already declared self.PopulateStates which, I'm guessing you have. You can do this:
var State = function (data) {
var self = this;
self.property = ko.observable().set(data, "property");
}
var viewModel = function () {
var self = this;
self.PopulateStates = ko.observable();
function getStates() {
var request = $.ajax();
request.done(function (data, msg) {
if (data) self.PopulateStates.map(data, State);
});
}
}
If you notice in the State model I have self.property using a custom observable function to set it. All this does is if there is data to set the property to, set it. Otherwise give it a default value. I also have a third parameter that I use when I want it to construct an object for me using the data. This is when I have say, a contact, with a modifiedBy property and this modifiedBy is a user object (or just a complex object)
EDIT
The main thing, which isn't an error, but isn't necessary is the jQuery inclusion. Knockout is built to work independant of jQuery so where you do $(document).ready(function () {}) to make sure this loads on DOM ready isn't needed. This means you don't have to include jQuery if the page doesn't need it.
Here is the update fiddle, this will now work!
I am trying to figure out how to use Pager.js in conjunction with Knockout.js to lazy-load a page and bind its contents. I am trying to translate the demo example, but I am not familiar with require.js and am just getting lost.
I have spent several hours trying to reimplement the system using jQuery's getJSON instead of require and define, but the bindings are failing silently. I am having two issues:
The view model is a JSON array, so I don't know what the array is called
The code is not actually doing a getJSON request (nothing in the logs). And is failing silently.
Here is the code:
<div data-bind="page: {id: 'history', title: 'History', withOnShow: $root.getHistory }">
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
self.getHistory = function () {
return function (f) {
$.getJSON("#{HistoryR}", function (data) {
viewModel.history = ko.mapping.fromJS(data, {});
f(viewModel.history);
});
}
}
};
$.getJSON("#{HomeR}", function (data) {
viewModel = new ViewModel(data);
pager.extendWithPage(viewModel);
ko.applyBindings(viewModel);
pager.start();
});
I refactored the code some, to fit in with huocp's answer:
self.getExamHistory = function (f) {
$.getJSON("#{ExamHistoryR}", function (data) {
self.history = ko.mapping.fromJSON(data, {});
f(self.history);
});
}
and the getJSON call is getting triggered (and I see the response in my console), but my viewModel.history is still empty.
You did a wrong wrap of withOnShow callback function.
Remove the wrap, you should be fine :-)
self.getHistory = function (f) {
$.getJSON("#{HistoryR}", function (data) {
self.history = ko.mapping.fromJS(data); // can u try self instead of viewModel
f(self.history);
});
};
The reason the Pager.js demo page with extra wrap, is that it use withOnShow: requireVM('invention'), not withOnShow: requireVM. It uses the return value of requireVM function, not the function itself.
Trying to learn Backbone and hitting a stumbling block when trying to fetch data, I fetch the data fine from with my view SearchBarView but once the data has been fetched I don't know how I can get this data in my SearchResultsView in order to template out each result?
Sorry if this sounds a little vague, struggling to get my head around this at the moment so could do with the guidance!
SearchBarView
performSearch: function(searchTerm) {
// So trim any whitespace to make sure the word being used in the search is totally correct
var search = $.trim(searchTerm);
// Quick check if the search is empty then do nothing
if(search.length <= 0) {
return false;
}
// Make the fetch using our search term
dataStore.videos.getVideos(searchTerm);
},
Goes off to VideoSearchCollection
getVideos: function(searchTerm) {
console.log('Videos:getVideos', searchTerm);
// Update the search term property which will then be updated when the url method is run
// Note make sure any url changes are made BEFORE calling fetch
this.searchTerm = searchTerm;
this.fetch();
},
SearchResultsView
initialize: function() {
// listens to a change in the collection by the sync event and calls the render method
this.listenTo(this.collection, 'sync', this.render);
console.log('This collection should look like this: ', this.collection);
},
render: function() {
var self = this,
gridFragment = this.createItems();
this.$el.html(gridFragment);
return this;
},
createItems: function() {
var self = this,
gridFragment = document.createDocumentFragment();
this.collection.each(function (video) {
var searchResultView = new SearchResultView({
'model': video
});
gridFragment.appendChild(searchResultView.el);
}, this);
return gridFragment;
}
Now I'm not sure how I can get this data within SearchResultView, I think I need to trigger an event from somewhere and listen for the event in the initialize function but I'm not sure where I make this trigger or if the trigger is made automatically.
Solution 1
If dataStore is a global variable then
SearchBarView
dataStore - appears like a global variable
videos - a collection attached to global variable
then in
SearchResultsView
this.listenTo(dataStore.videos, 'sync', this.render);
Solution 2
If dataStore is not a global variable
getVideos: function(searchTerm) {
console.log('Videos:getVideos', searchTerm);
// Update the search term property which will then be updated when the url method is run
// Note make sure any url changes are made BEFORE calling fetch
this.searchTerm = searchTerm;
var coll=this; //this should refer to the collection itself
this.fetch().done(function(){
var searchResultView = new SearchResultsView({collection:coll});
searchResultView.render();
});
},
It is not 100% clear how you are initializing your SearchResultView.
But, in order to have reference to the collection, can't you simply pass in the reference to the constructor of the view. Something like this:
// In your SearchbarView
var myCollection = new Backbone.Collection(); // and you are populating the collection somewhere somehow
var searchResultView = new SearchResultView(myCollection) // you just pass this collection as argument.
myCollection.bind("change", function(){
searchResultView.parentCollection = myCollection;
}
And inside your searchResultView you just refer this collection by parentCollection for instance.
If you make it more explicit as in how these 2 views are connected or related, I may be able to help you more. But, with given info, this seems like the easiest way.