2 Comments

Implementing an Offscreen Menu in Ember.js

Offscreen menus have become a standard in mobile apps and web interfaces over the past several years. I was recently given the task of implementing an offscreen menu for the Ember.js app that I have been working on over the past several months, so I figured I would share my solution.

There are many types of offscreen menu transitions available. You can play around with some of the most common options here. My personal favorite (and the one I chose for my app) is the “reveal” style. It keeps the menu in the background, uncovering it as the main page content slides off the screen. I like this style because the menu items are easier to read when they aren’t moving during the transition.

Here’s the process I followed:

1. Render your menu and page content simultaneously.

The general architecture of a page with an offscreen menu consists of two independent panes of content being rendered simultaneously:  the menu and the main page content. In Ember, this is easily implemented in the application template:

{{render 'offscreen-menu'}} 
{{outlet}} 

With this implementation, the offscreen menu will be rendered in every sub-outlet in the application.

2. Set the style for the offscreen menu.

I used the following styles for the offscreen menu:


.offscreen-menu {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  width: 70%;
  overflow: auto;
}

This gives us an absolutely-positioned pane that takes up 70% of the width of the screen. I like this ratio, but depending on the application, you might want more or less width.

3. Set the style for the main page content.

All templates in your app should be wrapped in this style (or something similar):


.page-content {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  transition: transform 0.2s ease-out;
  -webkit-transition: -webkit-transform 0.2s ease-out;
}

The transition property lets us define a few things about the desired transition:

  • The particular CSS property where you want the transition. For this app, we chose the transform property.
  • The duration of the transition. I think 0.2 seconds is a good duration. Anything longer seems sluggish to me.
  • The “speed curve” of the transition. In our case, we wanted a transition that “eases out” (i.e. slows down as it approaches the end).

4. Define the transition styles.

In order to animate the transition, I defined a shifted class:


.shifted.page-content {
  transform: translateX(70%);
  -webkit-transform: translateX(70%);
  overflow: hidden;
  pointer-events: none;
}

There are a few things going on here. When the shifted class is used, we want to shift the page content in a certain direction–in this case, to the right by 70%. Notice that the offscreen menu is 70% of the page width, and the page content also shifts 70%. This is intentional. We also set overflow:hidden and pointer-events:none to prevent the user from being able to scroll or click buttons on the page content while the menu is open.

5. Sprinkle in some Ember.

Now that we have the menu and the main page content rendered, we need to implement the transition logic. Most of the transitions can be implemented with CSS/HTML/Handlebars, but I chose to use a little JavaScript as well.
I wanted to add the shifted class to the DOM when the menu is opened and remove the class when the menu is closed. To do this, I created a “title bar” component which contains a menu button with an action:


<button class="menu-button" {{action ‘offscreenMenu’}}/>

and an action handler in the component:


offscreenMenu: function() {
  this.toggleProperty('menuOpen');
  this.sendAction('menuAction', this.get('menuOpen'));

  // While the menu is open, bind a click handler to the main content
  // so we can close the menu when the user clicks outside of it
  if(this.get('menuOpen')){
    var _this = this;
    Ember.$(".page-content").one('click', function(e) {
      var element = Ember.$(e.target);
      if(!element.hasClass(‘menu-button')) {
        _this.send('mainMenu');
      }
    });
  } else {
    Ember.$('.page-content').off('click');
  }
}

This action handler is doing a couple of things. First, it’s simply toggling a property in the component and sending an action to the parent controller. The last part of this action handler is using jQuery to bind a “click” handler to the page content (in order to catch click events on the page content) and send another offscreenMenu action when this happens. This allows us to minimize the menu when the user clicks on the page content. Also, this code prevents us from re-sending a “click” action if the user clicks on the menu button again to minimize the menu.

6. Add some glue.

Finally, we need to glue it all together in the controller and template. I accomplished this by catching the menuAction event in the parent controller and binding the template to the menuOpen variable, such that the menu visibility is directly controlled by the value of menuOpen. So here it goes…

The menuClicked action handler in the controller:


menuClicked: function(open) {
  this.set('menuOpen', open);
}

and the corresponding handlebars template:


<div class="page-content {{if menuOpen 'shifted'}}">
  <h2>This is my awesome page!</h2>
</div>

With all of these pieces, we have a fully functional page with an offscreen menu–transitions included!

What other techniques have you used to implement offscreen menus in your web apps?