What are the implications/potential problems of completing a binding asynchronously?
Consider the following binding:
ko.bindingHandlers.widget = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel)
{
var values = valueAccessor();
//depending on type, delegate binding work to another function
factory.getWidget(values.type)
.done(function (widget) {
widget.bind(element, values.settings, allBindingsAccessor, viewModel);
});
//widget.bind is responsible for all children
return { controlsDescendantBindings: true };
}
};
What are the potential problems that may occur as a result of the init function returning before the binding "work", which includes DOM manipulation, has actually been completed?
I can't clearly see any specific scenario that would cause a problem, but I want to see if anyone else knows something in the way knockout works that may cause problems later.
I was recently working on a similar problem. I have created a custom binding for jsTree that I'm using in my web application. So the UI is -> root -> children -> children's children and so on (on the left)... And for each entity, there is a table that shows some related data (on the right) and a form field showing the fields of the entity. Also, I'm loading both the tree and the data table asynchronously hitting the server with an AJAX call each time the user clicks on a tree node. However, I do have both the init and update methods filled out in my binding handlers.
To help you understand the context a little better, lets take USA as the root node in my tree, USA has many states (root's children) and each state has many counties and so on. Many of the fields inside the root object (USA) will most likely be different than the fields inside its children entities (states, in this case). I'm sorry I can't think of fields that would be there in USA and not in a State, but I'm hoping you get the point.
Also, I have an observable called 'currentSelectedObject' in my VM that gets updated each time the user clicks on a different node in the tree, I make an AJAX call to get the object, and I have a method called replace current object that handles changing the current selected object.. And as mentioned before, parent and child objects have (mostly) different fields. Here's where I ran into a lot of problems. In my html I bind the values of the element like this:
data-bind: value: currentSelectedObject().xyz
Now lets say the user clicks on a different object that doesn't have the field 'xyz,' I used to get this error:
can't read property xyz of undefined.
I got that error because for an extremely small time, the currenySelectedObject has changed and the view (the HTML template) hasn't. I thought the change would be atomic but apparently it isn't. The way I fixed this was by actually updating the current object in the success callback of my AJAX call.
self.replaceCurrentObject = function(type, id, callback){
server.getObject(type, id, // server.js is an abstraction over the data layer
function(returnObject){ // corresponds to: success: function(data){...}
self.currentSelectedObject(returnObject);
}
if(callback) callback(); // to handle other table related stuff
}
Related
I have several jquery dom events that are created on DOM load or document ready. These are mostly default behaviors that should be applied to all forms in my application. Example:
$('input:text').focus(function ()
{
$(this).select();
});
Right before applying knockout binding, I can check my dom elements and all events are there:
But when I run the applyBindings method to bind the viewmodel to my DOM, the "with" binding removes all events that are not related to knockout:
I have tried overwriting the cleanExternalData as explained on the documentation and on this answer. But that did not help with this, the function is replaced, but the events are still removed from the DOM when the templating is applied on the binding process.
For the record, this is not an exclusive behavior of the with function, but all anonymous templating functions also do that, foreach, if, ifnot. Using template, as expected, also behaves the same way. The DOM element is completely destroyed, stored as a template, then added again on my document when the condition is satisfied, but now without any jquery event handlers.
How to avoid that knockout removes the events from my DOM elements?
Instead of binding elements to a specific node, you can use a databinding to use the jquery on() functionality to handle events. Here's a binding I use:
define(['knockout'], function (ko) {
ko.bindingHandlers.eventListener = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var params = ko.utils.unwrapObservable(valueAccessor());
if (!(params instanceof Array)) {
params = [params];
}
params.forEach(function (param) {
$(element).on(param.event, param.selector, function (event) {
param.callback(ko.dataFor(this), ko.contextFor(this), event);
});
});
}
}
});
Usage:
<div data-bind="eventListener: [
{ event: 'click', selector: '.copyInclusionRule', callback: copyInclusionRule},
{ event: 'click', selector: '.deleteInclusionRule', callback: deleteInclusionRule}]">
... other knockout template stuff here ...
</div>
The above will listen for click events on either an element with the specified class and perform the callback when the event is received for anything within the div's 'scope'. The value of 'event' param can be anything that on() uses.
I think the reason why you can't leverage the cleanNode overrides is that your dom is being completely destroyed and re-created..at least that's my theory, if there was a way to get some kind of memory ID of the pre-applyBindings() dom elements and then after the applyBindings is called, are those new nodes? If they are new nodes, it's not something you can't fix with cleaning, those nodes are gone.
Alright, here is how I fixed my problem and I hope this can clarify things to others that don't want to destroy their DOM as well. If you don't want that knockout to destroy your DOM, that is possible since version 2.2. And thus, destroying the DOM when that is not necessary is not intended behavior and can be avoided.
I had tried several bidings created by Michael Best before, like his using binding that will come in knockout 3.5, and let or withLight (which became using now). None really worked. These simplified bidings would load the initial object, but not update the dom when this object properties had changed.
But this helped me to figure out what I am doing wrong. When I wanted to update my observable object, I was using myViewModel.observableObject(NewObject), like the documentation told me to do:
To write a new value to the observable, call the observable and pass the new value as a parameter. For example, calling myViewModel.personName('Mary') will change the name value to 'Mary'.
But I wasn't passing a single property's value, I was passing a new object that had the same structure (same properties). And this triggered knockout that the old object was destroyed (and thus, falsy for a second) and a new object took its place, even though all properties are there, they just got different values. Unlike the documentation told me, it didn't simply changed the value, but changed the entire object.
To go around this, instead of doing that, First, I had to initiate my viewModel with this object already created, using dummy data, this makes knockout not destroy the DOM when applyBindings is called. Then, when I want my object to update, I replaced the value of each property of the observable object to have the value of the new object. This didn't destroy the object and knockout updated my binding properly.
myViewModel.setSelectedItem = function setSelectedItem (newObject)
{
for (var prop in myViewModel.myObservableObject())
myViewModel.myObservableObject()[prop](newObject[prop]);
}
The with binding still killed some of my events (my angular ng-change for one of my components, for instance), but it kept all jquery events in there (which is great). And the using binding didn't kill any of my events at all (which is even better).
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
So here's a weird KnockoutJS problem I've never actually come across before.
I'm working on an application that uses Knockout Components very heavily.
In one part of the app, I have an editor page that's built dynamically from a JSON driven backend, and which populates a front end page with a number of widgets, depending on what it's told from the back end data.
Example
The back end might send
[{"widget": "textBox"},{"widget": "textBox"},{"widget": "comboBox"},{"widget": "checkBox"}]
Which would cause the front end to build up a page containing
<html>
....
<textbox></textbox>
<textbox></textbox>
<combobox></combobox>
<checkbox></checkbox>
....
</html>
Each of the custom tags is an individual KnockoutJS component, compiled as an AMD module and loaded using RequireJS, each component is based on the same boiler plate:
/// <amd-dependency path="text!application/components/pagecontrols/template.html" />
define(["require", "exports", "knockout", 'knockout.postbox', "text!application/components/pagecontrols/template.html"], function (require, exports, ko, postbox) {
var Template = require("text!application/components/pagecontrols/template.html");
var ViewModel = (function () {
function ViewModel(params) {
var _this = this;
this.someDataBoundVar = ko.observable("");
}
ViewModel.prototype.somePublicFunction = function () {
postbox.publish("SomeMessage", { data: "some data" });
};
return ViewModel;
})();
return { viewModel: ViewModel, template: Template };
});
The components communicate with each other and with the page using "Knockout Postbox" in a pub sub fashion.
And when I put them into the page I do so in the following manor:
<div data-bind="foreach: pageComponentsToDisplay">
<!-- ko if: widget == "textBox" -->
<textBox params="details: $data"></textBox>
<!-- /ko -->
<!-- ko if: widget == "comboBox" -->
<comboBox params="details: $data"></comboBox>
<!-- /ko -->
<!-- ko if: widget == "checkBox" -->
<checkBox params="details: $data"></checkBox>
<!-- /ko -->
</div>
and where pageComponentsToDisplay is a simple knockout observable array that I just push the objects received from the backend onto:
pageComponentsToDisplay = ko.observableArray([]);
pageComponentsToDisplay(data);
Where 'data' is as shown in JSON above
Now all of this works great, but here-in now lies the ODD part.
If I have to do a "reload" of the page, I simply
pageComponentsToDisplay = ko.observableArray([]);
to clear the array, and consequently, all my components also disappear from the page, as expected, however when I load the new data in, again using:
pageComponentsToDisplay(data);
I get my new components on screen as expected, BUT the old ones appear to be still present and active in memory, even though there not visible.
The reason I know the controls are still there, because when I issue one of my PubSub messages to ask the controls for some state info, ALL of them reply.
It seems to me that when I clear the array, and when KO clears the view model, it actually does not seem to be destroying the old copies.
Further more, if I refresh again, I then get 3 sets of components responding, refresh again and it's 4, and this keeps increasing as expected.
This is the first time I've encountered this behaviour with knockout, and I've used this kind of pattern for years without an issue.
If you want a good overview of how the entire project is set up, I have a sample skeleton layout on my github page:
https://github.com/shawty/dotnetnotts15
If anyone has any ideas on what might be happening here I'd love to hear them.
As a final note, I'm actually developing all this using Typescript, but since this is a runtime problem, I'm documenting it from a JS point of view.
Regards
Shawty
Update 1
So after digging further (and with a little 'new thinking' thanks to cl3m's answer) I'm a little bit further forward.
In my initial post, I did mention that I was using Ryan Niemeyer's excelent PubSub extension for Knockout 'ko postbox'.
It turn's out, that my 'Components' are being disposed of and torn down BUT the subscription handlers that are being created to respond to postbox are not.
The result is, that the VM (or more specifically the values that the subscription uses in the VM) are being kept in memory, along with the postbox subscription handler.
This means when my master broadcasts a message asking for component values, the held reference responds, followed by the visibly active component.
What I need to now do is figure out a way to dispose these subscriptions, which because I'm using postbox directly, and NOT assigning them to an observable in my model, means I don't actually have a var or object reference to target them with.
The quest continues.
Update 2
See my self answer to the question below.
I'm not sure this will help but, as per my comment, here is how I use the ko.utils.domNodeDisposal.addDisposeCallback() in my custom bindings. Perhaps there is a way to use it in knockout components:
ko.bindingHandlers.tooltip = {
init: function(element, valueAccessor) {
$(element).tooltip(options);
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).tooltip('destroy');
});
}
}
More reading on Ryan Niemeyer's website
The problem it seems was due to Knockout hanging onto subscriptions set up by postbox when the actual components where active.
In my case, I use postbox purely as a messaging platform, so all i'm doing is
ko.postbox.subscribe("foo", function(payload) { ... });
all the time, since I was only ever using single shot subscriptions in this fashion, I was never paying ANY Attention to the values returned by the postbox subscription call.
I did things this way, simply because in many of the components I create there is a common API that they all use, but to which they all respond in different ways, so all I ever needed was a simple this is what to do when your called handler that was component specific, but not application specific.
It turns out however that when you use postbox in this manner, there is no observable for you to target, and as such there is nothing to dispose. (Your not saving the return, so you have nothing to work with)
What the Knockout and Postbox documentation does not mention, is that the return value from postbox.subscribe is a general Knockout subscription function, and by assigning the return from it to a property within your model, you then have a means to call the functionality available on it, one of those functions provides the ability to "dispose" the instance, which NOT ONLY removes the physical manifestation of the component from it's collection, BUT ALSO ensures that any subscriptions or event handlers connected to it are also correctly torn down.
Couple with that, the fact that you can pass a dispose handler to your VM when you register it, the final solution is to make sure you do the following
/// <amd-dependency path="text!application/components/pagecontrols/template.html" />
define(["require", "exports", "knockout", 'knockout.postbox', "text!application/components/pagecontrols/template.html"], function (require, exports, ko, postbox) {
var Template = require("text!application/components/pagecontrols/template.html");
var ViewModel = (function () {
function ViewModel(params) {
var _this = this;
this.someDataBoundVar = ko.observable("");
this.mySubscriptionHandler = ko.postbox.subscribe("foo", function(){
// do something here to handle subscription message
});
}
ViewModel.prototype.somePublicFunction = function () {
postbox.publish("SomeMessage", { data: "some data" });
};
return ViewModel;
ViewModel.prototype.dispose = function () {
this.mySubscriptionHandler.dispose();
};
return ViewModel;
})();
return { viewModel: ViewModel, template: Template, dispose: this.dispose };
});
You'll notice that the resulting class has a "dispose" function too, this is something that KnockoutJS provides on component classes, and if your class is managed as a component by the main KO library, KO will look for and execute if found, that function when your component class goes out of scope.
As you can see in my example, Iv'e saved the return from the subscription handler as previously mentioned, then in this hook point that we know will get called, used that to ensure that I also call dispose on each subscription.
Of course this ONLY shows one subscription, if you have multiple subscriptions, then you need multiple saves, and multiple calls at the end. An easy way of achieving this, especially if your using Typescript as I am, is to use Typescripts generics functionality and save all your subscriptions into a typed array, meaning at the end all you need to do is loop over that array and call dispose on every entry in it.
tl;dr
How to use backbone.stickit with a html form to change an existing model fetched from the server and only PATCH the changed attributes (changed by user input within the html form) to the server?
/tl;dr
I'm using backbone.stickit in a backbone.js application to bind a model to a HTML-form which is part of a backbone view. This works fine so far, but it becomes a little bit complicated if I'm going to save the bound model. This is because I want to use the PATCH-method and only send the changed attributes to the server. I try to illustrate what I've done so far:
Fetching the model from Server
user = new User(); //instatiate a new user-model
user.fetch(); //fetching the model from the server
console.log(user.changedAttributes()); // Returns ALL attributes, because model was empty
The last line indicates my problem, because I thought I can used the changedAtrributes() method later to get the attributes which need a patch on the server. So I tried this workaround which I found here
user.fetch({
success: function (model, response, options) {
model.set({});
}
});
user.changedAtrributes(); //Returns now "false"
Do stickit-bindings
Now I render my view and call the stickit() method on the view, to do the bindings:
//Bindings specified in the view:
[...]
bindings: {
"#username" : "username"
"#age" : "age"
}
[...]
//within the render method of the view
this.stickit();
The bindings work fine and my user model gets updated, but changedAttributes() remain empty all the time.
Save the model to the server
If the user has made all required changes, the model should be saved to the server. I want to use the PATCH method and only send the changed attributes to the server.
user.save(null, {patch:true}); //PATCH method is used but ALL attributes are sent to the server
OR
user.save(user.changedAttributes(),{patch : true});
With the second approach there are different outcomes:
if I didn't use the user.set({}) woraround, all attributes get PATCHED to the server
if I use the user.set({}) woraround the return value of changedAttributes() is "false" and all attributes are PUT to the server
if I call a user.set("age","123") before calling save(), then only the age attribute is PATCHED to the server
So outcome 3 is my desired behaviour, but there are 2 problems with this: First stickit doesn't seem to use the set() method on the model to update the attributes if they are changed within the html-form. And second, if you call set() with one attribute and afterwards with another, only the second attributes is returned by changedAttributes().
Maybe I just overseen something in the backbone or backbone.stickit docs, so I didn't get the desired behaviour working. Any ideas about that?
NOTE: As found out the problem wasn't directly related to backbone.stickit, more to backbone itself.
Solved this problem on my own, maybe this helps someone who may stumble upon this question:
Backbone only keep track of unchanged attributes, but not of unsaved attributes. So with
model.changedAttributes();
you will only get the attributes of the model, which was changed since the last
model.set("some_attribute","some_value")
Finally I stumbled upon backbone.trackit which is a backbone.js plugin maintained by the creator of backbone.stickit. With this plugin you can track unsaved attributes (all attributes which have changed since the last model.save()) and then use them in the save-method of a model. Example (my usecase):
Backbone.View.extend({
bindings: {
"#name" : "name",
"#age" : "age"
},
initialize: function () {
this.model = new User();
this.model.fetch({
success: function (model, response, options) {
//this tells backbone.stickit to track unsaved attributes
model.startTracking();
}
});
},
render: function () {
this.$el.html(tmpl);
this.stickit();
return this;
},
onSaveUserToServer: function () {
//first argument: only unsaved attributes, second argument: tell backbone to PATCH
this.model.save(this.model.unsavedAttributes(), { patch: true });
});
});
I have the following html that is bound to an object containing id and status. I want to translate status values into a specific color (hence the converter function convertStatus). I can see the converter work on the first binding, but if I change status in the binding list I do not see any UI update nor do I see convertStatus being subsequently called. My other issue is trying to bind the id property of the first span does not seem to work as expected (perhaps it is not possible to set this value via binding...)
HTML:
<span data-win-bind="id: id">person</span>
<span data-win-bind="textContent: status converter.convertStatus"></span>
Javascript (I have tried using to modify the status value):
// persons === WinJS.Binding.List
// updateStatus is a function that is called as a result of status changing in the system
function updateStatus(data) {
persons.forEach(function(value, index, array) {
if(value.id === data.id) {
value.status = data.status;
persons.notifyMutated(index);
}
}, this);
}
I have seen notifyMutated(index) work for values that are not using a converter.
Updating with github project
Public repo for sample (not-working) - this is a really basic app that has a listview with a set of default data and a function that is executed when the item is clicked. The function attempts to randomize one of the bound fields of the item and call notifyMutated(...) on the list to trigger a visual updated. Even with defining the WinJS.Binding.List({ binding: true }); I do not see updates unless I force it via notifyReload(), which produces a reload-flicker on the listview element.
To answer your two questions:
1) Why can't I set id through binding?
This is deliberately prevented. The WinJS binding system uses the ID to track the element that it's binding to (to avoid leaking DOM elements through dangling bindings). As such, it has to be able to control the id for bound templates.
2) Why isn't the converter firing more than once?
The Binding.List will tell the listview about changes in the contents of the list (items added, removed, or moved around) but it's the responsibility of the individual items to notify the listview about changes in their contents.
You need to have a data object that's bindable. There are a couple of options:
Call WinJS.Binding.as on the elements as you add them to the collection
Turn on binding mode on the Binding.List
The latter is probably easier. Basically, when you create your Binding.List, do this:
var list = new WinJS.Binding.List({binding: true});
That way the List will call binding.as on everything in the list, and things should start updating.
I've found that if I doing the following, I will see updates to the UI post-binding:
var list = new WinJS.Binding.List({binding: true});
var item = WinJS.Binding.as({
firstName: "Billy",
lastName: "Bob"
});
list.push(item);
Later in the application, you can change some values like so:
item.firstName = "Bobby";
item.lastName = "Joe";
...and you will see the changes in the UI
Here's a link on MSDN for more information:
MSDN - WinJS.Binding.as
Regarding setting the value of id.
I found that I was able to set the value of the name attribute, for a <button>.
I had been trying to set id, but that wouldn't work.
HTH
optimizeBindingReferences property
Determines whether or not binding should automatically set the ID of an element. This property should be set to true in apps that use Windows Library for JavaScript (WinJS) binding.
WinJS.Binding.optimizeBindingReferences = true;
source: http://msdn.microsoft.com/en-us/library/windows/apps/jj215606.aspx