Is there a standard way to deal with non-saveable values in Backbone.
e.g.
MyModel = Backbone.extend(Backbone.Model, {
initialize: function () {
this.set({'inches': this.get('mm') / 25});
}
})
If I call save() on this model it will throw an error as there is no corresponding database field for inches. I can think of a few ways to fix this, but am wondering if there's a tried and tested approach generally best used for this?
At the moment my preferred solution is to extend Backbone's toJSON method and to allow passing of a boolean parameter dontCleanup to allow for it to still return all the model's values (including the non saveable ones) when it's needed e.g. for passing to a template.
I like Peter Lyon's idea. I've thought about that a few times, but never actually put it in place. For all the ways that I have handled this, though, here are my two favorites:
Non-"attribute" values
View Models
Non-Attribute Values
This one is simple: don't store the values you need in the model's standard attributes. Instead, attach it directly to the object:
myModel.someValue = "some value";
The big problem here is that you don't get all of the events associated with calling set on the model. So I tend to wrap this up in a method that does everything for me. For example, a common method I put on models is select to say that this model has been selected:
MyModel = Backbone.Model.extend({
select: function(){
if (!this.selected){
this.selected = true;
this.trigger("change:selected", this, this.selected);
}
}
});
In your case, I'm not sure this would be a good approach. You have data that needs to be calculated based on the values that are in your attributes already.
For that, I tend to use view models.
View models.
The basic idea is that you create a backbone model that is persist-able, as you normally would. But the you come along and create another model that inherits from your original one and adds all the data that you need.
There are a very large number of ways that you can do this. Here's what might be a very simple version:
MyModel = Backbone.Model.Extend({ ... });
MyViewModel = function(model){
var viewModel = Object.create(model);
viewModel.toJSON = function(){
var json = model.toJSON();
json.inches = json.mm / 25;
return json;
};
return viewModel;
});
The big benefit of wrapping this with Object.create is that you now have a prototypal inheritance situation, so all of your standard functionality from the model is still in place. We've just overridden the toJSON method on the view model, so that it returns the JSON object with the inches attribute.
Then in a view that needs this, you would wrap your model in the initialize function:
MyView = Backbone.View.extend({
initialize: function(){
this.model = MyViewModel(this.model);
},
render: function(){
var data = this.model.toJSON(); // returns with inches
}
});
You could call new MyViewModel(this.model) if you want, but that's not going to do anything different, in the end, because we're explicitly returning an object instance from the MyViewModel function.
When your view's render method calls toJSON, you'll get the inches attribute with it.
Of course, there are some potential memory concerns and performance concerns with this implementation, but those can be solved easily with some better code for the view model. This quick and dirty example should get you down the path, though.
I think this should do it. Define your Model defaults as your valid schema and then return only the subset of this.attributes that is valid during toJSON.
var Model = Backbone.Model.extend({
defaults: {
foo: 42,
bar: "bar"
},
toJSON: function () {
var schemaKeys = _.keys(this.defaults);
var allowedAttributes = {};
_.each(this.attributes, function (value, key) {
if (_.include(schemaKeys, key)) {
allowedAttributes[key] = value;
}
return allowedAttributes;
}
});
Note that _.pick would make the code a bit shorter once you have underscore 1.3.3 available. I haven't seen a "tried and tested" convention in my travels through the backbone community, and since backbone leaves so many options open, sometimes conventions don't emerge, but we'll see what this stackoverflow question yields.
Dealing with non-persisted attributes in Backbone.js has been doing my head in for a while, particularly since I've started using ember/ember-data, which handles the various situations through computed properties, ember-data attributes, or controllers.
Many solutions suggest customising the toJSON method. However, some popular Backbone plugins (particularly those that deal with nested models), implement their own toJSON method, and make a call to Backbone.Model.prototype.toJSON to obtain an object representation of a model's attributes. So by overwriting the toJSON method in a model definition, you'll lose some (potentially crucial) features of those plugins.
The best I've come up with is to include an excludeFromJSON array of keys in the model definition, and overwrite the toJSON method on Backbone.Model.prototype itself:
Backbone.Model.prototype.toJSON = function() {
var json = _.clone(this.attributes),
excludeFromJSON = this.excludeFromJSON;
if(excludeFromJSON) {
_.each(excludeFromJSON, function(key) {
delete json[key];
});
}
return json;
};
MyModel = Backbone.Model.extend({
excludeFromJSON: [
'inches'
]
});
In this way, you'll only have to define the non-persisted keys (if you forget to do so, you'll soon be reminded when your server throws an error!). toJSON will behave as normal if no excludeFromJSON property is present.
In your case, inches is a computed property, derived from mm, so it makes sense to implement this as a method on your model (ensuring that the value for inches is correct when mm is changed):
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
However, this has the downside of being accessed differently to everyother attribute. Ideally you'll want to keep it consistent with accessing other attributes. This can be achieved by extending the default get method:
var getMixin = {
get: function(attr) {
if(typeof this[attr] == 'function') {
return this[attr]();
}
return Backbone.Model.prototype.get.call(this, attr);
}
};
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
_.extend(MyModel.prototype, getMixin);
Which will let you do:
new MyModel().get('inches');
This approach doesn't touch the underlying attributes hash, meaning that inches will not appear in the toJSON representation, unless you set the value of inches later on, in which case you'll need something like the excludeFromJSON array.
If you have the need to set the inches value, you may also want to listen for changes and adjust the value of mm:
MyModel = Backbone.Model.extend({
initialize: function() {
this.on('change:inches', this.changeInches, this);
},
inches: function() {
return this.get('mm') / 25;
},
changeInches: function() {
this.set('mm', this.attributes.inches * 25);
}
});
_.extend(MyModel.prototype, getMixin);
See the complete example on JSBin.
It's also worth noting that the (official?) purpose of the toJSON method has recently been redefined as preparing a model for syncing with a server. For this reason, calling toJSON should always return only the "persistable" (or "saveable") attributes.
Related
It is end of day and my brain is down for the night, but I am working on learning how to use setters when dynamically binding to Html elements. Of the many examples I have read so far, it seems the Urls below are the most helpful on the subject of using setters with knockoutjs bindings but I still do not get the idea or yet understand how it ought to be done.
knockoutjs-data-bind-setter
conditionally-bind-a-function-in-knockoutjs
knockout-data-bind-on-dynamically-generated-elements
easy-two-way-data-binding-in-javascript
For instance my viewmodel below (see fiddle) would like to protect the private variables and probably do so by adding some sort of validation code later on.However, right now, it simply needs to get at the parameter or text value entered into the text box by user. What exactly is the best syntax for this kind of operation?
<!-- related bindings: valueUpdate is a parameter for value -->
Your value: <input data-bind="value: someValue, valueUpdate: 'afterkeydown'"/>
If your trying to achieve two way binding along with the validation, then knockouts computed functions should be the best approach.
In your view model have a private variable and and expose ko.computed function for the binding, then in either read/write part of the computed you can do the validation.
Technically you could do that, but it is not the way Knockout is meant to be used. For example, let's say our viewmodel has one <select> and one text <input> binding. Using the private vars to hold the actual values, means we need a writable computed observable to update it, because Knockout only binds properties to your view, not private vars.
function appWithPrivateVars() {
var selectedItem = ko.observable('Option 3'), //our private vars
textVal = ko.observable('Haha');
this.selected = ko.computed({
read: function() { return selectedItem(); },
write: function(value) { /* add validation code here */ selectedItem(value); }
});
this.textVal = ko.computed({
read: function() { return textVal(); },
write: function(value) { /* add validation code here */ textVal(value); }
});
this.listItems = ['Option 1','Option 2','Option 3'];
this.get = function() { return selectedItem(); }; //getter
}
Now compare with the code needed for the same viewmodel without caring about private vars (also notice you don't need an explicit getter/setter) :
function appWithProperties() {
var self = this;
this.textVal = ko.observable('Haha');
// example of computed
this.textValInput = ko.computed({
read: function() { return self.textVal(); },
write: function(value) { /* add validation code here */ textVal(value); }
this.selected = ko.observable('Option 3');
this.listItems = ['Option 1','Option 2','Option 3'];
}
The thing is you don't need to 'protect' your otherwise accessible model properties because if they are not bound to the view, they will not be able to be modified. Furthermore, you will get yourself in trouble if you use var's at the moment you want to easily serialize your data to JSON with the ko.toJSON function (unless you're willing to rewrite an entire parsing function). Compare the outputs of ko.toJSON for both viewmodels:
Sample data for appWithPrivateVars
// no private vars included, eg. 'selectedItem'
{"selected":"Option 1", // computed prop; not what we need
"textVal":"Haha",
"listItems":["Option 1","Option 2","Option 3"]}
See how the 'actual' values are not included in the mapping (which is logical because ko.toJSON doesn't have access to them). Now check out the JSON output for appWithProperties:
{"textVal":"Haha", // actual value
"textValInput: "Haha", // value filter
"selected":"Option 1",
"listItems":["Option 1","Option 2","Option 3"]
}
Check out the fiddle
I'm currently testing a backbone view with Jasmine and I am having some trouble. I'm trying to isolate the View from all the other elements (the other view that are instantiated, the collection), but it is nearly impossible.
initialize: function(options) {
if(options.return) {return;}
var view = this;
var name = options.name;
var localizedElements = app.helpers.Locale.l().modules.case[name];
var swap, notification;
this.name = name.capitalizeFirstLetter();
this.collection.on('sort', this.refreshGui, this);
return this.render('/case/' + name + '/' + this.name + 'Box.txt', localizedElements, this.$el).done(function() {
new app.views.Buttons({el: view.$el.find('.Buttons')});
_.each(view.collection.models, function(model) {
new app.views['Folded' + this.name]({model: model, el: this.$('table')});
}, view);
if(!view.collection.findWhere({isPreferred: true}) || !view.collection.findWhere({isPrescribedForms: true})) {
if(!view.collection.findWhere({isPreferred: true})) {
swap = {entity: 'address', preferenceType: 'preferred'};
notification = app.helpers.Locale.l().generic.warningMessages.missingPreference.swap(swap);
var preferredNotification = [notification];
app.helpers.Notification.addNotifications('warnings', {missingPreferred: preferredNotification});
}
if(!view.collection.findWhere({isPrescribedForms: true})) {
swap = {entity: 'address', preferenceType: 'prescribed forms'};
notification = app.helpers.Locale.l().generic.warningMessages.missingPreference.swap(swap);
var prescribedFormsNotification = [notification];
app.helpers.Notification.addNotifications('warnings', {missingPrescribedForms: prescribedFormsNotification});
}
}
});
},
For example, where there are the two "ifs", the view is talking to the collection and a helper: "Notification Helper". How am I suppose to test this part of the code if I mocked the collection and the notification helper? I mean I am testing the VIEW, but now it seems like I have to test other elements of my application in my view...
I'm trying to isolate the View from all the other elements
I'm not sure if this will be helpful for you, but I created my own strategy to handle this problem.
The problem with Backbone is often indeed, that the view objects become cluttered with functionality.
Personally, I like my views to handle DOM interactions, listen to DOM events.
But to execute business logic / interactions with the back end, I prefer to delegate this functionality to other 'external' objects.
Models on the other hand 'may' handle basic data validation but do nothing more than making RESTful interactions with the server in my application.
How I solved the problem is by instantiating custom Javascript ('controller') objects that act as intermediaries between the views and the models.
This object is nothing more than an object that looks approximately like this:
var Some_controller = function(options){
this.makeView();
};
Some_controller.prototype.makeView = function(){
var someView = new Some_view({'controller': this});
someView.render(); //Render, but can also take care of proper view cleanup to avoid zombie objects
};
Some_controller.prototype.getModel = function(){
var someModel = new Some_model();
var promise = model.fetch();
return promise; //Promise be called from the view, because the controller is passed via the options, when the view is instantiated.
};
Well, this is how I try to keep my views clean.
In my experience, it is very easy to find everything back immediately; and note that you can also use helper objects to isolate more specific business functionality.
Not sure if anyone else has a better solution for this.
is it possible to pass the questions variable into the view render?
Ive attempted calling this.render inside the success on the fetch however I got an error, presumably it's because this. is not at the correct scope.
app.AppView = Backbone.View.extend({
initialize: function(){
var inputs = new app.Form();
inputs.fetch({
success: function() {
var questions = inputs.get(0).toJSON().Questions;
app.validate = new Validate(questions);
app.validate.questions();
}, // End Success()
error: function(err){
console.log("Couldn't GET the service " + err);
}
}); // End Input.fetch()
this.render();
}, // End Initialize
render: function(){
el: $('#finder')
var template = _.template( $("#form_template").html(), {} );
this.$el.html(template);
}
The success callback is called with a different this object than your View instance.
The easiest way to fix it is to add something like this before you call inputs.fetch:
var self = this;
And then inside the success callback:
self.render();
I'm not quite sure what you're trying to achieve, but if your problem is calling render from the success callback, you have two options, Function#bind or assigning a self variable.
For more information about "self" variable, see var self = this? . An example:
var self = this;
inputs.fetch({
success: function () {
self.render();
},
...
});
You should probably do some reading on JavaScript scopes, for example "Effective Javascript" or search the topic ( for example this MDN article ) online to get a better idea what happens there.
For Function#bind(), see the MDN article about it. With Backbone I suggest you use Underscore/LoDash's _.bind instead though, to make sure it works even where Function#bind() is not supported.
As for more high-level concepts, the fetching operation looks like it belongs to the model or router level and instead you should assign the questions variable as the model of your view. Ideally views don't do data processing / fetching, they're just given a model that has the methods necessary to perform any data transformations you might need.
The views shouldn't even need to worry about where the model comes from, this is normally handled by a router in case of single page applications or some initialization code on the page.
I have this viewmodel, on my web I have a dropdown that updates the sortedallemployees option. It works fine except my table is empty initially. Once I sort the first time I get data. Seems like when the vm is created it doesn't wait for allemployees to be populated.
var vm = {
activate: activate,
allemployees: allemployees,
sortedallemployees:ko.computed( {
return allemployees.sort(function(f,s) {
var ID = SelectedOptionID();
var name = options[ ID - 1].OptionText;
if (f[name] == s[name]) {
return f[name] > s[name] ? 1 : f[name] < s[name] ? -1 : 0;
}
return f[name] > s[name] ? 1 : -1;
});
}
Without the rest of your code, its difficult to tell exactly how this will behave. That being said, you are doing several very odd things that I would recommend you avoid.
First, defining all but the simplest viewmodels as object literals will cause you pain. Anything with a function or a computed will almost certainly behave oddly, or more likely not at all, when defined this way.
I would recommend using a constructor function for your viewmodels.
var Viewmodel = function(activate, allEmployees) {
var self = this;
self.activate = activate;
self.allEmployees = ko.observableArray(allEmployees);
self.sortedEmployees = ko.computed(function() {
return self.allEmployees().sort(function(f,s) {
//your sort function
});
});
};
var vm = new Viewmodel(activate, allemployees);
This method has several advantages. First, it is reusable. Second, you can reference its properties properly during construction, such as during the computed definition. It is necessary for a computed to reference at least one observable property during definition for it to be reactive.
Your next problem is that your computed definition is not a function, but an object. It isn't even a legal object, it has a return in it. This code shouldn't even compile. This is just wrong. The Knockout Documentation is clear on this point: computed's are defined with a function.
Your last problem is that your sort function is referencing things outside the viewmodel: SelectedOptionID(). This won't necessarily stop it from working, but its generally bad practice.
I have been using Backbone on a new project and so far have loved it, but I have come to a blocker that I can't seem to get around.
Without trying to explain my whole domain model, I have found that when you save a model, the response comes back from the server and gets parsed again, creating new sub objects and therefore breaking and event bindings I had previously put on the object.
For instance, if I save ContentCollection (its a Backbone.Model not a collection) when it comes back from the server, the response gets parsed and creates a new Collection in this.contentItems, which breaks all the binding I had on this.contentItems. Is there any way to get around this? Tell backbone not to parse the response somehow? Grab the bindings off the original list, and then re-attach them to the new list?
App.ContentCollection = Backbone.Model.extend({
urlRoot: '/collection',
initialize: function() {
},
parse: function(resp, xhr) {
this.contentItems = new App.ContentList(resp.items)
this.subscriptions = new App.SubscriptionList(resp.subscriptions)
return resp
},
remove: function(model){
this.contentItems.remove(model)
this.save({'removeContentId':model.attributes.id})
},
setPrimaryContent: function(model){
this.save({'setPrimaryContent':model.attributes.id})
}
})
Has anyone run into this before?
I think the issue here is the way you're using the parse() method. Backbone just expects this method to take a server response and return a hash of attributes - not to change the object in any way. So Backbone calls this.parse() within save(), not expecting there to be any side-effects - but the way you've overridden .parse(), you're changing the model when the function is called.
The way I've dealt with this use case in the past is to initialize the collections when you first call fetch(), something like:
App.ContentCollection = Backbone.Model.extend({
initialize: function() {
this.bind('change', initCollections, this);
},
initCollections: function() {
this.contentItems = new App.ContentList(resp.items);
this.subscriptions = new App.SubscriptionList(resp.subscriptions);
// now you probably want to unbind it,
// so it only gets called once
this.unbind('change', initCollections, this)
},
// etc
});