Managing A Modal Dialog With Backbone And Marionette

My current client project is using Twitter Bootstrap, and I have to say I’m loving it. It provides a simple way to get started with common layout and design elements, and includes a handful of UI widgets and features that are easy to use as jQuery plugins. But enough about that… I want to manage a modal dialog that displays my Backbone view, which is a notoriously frustrating problem no matter what UI framework you’re using, it seems.

The Problem: Moving DOM Elements

The core of the problem that people run in to when using a modal dialog is that the modal plugin removes the DOM element that wraps the modal, from the DOM. It usually gets added to some special holding location where the modal plugin can guarantee that the element won’t be visible until the modal dialog is opened. I’m over-generalizing this a bit, but many of the modal dialogs work this way or in a similar manner.

The problem that this usually causes is that a Backbone view will lose it’s event handling when the DOM element gets moved around by the modal dialog. When the modal dialog removes the view’s `el` from the DOM, the `events` configuration is lost because the DOM element has been moved or removed from the DOM and jQuery had to let go of the events. When the `el` is re-added to the DOM for displaying it as a modal, then, the `events` are not re-attached.

This is usually caused by a developer using the Backbone view itself as the modal dialog – which is not what you want to do.

The Solution: Don’t Modal A Backbone View

No, really. That’s the solution.

I’m not saying you can’t use a Backbone view in a modal. I’m saying you shouldn’t try to modal the view itself. Instead, you should create a simple wrapper div for the modal plugin to use and then populate the contents of that div with the view’s `el`.

Let’s see an example of a Twitter Bootstrap modal dialog, displaying a Backbone view. First, the HTML and Underscore template:

<script id="modal-view-template" type="text/html">
  <div class="modal-header">
    <h2>This is a modal!</h2>
  </div>
  <div class="modal-body">
    <p>With some content in it!</p>
  </div>
  <div class="modal-footer">
    <button class="btn">cancel</button>
    <button class="btn-default">Ok</button>
  </div>
</script>

<div id="modal"></div>

Note that I have a “<div id=’modal’></div>” in there. This is explicitly for the Twitter Bootstrap modal dialog to use. This div is the modal. There’s no content in it, though. I will supply the content for the modal through a Backbone view, which looks like this:

var view = new MyView();
view.render();

var $modalEl = $("#modal");

$modalEl.html(view.el);
$modalEl.modal();

How easy is that? I’m just rendering a view like I always do, stuffing the view’s `el` in to the “#modal” div, and then calling `.modal` on the jQuery selector object. And that’s it. My modal is done and my view’s `events` will still work.

Managing A Modal With Backbone.Marionette

Yes, it always comes back to my Marionette plugin, right? :)

In this case, I want to manage my modal dialogs as I do any other area of my screen. I want a single object that keeps track of what view is currently displayed within the modal, handles rendering it that view for me, calls the `.modal` dialog method for me, and correctly closes the modal when the “cancel” or “close” button on my view is clicked.

This largely sounds like a Region object in Backbone.Marionette: a way to manage the contents of a particular DOM element, by rendering and displaying a Backbone View in to that DOM element. Only in this case, it’s a very specialized Region that needs to manipulate the DOM element as a Twitter Bootstrap modal.

To do this, I’m going to take advantage of Marionette’s “Region” and extend it with the behavior that I need for managing the modal:

  var ModalRegion = Backbone.Marionette.Region.extend({
    el: "#modal",

    constructor: function(){
      _.bindAll(this);
      Backbone.Marionette.Region.prototype.constructor.apply(this, arguments);
      this.on("view:show", this.showModal, this);
    },

    getEl: function(selector){
      var $el = $(selector);
      $el.on("hidden", this.close);
      return $el;
    },

    showModal: function(view){
      view.on("close", this.hideModal, this);
      this.$el.modal('show');
    },

    hideModal: function(){
      this.$el.modal('hide');
    }
  });

This is honestly more code than I wanted to write. There’s a few things that I need to clean up and add in my Region object to make this a little more elegant. But, it works! And that’s the important part.

The `constructor` function listens to the region’s “view”show” event, which is fired when a view’s contents are populated in to the `el` of the Region. In the event handler for this, I’m calling the `modal` plugin method on the `el` of the region.

