Turning off div wrap for Backbone.Marionette.ItemView
Asked Answered
D

5

30

I'm looking at the Angry Cats Backbone/Marionette tutorial posts here

http://davidsulc.com/blog/2012/04/15/a-simple-backbone-marionette-tutorial/

http://davidsulc.com/blog/2012/04/22/a-simple-backbone-marionette-tutorial-part-2/

and I came upon the same question/need posted here:

Backbone.js turning off wrap by div in render

But I can only get that to work for Backbone.Views, not Backbone.Marionette.ItemViews.

For example, from the simple backbone marionette tutorial links above, take AngryCatView:

AngryCatView = Backbone.Marionette.ItemView.extend({
  template: "#angry_cat-template",
  tagName: 'tr',
  className: 'angry_cat',
  ...
});

The template, #angry_cat-template, looks like this:

<script type="text/template" id="angry_cat-template">
  <td><%= rank %></td>
  <td><%= votes %></td>
  <td><%= name %></td>
  ...
</script>

What I don't like, is that the AngryCatView needs to have

  tagName: 'tr',
  className: 'angry_cat',

-- if I take tagName out, then angry_cat-template gets wrapped by a <div>.

What I would like is to specify the HTML in one place (the angry_cat-template) and not have most HTML (all the <td> tags) in angry_cat-template and a little HTML (the <tr> tag) in AngryCatView. I would like to write this in angry_cat-template:

<script type="text/template" id="angry_cat-template">
  <tr class="angry_cat">
    <td><%= rank %></td>
    <td><%= votes %></td>
    <td><%= name %></td>
    ...
  </tr>
</script>

It just feels cleaner to me but I've been mucking around with Derik Bailey's answer in "Backbone.js turning off wrap by div in render" and can't get it to work for Backbone.Marionette.

Any ideas?

Demosthenes answered 1/2, 2013 at 22:24 Comment(0)
A
42

2014/02/18 — updated to accommodate the improvements noted by @vaughan and @Thom-Nichols in the comments


In many of my itemView/layouts I do this:

var Layout = Backbone.Marionette.Layout.extend({

    ...

    onRender: function () {
        // Get rid of that pesky wrapping-div.
        // Assumes 1 child element present in template.
        this.$el = this.$el.children();
        // Unwrap the element to prevent infinitely 
        // nesting elements during re-render.
        this.$el.unwrap();
        this.setElement(this.$el);
    }

    ...

});

The above code only works when the wrapper div contains a single element, which is how I design my templates.

In your case .children() will return <tr class="angry_cat">, so this should work perfect.

I agree, it does keep the templates much cleaner.

One thing to note:

This technique does not force only 1 child element. It blindly grabs .children() so if you've incorrectly built the template to return more than one element, like the first template example with 3 <td> elements, it won't work well.

It requires your template to return a single element, as you have in the second template with the root <tr> element.

Of course it could be written to test for this if need be.


Here is a working example for the curious: http://codepen.io/somethingkindawierd/pen/txnpE

Avantgarde answered 4/2, 2013 at 3:24 Comment(5)
@somethingkindaweird What is the limitation in this that only makes it have one child? I just want to understand the code better before implementing it.Gulf
@Gulf — I clarified my answer regarding the need for 1 root element in the template. The javascript code, as written, does not enforce this. It requires the template to return a single root element wrapping the content, in the original question's case, a root <tr> element.Avantgarde
This causes problems if you re-render. The new el will be used as the container for the template on re-render creating and endless nested structure on re-render. You should override render. I have posted solution below.Avalokitesvara
@Avalokitesvara I found that adding this.$el.unwrap() after this.$el = this.$el.children() fixes the issue on re-render without having to redefine the entire render method.Nietzsche
Thank you @thom-nicols and vaughan. I've updated the answer to include the correction.Avantgarde
M
10

