Add new collection to parent view from child view - javascript

I have a parent view (which is for car engines) that contains a child view which displays a list of options. One of the options is to add a new collection to the parent view.
My child view init function looks like this:
initialize: function (engine) {
this.engine = engine; //parent object
this.valves = engine.valves; //may or may not be empty
};
Then I have this method that adds the collection(valves) when a button is pressed:
addPerformanceValves: function() {
var self = this;
if (this.valves.lentgh == 0) {
this.valves = new ValveCollection();
this.valves.url = function() {
return '/api/engines/auto/performance/parts/' + self.id + '/valves';
}
}
this.$('.performanceParts').show();
}
So now that I created the new collection, how do I add it to the parent?

There are multiple ways to achieve that.
Passing the parent object down the hierarchy
Like you're already doing, you could call a function from the parent object to pass the new collection.
var Child = Backbone.View.extend({
initialize: function(options) {
options = options || {};
this.engine = options.engine; //parent object
this.valves = engine.valves; //may or may not be empty
},
addPerformanceValves: function() {
var self = this;
if (this.valves.lentgh == 0) {
this.valves = new ValveCollection();
this.valves.url = function() {
return '/api/engines/auto/performance/parts/' + self.id + '/valves';
}
// call the parent
this.engine.addNewCollection(this.valves);
}
this.$('.performanceParts').show();
}
});
var Parent = Backbone.View.extend({
addNewCollection: function(collection) {
// do what you want with the collection
this.newCollection = collection;
}
});
Triggering events
One way to avoid strong coupling is to trigger events from the child view, to which the parent is listening.
var Child = Backbone.View.extend({
initialize: function(options) {
options = options || {};
this.valves = options.valves; //may or may not be empty
},
addPerformanceValves: function() {
var self = this;
if (this.valves.lentgh == 0) {
this.valves = new ValveCollection();
this.valves.url = function() {
return '/api/engines/auto/performance/parts/' + self.id + '/valves';
}
// call the parent
this.trigger('child:collection', this.valves);
}
this.$('.performanceParts').show();
}
});
var Parent = Backbone.View.extend({
initialize: function() {
this.child = new Child({ valves: this.valves });
// listen to child view events
this.listenTo(this.child, 'child:collection', this.addNewCollection);
},
addNewCollection: function(collection) {
// do what you want with the collection
this.newCollection = collection;
}
});
Then, the child view only has what it needs and nothing more. It help to keep responsibilities at the right place.

Related

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>

How to pass value from mainview to subview Backbone.js

Main View
events: {
"click .open-sku-details":"openSkuDetails"
},
openSkuDetails: function(ev) {
var self = this;
var sku_id = $(ev.target).attr('sku_id');
self.skuDetailsModel.set("id",sku_id);
self.skuDetailsModel.fetch({
}).done(function (response) {
});
this.skuDetails = new skuDetailsView({model:self.skuDetailsModel});
return this.skuDetails.render();
}
SubView
var $ = jQuery = require('jquery'),
Backbone = require('backbone'),
Handlebars = require('handlebars'),
_ = require('underscore'),
skuDetailsTemplate = require("../../templates/product/SkuDetails.html"),
skuDetailsModel = require('../../models/product/SkuDetailsModel');
var SkuDetailsView = Backbone.View.extend({
el: ".sku-details-container",
tagName:"div",
initialize: function () {
var self = this;
this.skuDetailsModel = new skuDetailsModel();
this.listenTo(self.skuDetailsModel, 'add', self.render);
this.listenTo(self.skuDetailsModel, 'change', self.render);
self.skuDetailsModel.fetch({
}).done(function (response) {
});
},
render: function () {
var self = this;
this.$el.html(skuDetailsTemplate({
skuDetails: self.skuDetailsModel.toJSON(),
}));
}
});
module.exports = SkuDetailsView;
How do i pass the id value from one view two another?
Pass it via the options argument.
In your main view
this.skuDetails = new skuDetailsView({
model:self.skuDetailsModel,
sku_id: sku_id
});
In your sub view
initialize: function (options) {
var sku_id = options.sku_id;
}
The options object is only available in the initialize function. Store it, for instance with this.sku_id = options.sku_id, if you need it when rendering.
Of course, you could also pass it via the model as an attribute, but if it's just view-related it makes more sense to pass it as an option.

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();

How do I access methods in the scope of an object created with an object factory in Javascript

I have a hard time wrapping my head around variable scope in JS. Is there a way of accessing instance variables of an object created with an object factory similar to the example below?
function Renderer(id, options) {
var id = id;
var options = options;
return {
render: function(selector) {
$(selector).each(function(index) {
this.renderOptions(); //This does not reference the Renderer, but the html element selected by jQuery.
});
},
renderOptions: function() {
console.log(this.options);
}
}
}
var myRenderer = new Renderer('test', [1, 2, 3, 5, 8, 13]);
You just need to keep a named reference to your object, as this gets redefined on every method call and is usually pointing to the wrong context inside callbacks:
var instance = {
render: function(selector) {
$(selector).each(function(index) {
instance.renderOptions();
});
},
...
}
return instance;
Modified code
function Renderer(id, options) {
var id = id;
var options = options;
return {
render: function(selector) {
var self = this;
$(selector).each(function(index) {
self.renderOptions(); // here this is a reference of dom element.
});
},
renderOptions: function() {
console.log(this.options);
}
}
}
Since var options... is within the scope of Renderer, you can simply use options inside of the renderOptions function.
You'll also want to create a reference to this, as other posters mentioned.
function Renderer(id, options) {
var id = id;
var options = options;
return {
render: function(selector) {
var self = this;
$(selector).each(function(index) {
self.renderOptions();
});
},
renderOptions: function() {
console.log(options);
}
}
}
And, if I'm reading the intent of this code correctly, you'll probably want to pass a reference to the element into the renderOptions function:
function Renderer(id, options) {
var id = id;
var options = options;
return {
render: function(selector) {
var self = this;
$(selector).each(function(index) {
self.renderOptions(this);
});
},
renderOptions: function(ele) {
$(ele).css(options); // or whatever you plan to do.
}
}
}

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