Declaring variables on a backbone model without setting defaults - javascript

I'm just starting out with backbone.js and I'm looking for a way of declaring fields on a model without having to provide defaults. It's really just for reference, so that when I start creating instances, I can see what fields I need to initialize.
With something like java I'd write
public class CartLine{
StockItem stockItem;
int quantity;
public int getPrice(){
return stockItem.getPrice() * quantity;
}
public int getStockID(){
//
}
}
However with backbone models, I'm referencing the fields in my method's but I'm not actually declaring them - It looks like I could easily create a CartLine object that doesn't contain a stockItem attribute or a quantity attribute. It feels strange not to mention the fields when I declare the object. Particularly as the object is supposed to represent an entity on the server.
var CartLine = Backbone.Model.extend({
getStockID: function(){
return this.stockItem.id;
},
getTotalPrice: function() {
return this.quantity * this.StockItem.get('price');
}
});
I guess I can add some sort of reference by using validate -
CartLine.validate = function(attrs){
if (!(attrs.stockItem instanceof StockItem)){
return "No Valid StockItem set";
}
if (typeof attrs.quantity !== 'number'){
return "No quantity set";
}
}
But my question is - am I missing something? Is there an established pattern for this?

The defaults are really for "fields" or data that is transferred back and forth from the server as part of the json.
If you just want to create some member variables as part of the Model, which are proprietary and not going to be sent back and forth to the server, then you can declare them a) on the object itself or b) in the initialize method (called during construction), and they can be passed in as part of opts:
var Widget = Backbone.Model.extend({
widgetCount: 0,
defaults: {
id: null,
name: null
}
initialize: function(attr, opts) {
// attr contains the "fields" set on the model
// opts contains anything passed in after attr
// so we can do things like this
if( opts && opts.widgetCount ) {
this.widgetCount = opts.widgetCount;
}
}
});
var widget = new Widget({name: 'the blue one'}, {widgetCount: 20});
Keep in mind that if you declare objects or arrays on the class, they are essentially constants and changing them will modify all instances:
var Widget = Backbone.Model.extend({
someOpts: { one: 1, two: 2},
initialize: function(attr, opts) {
// this is probably not going to do what you want because it will
// modify `someOpts` for all Widget instances.
this.someOpts.one = opts.one;
}
});

Related

How do you prevent Knockback.js creating view models for null relations?

If my backbone models have relationships (for example, created by backbone-relational), those relationships might be nullable, leading the foreign key fields to sometimes be null.
If I have several knockback view models, and I've specified factories so that when following relations I get the view models with the desired functionality for the model, when it encounters an attribute that is null, it goes ahead and creates a view model passing null as the model, which likely breaks most of the view model's functionality.
Example:
var ChildViewModel = kb.ViewModel.extend({
constructor: function (model, options) {
// this is the problem I'm trying to avoid - creating a view model with
// no model
if (!model) {
// just report the error somehow - the jsfiddle has the
// relevant HTML element
document.getElementById("error").innerHTML = "ChildModelView initialised without a model!";
}
kb.ViewModel.prototype.constructor.apply(this, arguments);
}
});
var ParentViewModel = kb.ViewModel.extend({
constructor: function (model, options) {
// specify factories here, because this way you can easily deal with
// reverse relationships, or complicated relationship trees when you
// have a large number of different types of view model.
kb.ViewModel.prototype.constructor.call(
this,
model,
{
factories: {relation1: ChildViewModel,
relation2: ChildViewModel},
options: options
}
);
}
});
// if we assume that relation2 is a nullable relationship, backbone-relational,
// for example, would give us a model that looks like this:
var model = new Backbone.Model({
id: 1,
relation1: new Backbone.Model({id: 2}), // this works fine
relation2: null // this causes a problem
});
var view_model = new ParentViewModel(model);
And the fiddle:
https://jsfiddle.net/vbw44vac/1/
I've just discovered what I think might be a reasonable solution.
Your factories don't have to be ViewModel "classes", but can be factory functions. So:
var nullable = function (view_model_class) {
var factory = function (object, options) {
if (object === null) return object;
return new view_model_class(object, options);
};
return factory;
};
And then when you're defining your factories:
kb.ViewModel.prototype.constructor.call(
this,
model,
{
factories: {relation1: nullable(ChildViewModel),
relation2: nullable(ChildViewModel)},
options: options
}
);

Knockout mapping - difference between objects and observables in class being mapped to

