Ionic 2 Directive

How to Create a Directive in Ionic 2 & 3 – Parallax Header



·

Ionic provides a range of directives by default that you can drop into your application and get a pretty slick looking interface almost instantly. This includes things like lists, inputs, prompts and so on. These are just a core set of components that provide vanilla functionality, so there’s going to come a time where you need to extend the functionality provided by default.

A great way to do this is with directives. I think the difference between a directive and a component is pretty hard to understand conceptually. The best way I’ve heard it explained is that you would use a directive when you want to modify the behaviour of an existing DOM (Document Object Model) element, and you would create a component when you want a completely new DOM element. Otherwise, a component and a directive are pretty much the same thing, a component is just a directive with its own template.

If you want a little more theory on components, which are one of the most important concepts in Angular 2, I recommend reading this post by Victor Savkin.

In this tutorial, I’m going to walk you through how to build and use your own custom directive in Ionic 2.

UPDATE: A new and improved version of this directive is available here.

Before we Get Started

Last updated for Ionic 3.3.0

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.

Building a Parallax Effect Directive in Ionic 2

A reader got in touch with me recently who was trying to recreate a directive he had been using in Ionic 1 which was created by Ola Christensson. You can take a look at this Codepen to see it in action.

This directive allows you to add a header image to a content area, and when the content area is scrolled the header image will shrink in a way that creates a parallax effect. Here’s what our Ionic 2 directive will look like when we finish it:

Ionic 2 Parallax Header

In Ionic 1, the code for this directive looks like this:

angular.module('example', ['ionic'])

.directive('elasticHeader', function($ionicScrollDelegate) {
	return {
		restrict: 'A',
		link: function(scope, scroller, attr) {
			var scrollerHandle = $ionicScrollDelegate.$getByHandle(attr.delegateHandle);
			var header = document.getElementById(attr.elasticHeader);
			var headerHeight = header.clientHeight;
			var translateAmt, scaleAmt, scrollTop, lastScrollTop;
			var ticking = false;
			
			// Set transform origin to top:
			header.style[ionic.CSS.TRANSFORM + 'Origin'] = 'center bottom';
			
			// Update header height on resize:
			window.addEventListener('resize', function() {
				headerHeight = header.clientHeight;
			}, false);

			scroller[0].addEventListener('scroll', requestTick);
			
			function requestTick() {
				if (!ticking) {					
					ionic.requestAnimationFrame(updateElasticHeader);
				}
				ticking = true;
			}
			
			function updateElasticHeader() {
				
				scrollTop = scrollerHandle.getScrollPosition().top;
			
				if (scrollTop >= 0) {
					// Scrolling up. Header should shrink:
					translateAmt = scrollTop / 2;
					scaleAmt = 1;
				} else {
					// Scrolling down. Header should expand:
					translateAmt = 0;
					scaleAmt = -scrollTop / headerHeight + 1;
				}

				// Update header with new position/size:
				header.style[ionic.CSS.TRANSFORM] = 'translate3d(0,'+translateAmt+'px,0) scale('+scaleAmt+','+scaleAmt+')';
				
				ticking = false;
			}
		}
	}
});

and you can use it in a template like this:

    <ion-content delegate-handle="example-scroller" elastic-header="example-elastic-header" overflow-scroll="false">
        <div id="example-elastic-header" class="background-image"></div>

        <div class="content">
            CONTENT GOES HERE
        </div>
    </ion-content>

There’s going to be a few difficulties in making this directive work in Ionic 2. Apart from the fact that the syntax will look entirely different in Ionic 2, we will also need to find replacements for things this directive relies on that aren’t available in Ionic 2, most notably $ionicScrollDelegate.

I don’t want to spoil the ending, we will get it working in the end, but let’s walk through the process step by step.

1. Generate a New Ionic 2 Application

We’ll start off by generating a new Ionic 2 application. Make sure you have the Ionic 2 CLI installed before attempting this step – if you don’t have it installed yet, make sure to read the articles I linked at the beginning of this post.

Run the following command to generate a new Ionic 2 application

ionic start ionic2-elastic-directive blank 

Once the application has finished generating, we are going to create a new directive using the Ionic CLI.

Run the following command:

ionic g directive ElasticHeader

We are also going to need to set this new directive up in our application’s @NgModule.

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

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ElasticHeader } from '../components/elastic-header/elastic-header';

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

2. Create the Elastic Header Directive

Time to start creating the directive, let’s start off by looking at the general structure of the directive:

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

