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
Related
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)
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.
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.
Is there a convenient way to add an effect when I leave a page (close a view/layout) and open a new one in the same region ? (something like a fade effect)
Marionette regions have a method called open that by default just replace the HTML of the old view with the new view. You can override this method to do any animation you like. From the region documentation:
Set How View's el Is Attached
If you need to change how the view is attached to the DOM when
showing a view via a region, override the open method of the
region. This method receives one parameter - the view to show.
The default implementation of open is:
Marionette.Region.prototype.open = function(view){
this.$el.html(view.el);
}
This will replace the contents of the region with the view's
el / content. You can change to this be anything you wish,
though, facilitating transition effects and more.
Marionette.Region.prototype.open = function(view){
this.$el.hide();
this.$el.html(view.el);
this.$el.slideDown("fast");
}
This example will cause a view to slide down from the top
of the region, instead of just appearing in place.
You could override the close function on the view, doing something like this:
close: function () {
// fancy fade-out effects
Backbone.Marionette.View.prototype.close.apply(this, arguments);
}
And do something similar with your render functions.
This seems to work for me:
this.views = {
messageItem: Marionette.ItemView.extend({
template: Handlebars.templates.messaging_item,
tagName: "li",
className: "messaging-item",
render: function(){
this.$el.html(this.template(this.model.attributes));
this.$el.hide();
},
onShow: function(){
this.$el.slideDown(800);
}
})
};
For future users people could user my plugin for Transition Support in marionette.
https://github.com/saqibshakil/Marionette.TransitionRegion/
I used css3 transitions as those have more hardware support than jquery animations. on the downside using this makes the code async so be carefull of that.
I think this could be useful for you.
The following marionette plugin that adds 4 kind of transitions. There can be easily added more transition types.
Basically instead of using yourRegion.show(view)...
you can use now yourRegion.showAnimated(view, {animationType: 'yourAnimation'});
it's very easy to use.
https://github.com/marcinkrysiak1979/marionette.showAnimated
see the documentation on github for more info
I have created a Backbone.js/Require.js application that dynamically loads HTML templates to use as "pages" in the application. This means my main HTML page looks like so.
<head>
// Necessary CSS and Javascripts here
</head>
<body>
<div id="container"></div>
</body>
And then I used underscore templates to render new elements dynamically to the DOM. However, a new feature requires the use of a Raphael.js chart. I created a new element <div id='canvas'></div> and call Raphael('canvas') but since the canvas element wasn't there on DOM ready, Raphael can't see the newly created element.
I have attempted to use a jQuery selector in place of the id reference like so Raphael($('#canvas')) but this attaches the canvas to the body element and not my container element.
Any suggestions on how to bind a Raphael canvas to a dynamically created element?
Raphael($('#canvas').first(), '100%', '100%')
Though I had errors else where, the main issue that caused Raphael not to fire was forgetting that a jQuery selector passes an array of Elements and Raphael's constructor want's a single element. Raphael was attaching itself to the body because it was the top level parent of the selector's result.
Mosselman was also correct in pointing out that you can build a view in Backbone entirely in memory and then append it to the DOM.
A way to overcome this issue is by creating an empty element in the view and binding everything onto that. I have never worked with Raphael, but I think this could work:
var someView = Backbone.View.extend({
el: document.createElement('div'), // This creates a DOM element '<div></div>'
initialize: function(){
Raphael(this.el); // Attach Raphael, you could also go with jQuery
},
render: function(){
jQuery('#container').append(this.el); // Add to DOM somehow
}
})
seems like a good approach is to either throw an event after the template has been added to the DOM and have your call to Raphael('canvas') listen for that event or use a callback to trigger Raphael('canvas'). in both cases you are ensuring that you don't call Raphael('canvas') before the target element is in place.
very roughly, something like this:
//from your raphael module / code
$(document).on('canvasAdded', function(){
var paper = Raphael('canvas');
//stuff!
});
//after you are sure your template has rendered
$(document).trigger('canvasAdded');
you probably want to make some kind of .init() method and call that from the event handler (vs. what I show above) but hopefully this points you in the right direction.
I know that this is too old question, but anyway it can help to someone. Its important to be sure that your view is placed on page, so use something like onShow functionality, or render. But anyway Raphael will not show right because if your send to Raphael this.$el or anything similar it will not accept it like you expect. What You need to do is something like this.$el.first() or this.$el[0].