This is driving me absolutely insane, and I just cannot see what I have done wrong. Please help before I start bibbling and gnawing my colleagues leg off. He doesn't deserve that.
I have an object I am mapping, which has a property that can or cannot contain an object of that same type. That is the only level of nesting there is. It is very simple; it is complicated only by the fact that the object is calling a base class constructor to set some default behaviour.
This base class sets up all the fields that can appear in the model (it is a generated file) and then maps the datasource, if it has one.
The mapping of the nested field to the correct constructor works if the field is set up initially as an observable. It does not if it is set up as a plain object.
var NS = {};
var _itest = 0;
NS.FieldModelBase = function(data, mapping)
{
var _this = this;
this.Text = ko.observable();
// DOES NOT WORK
this.AlternateField = {};
// WORKS
//this.AlternateField = ko.observable();
ko.mapping.fromJS(data, mapping, _this);
};
// =====================================================================
NS.FieldModel = function(data, mapping, parent)
{
var _this = this;
window.console && console.log('CREATING FIELD', data);
var _mapping = $.extend({}, mapping, {
'include': [ 'Test' ],
'AlternateField': {
create:
function(o)
{
window.console && console.log('FOUND SUBFIELD', o.data);
return o.data ? new NS.FieldModel(o.data) : null;
}
}
});
this.Test = ko.observable(_itest++);
NS.FieldModelBase.call(_this, data, _mapping);
}
// =====================================================================
var model = new NS.FieldModel({
Text: "Main option",
AlternateField: {
Text: "Alternate option",
AlternateField: null
}
}, { include: [ 'Test' ] });
ko.applyBindings(model);
https://jsfiddle.net/whelkaholism/fkr0w98u/
So, when setup as an object, printing model out after running the code gives:
{"Test":0,"Text":"Main option","AlternateField":{"Text":"Alternate option","AlternateField":null}}
There is no Test property on the alternate field. If you check the console, what happens is that the mapping create is in fact called, but the o.data property is null.
Change to an observable, and the output is, as expected:
{"Test":0,"Text":"Main option","AlternateField":{"Test":1,"Text":"Alternate option","AlternateField":null}}
So, what is the mapping plugin doing here? It was my understanding that it would map everything in source data, regardless of the existence or type of any existing properties on the object?
EDIT: I have solved my immediate problem with this change:
NS.FieldModel = function(data, mapping, parent)
{
var _this = this;
var _mapping = {
copy: [ 'AlternateField' ]
};
NS.FieldModelBase.call(_this, data, _mapping);
this.AlternateField = data.AlternateField ? new NS.FieldModel(data.AlternateField, null, _this) : null;
}
This manually creates the correct object type for the alternate field after the mapping. The copy directive in the mapping is absolutely required, or the newly created object has no properties mapped.
I don't now why this is, so I'm still looking for the answer on why the mapping plugin works differently depending on the content of pre-existing variables, because I despise having code that I don't know exactly why it works!
The mapping plugin maps only values, not objects e.g.
"myString" => ko.observable("myString")
null => ko.observable(null)
{ myStringProperty: "myString" } => { myStringProperty: ko.observable("myString") }
{ myProperty: null } => { myPropery: ko.observable(null) }
{} => {}

Match values in nested object to corresponding knockout bindings?

Let's say I have a list of knockout bindings placed in a nested/namespaced object, resembling this:
var bindings = {
event: {
eventid: ko.observable(),
office: ko.observable(),
employee: {
name: ko.observable(),
group: ko.observable()
}
},
...
}
Now let's say there are a number of different sets of data that might be loaded into this - so one does an ajax query and gets a JSON result like this:
{
"defaults": {
"event": {
"eventid": 1234,
"employee": {
"name": "John Smith"
}
},
...
}
}
Note that not every binding has a default value - but all defaults are mapped to a binding. What I want to do is read the defaults into whatever knockout binding they correspond to.
There are definitely ways to traverse a nested object and read its values. Adding an extra argument to that example, I can keep track of the default's full key (eg event.employee.name). Where I'm getting stumped is taking the default's key and using it to target the associated knockout binding. Obviously, even if i have key = "event.employee.name", bindings.key doesn't reference what I want. I can only think of using eval(), and that leaves me with a bad taste in my mouth.
How would one go about using a key to reference the same location in a different object? Perhaps knockout provides a way to auto-map an object to its bindings, and I've just overlooked it? Any insight would be helpful. Thanks in advance!
I would suggest you have a look at the Knockout Mapping Plugin which will do most of what you want to do. If that doesn't workout then you can turn your bindings object into a series of constructor functions that accepts a data parameter. Something like
var Employee = function (data){
var self = this;
self.name = ko.observbale(data.name || '');
self.group = ko.observable(data.group);
};
var Event = function(data){
var self = this;
self.eventid = ko.observable(data.id || 0);
self.office = ko.observable(data.office || '');
self.employee = ko.observable(new Employee(data.employee));
};
var bindings = function(data){
var self = this;
self.event = ko.observable(new Event(data));
}
I'll be putting Nathan Fisher's solution into a future update, but I wanted to share the fix I found for now as well. Each time the defaults object recurses, I simply pass the corresponding bindings object instead of tracking the entire keypath.
var setToDefaults = function(data){
loopDefaults(data.defaults, bindings);
};
var loopDefaults = function(defaults, targ){
for(var d in defaults){
if(defaults.hasOwnProperty(d) && defaults[d]!==null){
if(typeof(defaults[d])=="object"){
loopDefaults(defaults[d], targ[d]);
}else{
// defaults[d] is a value - set to corresponding knockout binding
targ[d](defaults[d]);
}
}
}
};