@Directive({
  selector: '[elastic-header]'
})
export class ElasticHeader {

  constructor() {
    console.log('Hello ElasticHeader Directive');
  }

}

What we have now is a bare bones directive that doesn’t do anything. We’ve set up the selector so that this directive will be associated with any DOM element that references the elastic-header attribute, i.e:

<ion-content elastic-header>

We’ve also created a basic class definition. Now we are going to walk through the rest of implementing this functionality in Ionic 2.

Modify src/components/elastic-header/elastic-header.ts to reflect the following:

import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({
  selector: '[elastic-header]'
})
export class ElasticHeader {

    scrollerHandle: any;
    header: any;
    headerHeight: any;
    translateAmt: any;
    scaleAmt: any;
    scrollTop: any;
    lastScrollTop: any;
    ticking: any;

    constructor(public element: ElementRef, public renderer: Renderer) {

    }

} 

Now we are importing the ElementRef service from the Angular 2 library, and injecting it into our constructor to make it available throughout the directive.

ElementRef allows us access to the element the directive is added to. You might assume this would return the DOM element that the directive is defined on, but instead, it returns a reference to the element. To access the DOM element itself you must use this.element.nativeElement which will return the node, which in this case is the <ion-content> element.

We are also injecting Renderer which provides us with a platform agnostic way to modify the properties of elements, e.g. if we want to add a class to an element or change its style. I talk a little more in-depth about why you may want to use the Renderer in this tutorial.

In order to implement the functionality, we need to keep track of a few things like the height of the scroll area, how far has been scrolled and so on. So we set up some member variables to keep track of that.

The next step is going to seem like a bit of a leap, a little like this, but the rest of the directive is basically just a line by line conversion of the existing Ionic 1 directive with some syntax changes. I think it’ll be easier to look at the whole thing in context, and talk through the changes.

Modify src/components/elastic-header/elastic-header.ts to reflect the following:

import { Directive, ElementRef, Renderer } from '@angular/core';

@Directive({
  selector: '[elastic-header]'
})
export class ElasticHeader {

    scrollerHandle: any;
    header: any;
    headerHeight: any;
    translateAmt: any;
    scaleAmt: any;
    scrollTop: any;
    lastScrollTop: any;
    ticking: any;

    constructor(public element: ElementRef, public renderer: Renderer) {

    }

    ngOnInit(){

        this.scrollerHandle = this.element.nativeElement.getElementsByClassName('scroll-content')[0];
        this.header = this.scrollerHandle.firstElementChild;
        this.headerHeight = this.scrollerHandle.clientHeight;
        this.ticking = false;

        this.renderer.setElementStyle(this.header, 'webkitTransformOrigin', 'center bottom');

        window.addEventListener('resize', () => {
            this.headerHeight = this.scrollerHandle.clientHeight;
        }, false);

        this.scrollerHandle.addEventListener('scroll', () => {

            if(!this.ticking){
                window.requestAnimationFrame(() => {
                    this.updateElasticHeader();
                });
            }

            this.ticking = true;

        });

    }

    updateElasticHeader(){

        this.scrollTop = this.scrollerHandle.scrollTop;

        if(this.scrollTop >= 0){
            this.translateAmt = this.scrollTop / 2;
            this.scaleAmt = 1;
        } else {
            this.translateAmt = 0;
            this.scaleAmt = -this.scrollTop / this.headerHeight + 1;
        }

        this.renderer.setElementStyle(this.header, 'webkitTransform', 'translate3d(0,'+this.translateAmt+'px,0) scale('+this.scaleAmt+','+this.scaleAmt+')');
        this.ticking = false;

    }

}    

and let’s take a look at the Ionic 1 directive again for comparison:

angular.module('example', ['ionic'])

