ivarni
ivarni

Reputation: 17878

How do I add a blank option to a select generated by a Marionette CollectionView?

I am rendering a Backbone collection as a select element, and I want it to have a blank option at the top.

I am a bit unsure how to best do this using a Marionette CollectionView. (I am aware of the CompositeView but I don't really feel it fits this usecase).

So far I've tried using the onBeforeRender hook to add the option

onBeforeRender: function() {
  var option = $('<option>').html('')
  this.$el.append(option);
}

The problem I'm facing is that render is actually being called twice, since my CollectionView is inside a LayoutView which calls the render function when it shows the view

this.getRegion('body').show(new SelectView({
  collection: collection
}));

And then render is triggered again when its collection is reset

collection.reset([{ name: 'a' },{ name: 'b' }]);

I've verified that this is what's happening by using the debugger and looking at the callstack.

Complete snippet:

$(document).ready(function() {
  
var collection = new Backbone.Collection({
  model: Backbone.Model
});


var Layout = Backbone.Marionette.LayoutView.extend({
  
  regions: {
    body: '#main'
  },
  
  template: false,
  
  onRender: function() {
    this.getRegion('body').show(new SelectView({
      collection: collection
    }));
  }
});

var OptionView = Backbone.Marionette.ItemView.extend({
  tagName: 'option',
  template: _.template('<%= name %>')
});
  
var SelectView = Backbone.Marionette.CollectionView.extend({
   tagName: 'select',
   childView: OptionView,
   onBeforeRender: function() {
     var option = $('<option>').html('')
     this.$el.append(option);
   }
});

var layout = new Layout({
  el: $('#app')
});
layout.render();
var app = new Marionette.Application();

app.start();
Backbone.history.start();

collection.reset([{ name: 'a' },{ name: 'b' }]);
});
<script data-require="[email protected]" data-semver="1.6.0" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>  
<script data-require="[email protected]" data-semver="2.1.3" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="[email protected]" data-semver="1.1.2" src="http://backbonejs.org/backbone-min.js"></script>
<script data-require="[email protected]" data-semver="2.2.2" src="http://cdnjs.cloudflare.com/ajax/libs/backbone.marionette/2.2.2/backbone.marionette.js"></script>

<body>
  <div id="app">
    <div id="main"></div>
  </div>
</body>

Now in this snippet I could fix the issue by simply resetting the collection at the start of the script instead of at the end, but in my application that is not a possible solution.

I can think of a few solutions to this, but they all feel a bit wrong, I most certainly don't want to maintain any state for this in my view. The least worst thing I could think of was to change to

onBeforeRender: function() {
  this.$el.html('')
  var option = $('<option>').html('')
  this.$el.append(option);
}

But since the framework doesn't seem to empty the $el itself I am guessing there's a reason for that.

Is this a legit usecase for CompositeView or is there some other smart way to do this that I have missed?

Upvotes: 0

Views: 356

Answers (1)

ivarni
ivarni

Reputation: 17878

In the case that CompositeView is OK for this usecase, the solution would be to change the CollectionView into

var SelectView = Backbone.Marionette.CompositeView.extend({
   template: _.template('<select><option></option></select>'),
   childView: OptionView,
   childViewContainer: 'select'
});

As demonstrated in this snippet:

$(document).ready(function() {
  
var collection = new Backbone.Collection({
  model: Backbone.Model
});


var Layout = Backbone.Marionette.LayoutView.extend({
  
  regions: {
    body: '#main'
  },
  
  template: false,
  
  onRender: function() {
    this.getRegion('body').show(new SelectView({
      collection: collection
    }));
  }
});

var OptionView = Backbone.Marionette.ItemView.extend({
  tagName: 'option',
  template: _.template('<%= name %>')
});
  
var SelectView = Backbone.Marionette.CompositeView.extend({
  template: _.template('<select><option></option></select>'),
  childView: OptionView,
  childViewContainer: 'select'
});

var layout = new Layout({
  el: $('#app')
});
layout.render();
var app = new Marionette.Application();

app.start();
Backbone.history.start();

collection.reset([{ name: 'a' },{ name: 'b' }]);
});
<script data-require="[email protected]" data-semver="1.6.0" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>  
<script data-require="[email protected]" data-semver="2.1.3" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="[email protected]" data-semver="1.1.2" src="http://backbonejs.org/backbone-min.js"></script>
<script data-require="[email protected]" data-semver="2.2.2" src="http://cdnjs.cloudflare.com/ajax/libs/backbone.marionette/2.2.2/backbone.marionette.js"></script>

<body>
  <div id="app">
    <div id="main"></div>
  </div>
</body>

Upvotes: 2

Related Questions