I have a VM with the following:
var myArray = ko.observableArray(),
....
addItem = function(data) {
myArray.unshift(data);
},
In my View I have some HTML that binds to it:
<div data-bind="foreach: myArray">
....<label data-bind:="attr: { id: 'myLabel_' + name }"></Label>
</div>
Everything works great - when I call the addItem function an item gets added to my obserableArray() and Knockout adds the appropriate HTML.
The problem I'm facing now is when, instead of just using plain HTML, I try to use a more complex control (like a Kendo grid). I need to call a Javascript method to initiate the Grid when I call newItem. But if I do that, it doesn't work (I assume because Knockout hasn't 'finished' doing what it does, when the Kendo control tries to do what it needs to do).
The following doesn't work:
var myArray = ko.observableArray(),
....
addItem = function(data) {
myArray.unshift(data);
var test = $('#myLabel_nameOne');
// Kendo specific code to setup test
},
If I run the same Kendo specific code from the console - it works great. If I use a setInterval, that will work too.
I know there is an 'afterRender' I can hook into on an entire template - but I'm just looking to know when the UI is done after adding a single item into that observableArray.
What's the proper way to say 'Wait until this is really added, then go do this other stuff?' Or 'Add this, and when the UI is ready, call this function for me'?
I've hit asynchronous issues with frameworks other than Knockout and usually solved them by writing wrappers with jQuery and the Deferred() object.
After reading Knockout documentation you can just use the subscribe function.
var myArray = ko.observableArray()
myArray.subscribe(function(data) {
var test = $('#myLabel' + data.name);
// Kendo specific code to setup test
});
....
addItem = function(data) {
myArray.unshift(data);
},
Related
I started developping a website using backbone.js and after trying during the whole morning, i'm quite stuck on the following problem.
I output here only the relevant code.
I've a View called Navigator, that contains a Collection of Records (initially empty) :
var NavigatorView = Backbone.View.extend({
template: JST['app/scripts/templates/Navigator.ejs'],
tagName: 'div',
id: '',
className: 'saiNavigator',
events: {},
initialize: function () {
this.currentRecords = new RecordsCollection();
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this));
},
onRecordsCollectionReseted: function(){
this.render();
},
render: function () {
var tplResult = this.template({
computeTemplate: this.computeTemplate,
records: this.currentRecords
});
this.$el.html(tplResult);
},
onDOMUpdated: function(){
var me = this;
var data = {
device : 'web',
gridId : this.model.get('gridId'),
filterId : this.model.get('filterId')
};
$.ajax({
url: App.getTokenedUrl() + '/task/getGridData.'+this.model.get('taskId')+'.action',
success: me.onRecordReceived.bind(me),
statusCode: {
500: App.handleInternalError
},
type: 'GET',
crossDomain: true,
data : data,
dataType: 'json'
});
},
onRecordReceived: function(result){
var newRecords = [];
for(var i = 0; i < result.items.length; i++){
var newRecord = new RecordModel(result.items[i]);
newRecords.push(newRecord);
}
this.currentRecords.reset(newRecords);
}
});
I've a View called dossier which html is
<div id="dossier1" class="dossier">
<div id="dossier1-navContainer" class="navigatorContainer"/>
<div class="pagesNavigatorContainer"/>
<div class="pagesContainer"/>
<div class="readOnlyFiche"/>
</div>
When i first render the dossier (and i render it only once) i create the navigator in the following render function
render: function () {
this.$el.html(this.template({
uniqBaseId: this.id,
className: this.className
}));
var nav = this.navigator = new NavigatorView({
model : this.model,
id: this.id+'navigator',
el: $('#'+this.id+'-navContainer')
});
this.navigator.render();
//We notify the navigator that it's ready. This will allow the nav to load records
nav.onDOMUpdated();
}
}
As we can see, i give the '#dossier1-navContainer' id to the navigator so that he renders there
So, here is how it works. When i render the dossier, it creates a navigator and inserts it in the DOM. When done, i notify the navigator that it can load its data from the server trough ajax request. When i receive the answer i reset the collection of data with the incoming record.
Juste before the this.$el.html(tplResult) in the navigator render function i output the resulting string.
First time it's
<div class="items"></div>
Second time when i get records, it's
<div class="items">
<div>item1</div>
<div>item2</div>
<div>item3</div>
</div>
So the template generation is correct. However, when the second rendering occurs, the this.$el.html(tplResult) does NOTHING. If i look at the DOM in the browser NOTHING CHANGED
However if i replace this line by
$('#dossier1-navigator').html(tplResult)
it works. Which means that the first time, $('#dossier1-navigator') and this.$el are the same object, the second time not.
I've NO idea why it doesn't work the second time with the standard this.$el.
Help!!
Thanks in advance
Edit : after discussing a lot with Seebiscuit, i'm adding the few lines that helped answering the question
newTask.render();
var taskHtml = newTask.$el.html();
$('#mainTaskContainer').append(taskHtml);
My hunch is that your having a binding problem. I would suggest that you replace
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this)); },
in your initialize, with:
this.listenTo(this.currentRecords, "reset", this.render);
No need to specially bind. Backbone's listenTo bids the callback to the Backbone object that sets the listener (the this in this.listenTo). Also has the added benefit that when you close the view (by calling this.remove()) it'll remove the listener, and help you avoid zombie views.
Try it out.
I think the problem is that you are not using what your are passing to your navigatorView;
In your navigatorView try this:
initialize:function(el) {
this.$el=el
...
}
Let me know if it helps
After countless minutes of discussion with seebiscuit, we came up with the solution. The problem is all on the definition of the $el element. The formal definition defines it as
A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time
This is actually not very exact from a standard cache point of view. From my point of view at least the principle of a cache is to look for the value if it doesn't have it, and use it otherwise. However in this case this is NOT the case. As Seebiscuit told me,
Because when you first bound this.$el = $(someelement) this.$el will always refer to the return of $(someelement) and not to $(someelement). When does the difference matter?
When the element is not in the DOM when you do the assignment
So actually, $el holds the result of the first lookup of the selector. Thus, if the first lookup misses then it won't succeed ever! Even if the element is added later.
My mistake here is to add the main dossierView into the DOM after rendering its NavigatorView subview. I could have found the solution if the $el was a real cache as the 2nd rendering in the ajax callback would have found the element. With the current way $el works i had just nothing.
Conclusion : make sure every part of your view is properly rendered in the DOM at the moment your try to render a subview.
I'm creating a bug-handling program in KnockoutJS, and I've run into an issue. I have a list of "bug" reports on my main page that is displayed using an observable array. When I click "View" on one of the bug reports in the list, I have a Bootstrap modal pop up. I want to populate the contents of the modal with report details, but for some reason the viewmodel is not getting passed in correctly.
Here is my view model, along with my ajax mapping below that:
function BugsViewModel(doJSON) { //doJSON determines whether to populate the observable with JSON or leave it empty
var self = this;
self.bugs = ko.observableArray();
self.removeBug = function (bug) {
$.ajax({
url: "/ajax/getbugs.ashx?deleteBug=" + bug.Id()
})
self.bugs.remove(bug);
}
self.currentBug = ko.observable(); // <--this is the bug that I want to reference in the modal
self.setModalBug = function (obj, event) { //obj is the individual array item
self.currentBug = obj; //setting currentBug so that I can use it in my view
alert(self.currentBug.Id()); //<--these alert properly, so I know that the value I'm passing in (obj) is correct
alert(self.currentBug.BugName());
}
if(doJSON)
getViewModelJSON(self.bugs); //this function loads my array (self.bugs) with observable items
}
function getViewModelJSON(model) {
$.ajax({
url: "/ajax/getbugs.ashx"
})
.done(function (data) {
ko.mapping.fromJSON(data, {}, model);
});
}
$(document).ready(function () {
viewModel = new BugsViewModel(true);
ko.applyBindings(viewModel);
});
I have my "View" button, which calls "setModalBug" and opens the modal:
View
Then, inside my details modal, I have the textbox I want to populate with data. This is where I'm having the problem--currentBug.BugName is not populating the value correctly.
<input type="text" id="txtBugName" data-bind="textInput: currentBug.BugName" />
(Note that I am using the Mapping plugin for Knockout, so even though you don't see "BugName" defined in my viewmodal, it is being generated from the JSON when I call "ko.mapping.fromJSON()".)
I'm a little befuddled. Is this a runtime issue, or am I calling the viewmodel improperly? Or something entirely different?
Thanks
You're not assigning your value to the observable correctly. You're doing:
self.currentBug = ko.observable();
self.currentBug = obj;
The first line creates an observable, but the second line throws it away and replacing it with the bug object. You want to assign to the observable, not throw it away, like:
self.currentBug = ko.observable();
self.setModalBug = function (obj, event) {
self.currentBug(obj); //<-- Assign the current bug the currentBug observable
alert(self.currentBug().Id()); //note the additional () to read the value of the observable
alert(self.currentBug().BugName());
}
I got it working. Apparently in my code I actually didn't add the extra '()' to my alerts. Sorry--not my best moment. Now it works:
self.currentBug(obj);
alert(self.currentBug().Id());
alert(self.currentBug().BugName());
Then I added an extra '()' to my textInput data-bind:
<input type="text" id="txtBugName" data-bind="textInput: currentBug().BugName" />
I didn't realize that I had to use '()' anywhere on data-binds. I thought it figured out that you were working with observables and treated it that way (which it does on the property "BugName", but I had to access currentBug with '()' at the end).
Thanks, Retsam! I spent quite a lot of time on this and it was getting frustrating. Turns out it was a really simple fix.
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.
I'm trying to write a plugin-like function in jQuery to add elements to a container with AJAX.
It looks like this:
$.fn.cacheload = function(index) {
var $this = $(this);
$.get("cache.php", {{ id: index }).done(function(data) {
// cache.php returns <div class='entry'>Content</div> ...
$(data).insertAfter($this.last());
});
}
and I would like to use it like this:
var entries = $("div.entry"),
id = 28;
entries.cacheload(id);
Think that this would load another "entry"-container and add it to the DOM.
This is works so far. But of course the variable that holds the cached jQuery object (entries) isn't updated. So if there were two divs in the beginning and you would add another with this function it would show in the DOM, but entries would still reference the original two divs only.
I know you can't use the return value of get because the AJAX-call is asynchronous. But is there any way to update the cached object so it contains the elements loaded via AJAX as well?
I know I could do it like this and re-query after inserting:
$.get("cache.php", {{ id: num }).done(function(data) {
$(data).insertAfter($this.last());
entries = $("div.entry");
});
but for this I would have to reference the variable holding the cached objects directly.
Is there any way around this so the function is self-contained?
I tried re-assigning $(this), but got an error. .add() doesn't update the cached object, it creates a new (temporary) object.
Thanks a lot!
// UPDATE:
John S gave a really good answer below. However, I ended up realizing that for me something else would actually work better.
Now the plugin function inserts a blank element (synchronously) and when the AJAX call is complete the attributes of that element are updated. That also ensures that elements are loaded in the correct order. For anyone stumbling over this, here is a JSFiddle: http://jsfiddle.net/JZsLt/2/
As you said yourself, the ajax call is asynchronous. Therefore, your plugin is asynchronous as as well. There's no way for your plugin to add the new elements to the jQuery object until the ajax call returns. Plus, as you discovered, you can't really add to the original jQuery object, you can only create a new jQuery object.
What you can do is have the plugin take a callback function as a second parameter. The callback could be passed a jQuery object that contains the original elements plus the newly inserted ones.
$.fn.cacheload = function(index, callback) {
var $this = this;
$.get('cache.php', { id: index }).done(function(html) {
var $elements = $(html);
$this.last().after($elements);
if (callback) {
callback.call($this, $this.add($elements));
}
});
return $this;
};
Then you could call:
entries.cacheload(id, function($newEntries) { doSomething($newEntries); } );
Of course, you could do this:
entries.cacheload(id, function($newEntries) { entries = $newEntries; } );
But entries will not be changed until the ajax call returns, so I don't see much value in it.
BTW: this inside a plugin refers to a jQuery object, so there's no need to call $(this).
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!