.directive('elasticHeader', function($ionicScrollDelegate) {
    return {
        restrict: 'A',
        link: function(scope, scroller, attr) {
            var scrollerHandle = $ionicScrollDelegate.$getByHandle(attr.delegateHandle);
            var header = document.getElementById(attr.elasticHeader);
            var headerHeight = header.clientHeight;
            var translateAmt, scaleAmt, scrollTop, lastScrollTop;
            var ticking = false;

            // Set transform origin to top:
            header.style[ionic.CSS.TRANSFORM + 'Origin'] = 'center bottom';

            // Update header height on resize:
            window.addEventListener('resize', function() {
                headerHeight = header.clientHeight;
            }, false);

            scroller[0].addEventListener('scroll', requestTick);

            function requestTick() {
                if (!ticking) {                 
                    ionic.requestAnimationFrame(updateElasticHeader);
                }
                ticking = true;
            }

            function updateElasticHeader() {

                scrollTop = scrollerHandle.getScrollPosition().top;

                if (scrollTop >= 0) {
                    // Scrolling up. Header should shrink:
                    translateAmt = scrollTop / 2;
                    scaleAmt = 1;
                } else {
                    // Scrolling down. Header should expand:
                    translateAmt = 0;
                    scaleAmt = -scrollTop / headerHeight + 1;
                }

                // Update header with new position/size:
                header.style[ionic.CSS.TRANSFORM] = 'translate3d(0,'+translateAmt+'px,0) scale('+scaleAmt+','+scaleAmt+')';

                ticking = false;
            }
        }
    }
});

This is the completed code for the directive, and you can probably see that it looks very similar to the directive in Ionic 1. Let’s talk through the significant changes that have been made.

  • The most significant change is the way in which we replace $ionicScrollDelegate. Instead, we access the scroll content by using ElementRef. After that what we do with it is basically the same, we add the scroll listener and we access the scrollTop value just slightly differently.
  • We assume the header area that the parallax effect is being applied to is the first element inside of the content area
  • The structure is also obviously different as we are using the Angular 2 class syntax, rather than creating an Angular 1 module, and we also make use of some ES6 features.

3. Add the CSS

Now we’re going to add the CSS required to get this directive working, this will set up the image for our header and also add some styling to the content area so that it overlaps the header image.

Add the following rules to your pages/home/home.scss file:

