20
votes

Using Backbone and Marionette, I've created a new layout that goes into the main content div on my page. The layout looks like this:

<div id='dash-sidebar'>
    <div id='dash-profile'></div>
    <div id='dash-nav'></div> 
</div>
<div id='dash-content'></div>

The issue is that when I render the layout, Backbone automatically wraps it in a div before putting it into the main content div like this:

<div id='main-content'>    
  <div>
    <div id='dash-sidebar'>
      <div id='dash-profile'></div>
       <div id='dash-nav'></div> 
    </div>
    <div id='dash-content'></div>
  </div>
</div>

I know that I can change the element with tagName, but is it possible to avoid wrapping the template altogether and just insert it directly into the main content div on the page?

4

4 Answers

13
votes

Each Backbone View must be represented by a single element. Your first HTML block has two elements, which is why it cannot be represented by a view without first wrapping it in an outer div.

Could you refactor your Layout to include the main-content area as well? Then the Layout's el would correspond to the entire outer div.

Another thing to try would be using Backbone.View's setElement() method to override the creation of the outer div, and manually inject the HTML that you want for the element in a View. Something like:

onRender: function() {
    this.setElement( /* the HTML you want for your layout */ );
}

I'm not sure how this would work if you passed in HTML that had two parent elements instead of just one, however.

10
votes

EDIT3: Warning! This answer may be out of date. I received a comment that this answer no longer works and have not had the time to investigate (I personally do not use this method).

I like to use Twitter/Bootstrap as my UI library and was having some issues with table styling because of the default tag wrapping (specifically a <div> between my <tbody> and my <tr>s).

After some digging, I found something in the Marionette docs on the Region about how the View attaches the el. Backbone builds the el from various View attributes and keeps the built element up to date so it can be rendered at any time. So I figured, if view.el is the parent, why not just use the HTML contents? Marionette also provides a way to create a custom Region

I was able to get everything running by creating a custom Region with an overridden open function. That way I can specify which regions I want to wrap with a tag and those that I do not. In the following example snippet, I create my custom non-wrapping Region (NowrapRegion) and use it in my Layout (MyLayout) for my <tbody> (the views I pass in my real program create <tr>s).

var NowrapRegion = Marionette.Region.extend({
  open: function(view){
    this.$el.html(view.$el.html());
  }
});

var MyLayout = Marionette.Layout.extend({
  template: templates.mylayout,
  regions: {
    foo: '#foo',
    columns: '#columns', //thead
    rows: { selector: '#rows', regionType: NowrapRegion } //tbody
  }
});

BOOM! Wrapping when I want it and none when I don't.

EDIT: This seems to affect events applied at the *View level. I haven't looked into it yet, but be warned that they don't seem to be getting triggered.

Whether this is the "right" way to do it or not, I am not sure. I would welcome any feedback from @derick-bailey should he see this.

EDIT2: @chris-neale suggested the following, I have not verified this yet but it seems sound. Thanks!

Rather than copying the html in the view, using a deep clone on the child nodes will maintain events and data.

var NowrapRegion = Marionette.Region.extend({
  open: function(view){
    view.$el.children().clone(true).appendTo(this.$el);
  }
});
6
votes

UPDATE: Marionette.Layout is an extension of the Backbone view, so this is the normal behavior, see backbone documentation:

The "el" property references the DOM object created in the browser. Every Backbone.js view has an "el" property, and if it not defined, Backbone.js will construct its own, which is an empty div element.

So my previous answer had nothing to do with your problem, sorry.

Update:

Found an issue 546 in Backbone gitHub on this subject (wich was closed as wontfix), jashkenas posted this comment to explain why it is not easy to implement:

A large part of the advantage of using Backbone Views is the fact that they have their element available at all times -- regardless of whether a template has been rendered (many views have multiple templates) -- regardless of wether the view is present in the DOM or not.

This allows you to create and add views to the DOM, rendering later, and be sure that all of your events are bound correctly (because they're delegated from view.el).

If you can come up with a good strategy that allows a view to have "complete" templates while preserving the above characteristics ... then let's talk about it -- but it must allow the root element to exist without having to render the contents of the template (which may depend on data that might not arrive until later), and it must allow a view to easily have multiple templates.

1
votes

I wanted to achieve pretty much the same thing. I'd like to have all the HTML markup in my templates and let Backbone do the rest. So I wrote the following snippet which removes the extra 'div'.

First, you set the tagName to 'detect' and then after the first render you detect the tagName of the first element inside your view. Obviously this only works when you provide your own wrapper in your template.

// Single table row inside tbody
tableRowView = Backbone.Marionette.ItemView.extend({
  tagName: 'detect',
  template: function(data) {
    return Handlebars.templates['tablerow/single'](data);
  },
  onRender: function() {
    if (this.tagName == 'detect')
      this.tagName = this.$el.children().first().prop('tagName').toLowerCase();
    this.setElement( this.$el.children( this.tagName ) );
    var $parent = this.$el.parent( this.tagName );
    if ($parent.length) {
      this.$el.detach();
      this.$el.insertAfter($parent);
      $parent.remove();
    }
  },
  modelEvents: {
    "change": "render"
  }
});

Template:

<tr>
  <td>{{artist}}</td>
  <td>{{title}}</td>
  <td>{{length}}</td>
</tr>

However, I've just played around with it a little - if anybody sees any problems that this approach could cause (performance, memory, zombies, etc) I'd be very open to learn about them.

BTW, this could probably easily packaged into a plugin.