I am learning Backbone.
I am wondering whether or not a Backbone View always requires a Backbone Model.
For example, let's say I have a panel that contains two child panels. The way I would structure this is with a parent view for the main panel, then two child views for the child panels...
var OuterPanel = Backbone.View.extend({
initialize: function() {
this.innerPanelA = new InnerPanelA(innerPanelAModel);
this.innerPanelB = new InnerPanelB(innerPanelBModel);
},
});
var outerPanel = new OuterPanel();
The parent view is really just a container. It may have some controls in it, but no data that needs to be persisted. Is this the proper way to do it? Or is this bad practice?
Thnx (in advance) for your help
As said in Backbone.View docs
Backbone views are almost more convention than they are code — they
don't determine anything about your HTML or CSS for you, and can be
used with any JavaScript templating library.
In other words, if you don't have a model, don't use a model. On the other hand, I would inject the children models as options to the outer view instance and not rely on global variables, something like this:
var OuterPanel = Backbone.View.extend({
initialize: function(options) {
this.innerPanelA = new InnerPanelA({model: options.modelA});
this.innerPanelB = new InnerPanelB({model: options.modelB});
}
});
var outerPanel = new OuterPanel({
modelA: innerPanelAModel,
modelB: innerPanelBModel
});
Related
giving a parent and a child view, I'd like 2 things:
the child view should render itself on instantiation
the child's render method should know the current parent's dom element
In practice, from the parent view, instead of this:
,add_bannerbox_view:function(model, collection, options){
var bannerbox = new BannerBoxView({ model: model });
this.bannerbox_views[model.cid] = bannerbox;
this.bannerbox_container.append(bannerbox.el);
bannerbox.render();
}
I'd like simply this;
,add_bannerbox_view:function(model, collection, options){
//here, BannerBoxView is supposed to render itself from initialize()
this.bannerbox_views[model.cid] = new BannerBoxView({ model: model, parent:this.el });
}
But I was wondering: is passing a parent's elem to the child a good practice? Or does it have some bad drawback?
Loose coupling is almost always preferable to tight coupling. The two best reasons I can think of are:
Reusable. Can be used by anywhere in your app without worrying about dependencies.
Testable. Can be tested independent of other components.
By requiring the child view to have a reference to the parent view, you are promoting tight coupling i.e. the child view becomes dependent on the parent view. This makes reusability extremely difficult, and if you're writing unit tests, you're going to have to instantiate or mock a parent class just so you can test the child. This is unnecessary and tedious.
If really what you're trying to do is have the child view automatically render, just extend the core Backbone.View and include a helper function that your parent views can call.
var MyView = Backbone.View.extend({
renderChild: function(view, options) {
var childView = new view(options);
this.views[options.model.cid] = childView;
this.$el.append(childView.el);
childView.render();
}
});
Then, you can define your parent views like so:
var ParentView = MyView.extend({
add_bannerbox_view: function() {
this.renderChild(BannerBoxView, {model: model});
}
});
The helper function we made will let you instantiate, append and render your child views with a single line of code.
I partially answer to myself. More than circular references (I'm passing only a dom element), drawbacks could arise for the self-appending functionality I'd like to use in child's render() method. The reason is possible memory leaks when having large number of views. There is a good explanation here:
http://ozkatz.github.io/avoiding-common-backbonejs-pitfalls.html
I should use var container = document.createDocumentFragment() in the parent view and then maybe pass container to the child view.
Also, following discussions above, and still not fully convinced of the various points (mine first :P) I'm using sort of bridge code. For now, I like doing this: I don't pass parent's dom element as a constructor argument. Instead, I pass it directly to the child's render(). The code is cleaned out to the bare bones:
//parent
var CustomBannersView = Backbone.View.extend({
initialize:function(){
this.groups_container = $('.groups-container');
this.group_views = {};
this.init();
this.set_events();
}
,init:function(){
//instantiate views without rendering for later use
this.collection.each(function(model){
this.group_views[model.cid] = new GroupView({ model:model, id:'group-' + model.cid });
},this);
}
,render:function(){
var temp_box = document.createDocumentFragment();
//render views without dom refresh. Passing the box.
_.each(this.group_views, function(groupview){ groupview.render(temp_box); });
//add container
this.groups_container.append(temp_box);
}
//dom events ----
,events:{
'click .create-gcontainer-button': function(){
this.collection.add(new Group());
}
}
,set_events:function(){
this.listenTo(this.collection,'add',function(model, collection, options){
//render a single subview, passing the main container
//no refresh problem here since it's a single view
this.group_views[model.cid] = new GroupView({ model: model, id:'group-' + model.cid }).render(this.groups_container);
});
}
});//end view
//child
var GroupView = Backbone.View.extend({
tagName: 'fieldset'
,className: 'group'
,initialize:function(){
this.template = Handlebars.compile($('#group-container').html());
}
,render:function(box){//box passed by parent
this.$el.html(this.template(this.model.toJSON()));
$(box).append(this.$el);
//now I can set things based on dom parent, if needed
return this;
}
});
I am currently trying to put a backbone model inside an already existing model. I was wondering if this is even possible.
var rf = this.collection.create(attrs, options);
Model.set(table, rf);
Thanks
What you trying to do is "Nested Models & Collections". Backbone already has preferable approach. The common idea consist in storing of nested model directly in the instance of another model instead attributes.
So, you could create child model first and then pass it to parent model through options like the following:
var rf = this.collection.create(attrs, options);
var Model = Backbone.Model.extend({
initialize: function(attributes, options) {
_.isObject(options) || (options = {});
if (options.child) {
this.child = new options.child;
}
}
});
var model = new Model({}, {child: rf});
If you want to get a fully supported tree-like Backbone models you could try to use one of the following plugins.
Hope this helps!
I have a view and I need to re-render it in the same page.
If I call .render() again the first rendered is gone.
Cloning view object with jQuery.extend() has same result.
var cloneView = $.extend(true, {}, view);
$('#container').append(cloneView.render().el);
I cannot call new View() because there are various view classes.
How can I make a proper clone of a view?
You can't just clone your view and use it the way you want, because there's a lot of work done in the background, like :
cid generation : unique id of your view in your application.
$el generation : the view main DOM element
events delegation : the view events delegation
So if you insist in cloning your view, I will suggest you to create a clone of it var cloneView = $.extend(true, {}, view); that do what exactly new View do.
And as mu is too short suggested, event $.extend won't work.
So the best way to do it, is to instantiate a new View
It is dangerous to go blindly clone any view. You need to check for subtle things like hard-coded IDs in the template and in the view, styles of the template when it's re-used in different locations,..etc.
I would suggest either:
Create a new instance of the view. (if you're sure it's safe to render the another instance simultaneously with the previous instance of the view on the same page).
var my_view = new MyView({ el: $("#id") });
Create a new view extending that view. (this is a better bet)
CloneView = MyView.extend({
// Stuff for CloneView
});
var my_view = new CloneView({ el: $("#id") });
In my backbone app I have a view that needs to be rendered using different templates, dependant on a variable that is passed to the view. I am using require js and want to know if there is smart way of doing this.
Here is how I would call instantiate the new view:
var templateType = 'Template1'
var view = new DetailView({model:thisModel, 'templateType': templateType});
Here is an example of the view:
define(['backbone',
'text!templates/template1.html',
'text!templates/template2.html' ],
function(Backbone,
Template1,
Template2){
var DetailView = Backbone.View.extend({
tagName : 'li',
className: 'detail-tag',
initialize : function(){
this.detailTemplate = _.template( this.options.templateType );
this.render();
},
This does not give me the required result. How do I use the options value to link to the require variable - Template1? I realise I could probably set up a switch statement or something, but that seems a bit messy. Is there a neater way of doing it?
Thanks
I decided to pass the template to the view, instead of trying to match it up with a template included with require. I used this question as a model for my solution:
How to pass a template to a view in Backbone
I am cleaning up a multi-page app of 65+ html pages and a central javascript library. My html pages have a ton of redundancies and the central js library has become spaghetti. I face limitations on consolidating pages because I am working within a larger framework that enforces a certain structure. I want to reduce the redundancies and clean up the code.
I discovered backbone, MVC patterns, microtemplating and requirejs, but they seem best for single page applications. Somehow I need to let the main module know what page is being loaded so it will put the right elements on the page. I am thinking of passing in the title of the html which will turn grab the correct collection of page elements and pass them into App.initialize as an object.
1) Can anyone validate this approach? If not are there alternate approaches recommended? How about extensions to backbone like marionette?
2) Can anyone recommend a means to get page specifics into the backbone framework?
Following backbone tutorials I built a successful test page with a main.js that calls an App.initialize method that calls a view.render method. My first thought is to read the html page title and use it to select a model for the specific page being loaded. I'd have to pass in an object with the specifics for each pages layout. Here's the view's render method so you can see what I am trying to do:
render: function () { // pass parameter to render function here?
var data = new InputModel,
pageTitle = data.pageTitle || data.defaults.pageTitle,
compiled,
template;
var pageElements = [
{ container: '#page_title_container', template: '#input_title_template' },
{ container: '#behavior_controls_container', template: '#behavior_controls_template' },
{ container: '#occurred_date_time_container', template: '#date_time_template' }]
for (var i = 0; i < pageElements.length; i++) {
this.el = pageElements[i].container;
compiled = _.template($(InputPageTemplates).filter(pageElements[i].template).html());
template = compiled({ pageTitle: pageTitle });
//pass in object with values for the template and plug in here?
$(this.el).html(template);
}
}
Your help will be greatly appreciated. I am having a lot of fun updating my circa 1999 JavaScript skills. There's a ton of cool things happening with the language.
Using the document title to choose the loaded scripts sounds a tad kludge-y. If it works, though, go for it.
Another idea worth exploring might be to utilize Backbone.Router with pushState:true to setup the correct page. When you call Backbone.history.start() on startup, the router hits the route that matches your current url, i.e. the page you are on.
In the route callback you could do all the page-specific initialization.
You could move the template and container selection out of the view into the router, and set up view in the initialize() function (the view's constructor). Say, something like:
//view
var PageView = Backbone.View.extend({
initialize: function(options) {
this.model = options.model;
this.el = options.el;
this.title = options.title;
this.template = _.template($(options.containerSelector));
},
render: function() {
window.document.title = title;
var html = this.template(this.model.toJSON());
this.$el.html(html);
}
});
Handle the view selection at the router level:
//router
var PageRouter = Backbone.Router.extend({
routes: {
"some/url/:id": "somePage",
"other/url": "otherPage"
},
_createView: function(model, title, container, template) {
var view = new PageView({
model:model,
title:title
el:container,
templateSelector:template,
});
view.render();
},
somePage: function(id) {
var model = new SomeModel({id:id});
this._createView(model, "Some page", "#somecontainer", "#sometemplate");
},
otherPage: function() {
var model = new OtherModel();
this._createView(model, "Other page", "#othercontainer", "#othertemplate");
}
});
And kick off the application using Backbone.history.start()
//start app
$(function() {
var router = new PageRouter();
Backbone.history.start({pushState:true});
}
In this type of solution the view code doesn't need to know about other views' specific code, and if you need to create more specialized view classes for some pages, you don't need to modify original code.
At a glance this seems like a clean solution. There might of course be some issues when the router wants to start catching routes, and you want the browser to navigate off the page normally. If this causes serious issues, or leads to even bigger kludge than the title-based solution, the original solution might still be preferrable.
(Code examples untested)