Extra wrappers in Backbone and Marionette
Asked Answered
W

4

20

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?

Wedged answered 25/6, 2012 at 18:36 Comment(0)
G
13

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.

Gnosis answered 6/7, 2012 at 16:52 Comment(2)
Is there a marionette specific way to accomplish this? Preferably I think it would be nice to just use the 'template' attributeDoing
Excellent. I really dislike all those other answers I see saying to manipulate the DOM to solve this problem. Why, when you can just setElement()?Dramatist
F
10

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);
  }
});
Flews answered 26/2, 2013 at 0:44 Comment(5)
Shouldn't that be part of Marionette? I mean ItemViews should reuse divs used only for selecting regions.Convertible
I don't disagree, it would sure make my views a lot less verbose. In the end I just ended up going with the flow and structuring my HTML templates to work with the default behavior. It wasn't so bad.Flews
This solution is buggy, and should be avoided, DANGER!! cost me 1 hour.Consuelaconsuelo
@eguneys Which solution? The original (I know that one is buggy - see my first edit) or the one suggested by @chris-neale? Also, what kind of bugs did you run into. I haven't touched this in ages, I'm sure Marionette has changed a bunch since I wrote this.Flews
i tried both, this.$el doesn't get copied properly. dom context is undefined.Consuelaconsuelo
N
6

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.

Nerin answered 25/6, 2012 at 22:49 Comment(2)
No dice on either method. Setting the ID to match the parent element simply adds the same id to the wrapper div so you have two divs with the same ID. The second method fails to render the content. I tried both $('#main-content') and '#main-content' with no luck. Any other ideas?Wedged
Maybe you could have a look at regions, have a look at this postNerin
G
1

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.

Greggs answered 15/3, 2013 at 18:7 Comment(2)
Do you still use it and did you encounter any problem ? This solution is quite interesting but I am wondering of its reliability. It's working so far though, thx!Netta
Why would you not just make tagName: 'tr', and then remove the wrapping <tr></tr> from your template? If you need to add classes or attributes to the row, just add them via Backbone.View's attributes property.Charwoman

© 2022 - 2024 — McMap. All rights reserved.