Sliding Content Drawer in Ionic 2

How to Create a Sliding Drawer Component for Ionic 2



·

Since I create a lot of content, I often ask for suggestions on tutorials I should write. If I find a suggestion is particularly interesting or I think it will benefit a lot of people, I’ll generally end up creating a tutorial at some point – so do feel free to suggest them.

The other day on Twitter, @dylanvdmerwe suggested attempting to make the following UI component for Ionic 2:

There is the normal view, and then at the bottom of the screen, there is another view overlayed that you can drag up and over the main content. The idea seemed straightforward enough, so I spent a little time attempting to recreate the layout in Ionic 2 and ended up with this:

Sliding Drawer in Ionic 2

The GIF capture is a little choppy, but the actual result is very smooth. The component can easily be added underneath any <ion-content> area by using an additional <content-drawer> component. It is also configurable to allow you to specify how tall the handle that the user can slide to bring the view up should be, what the top and bottom thresholds should be (the points at which the view will automatically snap to the top or bottom), and whether the view should always snap to the top or bottom.

In this tutorial, I am going to walk through how to create this sliding drawer component in Ionic 2. We will also touch on some important performance considerations for creating smooth transitions.

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2.

I won’t be spending too much time discussing the basics of how to build components in Ionic and Angular, so if you need a bit more background I would recommend checking out some of my other tutorials:

1. Generate a New Ionic 2 Application

We will start off by generating a new Ionic 2 application by running the following command:

ionic start ionic2-content-drawer blank --v2

Once the project has finished generating, make it your working directory by running the following command:

cd ionic2-content-drawer

Now we are going to generate the component that will eventually be our content drawer component. To do that, you will need to run the following command:

ionic g component ContentDrawer

This will create a component called ContentDrawerComponent, we are going to rename this to just ContentDrawer though.

Modify src/components/content-drawer/content-drawer.ts to reflect the following:

import { Component } from '@angular/core';

@Component({
  selector: 'content-drawer',
  templateUrl: 'content-drawer.html'
})
export class ContentDrawer {

}

In order to be able to use this component throughout the application, we will need to add it to the applications app.module.ts file.

Modify src/app/app.module.ts to reflect the following:

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ContentDrawer } from '../components/content-drawer/content-drawer';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    ContentDrawer
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

2. Implement the Content Drawer Component

There’s quite a bit of logic to this component, but we are going to add it all in one go and then talk through all of the important bits.

Modify src/components/content-drawer/content-drawer.ts to reflect the following:

import { Component, Input, ElementRef, Renderer } from '@angular/core';
import { Platform, DomController } from 'ionic-angular';

@Component({
  selector: 'content-drawer',
  templateUrl: 'content-drawer.html'
})
export class ContentDrawer {

  @Input('options') options: any;

  handleHeight: number = 50;
  bounceBack: boolean = true;
  thresholdTop: number = 200;
  thresholdBottom: number = 200;

  constructor(public element: ElementRef, public renderer: Renderer, public domCtrl: DomController, public platform: Platform) {

  }

  ngAfterViewInit() {

    if(this.options.handleHeight){
      this.handleHeight = this.options.handleHeight;
    }

    if(this.options.bounceBack){
      this.bounceBack = this.options.bounceBack;
    }

    if(this.options.thresholdFromBottom){
      this.thresholdBottom = this.options.thresholdFromBottom;
    }

    if(this.options.thresholdFromTop){
      this.thresholdTop = this.options.thresholdFromTop;
    }

    this.renderer.setElementStyle(this.element.nativeElement, 'top', this.platform.height() - this.handleHeight + 'px');
    this.renderer.setElementStyle(this.element.nativeElement, 'padding-top', this.handleHeight + 'px');

    let hammer = new window['Hammer'](this.element.nativeElement);
    hammer.get('pan').set({ direction: window['Hammer'].DIRECTION_VERTICAL });

    hammer.on('pan', (ev) => {
      this.handlePan(ev);
    });

  }

  handlePan(ev){

    let newTop = ev.center.y;

    let bounceToBottom = false;
    let bounceToTop = false;

    if(this.bounceBack && ev.isFinal){

      let topDiff = newTop - this.thresholdTop;
      let bottomDiff = (this.platform.height() - this.thresholdBottom) - newTop;      

      topDiff >= bottomDiff ? bounceToBottom = true : bounceToTop = true;

    }

    if((newTop < this.thresholdTop && ev.additionalEvent === "panup") || bounceToTop){

      this.domCtrl.write(() => {
        this.renderer.setElementStyle(this.element.nativeElement, 'transition', 'top 0.5s');
        this.renderer.setElementStyle(this.element.nativeElement, 'top', '0px');
      });

    } else if(((this.platform.height() - newTop) < this.thresholdBottom && ev.additionalEvent === "pandown") || bounceToBottom){

      this.domCtrl.write(() => {
        this.renderer.setElementStyle(this.element.nativeElement, 'transition', 'top 0.5s');
        this.renderer.setElementStyle(this.element.nativeElement, 'top', this.platform.height() - this.handleHeight + 'px');
      });

    } else {

      this.renderer.setElementStyle(this.element.nativeElement, 'transition', 'none');

      if(newTop > 0 && newTop < (this.platform.height() - this.handleHeight)) {

        if(ev.additionalEvent === "panup" || ev.additionalEvent === "pandown"){

          this.domCtrl.write(() => {
            this.renderer.setElementStyle(this.element.nativeElement, 'top', newTop + 'px');
          });

        }

      }

    }

  }

}