I’m also setting up a “close” event handler on the view itself at this point. That way when I close the view from within my view’s code (a cancel button, or otherwise), the modal will shutdown. The handler for this simply hides the modal using the modal’s ‘hide’ option.

As I said before, I want to reduce the amount of code that is required to make this happen. But for now, this is a functional modal dialog region. I’m using it in my application, like this:

var App = new Backbone.Marionette.Application();

App.addRegions({
  main: "#main-content",
  modal: ModalRegion
});

// somewhere else in the app
var view = new MyView();
App.modal.show(view);

Note that I’m registering my ModalRegion object in my Marionette.Application with my other regions. Instead of passing in a DOM selector, though, I passing in the ModalRegion type.

More Than Just Twitter Bootstrap

The examples I’ve shown here cover the use of Twitter Bootstrap, but are applicable to more than that. You should be able to easily modify the ModalRegion object to work with any JavaScript modal dialog. I’ve used the same basic solution for jQuery UI modals, and I expect that KendoUI and other modals would work in a similar manner as well (though I may be wrong about that).

The best part about this is that you’ll only need to modify the region object. You shouldn’t have to worry about changing your entire application to use a different modal system, because we’ve encapsulated the modal system in to our ModalRegion object, and our application makes use of that to display the modal dialog with any Backbone.View that we want.


Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Derick Bailey

