Get rid of surrounding elements in Backbone.Marionette - backbone.js

I have the following view:
return Marionette.ItemView.extend({
el: '<section>',
template: JST['app/scripts/templates/grid.ejs'],
that is called like this:
// a Layout
regions: {
grid: '#grid',
detail: '#detail'
},
onShow: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
this.grid.show(g);
}
The question is: How do I get rid of the surrounding section element ? I tried to omit the el property but that gives me the following strange looking div:
<div productname>
Regards Roger

The surrounding element is required for backbone to work. It is essentially a container/placeholder for your view to sit in, whether its contents have been rendered or not.
If you really insist on not having the container then I would consider resorting to the following:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.region.md#set-how-views-el-is-attached
Marionette.Region.prototype.open = function(view){
this.$el.empty().append(view.$el.children());
}
I say 'resorting' because, in my opinion, this is not how Backbone is supposed to be used and may have side-effects. (im not quite sure what will happen when the view in that region tries to re render; what will it's el element be pointing to?)

To expand on Scott's answer, it's probably a very bad idea to try and force the removal of the surronding view tags.
All Backbone views are contained within an DOM element. Given this fact, you have 2 main options:
have Backbone put your view into the default div element
specify which element you want Backbone to wrap your view with, using the el or tagName attributes
If the "extra" tags are creating issues (e.g. you need to generate a specific HTML set for use with a plugin), then you're not defining the wrapping element properly. For more on the subject, take a look at this blog post of mine: http://davidsulc.com/blog/2013/02/03/tutorial-nested-views-using-backbone-marionettes-compositeview/
Edit based on jsFiddle: the reason for your strange behavior is that you were passing a model instance to the initialize function. This is then interpreted as attributes for the view and get set as HTML attributes.
The correct way to provide a model instance to a view is :
new App.FooterView({
model: new App.Model()
})
In other words, you provide a javascript object to the view, with a model property. If you want to learn Marionette basics quickly, check out the free preview to my book: http://samples.leanpub.com/marionette-gentle-introduction-sample.pdf (You'll find how to instantiate a view with a model on pages 15-21)

Related

How to use Select2 with Backbone Marionette views outside the DOM

I'm trying to integrate the Select2 widget with Backbone Marionette views. My simple setup uses a Marionette.CollectionView to create and handle the select tag and Marionette.ItemViews to render the option tags.
That basically looks like this:
SelectCollectionView = Backbone.Marionette.CollectionView.extend({
itemView : SelectItemView,
tagName : "select",
onRender : function() {
this.$el.select2();
},
onClose : function() {
this.$el.select2("destroy");
}
}
SelectItemView = Backbone.Marionette.ItemView.extend({
tagName : "option",
render : function() {
// create the needed option tags
}
}
As you can see, I have to call the Select2 initialize and destroy methods upon render and close to have the needed additional tags added to the DOM.
This setup works very well, as long as the view handling the select tag (SelectCollectionView) has already been added to the DOM. If that is not the case, Select2's additional tags get lost, as they are not part of SelectCollectionView's $el and thus not added to the DOM.
I wonder how to elegantly solve this? One could add an extra div container and render everything inside it, but that would produce extra code for the script and the DOM. It also makes my view less versatile. I just hope for a better solution I didn't think of.
As you suspect, you are going to need a containing div to surround your template. Fortunately, this is really simple. Instead of what you currently have:
SelectCollectionView = Backbone.Marionette.CollectionView.extend({
tagName : "select"
Get rid of tagName, and in your template (I'm assuming you use Handlebars or Underscore or something like that), define your HTML:
template.html
<select class="whatever-you-want"></select>
Then in your view:
SelectCollectionView.js
SelectCollectionView = Backbone.Marionette.CollectionView.extend({
template: _.template(templateHtml)
Marionette will automatically wrap a div around your <select>, and then all of the extra markup that select2 adds will be safely contained within the view's $el.
It took me some time to figure it out, but I used to solve it this way:
var Select2View = Mn.View.extend({
template: _.template('<select class="form-control"><option></option></select>'),
onRender:function() {
this.$el.find('select').select2();
}
});
The important part here is
this.$el.find('select').select2();
which select the select tag, inside the element. But I don't use collection view in my example. If you need to fetch data you can use:
this.$el.find('select').select2({data: ['1','2','3','4']});
Also select2 provide very good API for manipulating select items during runtime.

Append backbone EL twice in Ajax Success Not Working

When I try to append my backbone view to two different places at the same time in my success method only the second appending works. Do you know why?
$(content).prepend(this.$el.append(this.template({ data: data })));
$(chat_window).prepend(this.$el.append(this.template({ data: data })));
Each DOM node can have exactly 0 or 1 parent nodes, never more than 1. If you append a node somewhere, it gets removed from it's current parent and then appended to the new parent. What you need here is 2 distinct view instances each with it's own element.
el corresponds to one html element that a backbone view generates. Into that html element you can append more html weather it be another backbone view or a rendered template.
Hence in your case if the el is attached twice it finally stays where it was attached last to the dom tree. If you want attach in multiple places then I guess you should instantiate the backboneview twice.
In my opinion, the view you're talking about should not know about its distant parents or cousins but rather should trigger an event "I have new content" and then the interested views can act upon this the way they want.
That being said there is a difference between a view and its html representation(s), you could design your app so that you get 2 places in the html where you put ".new-content-holder" and pass this selector as the el of your view upon creation. Then the 2 places will be updated at the same time without you explicitly programming it. I sometimes use this technique for example when I want a paginator for a long list to be displayed over and under the list.
Some html :
<div class="content">
<p>Recent comments</>
<ul class="new-content-holder"></ul>
</div>
<div class="chat-room">
Live feed
<ul class="new-content-holder">
<li>a chat message</li>
<li>another chat message</li>
</ul>
</div>
And a view
....
var MessageView = Backbone.View.extend({
template: _.template('<li class="chat-message"><%= message %></li>'),
prependData: function(data){
this.$el.prepend(this.template(data))
},
onMessage: function(message) {
this.prependData({message: message.data})
}
});
....
//And in a super controller of sorts :
var messageView = new MessageView(el: '.new-message-holder')
Again, this is not a very good separation of concerns...but I hope that helps.
I agree with #Peter Lyons, You can not inject the same node into two elements. Ultimately, the node will move to new element. One of the ways is to get HTML from the element you want to inject and inject the same HTML twice. Since html is a string and not a dom element. You can add it as many times and inside as many elements.
Try this one:
var html = this.$el.append(this.template({ data: data })).html();
$(content).prepend(html);
$(chat_window).prepend(html);
I hope you are not using id's on elements inside your template.
PS: I don't know your use case exactly.

UI hash problems with Backbone Marionette Layout

In the following Layout, I am adding a CollectionView to display a SELECT list within onRender. Immediately after that, I am using the ui hash to enable or disable all controls within the view. This does not work for the SELECT generated by new App.View.Categories.
Should it? Or does the UI hash not work on Regions within a Layout?
App.View.UploadFile = Backbone.Marionette.Layout.extend({
template: '#upload-file-template',
regions:{
category: 'td:nth-child(4)'
},
ui:{
inputs: 'textarea, select, .save'
},
onRender: function(){
this.category.show(
new App.View.Categories({
collection: App.collection.categories
}) // generates the SELECT list
);
console.log(this.ui.inputs); // Length 2. Missing select.
console.log(this.$('textarea, select, .save')); // Length 3
this.ui.inputs.prop(
'disabled', (this.model.get('upload_status')!='staged')
);
}
});
This should be working the way you expect it to work. The code in question in the Marionette source is here: https://github.com/marionettejs/backbone.marionette/blob/master/src/marionette.itemview.js#L49-L51
The call to bindUIElements() is what converts the ui hash in to jQuery selector objects, and it is called right before the onRender method is called.
Are you seeing errors? Or is the selector simply returning nothing, and having no affect on the elements?
Update:
Ah! Of course... I wasn't paying attention to your code close enough. You're correct in that the UI element selectors happen before you're adding the the sub-view to the region. I've never run in to this situation before... but this seems like something we would want to fix / support.
For now, the best workaround I can suggest would be to call 'this.bindUIElements();' at the very end of your onRender method. This would force the ui elements to re-bind to the selectors.
I'll also add an issue to the github issues list, to look in to a better solution for this. i don't know when i'll be able to get to this, but this will at least get it on the list of things to fix.

Marionette.js: Should regions replace instead of insert?

The default behavior of marionette.js is to append a template into the element specified by the regions selector. However, I usually end up having to create a special region type and override the appendHtml function to do a replace instead.
That is not too difficult, but why is append the default?
I usually will create the layout template with an empty div to specify where the region should go. Then I replace that div with sub template when I show it.
I guess am wondering if there I'm missing the design pattern for templates that makes append more intuitive.
Thanks for the help.
Update:
So I usually will have some view for something I want rendered into the page and will want to todd n element onto the page where I want it. I will do javascript that will look something like this:
ReplaceRegion = Marionette.Region.extend({
open: function(view){
this.$el.replaceWith(view.el);
}
});
App = new Backbone.Marionette.Application();
App.addRegions({
myRegion: {
selector: "#someRegion",
regionType: ReplaceRegion
}
};
var view = new CoolWidgetView();
App.myRegion.show(view);
And then somewhere in my html I'll throw an empty div in the mix where I want my template to show up.
<div id="mywidget"></div>
Now if it is the only child element, I can use a selector that would just be the parent, but that becomes more tricky when the view i'm inserting has siblings.
Also, I'm not really asking for a change to the default, as much as I'm wondering if there's a better way to insert items where you would like them in your layouts. I'm still relatively new to the layout and design of these things so anything helps!
Thanks Derick for writing great software!
Marionette regions provide both a show and a close method. Have you tried closing the region before you show the new one?
Marionette.Region docs

Re-Rendering Handlebars partial from backbone view

I have a view, which holds handlebars template.
that template consist of another partial template.
that partial template holds a list of results, which i am using in different parts of my app.
anyhow, when trying to filter the results, i'd like to render only that part. meaning the backbone view should not render the whole view just the partial.
can it be done?
Yes, it's possible. The easiest way is to execute the whole template as you do when rendering the complete view, but only replace the the part you need in the view's el.
Something like:
template: Handlebars.compile(templateHtml),
render: function() {
//let's say your render looks something like this
this.$el.html(this.template(this.model.toJSON());
},
renderList: function() {
var html = this.template(this.model.toJSON());
var selector = "#list";
//replace only the contents of the #list element
this.$el.find(selector).replaceWith($(selector, html));
}
Depending on how dynamic your template is, you may have to call this.delegateEvents() after replacing the list for the view's events to work correctly.
Edit based on comments:
To clarify, the method I propose here does execute the view's main handlebars template again, but it doesn't render the whole view again.
Step by step:
Execute the Handlebars template function as you do in normal render.
var html = this.template(this.model.toJSON());
The variable html now contains a string of HTML markup. Nothing has yet been rendered.
Define a selector for the element, which you would like to re-render.
var selector = "#list";
Find the DOM element to replace. This presumes that you have already rendered the view once. Otherwise there will be no #list element within this.$el.
this.$el.find(selector)
Find the corresponding element in the templated html string, and replace the existing element with the new one:
.replaceWith($(selector, html));
This will only replace the #list element that's currently on the page. Anything outside #list will not be re-rendered or touched in any way.
The main reason I propose you do it this way instead of executing and rendering the partial template separately is that your view doesn't need to know anything about the implementation details of the template and the templating engine. All it needs to know that there is an element #list. I believe this is a cleaner solution, and keeps your template details separate from your view logic.

Resources