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.
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 am trying to pass data from directive to controller via service, my service looks like this:
angular
.module('App')
.factory('WizardDataService', WizardDataService);
WizardDataService.$inject = [];
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
wizardFormData = newFormData;
};
var getWizardData = function () {
return wizardFormData;
};
var resetWizardData = function () {
//To be called when the data stored needs to be discarded
wizardFormData = {};
};
return {
setWizardData: setWizardData,
getWizardData: getWizardData,
resetWizardData: resetWizardData
};
}
But when I try to get data from controller it is not resolved (I think it waits digest loop to finish), So I have to use $timeout function in my controller to wait until it is finished, like this:
$timeout(function(){
//any code in here will automatically have an apply run afterwards
vm.getStoredData = WizardDataService.getWizardData();
$scope.$watchCollection(function () {
console.log("getStoredData callback: " + JSON.stringify(vm.getStoredData));
return vm.getStoredData;
}, function () {
});
}, 300);
Despite of the fact that it works, what I am interested in is, if there is a better way to do this, also if this is bug free and the main question, why we use 300 delay and not 100 (for example) for $timeout and if it always will work (maybe for someone it took more time than 300 to get data from the service).
You can return a promise from your service get method. Then in your controller, you can provide a success method to assign the results. Your service would look like this:
function getWizardData() {
var deferred = $q.defer();
$http.get("/myserver/getWizardData")
.then(function (results) {
deferred.resolve(results.data);
}),
function () {
deferred.reject();
}
return deferred.promise;
}
And in your ng-controller you call your service:
wizardService.getWizardData()
.then(function (results) {
$scope.myData = results;
},
function () { });
No timeouts necessary. If your server is RESTFULL, then use $resource and bind directly.
Use angular.copy to replace the data without changing the object reference.
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
angular.copy(newFormData, wizardFormData);
};
From the Docs:
angular.copy
Creates a deep copy of source, which should be an object or an array.
If a destination is provided, all of its elements (for arrays) or properties (for objects) are deleted and then all elements/properties from the source are copied to it.
Usage
angular.copy(source, [destination]);
-- AngularJS angular.copy API Reference
This way the object reference remains the same and any clients that have that reference will get updated. There is no need to fetch a new object reference on every update.
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 fear this is something as embarrassing as a typo, but since I´m stuck on this and quite desperate I´m willing to pay with pride. ;)
This is my case:
Task = function (data) {
var self = this;
self.TaskId = data.TaskId;
self.TaskName = ko.observable(data.TaskName);
}
ViewModel = function () {
var self = this;
self.Tasks = ko.observableArray();
self.SelectedTask = ko.observable();
}
$.getJSON("/myService/GetAllTasks",
function (tData) {
var mappedTasks = $.map(tData, function (item) {
return new Task(item)
});
self.Tasks(mappedTasks); // Populate Tasks-array...
});
self.newTaskItem = function () {
var newitem = new Task({
TaskId: -1,
TaskName: "enter taskname here"
});
self.Tasks.push(newitem); // THIS ONE CRASH
self.Tasks().push(newitem); // BUT SUBSTITUTED WITH THIS ONE IT RUNS ON...
self.editTaskItem(newitem);
};
self.editTaskItem = function (item) {
self.SelectedTask(item); // UNTIL TIL LINE WHERE IT CRASHES FOR GOOD...
self.showEditor(true); // makes Task-edior visible in HTML
};
I also hava an "self.SelectedTask.subscription" in my file, but leaving it out of the code makes no difference.
I also should mention that my database table is empty, so the getJSON returns no data to the mappedTasks, leaving self.Tasks() = [ ] (according to Firebug)
I have fixed the incorrectly closed tags in my code.
Part 2:
Decided after a while to redo my code from the starting point. It got me one step further.
The code now stops on the second of these lines (in "self.newTaskItem"):
self.Tasks.push(newitem);
self.SelectedTask(newitem); // Here it fails.
These two observables are connected in my HTML like this:
<select data-bind="options: Tasks, optionsText: '$root.TaskName', value: SelectedTask"</select>
It looks like your ViewModel() function never gets closed. Add a closing } to wherever you want that function declaration to end. It looks to me (based on your formatting) that you want this:
ViewModel = function () {
var self = this;
self.Tasks = ko.observableArray();
self.SelectedTask = ko.observable();
}
Additionally, you need to close your$.getJson call with a );:
$.getJSON("/myService/GetAllTasks",
function (tData) {
var mappedTasks = $.map(tData, function (item) {
return new Task(item)
});
self.Tasks(mappedTasks); // Populate Tasks-array...
});
I am not 100% sure what your problem is or what error you are getting but this is what I would do - change your Task = function to function Task -
function Task(data) {
var self = this;
self.TaskId = data.TaskId;
}
By saying Task = function without using a var in front of it you are registering Task in the global namespace, not a good idea. Same thing with your view model... Fix it if you can still...
self.newTaskItem = function () {
var newitem = new Task({
// Your Task is looking for a TaskId, not a TextBatchId
TaskId: 1
});
self.Tasks.push(newitem);
self.editTaskItem(newitem);
};
Also, you are creating a TextBatchId where I think your Task object is looking for a TaskId. Fix that, or if you are doing it on purpose for some reason please show your view code and give a better explanation of what is going wrong and what errors you see.
(assuming the unclosed stuff isn't present in your real code)
In Task, TaskId isn't an observable, so when you set SelectedTask to a particular task your editor fields won't properly update (it's a fairly common mistake to assume that the elements of an observableArray are themselves observable, but they aren't unless you explicitly make them so).
I have two Backbone collections. I want to bind to the reset event one one. When that event is fired, I want to call fetch on the second collection, like so:
App.collections.movies.bind("reset", App.collections.theaters.fetch);
The second fetch never fires though. However, if I pass an anonymous function that calls theaters.fetch, it works no problem:
App.collections.movies.bind("reset", function () { App.collections.theaters.fetch(); });
Any idea why this might be the case?
Heres my full code. I'm not showing any of the models or collections, because it's a lot of code, but let me know if you think that might be the source of the problem:
var App = {
init: function () {
App.collections.theaters = new App.Theaters();
App.collections.movies = new App.Movies();
App.events.bind();
App.events.fetch();
},
events: {
bind: function () {
App.collections.theaters.bind("reset", App.theaterManager.assign);
App.collections.movies.bind("reset", function () { App.collections.theaters.fetch(); });
},
fetch: function () {
App.collections.movies.fetch();
}
},
collections: {},
views: {},
theaterManager: {
// Provide each model that requires theaters with the right data
assign: function () {
// Get all theaters associated with each theater
App.theaterManager.addToCollection("theaters");
// Get all theaters associated with each movie
App.theaterManager.addToCollection("movies");
},
// Add theaters to a collection
addToCollection: function (collection) {
App.collections[collection].each(function (item) {
item.theaters = App.theaterManager.getTheaters(item.get(("theaters")));
});
},
// Returns a collection of Theaters models based on a list of ids
getTheaters: function () {
var args;
if (!arguments) {
return [];
}
if (_.isArray(arguments[0])) {
args = arguments[0];
} else {
args = Array.prototype.slice.call(arguments);
}
return new App.Theaters(_.map(args, function (id) {
return App.collections.theaters.get(id);
}));
}
}
};
$(function () {
App.init();
});
This all has to do with function context. It is a common confusion with the way functions are called in Javascript.
In your first way, you are handing a function to be called, but there is no context defined. This means that whoever calls it will become "this". It is likely that the equivalent will be of calling App.collections.movies.fetch() which is not what you want. At least, I am guessing that is what the context will be. It is difficult to know for sure... it might be jQuery, it might be Backbone.sync. The only way to tell is by putting a breakpoint in the Backbone.collections.fetch function and print out the this variable. Whatever the case, it won't be what you want it to be.
In the second case, you hand it a function again but internally, you specify the context in which the function is called. In this case, fetch gets called with App.collections.theaters as the context.
... was that clear?