Derick Bailey is an entrepreneur, problem solver (and creator? :P ), software developer, screecaster, writer, blogger, speaker and technology leader in central Texas (north of Austin). He runs SignalLeaf.com - the amazingly awesome podcast audio hosting service that everyone should be using, and WatchMeCode.net where he throws down the JavaScript gauntlets to get you up to speed. He has been a professional software developer since the late 90's, and has been writing code since the late 80's. Find me on twitter: @derickbailey, @mutedsolutions, @backbonejsclass Find me on the web: SignalLeaf, WatchMeCode, Kendo UI blog, MarionetteJS, My Github profile, On Google+.
This entry was posted in Backbone, DOM, Javascript, JQuery, Marionette, Twitter Bootstrap. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://about.me/adammokan Adam Mokan

    Good post, Derick. I ran into the Bootstrap modal issue (making a view itself modal) a couple weeks ago and feel back to the exact same solution. I will have to give this a shot with Marionette now.

  • http://twitter.com/aaronmcadam Aaron Mc Adam

    Great post Derick, will definitely give Marionette a go. I’ve been laying out my app in a similar, namespaced way, but the framework you’ve set out looks cool! Thanks!

  • Kevin Jensen

    nice post..was waiting for something like this!

  • Wwwboy

    Nice idea!
    But this for the only one modal window
    What for cascading modals?

    • http://mutedsolutions.com Derick Bailey

      Good question. 

      You would probably need two views: 1 for the modal, 1 for the contents displayed in the modal. I’ve done this before, but it gets a bit hairy. 

      You might be better off falling back to a different solution without backbone, for cascading modals, though. 

    • Justin

      My quickie solution for cascading modals in marrionette is to use multiple DOM elements and the “selector” property of the region options object to target them ie.

      and then in your app region declarations go:

      App.addRegions({
      modal: modalRegion,
      modalSub: {
      selector: #modal-sub”,
      regionType: modalRegion
      }
      });

      not the cleanest but it works!

  • Nikhilesh

    Hi Derick,

    I am using a Backbone.Marionette.ItemView for my dialogs, I pass the dialog content as a template for my ItemView and then on the onRender() function, I make a call to jquery dialog() to render the view as a dialog . 

    Here is a demo fiddle :  http://jsfiddle.net/niki4810/hD56e/14/  

    Could this be a right way of rendering a dialog?

    Thanks,

    Nikhilesh

    • http://mutedsolutions.com Derick Bailey

      Hi Nikhilesh,

      If it works, then it’s all good :)

  • Anonymous

    I’ve been trying to implement my dialogs as Backbone routes, mostly because I noticed that Trello (trello.com) do it. I think it is useful because I have some important functionality which lives in modals and it is useful to be able to link directly to an open modal.

    The main area I’ve been having trouble is in managing the differences beween changing page within the app and simple closing a dialog. In the first case, all views have to close whereas when moving to a dialog router action to a page router action I only want to close the modal and leave the background app in place.

    It’s really hard to explain but I’d love to hear your thoughts on how to manage dialogs with routing.

    • http://mutedsolutions.com Derick Bailey

      don’t manage dialogs with routing.

      write another object somewhere else that manages the dialog, then call that method on that object from the route.

      that’s how routers should be used, in general, in my opinion. nothing special or different for modals vs switching a page out.

  • http://twitter.com/urosgruber Uroš Gruber

    Can you please explain why did you include $el.on(“hidden”, this.close);

    If I close modal region I somehow get hide fired twice. If I remove this line from getEl, there is no double hide effect.

    • http://mutedsolutions.com Derick Bailey

      i think that may have been something i needed to work around a specific scenario in my app… if you don’t need it, well… you dont’ need it. :)

    • http://twitter.com/apfritts AP Fritts

      Technically, when you click off or close a modal, it just hides it…it doesn’t close the Backbone Marionette View. The view is automatically closed for you the next time you show a modal but it might be a little cleaner to close the modal so that any events associated with it don’t accidentally fire …. which probably indicates another problem in the code but better be safe than sorry?

  • http://twitter.com/techpines Brad Carleton

    Oops tried posting code.

  • http://www.facebook.com/scoarescoare Scott Coates

    This is perfect. I hate having to look up the names of events and figure out when they’re fired. Not that hard I know, it’s just annoying. Thanks Derick!

  • http://www.facebook.com/scoarescoare Scott Coates

    Can you explain why you add _.bindAll(this) in the constructor function?

    • http://mutedsolutions.com Derick Bailey

      it’s not needed. leftover from previous version that i forgot to remove

      • http://www.facebook.com/scoarescoare Scott Coates

        $el.on(“hidden”, this ); will need to be changed:
        $el.on(“hidden”, _.bind(this.close, this));

        So I guess it makes sense to leave the _bindAll

  • Paul Iannazzo

    does this solution still work? it would be great if you could post to a jsfiddle with the current BB/marionette/BS. thanks
    i’m having a bit of trouble with my app, but i’m using an iframe and i’m not sure if my problems are from that or from old code in this post :)

  • Guy

    this seems exactly what i need (had a dialog problem calling the jqueryUI plugin from my marionette views).

    However when i try to use the code in this article i get a JS error:

    Uncaught NoElError: An ‘el’ must be specified

    ny ideas why?

  • Guy

    apparently i was trying to do this with a layout and not on the application object. i guess application objects can support custom regions but layouts cannot.

  • Luca Canducci

    Great article, thanks!

    For me it worked changing this line:
    this.on(“view:show”, this.showModal, this);
    into this:
    this.on(“show”, this.showModal, this);

    Does it make any sense to you?

    • Ahmad Khatib

      Just upgraded from 0.1 to 1.0-beta5 and had to make this change as well.

  • tonthon

    I tried the provided example with the jquery simplemodal plugin, but I had to add a call to the getEl method after closing the modal dialog (overriding the ensureEl method was another way to come to the same solution).

  • http://twitter.com/apfritts AP Fritts

    If you run into a problem with the modal hiding on the page and not up front, try adding:

    class=”modal hide fade”

    To the #modal div.

  • tonthon

    Since I struggled quite a lot to get it work fine with jquery dialog, I end up with sharing my code. Combining the following region with a view implementing onBeforeClose and onClose methods, I’m able to handle the most use cases.
    Hope this can help

    var Popup = Backbone.Marionette.Region.extend({
    el: “#modal”,
    title:”My title”,
    constructor:function(){
    _.bindAll(this);
    Backbone.Marionette.Region.prototype.constructor.apply(this, arguments);
    },
    getEl: function(selector){
    var $el = $(selector);
    return $el;
    },
    onShow: function(view){
    var this_ = this;
    view.on(“close”, this.closeModal, this);
    this.$el.dialog({
    resize:’auto’,
    modal:true,
    width:”auto”,
    height:”auto”,
    title:this_.title,
    hide: “fadeOut”,
    close:function(event, ui){
    this_.close();
    }
    });
    },
    closeModal: function(){
    if (this.$el.dialog(“isOpen”)){
    this.$el.dialog(“close”);
    }
    }
    });

    • Tim Kosem

      Derick & tonthon – brilliant. This worked perfectly. I added the ability to pass in jQuery UI dialog options to permit setting dialog buttons (and their callbacks) and other settings. If you add a “dialog” property to the view, you can extend the default dialog options you have set above with whatever options you want, with the caveat that any option that controls dialog lifetime will cause havoc. Here is my variant:

      var JqueryModalDialogRegion = Backbone.Marionette.Region.extend({
      el: “#modal”,
      constructor: function () {
      _.bindAll(this);
      Backbone.Marionette.Region.prototype.constructor.apply(this, arguments);
      },
      defaultDialogOptions: {
      modal: true,
      autoOpen: true,
      width: ‘auto’,
      height: ‘auto’
      },
      getEl: function (selector) {
      var $el = $(selector);
      return $el;
      },
      onShow: function (view) {
      view.on(“close”, this.closeModal, this);
      var region = this;
      var context = {
      view: view,
      close: function (event, ui) {
      region.close();
      }
      }
      this.$el.dialog($.extend(this.defaultDialogOptions, view.dialog, context));
      },
      closeModal: function () {
      if (this.$el.dialog(“isOpen”)) {
      this.$el.dialog(“close”);
      }
      }
      });

      I also extended this idea to support persistent hosting of dialogs. This was required in our case because TinyMCE really hates when you remove DOM elements to which you’ve attached the editor, which happens when the view is closed by the region. If you use “load” instead of “show” to bind the view to the region, the dialog will render to the DOM but not be shown straight away. If the view publishes an “open” event, the region will open the dialog.

      var PersistentJqueryModalDialogRegion = JqueryModalDialogRegion.extend({
      el: “#persistentModal”,
      close: function () {
      // no-op
      },
      onShow: function(view) {
      if (this.view) {
      if (this.view === view) {
      this.openModal();
      } else {
      throw new Error(“Persistent modal dialog region already initialized with a different view”);
      }
      } else {
      this.view = view;
      view.on(“open”, this.openModal, this);
      JqueryModalDialogRegion.prototype.onShow.apply(this, [view]);
      }
      },
      openModal: function () {
      if (!this.$el.dialog(“isOpen”)) {
      this.$el.dialog(“open”);
      }
      },
      // loads a view into a dialog without opening the dialog
      load: function (view) {
      view.dialog = $.extend(view.dialog, { autoOpen: false });
      this.show(view);
      }
      });

  • andrew

    Hey Derick,

    Is this article missing the code? I see:

    ” I will supply the content for the modal through a Backbone view, which looks like this:

    How easy is that?”

    Shouldn’t there be code between “which looks like this:” and "How easy is that?"

    Thanks Andrew

    • http://mutedsolutions.com Derick Bailey

      The code examples are hosted by github gists. Is it possible that your firewall is preventing the gist from loading? or perhaps another network issue is preventing it? Here’s a screenshot of what I see in that section of the post: http://cl.ly/image/0e2n3c112g1h

      • Andrew

        Thanks for the image. Not sure what my problem is. After seeing the image I looked at the page source and it had the code that was in the image.

        Thanks again,

        Andrew

  • ivan

    this is way too much code… what is the benefit of doing bootstrap modals this way?

  • Sid Jain

    Really nice post. I was struggling with the same issue and this came in handy. I have two questions for you regarding a non-marionette implementation:

    1. Could you encapsulate the code for creating the modal inside the View’s render method()? For example

    render: function(){
    $(“#modal”).modal(); // Launch the modal
    this.$el.html(this.template(this.model.toJSON()); // populate view el with html
    $(“#modal”).html(this.el); // inject view el into modal
    }

    2. How do you make sure the Backbone view object is destroyed after the user closes the modal? Most pop up plugins provide “close” modal event. Would you just bind that to a function that calls view.remove() ?

    Cheers,
    Sid

  • penx

    In an AMD application, if I want to show a modal dialog from within a view, how could I handle this? My views don’t have access to the Application object, and ‘show modal’ isn’t something I’d expect to do through the EventAggregator…

  • http://web-scents.blogspot.com/ Marhamah

    this modal is much similar to what i’m working now with google map info window. What i’m trying to do is having a form inside the info window and until now couldnt attach the event inside that form. Is there anyway you could help? thanks

  • Uchiha Itachi

    This reminded me of the .NET Dialogue Component.