Solving modals in Ember.js was such a major PITA for me. I googled a lot and read everything I could only to find as many different approaches as posts, which of none felt like a good fit for me. This post probably just fuels that fire with another approach but this is my way to Master your modals in Ember.js.

Update December 8, 2014: This article has been updated to use Ember 1.8.1, the latest stable version at the time of writing. The ember-cli version, available at github.com/wecc/ember-cli-modals, has been updated to ember-cli 0.1.4.

Getting started

So, for this demo we’ll be using Bootstrap, mostly to simplify things. It’s really just the Modal part of Bootstrap we’re interested in but I went ahead and included the full package. You can easily create a custom slimmer version if you want. Or just use it as is, it’s a free world.

So, first off, be sure to include jQuery, Handlebars, Ember and Bootstrap:

<script src="http://code.jquery.com/jquery-2.1.0.js"></script>
<script src="http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v1.3.0.js"></script>
<script src="http://builds.emberjs.com/tags/v1.8.1/ember.js"></script>
<script src="http://getbootstrap.com/dist/js/bootstrap.js"></script>

Then add some pzaz:

<link href="http://getbootstrap.com/dist/css/bootstrap.css" rel="stylesheet">

A component a day keeps the doctor away

As we want our modal to be customizable yet re-usable, it’s a good idea to create a MyModalComponent. To do that we start with the template. As you can see this is mostly just Bootstrap Modal markup. A lot of it but it’ll look pretty in the end, I promise.

<div class="modal fade">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
        <h4 class="modal-title">{{title}}</h4>
      </div>
      <div class="modal-body">
        {{yield}}
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary" {{action 'ok'}}>OK</button>
      </div>
    </div>
  </div>
</div>

Then, we create our component class:

App.MyModalComponent = Ember.Component.extend({
  show: function() {
    this.$('.modal').modal();
  }.on('didInsertElement')
});

Easy, right? Yes indeed, thank you very much. Well, it’s not really that useful yet since everything it does is opens up when created, but we’ll get to that. The purpose of {{title}} and {{yield}} in our template is so that we can customize the title and content when we want to use our modal, like this:

{{#my-modal title="My modal #1"}}
  This is the content of my modal #1
{{/my-modal}}

“customizable yet re-usable”, I did not lie, at least I tried not to. Here’s a working JS Bin of what we got so far: http://emberjs.jsbin.com/pucup/2/edit

That’s nice, but doesn’t really solve anything!?

I know, that’s a bit too simple for our needs so let’s step it up a bit. What’s next might feel like a lot but it’s really not (unintended rhyme). I’ve just broken it down to smaller pieces to simplify as much as possible. We’re going to:

  1. Add outlets to our application template
  2. Add showModal and removeModal actions to our ApplicationRoute
  3. Improve our MyModalComponent to pass ok and close actions to the controller
  4. Create two separate modal templates, settings-modal and logout-modal
  5. Create controllers for our two modals, SettingsModalController and LogoutModalController
  6. Create index template and IndexRoute for demo purposes

Outlets in our application template

For this demo we’re going to add the default {{outlet}} as well as an additional {{outlet 'modal'}}, this will be the place where we render our modals into.

<h2>Master your modals with Ember.js</h2>
{{outlet}}
{{outlet 'modal'}}

Add some spice to our ApplicationRoute

Since we’re about to create a template named settings-modal and a controller named SettingsModalController wouldn’t it be nice if we could just have a single re-usable action for showing modals? Something like {{action 'showModal' 'settings-modal'}}? Don’t mind if I say so but that would be really nice.

In addition to the showModal action we’re also creating a removeModal action to do some cleanup when we don’t want to show out modal anymore.

App.ApplicationRoute = Ember.Route.extend({
  actions: {
    showModal: function(name, model) {
      this.render(name, {
        into: 'application',
        outlet: 'modal',
        model: model
      });
    },
    removeModal: function() {
      this.disconnectOutlet({
        outlet: 'modal',
        parentView: 'application'
      });
    }
  }
});

That’s a big chunk of code but it’s not as complicated as it might look. We provide the showModal action with a name and model and we render the specified template into the outlet named modal inside of the application template, passing the model. removeModal disconnects (de-render?) that same outlet.

Improve our MyModalComponent

We want our different modals to be able to act when users press the OK button. As you might have spotted in the components/my-modal template we have {{action 'ok'}} inside of the button, let’s make sure our component handles that and passes it on to the controller. We also want to listen to the Bootstrap Modal’s hidden.bs.modal event (triggered when the modal has been completely hidden) and pass that action on to the controller too.

App.MyModalComponent = Ember.Component.extend({
  actions: {
    ok: function() {
      this.$('.modal').modal('hide');
      this.sendAction('ok');
    }
  },
  show: function() {
    this.$('.modal').modal().on('hidden.bs.modal', function() {
      this.sendAction('close');
    }.bind(this));
  }.on('didInsertElement')
});

Templates for our modals

This is our first modal, named settings-modal:

{{#my-modal title='Settings' ok='save' close='removeModal'}}
  <form {{action 'ok' on='submit' target=view}}>
    <div class="form-group">
      <label>Name</label>
      {{input class="form-control" value=name}}
    </div>
  </form>
{{/my-modal}}

And here’s the second modal, named logout-modal:

{{#my-modal title='Logout' ok='logout' close='removeModal'}}
  Are you sure you want to logout?
{{/my-modal}}

We’re listening to the ok and close actions and passing them along as they happen. As actions bubble up through your controller and up the routes our controller doesn’t have to handle the close action, just let it bubble to removeModal in our ApplicationRoute.

Controllers for our modals

These are very simple, but here goes:

App.SettingsModalController = Ember.ObjectController.extend({
  actions: {
    save: function() {
      // save to server
    }
  }
});

App.LogoutModalController = Ember.Controller.extend({
  actions: {
    logout: function() {
      alert('logout');
    }
  }
});

Note that our SettingsModalController is extending from ObjectController and LogoutModalController from Controller, this is just to suit our demo a bit better. You have to decide on your own if Controller, ObjectController or ArrayController suits your modal best.

Wire everything together, create our demo

So, to show off our brand new modal handling skills, we need a demo. And to actually have something to edit in our settings-modal we’re going to create our IndexRoute and have it’s model hook return a dummy user:

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return Ember.Object.create({ name: 'My name' });
  }
});

Then, our index template:

<p><strong>Name:</strong> {{name}}</p>
<button {{action 'showModal' 'settings-modal' model}}>settings</button>
<button {{action 'showModal' 'logout-modal'}}>logout</button>

Yes, that’s really it! Not bad eh? Are you eager to try it out? Me too! Have a go at this fine JS Bin: http://emberjs.jsbin.com/qijaro/2/edit

Fin

This demo was kept simple on purpose. I really want you to be able to use these ideas and implement it in your own code without having to clean it up a whole lot first. Please leave a comment if you have any feedback and follow me on Twitter (@EmberGuru) for updates.

Thanks for reading, if you made it this far! :)