I have just started trying knockout.js. The ko.mapping offers a nifty way to get and map data from server. However I am unable to get the mapping to work.
I have a simple model:
//var helloWorldModel;
var helloWorldModel = {
name: ko.observable('Default Name'),
message: ko.observable('Hello World Default')
};
$(document).ready(function() {
ko.applyBindings(helloWorldModel);
//a button on the form when clicked calls a server class
//to get json output
$('#CallServerButton').click(getDataFromServer);
});
function getDataFromServer() {
$.getJSON("HelloSpring/SayJsonHello/chicken.json", function(data) {
mapServerData(data);
});
}
function mapServerData(serverData) {
helloWorldModel = ko.mapping.fromJS(serverData, helloWorldModel);
alert(JSON.stringify(serverData));
}
The helloWorldModel has only 2 attributes - exactly the same thing I return from the server. The alert in mapServerData shows -
{"name":"chicken","message":"JSON hello world"}
I have looked up other posts regarding similar problem, but none of them seemed to be solve this issue. Maybe I am missing something very basic - wondering if anyone can point it out.
Also note if I do not declare the model upfront and use
helloWorldModel = ko.mapping.fromJS(serverData);
it is mapping the data to my form correctly.
From Richard's reply and then a little more investigation into this I think that the way I was initializing the model is incorrect. I guess that one cannot use an existing view model and then expect it to work with mapper plugin. So instead you initialize view model with raw JSON data using the ko.mapping.fromJS:
var helloWorldModel;
$(document).ready(function() {
var defaultData = {
name: 'Default Name',
message: 'Hello World Default'
};
helloWorldModel = ko.mapping.fromJS(defaultData);
ko.applyBindings(helloWorldModel);
$('#CallServerButton').click(getDataFromServer);
});
function getDataFromServer() {
$.getJSON("HelloSpring/SayJsonHello/chicken.json", function(data) {
mapServerData(data);
});
}
function mapServerData(serverData) {
alert(JSON.stringify(serverData));
ko.mapping.fromJS(serverData, helloWorldModel);
}
This code works and provides the expected behavior
You can't just overwrite your model by reassigning it this way.
When you do:
ko.applyBindings(helloWorldModel);
You are saying "bind the model helloWorldModel to the page". Knockout then goes through and hooks up the observables in that model and binds them with the page.
Now when you overwrite your form model here:
helloWorldModel = ko.mapping.fromJS(serverData, helloWorldModel);
It is overwriting your model object with a brand new object with entirely new observables in it.
To fix it you need to change this line to just:
ko.mapping.fromJS(serverData, helloWorldModel);
This takes care of the properties inside the model and reassigns them for you, without overwriting your model.
Related
I've built an app that is form-based. I want to enable users to partially fill out a form, and then come back to it at a later date if they can't finish it at the present. I've used iron router to create a unique URL for each form instance, so they can come back to the link. My problem is that Meteor doesn't automatically save the values in the inputs, and the form comes up blank when it is revisited/refreshes. I tried the below solution to store the data in a temporary document in a separate Mongo collection called "NewScreen", and then reference that document every time the template is (re)rendered to auto fill the form. However, I keep getting an error that the element I'm trying to reference is "undefined". The weird thing is that sometimes it works, sometimes it doesn't. I've tried setting a recursive setTimeout function, but on the times it fails, that doesn't work either. Any insight would be greatly appreciated. Or, if I'm going about this all wrong, feel free to suggest a different approach:
Screens = new Meteor.Collection('screens') //where data will ultimately be stored
Forms = new Meteor.Collection('forms') //Meteor pulls form questions from here
NewScreen = new Meteor.Collection('newscreen') //temporary storage collection
Roles = new Meteor.Collection('roles'); //displays list of metadata about screens in a dashboard
//dynamic routing for unique instance of blank form
Router.route('/forms/:_id', {
name: 'BlankForm',
data: function(){
return NewScreen.findOne({_id: this.params._id});
}
});
//onRendered function to pull data from NewScreen collection (this is where I get the error)
Template.BlankForm.onRendered(function(){
var new_screen = NewScreen.findOne({_id: window.location.href.split('/')[window.location.href.split('/').length-1]})
function do_work(){
if(typeof new_screen === 'undefined'){
console.log('waiting...');
Meteor.setTimeout(do_work, 100);
}else{
$('input')[0].value = new_screen.first;
for(i=0;i<new_screen.answers.length;i++){
$('textarea')[i].value = new_screen.answers[i];
}
}
}
do_work();
});
//onChange event that updates the NewScreen document when user updates value of input in the form
'change [id="on-change"]': function(e, tmpl){
var screen_data = [];
var name = $('input')[0].value;
for(i=0; i<$('textarea').length;i++){
screen_data.push($('textarea')[i].value);
}
Session.set("updateNewScreen", this._id);
NewScreen.update(
Session.get("updateNewScreen"),
{$set:
{
answers: screen_data,
first: name
}
});
console.log(screen_data);
}
If you get undefined that could mean findOne() did not find the newscreen with the Id that was passed in from the url. To investigate this, add an extra line like console.log(window.location.href.split('/')[window.location.href.split('/').length-1], JSON.stringify(new_screen));
This will give you both the Id from the url and the new_screen that was found.
I would recommend using Router.current().location.get().path instead of window.location.href since you use IR.
And if you're looking for two way binding in the client, have a look at Viewmodel for Meteor.
Been getting into Knockout and and slowly getting used to it. Trying to use it in a new project, but am having a hard time getting things lined up to work. While I understand and can do simple examples (simple form with text boxes bound to ko.observables, or a table or list bound to a ko.observableArray), I can't get the syntax right for a combination, especially if I want to convert the data to JSON format in order to transmit it, via a webservice, to be saved into a database.
Basically it's a data entry form, with some text entry boxes, then a list of items (think company information + a list of it's employees).
I have a sample Fiddle here: http://jsfiddle.net/rhzu6/
In the saveData function, I just don't know what to do to get the data packaged. Doing ko.toJS(self) just shows "Object".
I tried defining the data as objects, but quickly got lost:
function Company(CompanyName, ZipCode) {
var self = this;
self.ZipCode = ko.observable(ZipCode);
self.CompanyName = ko.observable(CompanyName );
self.Employees = ko.observableArray();
}
function Employee(FirstName, LastNameB) {
var self = this;
self.FirstName = ko.observable(FirstName);
self.LastName = ko.observable(LastName);
}
Then the ViewModel looked like:
function viewModel() {
var self = this;
self.Company = ko.observable(); // company?
self.Employees = ko.observableArray(); // ?
}
But ran into the same issue. And also had binding problems - data-bind:"value: CompanyName" threw an exception saying it didn't know what CompanyName was...
Color me stumped. I'm sure it's something easy that I'm just missing.
Any and all help would be appreciated!
Thanks
You are looking for ko.toJSON which will first call ko.toJS on your ViewModel and afterwards JSON.stringify.
ko.toJS will convert your knockout model to a simple JavaScript object, hence replacing all observables etc. with their respective values.
I updated your Fiddle to demonstrate.
For more info, take a look at this post from Ryan Niemeyers blog.
An alternative is to make use of ko.utils.postJson:
ko.utils.postJson(location.href, {model: ko.toJS(viewModel) });
Notice the ko.toJS again.
It looks to me as if you (semantically) want to submit a form. Therefore, I think that you should use the submit binding. The biggest benefit is that you listen to the submit event, which allows submit by other means, such as Ctrl+Enter or any other keyboard combination you want.
Here is an example on how that submitEvent handler could look like. Note that it uses ko.mapper, which is a great way to create a viewModel from any JS/JSON-object you want. Typically, you would have
[backend model] -> serialization -> [JS/JSON-ojbect] -> ko.mapper.fromJSON(obj) -> knockout wired viewModel.
viewModel.submitEvent = function () {
if (viewModel.isValid()) { //if you are using knockout validate
$.ajax(
{
url: '/MyBackend/Add',
contentType: 'application/json',
type: 'POST',
data: ko.mapping.toJSON(viewModel.entityToValidateOnBackend),
success: function (result) {
ko.mapping.fromJSON(result, viewModel);
}
}
);
}
};
Good luck!
I've got a "LocalStore" object for storing data locally. It's based around a Lawnchair object.
var LocalStore = function(name) {
var that = this;
that.name = name;
that.lawnchair = Lawnchair({ name: that.name }, function(store) {
this.before('save', function(record){
console.log("saving " + that.name);
console.log(record);
});
this.after('save', function(record){
console.log("saved " + that.name);
console.log(record);
that.getData(function(records){
console.log("now it's this");
console.log(records);
});
});
});
that.getData = function(callback) {
that.lawnchair.get(that.name, callback);
};
};
LocalStore is then extended with _.extend(from the Underscore.js library) with this method:
save: function(collection, callback) {
this.lawnchair.save({ key:this.name, value: collection }, function(record) {
callback(record);
});
}
This code is used to update a Backbone.js Collection object to Lawnchair. The first time "save" runs for my Users Collection it saves correctly and shows that the object is a simple key/value pair where value is an Array.
Later in my code when a User selects a Default Project, I modify the Users Collection and call "save" again with an updated "defaultProjectId" on the User. The code runs error free, but the after('save') code for Lawnchair runs and shows me that:
- The record object returned is a key/value pair where value is a full Backbone.js Collection with the defaultProjectId property set correctly.
- The getData method that grabs the latest from the Database still shows as a key/value pair with value a simple Array and defaultProjectId is set incorrectly.
I'm at a loss as what to do. It should just be simply calling "lawnchair.save" updates the record, but it just doesn't do it.
Could you try this jsfiddle?
http://jsfiddle.net/QUgtg/1/
I have recreated your code. Instead of a backbone collection, I am passing in an array of objects. This seems to work. You can see the logging output in Firebug.
I have used my own extend code to add the save(). Though honestly, I don't see why you would want to do it that way, instead of just adding a property to the prototype. Your code may differ in that aspect.
If what I have posted works on your end, could you modify that code to show what are you doing differently? If possible, recreate the issue on jsfiddle...
I may be completely missing something here, but I have the following:
a Model which encapsulates 'all' the data (all JSON loaded from one URL)
the model has one (or more) Collections which it is instantiating with the data it got on construction
some code which I want to run on the Collection when the data is initialized and loaded
My question is about the composed Collection. I could do this outside the scope of the Collection, but I'd rather encapsulate it (otherwise what's the point of making it a 'class' with an initializer etc).
I thought I could put that code in the initialize() function, but that runs before the model has been populated, so I don't have access to the models that comprise the collection (this.models is empty).
Then I thought I could bind to an event, but no events are triggered after initialization. They would be if I loaded the Collection with a fetch from its own endpoint, but I'm not doing that, I'm initializing the collection from pre-existing data.
My question: How to get initialize code to run on the Collection immediately after it is initialized with data (i.e. this.models isn't empty).
Is it possible to do this without having to get 'external' code involved?
Okay here is the demo code, perhaps this will explain things better.
var Everything = Backbone.Model.extend({
url: "/static/data/mydata.json",
parse: function(data)
{
this.set("things", new Things(data.things, {controller: this}));
}
});
var Thing = Backbone.Model.extend({
});
var Things = Backbone.Collection.extend({
model: Thing,
initialize: function(data, options)
{
// HERE I want access to this.models.
// Unfortunately it has not yet been populated.
console.log("initialize");
console.log(this.models);
// result: []
// And this event never gets triggered either!
this.on("all", function(eventType)
{
console.log("Some kind of event happend!", eventType);
});
}
});
var everything = new Everything();
everything.fetch();
// Some manual poking to prove that the demo code above works:
// Run after everything has happened, to prove collection does get created with data
setTimeout(function(){console.log("outside data", everything.get("things").models);}, 1000);
// This has the expected result, prints a load of models.
// Prove that the event hander works.
setTimeout(function(){console.log("outside trigger", everything.get("things").trigger("change"));}, 1000);
// This triggers the event callback.
Unfortunately for you the collection gets set with data only after it was properly initialized first and models are reset using silent: true flag which means the event won't trigger.
If you really wanted to use it you can cheat it a bit by delaying execution of whatever you want to do to next browser event loop using setTimeout(..., 0) or the underscore defer method.
initialize: function(data, options) {
_.defer(_.bind(this.doSomething, this));
},
doSomething: function() {
// now the models are going to be available
}
Digging this an old question. I had a similar problem, and got some help to create this solution:
By extending the set function we can know when the collection's data has been converted to real models. (Set gets called from .add and .reset, which means it is called during the core function instantiating the Collection class AND from fetch, regardless of reset or set in the fetch options. A dive into the backbone annotated source and following the function flow helped here)
This way we can have control over when / how we get notified without hacking the execution flow.
var MyCollection = Backbone.Collection.extend({
url: "http://private-a2993-test958.apiary-mock.com/notes",
initialize: function () {
this.listenToOnce(this, 'set', this.onInitialized)
},
onInitialized:function(){
console.log("collection models have been initialized:",this.models )
},
set: function(models,options){
Backbone.Collection.prototype.set.call(this, models, options);
this.trigger("set");
}
})
//Works with Fetch!
var fetchCollection= new MyCollection()
fetchCollection.fetch();
//Works with initializing data
var colData = new MyCollection([
{id:5, name:'five'},
{id:6, name:'six'},
{id:7, name:'seven'},
{id:8, name:'eight'}
])
//doesn't trigger the initialized function
colData.add(new Backbone.Model({id:9,name:'nine'};
Note: If we dont use .listenToOnce, then we will also get onInitialized called every time a model is added to or changed in the collection as well.
Backstory: To specify the correct route for a jqGrid that I show on my ASP.NET MVC 3 page, I do something like so:
$('#jqgFlavors').jqGrid({
url: '#Url.Action("FlavorData", "IceCream")',
etc...
and that will produce the correct route either when running locally out of Visual Studio (where things live at something like "http://localhost:90125/IceCream" or on the deployed site where things live at something like "http://thehostsite/mydeployedsitename/IceCream".
Great. Now the issue I'm having is that I use the onSelectRow in the grid to do a master/details thing based on the selected row's flavor id value. First, I tried doing this to just get the route correct:
onSelectRow: function(theRow){
$('#flavorDetails').load('#Url.Action("Details","IceCream", new {id = 42)})');
}
So that I can pass the value 42 in as the 'id' parameter in the Details action of the IceCream controller. And that works fine, but of course I don't want to hard code the value 42, rather pull the flavor id from the grid itself. So I have tried to reference the flavorID but can't seem to get the syntax correct:
onSelectRow: function(theRow){
var grid = jQuery('#jqgFlavors');
var flavorID = grid.jqGrid('getCell', theRow, 'FlavorID');
$('#flavorDetails').load('#Url.Action("Details","IceCream", new {id = flavorID)})');
}
I'm sure you get what I'm going for here - referencing the flavorID value I extract from the grid. But what I get is a compilation error:
The name 'flavorID' does not exist in the current context.
I suspect this is really simple. How do I reference correctly that variable?
You could use the second argument of the .load() method which allows you to pass additional parameters:
var flavorID = grid.jqGrid('getCell', theRow, 'FlavorID');
$('#flavorDetails').load('#Url.Action("Details", "IceCream")', { id: flavorID });
This might probably use the following url: /IceCream/Details?id=123 instead of what you might want /IceCream/Details/123 because javascript doesn't know anything about your routes but why care? It will still map correctly to the controller action:
public ActionResult Details(int id)
{
...
}
But if you are really anal about urls and insist on having the first type of url I've seen people doing the following:
var flavorID = grid.jqGrid('getCell', theRow, 'FlavorID');
var url = '#Url.Action("Details", "IceCream", new { id = "_TOREPLACE_" })';
url = url.replace('_TOREPLACE_', flavorID);
$('#flavorDetails').load(url);
Personally I wouldn't do it but providing it just for the record.