First up, let’s cover the imports and dependency injections we have set up. We’re importing Input from the Angular library so that we can supply some options to the component from wherever we are using it, and we import and inject both ElementRef (which will allow us to reference the <content-drawer> elements) and Renderer (which will allow us to update the element’s style in a platform agnostic manner).

We also import Platform and DomController from the Ionic library. The Platform will be used to allow us to grab the height of the device, and we need DomController so that we can access its write method. Using the write method from the DomController allows us to update the DOM in the most efficient way possible (Ionic will handle scheduling the update at the most opportune time to avoid layout issues). This is especially important for a component like this where lots of pan events will be firing off all the time – if we were to force DOM updates for each event we would likely cause some jankiness.

Next, we grab the options object which we will be able to supply as input, and we override the defaults if the options values are present. We also set up the initial positioning for the drawer component, and we give it a padding-top equal to whatever the handleHeight is – this creates an area that will always be displayed on screen that the user can drag up (and down).

The other important thing in the ngAfterViewInit method is that we set up a handler for the pan event using Hammer. Ionic uses hammer.js for gestures, but by default, the pan event doesn’t have vertical panning enabled (which is exactly what we need). So, rather than adding a host object to the decorator and listening for the pan event like we usually would, we instead set up a handler directly through Hammer after setting up vertical panning.

Then we get into the bulk of the logic for the component: the handlePan method which is called every time we receive a pan event. We grab the coordinates of where the user is panning on the screen through ev.center.y and then we use that value to do some calculations.

The pan event will also set its isFinal property to true if it is the last event being emitted (i.e. the user has stopped panning). If the user has stopped panning, and the bounceBack option is set, then we want to figure out which threshold (top or bottom) is closer and then snap to that. We set a flag that can be used as an override for the normal snapping that occurs once a threshold is crossed.

The next two if blocks check if the drawer has been dragged past a threshold point, and if it is then it will automatically set the position of the drawer to the fully open or fully closed position. We also set the transition property so that this animates, rather than just instantly updating. As I mentioned just before, this snapping will also be invoked if the bounceBack property is set.

The final if block just handles the normal updating of the drawer position, so it follows the user as it is dragged. If bounceBack is not set, then the drawer will stop wherever the user drags it to. This if block also makes sure the drawer is not dragged outside of the bounds of the screen.

That’s the bulk of the component finished, now we just need to implement the template and the styling.

Modify src/components/content-drawer/content-drawer.html to reflect the following:

<ion-content>
    <ng-content></ng-content>
</ion-content>

There’s not a whole lot going on here. We just reuse a normal <ion-content> component and then we add <ng-content> which enables us to project content that is supplied to the component into the template. So, if the user were to do the following:

<content-drawer>
    Hello!
</content-drawer>

the components template would become:

<ion-content>
    Hello!
</ion-content>

This allows us to add whatever we want inside of the drawer.

Modify src/components/content-drawer/content-drawer.scss to reflect the following:

.ios, .md {

    content-drawer {

        width: 100%;
        height: 100%;
        position: absolute;
        z-index: 10 !important;
        box-shadow: 0px -4px 22px -8px rgba(0,0,0,0.75);

    }

}

We are positioning this component with absolute so we add that here, and we also give it a width and height of 100% so that it occupies the screen space. The z-index is also important so that it overlaps the original content area, and the box-shadow just makes it look a little nicer.

3. Use the Component

We’ve done the hard work, now we just need to make use of it. We are just going to go through a simple example to recreate the GIF I posted earlier.

Modify src/pages/home/home.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

    drawerOptions: any;

    constructor(public navCtrl: NavController) {

        this.drawerOptions = {
            handleHeight: 50,
            thresholdFromBottom: 200,
            thresholdFromTop: 200,
            bounceBack: true
        };

    }

}

If you want to supply options to the component, you can do so by creating an object like this and then creating a property binding in the template where the component is being used (just as we will in a second).

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  The world is your oyster.
  <p>
    If you get lost, the <a href="http://ionicframework.com/docs/v2">docs</a> will be your guide.
  </p>
</ion-content>

<content-drawer [options]="drawerOptions">
    <div class="content">
      The world is your oyster.
      <p>
        If you get lost, the <a href="http://ionicframework.com/docs/v2">docs</a> will be your guide.
      </p>
    </div>
</content-drawer>