While I'm sure there's a way to hack the internals of render to get it to behave the way you'd like, taking this approach means you'll be fighting the conventions of Backbone and Marionette through the whole development process. ItemView needs to have an associated $el, and by convention, it's a div unless you specify a tagName.

I empathize -- especially in the case of Layouts and Regions, it appears to be impossible to stop Backbone from generating an extra element. I'd recommend accepting the convention while you learn the rest of the framework and only then deciding if it's worth hacking render to behave differently (or to just choose a different framework).

Marriott answered 3/2, 2013 at 20:27 Comment(1)
I definitely get your point -- don't fight the framework. The framework as-is allows one top element and also allows N top elements, which is flexible, that's good, but then the wrapping element (div or tagName) kicks in and I was looking to see if that was configurable. I like your answer and thx - I'm giving the check to @something because it includes code I can use right nowDemosthenes
A
3

This solution works for re-rendering. You need to override render.

onRender tricks won't work for re-render. They will cause nesting on every re-render.

BM.ItemView::render = ->
  @isClosed = false
  @triggerMethod "before:render", this
  @triggerMethod "item:before:render", this
  data = @serializeData()
  data = @mixinTemplateHelpers(data)
  template = @getTemplate()
  html = Marionette.Renderer.render(template, data)

  #@$el.html html
  $newEl = $ html
  @$el.replaceWith $newEl
  @setElement $newEl

  @bindUIElements()
  @triggerMethod "render", this
  @triggerMethod "item:rendered", this
  this
Avalokitesvara answered 10/2, 2014 at 13:3 Comment(1)
As the OP has written in JavaScript, it would be helpful if your answer was also written in JavaScript. IMO this should be the accepted answer as you are indeed right about reRender, but sadly coffeescript is horrible.Madelenemadelin
B
3

Wouldn't it be cleaner to use vanilla JS instead of jQuery to accomplish this?

var Layout = Backbone.Marionette.LayoutView.extend({

  ...

  onRender: function () {
    this.setElement(this.el.innerHTML);
  }

  ...

});
Bearded answered 13/2, 2015 at 5:27 Comment(1)
Actually, depending on the situation, this may lead to the loss of elements in the template (when it has more than one), not to mention their properties (due to the use of innerHTML). Not the OP's case but if your templates can have more than one element and you need a general solution, see my answer.Epithalamium
E
1

For IE9+ you could just use firstElementChild and childElementCount:

var Layout = Backbone.Marionette.LayoutView.extend({

  ...

  onRender: function () {
      if (this.el.childElementCount == 1) {
          this.setElement(this.el.firstElementChild);
      }
  }

  ...

});

There is a good reason why Marionette automatically inserts the wrapper DIV. It's only when your template consists of just one element when you can drop it. Hence the test for number of child elements.

Another option is to use the attachElContent method present in every Marionette view. Its default implementation means re-renders of the view will overwrite the root element's inner HTML. This ultimately gives rise to the infinite nesting mentioned in bejonbee's answer.

If you would rather not overwrite onRender and/or require a pure-JS solution, the following code might be just what you want:

var Layout = Backbone.Marionette.LayoutView.extend({

  ...

  attachElContent: function (html) {
      var parentEl = this.el.parentElement;
      var oldEl;

      //View already attached to the DOM => re-render case => prevents
      //recursive nesting by considering template's top element as the
      //view's when re-rendering
      if (parentEl) {
          oldEl = this.el;
          this.setElement(html);                   //gets new element from parsed html
          parentEl.replaceChild(this.el, oldEl);   //updates the dom with the new element 
          return this;

      //View hasn't been attached to the DOM yet => first render 
      // => gets rid of wrapper DIV if only one child
      } else {
          Marionette.ItemView.prototype.attachElContent.call(this, html);
          if (this.el.childElementCount == 1) {
              this.setElement(this.el.firstElementChild);
          }
          return this;
      }
  }

  ...

});

Note that for re-rendering to work, the code assumes a template with a single child that contains all markup.

Epithalamium answered 22/3, 2016 at 21:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.