Backbone.js view variables being overwritten - javascript

I am creating views via a $.each and the myId attribute of all the views always ends up being overwritten by the myId of the last element.
Creation of views
$('.form').each(function() {
// split class name
var classNameArray = $(this).attr('class').split('-');
// get typeId from last part of classname
var myId = classNameArray[classNameArray.length-1];
new myView({ 'el': $('.form-' + myId), 'myId': myId });
});
Initialization of my view
var myView = Backbone.View.extend({
events: {
},
initialize: function() {
var myId = this.options.myId;
},
test: function() {
console.log(myId); // will return myId of last view created
}
});
How can I get the views to keep their unique myId's?

When you write it like this --
initialize: function() {
var myId = this.options.myId;
},
It creates a variable local to the initialize function (that's a Javascript thing, not a Backbone thing).
Try this instead (create a variable local to the View object):
initialize: function() {
this.myId = this.options.myId;
},

test: function() {
console.log(myId); // will return myId of last view created
}
this [myId] is not the [myId] in :
initialize: function() {
var myId = this.options.myId;
}
it is the [myId] from :
$('.form').each(function() {
// split class name
var classNameArray = $(this).attr('class').split('-');
// get typeId from last part of classname
var myId = classNameArray[classNameArray.length-1]; //from this myId!!!
new myView({ 'el': $('.form-' + myId), 'myId': myId });
});
because:
first:
"var myId = this.options.myId; "
this [myId] is the initialize within the local variables,you can not access this variable outside the method.
second:
console.log(myId); // will return myId of last view created
why are we here be able to access to [myId]?because this [myId] is the [this.myId],[this] is the context of the calling method :
new myView({ 'el': $('.form-' + myId), 'myId': myId });
so,this [myId] is the :
var myId = classNameArray[classNameArray.length-1];
[myId] here.this [myId] is cyclic change, so you always get the last cycle the value of the [myId].
solution:
initialize: function() {
this.myId = this.options.myId;
},
because [this.myId] is [myView]'s internal variables, each time you create the instance of [myView], in the [initialize] method dynamically the [this.myId] is set to the correct value form [this.options.myId].
kind of problem, you can refer to this article:
http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain

Related

Cannot access model property on class definition

I send value to model as attribute
Views.App = Backbone.View.extend({
initialize: function () {
this.items = new Models.Items()
.on('sync:subitems', function (item) {
this.subitems = new Models.Subitems({itemId: item.attributes.id});
And successfully receive it in initilize of model
var Subitems = Models.Subitems = Backbone.Collection.extend({
initialize: function(attributes, options) {
this.itemId = attributes.itemId;
console.log(this.itemId);
},
model: Subitem,
url: './subitems?item_id=' + encodeURIComponent(this.itemId)
});
console.log returns the value of attribute in initialize.
But request is sent like:
'./subitems?item_id=undefined'
Why do I have undefined if I have access to the property in initialize? How can it be fixed?
Change url to a function and you'll be in the context of the backbone collection as appose to the window.

Backbone view inheritance

I know someone will mark this as duplicate but I went through almost all posts related to this subject and that's not what I am looking for. So here is the thing bugling my mind since last week.
I have been tasked to create an atomic design of views. There will be a core base view, then another view will extend it and so on. element->panel->window, element->panel->popup etc. With Backbone.View.extend I can simply do it like
var BaseView = Backbone.View.extend({
initialize : function(options) {
this.$el.attr('cid', this.cid);
this.base = options.base || arguments[0]['base'];
this.parent = options.parent || arguments[0]['parent'];
this.children = [];
if(typeof this.template !== 'undefined') {
if (typeof this.template=='function') {
// do nothing. template is already a underscore template and will be parsed when first call;
}
else if (typeof this.template=='string' && this.template.substr(0,1)=='#') {
if ($(this.template).length >0 ) {
this.template = _.template($(this.template).html());
}
else {
console.warn('Template element ' + this.template + 'could not be located in DOM.');
}
}
else {
this.template = _.template(this.template);
}
}
else {
this.template = _.template('<span></span>');
}
if (typeof this.parent!=='undefined' && this.parent ) {
this.parent.add.apply(this.parent, [this]);
}
},
add: function(child){
this.children.push(child);
},
getChildren : function(){
return this.children;
},
clean: function(){
this.$el.empty();
},
close: function () {
BaseView.prototype.clear.apply(this, [true]);
this.undelegateEvents();
this.unbind();
this.stopListening();
this.remove();
},
clear: function(){
if (this.children.length > 0) {
empty = empty || false;
_.each(this.getChildren(), function (child, index) {
Baseview.prototype.close.apply(child);
});
this.children = [];
if (empty) {
this.$el.empty();
}
}
return this;
}
})
then if I try use it as
var Layout = new BaseView.extend({
el: '#someElement',
template : '#sometemplate',
initialize : function(){
this.childView = new ChildView({parent: this, base: this, collection: someCollection});
return this;
},
render: function(){
this.clean().$el.append(this.template({}));
this.$('.content').append(this.childView.render().$el);
return this;
},
});
var ChildView = BaseView.extend({
tagName : 'div',
template : '#childTemplate',
initialize : function(){
return this;
},
render: function(){
var self = this;
this.clean().$el.append(this.template({}));
this.$list = this.$('ul');
_.each( this.collection.models, function(model){
var grandChildView = new GrandChildView({parent: self, base: self.base, model: model});
self.$list.append(grandChildView.render().$el);
})
return this;
}
});
var GrandChildView = BaseView.extend({
tagName : 'li',
template : '#grandChildTemplate',
initialize : function(){
return this;
},
render: function(){
this.clean().$el(this.template(this.model.toJSON()));
return this;
}
});
$(function(){
new Layout();
})
doesn't work because instead of running initialize on BaseView, Backbone calls for initiated first and this.template and all others are undefined.
Then I tried to replace it with constructor instead of initialize on BaseView. But then I end up this.$el undefined error because Backbone.View.constructor has not been called yet so no this.$el yet which is being created by _ensureElement
so with some researching the only thing I found was using
Backbone.View.prototype.constructor.apply(this,[options]);
But this also causes similar issue that at the end of Backbone.View, calls for this.initialize.apply(this, [options]), which then goes to child objects initialize instead. So I am stuck and couldn't wrap my head around this.
I also know that I can call parent's initialize function from childview but that's not preferable since there are lots of subviews extending each other. That's the reason I pass parent object to attach later object to it's children.
what I am trying to accomplish is creating a wrapper extended object that I can extend later for another object, but at the same time it should run some common tasks on original base view, attach additional prototype methods then call for callers initialize.
so pseudo
var BaseView {
extend Backbone view with the passed arguments,
check for base, parent arguments and set them
check for template option, if exists then get the element, create template function and replace with the string one,
if there is parent view passed, then attach yourself to children of parent
call your own initialize method,
return
}
If I understand you correctly, you want to run the "parent view's" initialize method when instantiating a child? If that is correct see this post: https://stackoverflow.com/a/8596882/1819684
What you're missing is the note in the backbone docs on "super" as referenced in that post.
Based on your comment I think this is what you're looking for. You have to call the "super" method (method of parent class) explicitly in backbone as shown in the post I referenced. You can do whatever you want/need to do in your "initialize" both before and after your call to the parent method. I also found this: Defining child views without an initialize method which may help.
var BaseView = Backbone.View.extend({
el: '#container1',
initialize: function(){
console.log('base view init');
this.render();
},
render: function(){
this.$el.html("Hello World 1");
}
});
var ChildView = BaseView.extend({
el: '#container2',
initialize: function() {
console.log('before call to parent (BaseView) init');
BaseView.prototype.initialize.apply(this, arguments);
console.log('ChildView init');
},
render: function(){
this.$el.html("Hello World 2");
}
});
var GrandChildView = ChildView.extend({
el: '#container3',
initialize: function() {
console.log('before call to parent (ChildView) init');
ChildView.prototype.initialize.apply(this, arguments);
console.log('GrandChildView init');
},
render: function(){
this.$el.html("Hello World 3");
}
});
var appView = new BaseView();
var appView = new ChildView();
var appView = new GrandChildView();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
<div id="container1">Loading...</div>
<div id="container2">Loading...</div>
<div id="container3">Loading...</div>

Passing parameters to a Backbone View (id, className and so on)

I have some code that makes a Backbone.View, based on some params I pass to it, like so:
// The Form View
var FormView = Backbone.View.extend({
initialize: function (opts) {
debugger; // This is here only to figure out what gets executed first: if "id" method or "initialize"
this.options = {};
this.options.id = opts.id;
this.options.className = opts.class;
},
id: function () {
debugger; // The application will stop here before the debugger I set in the initialize method
return this.options.id; // options is undefined!
},
className: function () {
return this.options.className; // options is undefined!
}
});
// The params
var params =
fid: "some-form",
class: "form-horizontal"
};
var myForm = new FormView(params);
But the this.options property is always undefined. As I can see, the method that sets the view's properties runs BEFORE the initialize method. As a workaround I think I could access the initialize method inside de id callback and call its arguments, but I'm not sure how to do this properly. And I don't think this is a good approach either.
Any ideas? - Thanks in advance.
Why don't you use the usual way of passing options to the view? Something like:
var FormView = Backbone.View.extend({
initialize: function () {
this.foo = this.options.foo;
this.bar = this.options.bar;
}
});
var params = {foo: '1', bar: '2'};
var v = new FormView(params);
You can also pass as params {id: '1', className: 'your-class'} and Backbone will apply it automatically to the respective id and className properties of the target View.
You don't seem to be initializing the options attribute properly. You should have
initialize: function (opts) {
this.options = {}; // you were missing this part
this.options.id = opts.id;
this.options.className = opts.class;
},

Getting 'type' of a View's model

you can access View's model from the View methods - like render() (through its model property). But let's say you have many different models and use the them with the same type of View, changing view's model property when you need.
How can you determine from the View what type of the model it's using?
var Model1 = Backbone.Model.extend();
var Model2 = Backbone.Model.extend();
var MyView = Backbone.View.extend({
render:function(){
console.log(" you're using: "+ model); // how to determine here is it using Model1 or Model2
}
})
var mv1 = new MyView({model: new Model1()})
var mv2 = new MyView({model: new Model2()})
Objects have identity, not name. If you want to know the name of the constructor that created an object (which can be considered the object's type; see the ECMAScript Specification), the bullet-proof approach is to make that information part of the constructed object:
var Model1 = Backbone.Model.extend({type: "Model1"});
var Model2 = Backbone.Model.extend({type: "Model2"});
var MyView = Backbone.View.extend({
render: function () {
console.log("You are using " + this.model.type);
}
});
var mv1 = new MyView({model: new Model1()});
var mv2 = new MyView({model: new Model2()});
This way, the model objects inherit the type property through the prototype chain. See also http://backbonejs.org/#View-render.
Alternative, less efficient and less reliable solutions include:
Giving the constructor a name and accessing that:
var Model1 = Backbone.Model.extend();
Model1.name = "Model1";
var MyView = Backbone.View.extend({
render: function () {
console.log("You are using " + this.model.constructor.name);
}
});
var mv1 = new MyView({model: new Model1()});
var mv2 = new MyView({model: new Model2()});
(The additional assignment to the name property was not necessary if the constructor was
var Model1 = function Model1 () {
// …
};
or
function Model1 ()
{
// …
}
but that is – unfortunately – not the Backbone.js way.)
Parsing the string representation of a named Function instance such as a constructor.
But that is implementation-dependent:
var Model1 = function Model1 () {
// …
};
var MyView = Backbone.View.extend({
getModelName: function () {
var aFunction = this.model.constructor;
return (aFunction != null && typeof aFunction.name != "undefined" && aFunction.name)
|| (String(aFunction).match(/\s*function\s+([A-Za-z_]\w*)/) || [, ""])[1];
},
render: function () {
console.log("You are using " + this.getModelName());
}
});
(Implemented as jsx.object.getFunctionName(). But again, AIUI not fully possible with Backbone.js.)
Using instanceof as suggested in DashK's answer. But that does not give you information immediately or reliably.
Because you have to put the most significant name in the prototype chain first, and change the method whenever you change the prototype chain, or you
will get false positives and false negatives:
var MyView = Backbone.View.extend({
render: function () {
var modelName = "an unknown model";
if (this.model instanceof Model1)
{
modelName = "Model1";
}
else if (this.model instanceof Model2)
{
modelName = "Model2";
}
else if (this.model instanceof Supermodel)
{
modelName = "Supermodel";
}
console.log("You are using " + modelName);
}
});
Keeping this simple...
var Model1 = Backbone.Model.extend();
var Model2 = Backbone.Model.extend();
var MyView = Backbone.View.extend({
render:function(){
if (this.model instanceof Model1) {
alert("You're using model1");
}
else if (this.model instanceof Model2) {
alert("You're using model2");
}
else {
alert("I've no idea what you're using");
}
}
})
var mv1 = new MyView({model: new Model1()});
var mv2 = new MyView({model: new Model2()});
mv1.render();
mv2.render();

backbone.js nested model's object not unique to instance

I have a parent Model that has by default and nested Model. The nested Model itself has propertyA, which is an object with defaultValue and userValue. The idea is that every instance of parent Model comes with a nested Model that has a userValue of null, and a static default value.
The problem is, when I update the userValue for one instance, it ends up changing for all instances going forward. Instead of updating the userValue for a particular instance, I'm doing something wrong and updating the nested Model "prototype".
Below code available at http://jsfiddle.net/GVkQp/4/. Thanks for help.
var ParentModel = Backbone.Model.extend({
initialize: function(){
var defaultObjs = {
nestedModel : new NestedModel()
};
$().extend(this.attributes, defaultObjs);
}
});
var NestedModel = Backbone.Model.extend({
defaults: {
"propertyA" : {
"defaultValue" : "ABC",
"userValue": null
}
}
});
var ParentView = Backbone.View.extend({
initialize: function(){
var self = this;
var defaultValue = self.model.get("nestedModel").get("propertyA")["defaultValue"];
var userValue = self.model.get("nestedModel").get("propertyA")["userValue"];
var div = $("<div class='box'>defaultValue = <span id='defaultValue'>" +
defaultValue+ "</span>, userValue = <span id='userValue'>" +
userValue + "</span></div>");
var divButton = $('<input type="button" value="Change my userValue to DEF">')
.click(function(){
temp = self.model.get("nestedModel").get("propertyA");
temp.userValue = "DEF";
//wherewas my intention is just to set the userValue for a particular instance,
//generating another instance of ParentView (by clicking button) reveals that
//the userValue is set even for new instances.
//How can I change it such that I only change the userValue of the particular
//instance?
//set value
self.model.get("nestedModel").set({"propertyA": temp});
//update userValue in View
userValue = self.model.get("nestedModel").get("propertyA")["userValue"];
$(this).parent().find("span#userValue").text(userValue);
});
//append divButtont to div
div.append(divButton)
//append div to body
$('body').append(div)
},
});
$("#add").click(function(){
var newBoxView = new ParentView({
model: new ParentModel()
});
});
Thanks for the jsFiddle. It illustrates your problem perfectly.
You see, when you set your "propertyA" in your defaults, the object you create is the default value being copied along with every other nested class you create. For this reason, when you define your defaults, you have the option to define it as a function in order to create the default value new every time. If you do this, you solve your problem:
var ParentModel = Backbone.Model.extend({
defaults: function() {
return {nestedModel: new NestedModel()};
}
});
var NestedModel = Backbone.Model.extend({
defaults: function() {
return {
"propertyA" : {
"defaultValue" : "ABC",
"userValue": null
}
};
}
});

Categories

Resources