How to model my data in angular js?

Two questions:
1) I am using angular-js and I am looking for a way to model my data.
I have experience with the idea of a Model in backbone.
Is this a factory in angular? Is it an ok "best practice" to have a LOT of factories (one for each type of model), basically mimicking "class" with "factory"?
The factory represents my model with some "helper" functions (like a model on Backbone) or my factory represents a list of members (like a collection on backbone)?
2) For example, Let's say I want to create objects to map to REST resources and I have an "member" resource that I get by GET-ing: /members/123. This returns a json object with various fields. Something like:
{id: 123, name: 'angularjs', date_created: 1235845}
Now, I want some kind of representation in my AngularJS app of this "member" object. This representation is more than just a mapping of the fields - I want to add "helper" functions, for example a function that converts the date_create field into something human-readable.
How to represent this? using factory + $resource
Here is good example how tot use factory in your case.
Factory
angular.module('myApp').factory('Member', function($http) {
// Member is a class which we can use for retrieving and
// updating data on the server
var Member = function(data) {
angular.extend(this, data);
}
// a static method to retrieve Member by id
Member.get = function(id) {
return $http.get('/Member/' + id).
then(function(response) {
return new Member(response.data);
});
};
// an instance method to create a new Member
Member.prototype.create = function() {
var member= this;
return $http.post('/Member/', member).then(function(response) {
book.id = response.data.id;
return member;
});
}
return Member;
});
Then in controller you can write something like:
Controller
var controller = function(Member) {
var memeber= new Member();
memeber.name = 'Fox';
memeber.create();
// to retrieve a memeber
var memeberPromise = Memeber.get(123);
memeberPromise.then(function(b) {
memeber = b;
});
};
Here I used id only but you understand the flow
Hope it will help you to sort things out

Backbone: annoying behaviour of prototype object

I understand this is a problem (or behaviour) of javascript itself rather than Backbone's extend method, but I'd like to know what is the best strategy to avoid it.
Let's better put it in code:
var MyModel = Backbone.Model.extend({
value: 0,
values: []
});
var myFirstModel = new MyModel();
myFirstModel.value // 0, as expected
myFirstModel.values // [], as expected
var mySecondModel = new MyModel();
mySecondModel.value = 2;
mySecondModel.values.push(2)
mySecondModel.value // 2, as expected
mySecondModel.values // [2], as expected
myFirstModel.value // 0, as expected
myFirstModel.values // [2], ... WAT!!!
I do understand that the problem is I'm not assigning a new value to mySecondModel.values I'm just operating on the values variable that is in the prototype, that is MyModel.prototype.values (same problem with any other object, of course)
But it's very easy to mess with that. The most intuitive thing is to just think of those as INSTANCE variables, and not variables common to every instance (static or class variables in class based languages).
So far now the general solution I've found is to initialize every variable in the initialize method, like this:
var MyModel = Backbone.Model.extend({
initialize: function() {
this.value = 0;
this.values = [];
}
});
That way everything works as expected, and even though it wouldn't be neccesary for a simple value (like this.value) I find it much easier to just stick to this prnciple in every case.
I'm wondering if there's some better (more elegant, clearer) solution to this problem
This is an effect of JavaScript's prototypical inheritance and the fact that Array objects are reference types. The key/value pairs of the object you pass to extend are copied onto the prototype of MyModel, so they will be shared by all instances of MyModel. Because values is an array, when you modify it, you modify the array for every instance.
What you are doing by setting values inside initialize is called shadowing the prototype, and it is the correct way to solve this issue.
That said, in the case of Backbone.Model, if you are attempting to deal with the model's attributes, you can use the defaults function to provide defaults like this:
var MyModel = Backbone.Model.extend({
defaults: function() {
return {
value: 0,
values: []
}
}
});
Again, this is only for attributes of an instance.
var inst = new MyModel();
// The defaults will be created for each new model,
// so this will always return a new array.
var values = inst.get('values');
For what you are doing, where you are specifying properties on the model itself, it is up to you to set the defaults inside of initialize, as you have done.
Are you intentionally not setting value and values as backbone attributes? If you set attributes on an instance, instead of putting them in the extended backbone model definition, it might work how you expect.
var MyModel = Backbone.Model.extend();
var myFirstModel = new MyModel({
value: 0,
values: []
});
console.log(myFirstModel.get('value'); // 0
console.log(myFirstModel.get('values'); // []
var mySecondModel = new MyModel({
value: 2,
values: [2]
});
//mySecondModel.value = 2;
//mySecondModel.values.push(2)
console.log(mySecondModel.get('value'); // 2
console.log(mySecondModel.get('values'); // [2]
console.log(myFirstModel.get('value'); // 0
console.log(myFirstModel.get('values'); // []
jsFiddle, check the console log.
I too had stumbled across this problem some time back and solved it by defining a defaults method in the model.
var MyModel = Backbone.Model.extend({
defaults: function() {
return {
value: 0,
values: []
}
}
});

Categories

Resources