关闭 Backbone.Marionette.ItemView 的 div 包装

30

我正在查看这里的 Angry Cats Backbone/Marionette 教程文章:

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/

然后我在这里发现了同样的问题/需求:

Backbone.js turning off wrap by div in render

但是我只能使它适用于 Backbone.Views,而不是 Backbone.Marionette.ItemViews。

例如,从上面提供的简单的 Backbone Marionette 教程链接中,可以取得 AngryCatView 的代码:

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

模板 #angry_cat-template 的外观如下:

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

我不喜欢的是,AngryCatView 需要拥有

  tagName: 'tr',
  className: 'angry_cat',
--如果我把tagName拿出来,那么angry_cat-template将被<div>包裹。
我希望的是在一个地方指定HTML(即angry_cat-template),而不是让大多数HTML(所有的<td>标签)在angry_cat-template中并且只有一点HTML(<tr>标签)在AngryCatView中。我希望在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>

我觉得这样更清晰,但是我一直在尝试着使用 Derik Bailey 在“Backbone.js turning off wrap by div in render”中的答案来解决问题,但无法让它适用于 Backbone.Marionette。

有什么想法吗?

5个回答

42

2014/02/18 — 经过@vaughan和@Thom-Nichols在评论中指出的改进,进行了更新。


在我的许多itemView/layouts中我这样做:

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);
    }

    ...

});

上面的代码仅适用于包含单个元素的包装 div,这是我设计模板的方式。

在您的情况下,.children() 将返回 <tr class="angry_cat">,所以这应该完美地工作。

我同意,它确实使模板更加清晰。

需要注意一点:

这种技术不强制只有1个子元素。它盲目地获取 .children(),因此,如果您错误地构建了返回多个元素的模板(例如第一个模板示例中的 3 个 <td> 元素),则它将无法正常工作。

它要求您的模板返回单个元素,就像第二个模板中的根 <tr> 元素一样。

当然,如果需要,它也可以编写为测试此内容。


这里有一个好奇的工作示例:http://codepen.io/somethingkindawierd/pen/txnpE


1
@somethingkindaweird 这里的限制是什么,只让它有一个子节点?在实现代码之前,我想更好地理解它。 - streetlight
@streetlight — 我澄清了关于模板需要一个根元素的答案。如所写的JavaScript代码并没有强制执行此操作。它要求模板返回一个包装内容的单个根元素,在原问题的情况下,是一个根<tr>元素。 - Jonathan Beebe
1
如果您重新渲染,这会导致问题。新的元素将在重新渲染时用作模板的容器,从而创建无限嵌套的结构。您应该覆盖render方法。我已经在下面发布了解决方案。 - vaughan
2
@vaughan 我发现在 this.$el = this.$el.children() 之后添加 this.$el.unwrap() 可以解决重新渲染时出现的问题,而无需重新定义整个 render 方法。 - thom_nic
谢谢@thom-nicols和vaughan。我已经更新了答案,包括更正内容。 - Jonathan Beebe

10

虽然我确信有一种方法可以破解render的内部结构以使其按您想要的方式运行,但采用这种方法意味着在整个开发过程中您将与Backbone和Marionette的惯例抗争。 ItemView需要一个相关联的$el,而按照惯例,它是一个div,除非您指定tagName

我能理解您的感受——尤其是在布局和区域的情况下,似乎无法阻止Backbone生成额外的元素。我建议在学习完框架的其余部分并仔细考虑是否值得破解render来改变它的行为(或者选择不同的框架)之后,接受这种惯例。


1
我完全理解你的意思——不要与框架对抗。框架本身允许一个顶部元素,也允许N个顶部元素,这是灵活的,很好,但随后包装元素(div或tagName)会介入,我想知道是否可配置。我喜欢你的答案,谢谢——我将把检查交给@something,因为它包含了我现在可以使用的代码。 - mrk

3

使用纯JS代替jQuery来完成这个任务,会不会更加简洁?

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

  ...

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

  ...

});

实际上,根据情况,这可能会导致模板中的元素丢失(当它有多个时),更不用说它们的属性了(由于使用innerHTML)。虽然这不是OP的情况,但如果您的模板可以有多个元素并且需要一个通用解决方案,请参阅我的答案。 - HQCasanova

3

这个解决方案适用于重新渲染。您需要覆盖render方法。

onRender技巧在重新渲染时不起作用。它们会导致每次重新渲染时出现嵌套问题。

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

8
由于原帖是用JavaScript编写的,如果您的回答也使用JavaScript编写,那会很有帮助。在我看来,这应该是被接受的答案,因为您确实对reRender做出了正确的解释,但可惜的是coffeescript太糟糕了。 - designermonkey

1
对于IE9+,您可以使用firstElementChildchildElementCount
var Layout = Backbone.Marionette.LayoutView.extend({

  ...

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

  ...

});

Marionette自动插入包装DIV的原因很充分。只有在模板只包含一个元素时才可以删除它。因此需要测试子元素的数量。

另一个选项是使用每个Marionette视图中都存在的attachElContent方法。其默认实现意味着视图的重新渲染将覆盖根元素的内部HTML。这最终导致了bejonbee答案中提到的无限嵌套问题。

如果您不想覆盖onRender和/或需要纯JS解决方案,那么以下代码可能正是您想要的:

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;
      }
  }

  ...

});

请注意,为了使重新渲染正常工作,代码假定模板具有一个包含所有标记的单个子元素。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接