page-home {

    .background-image {
        background-image: url(https://ununsplash.imgix.net/photo-1421091242698-34f6ad7fc088?fit=crop&fm=jpg&h=650&q=75&w=950);
        background-size: cover;
        background-position: center;
        height: 50vw;
    }

    .main-content {
        padding: 20px;
        background-color: white;
        position: absolute;
    }

}

4. Use the Directive in a Template

To use our directive in our HomePage component, all we need to do is add the elastic-header attribute to the <ion-content>, add the parallax image as the first element in the content area, and also add our own main-content area.

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

<ion-header>
  <ion-navbar color="danger">
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content elastic-header>

  <div class="background-image"></div>

  <div class="main-content">

        <h2>Parallax Header</h2>

        <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>

        <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p>

  </div>

</ion-content>

If you load up your application now, you should have a finished and working elastic header directive!

Ionic 2 Parallax Header

Summary

Looking at the directive from Ionic 1 and the one we just created for Ionic 2, you can see they are very similar. Once you’re used to the component style architecture of an Ionic 2 app and the ES6 syntax, there’s really only a few changes required to convert an existing Ionic 1 directive to work in Ionic 2.

What to watch next...

  • Simon Lucas

    Thanks again for your help on this Josh, it’s nice to see it explained step-by-step. It’s an exciting time to be an Ionic developer! I’ve been trying to replicate the ‘stretch’ effect of the image where the image gets larger when you scroll up to the top (the Groupon app also has this effect). I’m sure it’s something simple in the CSS, maybe something to do with overflow-scroll?

  • Luís Cunha

    Very useful article, added the functionality to my app right away. Thanks.

  • Pingback: MOBILE WEB WEEKLY NO.86 | ENUE()

  • Pingback: How to Use Pipes to Manipulate Data in Ionic 2 | HTML5 Mobile Tutorials | Ionic, Phaser, Sencha Touch & PhoneGap()

  • Moe

    Thank you very much for this article! Have you ever tested with ion-refresher? If I put the refresher in my page, it appears above the header. Do you think it is possible to display the refresher under the header?

  • Simon

    Thank you for this tuto, but I can’t get it working. I have a problem with the elastic-header’s directory. Ionic can’t find the file with what is in the tuto so I try this : “import {ElasticHeader} from ‘../../directives/elastic-header’;” in page1.js so ionic is lauching the app but it’s a blank page… Maybe something has changed in Ionic since the last release.

  • Hazou Shebly

    there was an error in this tutorial

    this.headerHeight = this.scrollerHandle.clientHeight;

    TypeError: Cannot read property ‘clientHeight’ of undefined
    at new ElasticHeader (app.bundle.js:61802)
    at app.bundle.js:8062

  • Hazou Shebly

    why this.scrollerHandle.clientHeight always returning 0?

  • Arno

    I got the same error as Hazou: “TypeError: Cannot read property ‘clientHeight’ of undefined”. Any ideas?

    • Newer versions of Ionic 2 have broken this, I’ll see if I can work out what’s going on and update when I have time

      • Hazou Shebly

        any updates on this?

      • Say it ain’t soap

        all the constructor code you need to put it in ngAfterViewInit(){}

  • Indra Lesmana

    how to implement this to ionic slide box? so, there are two or more item use this directive.

    • Say it ain’t soap

      You could pass an extra parameter so that you can establish either your parallax image html element or a way to select it. You can do this something like this…

      1. In your directive, add Input at line1 so that you import the class “Input”
      2. Then add this parameter inside your ElasticHeader class

      @Input(‘parallax-element’) parallaxElement:string;

      3. Now you can use that parameter inside your class and you can pass it like this:

  • Ashish Patel

    I am using 2.0.0-beta.24 of ionic2 and I am getting same error as Arno getting.

    • Guillaume Le Mière

      Hi ! Juste change your code to the following one, and it will work :

      import {Directive, ElementRef} from ‘angular2/core’;

      @Directive({

      selector: ‘[elastic-header]’

      })

      export class ElasticHeader {

      constructor(element: ElementRef){

      this.element = element;

      this.ticking = false;

      this.translateAmt = null;

      this.scaleAmt = null;

      this.scrollTop = null;

      this.lastScrollTop = null;

      }

      ngOnInit() {

      this.scrollerHandle = this.element.nativeElement.children[0];

      this.header = document.getElementById(“elastic-header”);

      this.headerHeight = this.scrollerHandle.clientHeight;

      this.header.style.webkitTransformOrigin = ‘center bottom’;

      var me = this;

      window.addEventListener(‘resize’, function() {

      headerHeight = this.scrollerHandle.clientHeight;

      }, false);

      this.scrollerHandle.addEventListener(‘scroll’, function(){

      if(!me.ticking){

      window.requestAnimationFrame(function(){

      me.updateElasticHeader();

      });

      }

      this.ticking = true;

      });

      }

      updateElasticHeader(){

      this.scrollTop = this.scrollerHandle.scrollTop;

      if (this.scrollTop >= 0) {

      this.translateAmt = this.scrollTop / 2;

      this.scaleAmt = 1;

      } else {

      this.translateAmt = 0;

      this.scaleAmt = -this.scrollTop / this.headerHeight + 1;

      }

      this.header.style.webkitTransform = ‘translate3d(0,’+this.translateAmt+’px,0) scale(‘+this.scaleAmt+’,’+this.scaleAmt+’)’;

      this.ticking = false;

      }

      }

  • Piccaza

    Hi Josh,
    Many many thanks for the tutorial.
    And on doubt, how can I output a event through directive? Like in the case of segment-switch, after segment new view element is loaded I want to call google maps into the element.

  • Rahul Bussa

    Hi Josh,
    For this.element.nativeElement.children i am getting empty array.
    what could be reason for it.Thanks

    • Diego García

      Try to use other phases on the lifecycle component like “ngAfterContentInit” or “ngAfterViewInit” instead of using the constructor. That worked for me.

      • liuwenzhuang

        Thanks, you save my day.

  • Say it ain’t soap

    I think this is not detecting scroll momentum. Any ideas on how to detect this?

    • sebastião Realino

      Some resolution to this?

      • Say it ain’t soap

        not yet for me, for what I’ve been reading it is impossible to detect and it should be emulated or something by calculating the scroll velocity and how it decreases.

      • sebastião Realino

        No way for the stop event? I found this: https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-overflow-scrolling
        but it does not work.
        If we get the update screen event? do you think it is possible?

  • If you’re using Ionic Beta 7, a breaking change happened. Change all ‘angular2/core’ to ‘@angular/core’ Basically, all angular2 should change to @angular instead

  • Leandro Baptista

    It works fine when I’m testing in browser, but in the smartphone shows some lag… Someone have issues like this?

    • Timmy

      It’s lagging for me aswell, or more like flickering. Have to disable this for now because of it

  • Oliver Duda

    I do had a problem, because me Ionic2 app is in type script. I changed it for this reason a little bit, but it’s working fine. Here’s my code snipplet:

    import {Directive, ElementRef} from ‘@angular/core’;

    @Directive({
    selector: ‘[elastic-header]’
    })

    export class ElasticHeader {

    private el: HTMLElement;
    private ticking: any;
    private translateAmt: any;
    private scaleAmt: any;
    private scrollTop: any;
    private lastScrollTop: any;
    private scrollerHandle: any;
    private header: any;
    private headerHeight: any;

    constructor(element: ElementRef){
    this.el = element.nativeElement;
    this.ticking = false;
    this.translateAmt = null;
    this.scaleAmt = null;
    this.scrollTop = null;
    this.lastScrollTop = null;
    }

    ngOnInit() {
    this.scrollerHandle = this.el.children[0];
    this.header = document.getElementById(“elastic-header”);
    this.headerHeight = this.scrollerHandle.clientHeight;
    this.header.style.webkitTransformOrigin = ‘center bottom’;
    var me = this;

    window.addEventListener(‘resize’, function() {
    this.headerHeight = this.scrollerHandle.clientHeight;
    }, false);

    this.scrollerHandle.addEventListener(‘scroll’, function(){
    if(!me.ticking){
    window.requestAnimationFrame(function(){
    me.updateElasticHeader();
    });
    }
    this.ticking = true;
    });
    }

    updateElasticHeader(){
    this.scrollTop = this.scrollerHandle.scrollTop;
    if (this.scrollTop >= 0) {
    this.translateAmt = this.scrollTop / 2;
    this.scaleAmt = 1;
    } else {
    this.translateAmt = 0;
    this.scaleAmt = -this.scrollTop / this.headerHeight + 1;
    }
    this.header.style.webkitTransform = ‘translate3d(0,’+this.translateAmt+’px,0) scale(‘+this.scaleAmt+’,’+this.scaleAmt+’)’;
    this.ticking = false;
    }
    }

    There’s only one issue. When I scroll up again it flicker’s white at the very top 🙁 Does anyone of you have a solution?

  • Can you show us an example using ionic 2 Beta 10 Cards, please? 🙁

  • Willian Tenfen Wazilewski

    Any chance to update this to Ionic2 rc.0 ? thanks…

  • Update to Ionic 2 RC 3?

  • Ryan Loggerythm

    Will there be an update since Angular RC6 deprecated @Component and the code no longer works?

  • Hey all – this has just been updated for the latest version of Ionic 2

    • Ryan Loggerythm

      Hey Josh, not sure I’m seeing the RC3 changes. Did you solve this differently from editing the app.module.ts file?

      Thanks

      • You might still be seeing a cached version, try hard refreshing your browser – the entire application is different now (substantial changes to both the directive and the template). If you can see the section about updating the app.module.ts file then you will have the new content.

      • Ryan Loggerythm

        You rock! Next time I’m in Australia, I owe you a beer.

    • Shay Shaked

      Hey Josh, start develop app using cordova with ionic2 + angular2. I have a simple scrollable (also using ion-infinite-scroll) page with list. I need to know if the list causing the scroll to be present or not..
      if not than present a button to the user, else user will be able to scroll down, and trigger the ion-infinite-scroll to fetch data to the list.
      When I write my ion-content to console I see in the “_scroll: ScrollView” property that give me the indication whether scroll present by its “scroll-content.scrollHeight” property.
      how can i access it in my component/view file?

      Thanks!!!

  • Robert Young

    For anyone experiencing some issues on a device/simulator, try adding “transform: translateZ(0);” to the “main-content” class. https://gist.github.com/chrisjlee/9299678

  • Maitham Deeb

    How would I tie this up with angular animations so that the header fades out as it disappears ?

  • caquillo

    Awesome, thanks so much! Now I have a question, I found the same code on another site (http://codepen.io/olach/pen/NqrYQL), Im sure its also yours.

    In this demo the header does a paralax effect, but it also stretches when its pulled up beyond the view’s top. How do I achieve that effect with this code? I cant seem to figure that one out.

    Thank you!!

  • Sylvia Dantonio

    Hi! Excellent example, works perfectly!
    However I must implement with the image of the header being loaded dynamically… how can I pass the src and the alt of the image ? couldn’t find a way. 🙁

  • Oliver Duda

    How can I achieve an effect like this? Any suggestions or examples?
    http://codepen.io/infacq/pen/ZGabYq

  • passionate_prog

    I have tried a simple directive. It does not work on android 5.1.1 but works on 4.2.2.

    Can someone please advise?

    Regards

  • Jeremy Flowers

    I just think of a Directive as a custom attribute you can add to an HTML Element or custom Element (Component). The Directive decorator syntax for the selector then uses the square brackets to signify your targeting an attribute. If there’s more to directives than that, it would be good to know.