You can see that we have added the <content-drawer> below our original <ion-content> and then we supply the options object through a property binding. Then all we have to do is add whatever content we want to display in the drawer inside of the component. I’ve added a content container so that I can style that content.

Modify src/pages/home/home.scss to reflect the following:

.ios, .md {

    page-home {

        content-drawer {
            background-color: #34495e !important;
        }

        content-drawer .content {
            padding: 20px;
        }

    }

}

Summary

The component that we have created can now easily be added to any page in Ionic 2 simply by adding it underneath the existing content area. It’s very adaptable too, as we can also add just about anything we like inside of the drawer (including more Ionic components like cards).

Dylan, I hope you’re happy with the result, and a big thanks to Dan from Ionic for helping me out with the pan event weirdness.

What to watch next...

  • Whoaa, i didn’t even think this would be possible with Hybrid. Thanks Josh!

  • Awesome

  • Pie Cy

    challenge: Android Share Element Transition

  • Interestingly, I’ve been using a similar pull up component for ionic v1 https://github.com/arielfaur/ionic-pullup – works well

  • Eugene Vedensky

    So cool.

  • Cafer Elgin

    This is an awesome component, thanks. And here comes the questions :). Why didn’t you use ‘webkit transform’ instead of ‘top’. Does it make any difference in terms of smoothness?

  • Kewlest guy evvahhhh!!!!

    Thank you so much!

  • Mubashir Mushir

    i am new on ionic 2. all is working perfectly but my content did not showed in this drawer

  • Mubashir Mushir

    and i want this drawer on a click.

  • Marc Yves ST-VICTOR

    Hi thank you for this. Awesome. What if I would like to put an arrow at the top to show user it could drag up.

    Thank you,

  • masimak

    Any idea how could this component respect header and tabs (meaning not overlap them but just go over current ion-content)?

  • Mike Marsh

    You might want to update the code for setting the options. I was trying to set bounceBack to false, but… if(this.options.bounceBack){…}
    improve to:
    if(‘bounceBack’ in this.options){…)

    • Matt Gurzenski

      Agreed. The ability to turn bounceBack to false in the “options” property doesn’t work unless you tweak the ngAfterViewInit() method of the ContentDrawer component to handle setting that value to false. I changed the conditional in the ngAfterViewInit() to check if the bounceBack property is available to this to get it to work:
      if(this.options.hasOwnProperty(‘bounceBack’)){
      this.bounceBack = this.options.bounceBack;
      }

  • anon

    possible to have this as an action sheet?

  • Thanks 🙂 This is exactly what I’ve been searching for for a week!

  • vishnuvardhan siddareddy

    i am getting error saying, ‘Element implicitly has an ‘any’ type because type ‘Window’ has no index signature’ in content-drawer.ts file.

    let hammer: any = new window[‘Hammer’](this.element.nativeElement);
    hammer.get(‘pan’).set({ direction: window[‘Hammer’].DIRECTION_VERTICAL });

  • Qasim Soomro

    how would you have a scrollable list as the drawer content?
    currently that does not work well

  • Nicolas Naso

    How do you manage the pan event if the content-drawer includes a scrollable list?

  • Navin Kumar

    how to over ride the header , with custom design and . how to set height based on the content ? . ITs dragging full screen . If my content is just 50% how to make it small . height should be dynamically set ?
    %

    • Navin Kumar

      reply

  • How can I put it in half when I start the application? Like this
    https://uploads.disquscdn.com/images/5ef49cf6fcdccc8789436aab2aa007cabab46ae3677b4c8a9eebfce76f519c21.png
    Sorry for bad english, btw

    • I did, just changing https://uploads.disquscdn.com/images/630aa68351d9f64a29d0c327e3f58a3dd7f9a6325816c40b8182cd2141449d96.png
      this.platform.height() – this.handleHeight + ‘px’ to this.platform.height() – this.handleHeight*10 + ‘px’

      • seapig

        like this ?
        this.renderer.setElementStyle(this.element.nativeElement, ‘top’, this.platform.height() – this.handleHeight + ‘px’ to this.platform.height() – this.handleHeight*10 + ‘px’ );
        this.renderer.setElementStyle(this.element.nativeElement, ‘padding-top’, this.handleHeight + ‘px’);

    • Josh, thanks so much, ur tutorials helped me a tons of times.

  • Matt Gurzenski

    Great example. Just what I was looking for!

  • seapig

    How can I set it only to pull up a certain distance so not all the way up but only like 1/3 of the way and stop there?

    • seapig

      figured it out
      this.renderer.setElementStyle(this.element.nativeElement, ‘top’, ‘0px’);

      changed too

      this.renderer.setElementStyle(this.element.nativeElement, ‘top’, ‘80%’);

      and it works ^_^

  • seapig

    New questions lol, How do I make it so I do not need to pull past 50% for it to stay on screen? How can I set it so a swipe up will keep it on screen and a swipe down will remove it?

  • How to reset slider when user navigates to different page and comes back….