How to Create a Directive in Ionic (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. Perhaps the best way to think of it 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.

In this tutorial, I’m going to walk you through how to build and use your own custom directive in Ionic which will allow you to easily add a parallax header to the main content area. Unlike a normal static header image, a parallax header image will scale as you scroll, causing the header and content area to scroll at different speeds which creates the illusion of depth.

Before we Get Started

Last updated for Ionic 4.0.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 already, I’d recommend reading my beginner Ionic tutorials first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic, then take a look at Building Mobile Apps with Ionic & Angular.

Building a Parallax Effect Directive in Ionic

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.

We are going to create something much in the same spirit with just a few differences (and obviously it is going to be designed to work with the current version of Ionic). In the end, we will be able to use the directive like this:

<ion-content [scrollEvents]="true" parallaxHeader="https://ununsplash.imgix.net/photo-1421091242698-34f6ad7fc088?fit=crop&fm=jpg&h=650&q=75&w=950" [parallaxHeight]="300">

Where we just supply the image we want to use as the header directly to the <ion-content> along with the height we want for the parallax header. Here’s what our Ionic directive will look like when we finish it:

Ionic 2 Parallax Header

1. Create the Directive

We’ll start off by generating a new directive and setting it up in our application.

Run the following command:

ionic g directive directives/ParallaxHeader

In order to be able to use this directive, we will need to add it to the module for the page we want to use it on. Let’s assume we are going to use this on the home page.

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

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { HomePage } from './home.page';
import { ParallaxHeaderDirective } from '../directives/parallax-header.directive';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomePage
      }
    ])
  ],
  declarations: [HomePage, ParallaxHeaderDirective]
})
export class HomePageModule {}

If you want to be able to use this directive on more than one page, you will need to set up a shared component module or modules. If you are unsure how to do this, take a look at: Using Custom Components on Multiple Pages in Ionic.

2. Create the Parallax 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: '[parallaxHeader]'
})
export class ParallaxHeaderDirective {

  constructor() {
  }

}

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 uses the parallaxHeader attribute, i.e:

<ion-content parallaxHeader>

Now we are going to walk through implementing the parallax header functionality in Ionic.

3. Set up a Host Listener

In order for this directive to work, we are going to have to react to scroll events coming from the <ion-content>. To do this, we will need to add a host listener:

import { Directive, AfterViewInit, ElementRef, Renderer2, Input } from '@angular/core';
import { DomController } from '@ionic/angular';

@Directive({
	selector: '[parallaxHeader]',
	host: {
		'(ionScroll)': 'onContentScroll($event)'
	}
})

By adding the host property to the @Directive metadata, we can listen for a specific event and then call a particular method in our class when that event occurs (in this case, onContentScroll).

We’ve also added some additional imports at the top that we will be making use of throughout the directive:

  • The AfterViewInit hook will be used to run some code after the view for the directive has initialised
  • ElementRef will be used to get a reference to the element that the directive is attached to (e.g. <ion-content>)
  • Renderer2 will allow us to modify elements in a platform agnostic way (i.e. we use the renderer instead of interacting directly with the DOM)
  • Input will allow us to pass data into this directive through the host element

4. Set up Inputs

We will need to supply two bits of data to this directive:

  • A path for the image we want to display in the parallax header
  • A height for the parallax header

To do this, we will set up two inputs using @Input. This allows us to define some additional data on the <ion-content> which we can then access inside of the directive:

import { Directive, AfterViewInit, ElementRef, Renderer2, Input } from '@angular/core';
import { DomController } from '@ionic/angular';

@Directive({
	selector: '[parallaxHeader]',
	host: {
		'(ionScroll)': 'onContentScroll($event)'
	}
})
export class ParallaxHeaderDirective implements AfterViewInit {

	@Input('parallaxHeader') imagePath: string;
	@Input('parallaxHeight') parallaxHeight: number;

	private headerHeight: number;
	private header: HTMLDivElement;
  	private mainContent: HTMLDivElement;

	constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {

	}

}

We have also set up a few class members here, we have:

  • headerHeight which is just a reference to the parallax height
  • header which will be a reference to the actual header element we will create and inject
  • mainContent which will be a reference to the main content area we will add to the home page

Because of the way in which we are going to scale the header image, we will need to create a “main content” area inside of <ion-content> that will hold our normal content. You will see this in practice a little later.

5. Initialise the Directive

Now we are going to set up some initial styles and create our header element inside of the ngAfterViewInit() hook:

import { Directive, AfterViewInit, ElementRef, Renderer2, Input } from '@angular/core';
import { DomController } from '@ionic/angular';

@Directive({
	selector: '[parallaxHeader]',
	host: {
		'(ionScroll)': 'onContentScroll($event)'
	}
})
export class ParallaxHeaderDirective implements AfterViewInit {

	@Input('parallaxHeader') imagePath: string;
	@Input('parallaxHeight') parallaxHeight: number;

	private headerHeight: number;
	private header: HTMLDivElement;
  	private mainContent: HTMLDivElement;

	constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {

	}

	ngAfterViewInit(){

		this.headerHeight = this.parallaxHeight;
    	this.mainContent = this.element.nativeElement.querySelector('.main-content');

		this.domCtrl.write(() => {

			this.header = this.renderer.createElement('div');

			this.renderer.insertBefore(this.element.nativeElement, this.header, this.element.nativeElement.firstChild);
			
			this.renderer.setStyle(this.header, 'background-image', 'url(' + this.imagePath + ')');
			this.renderer.setStyle(this.header, 'height', this.headerHeight + 'px');
			this.renderer.setStyle(this.header, 'background-size', 'cover');

		});

  	}

}

Inside of this hook we handle grabbing a reference to the “main content” area that we will add to our home page later. Then we create a new <div> element and inject it as the first child inside of <ion-content>. This <div> element is what will hold our header image. We set up some initial styles on this including setting up the imagePath that was passed in as the background image for the element.

You will notice that whenever we read from the DOM or write to the DOM we use the DomController, if you are unsure why this is you might want to read: Increasing Performance with Efficient DOM Writes in Ionic.

6. Handle Scroll Events

Now we get into the “core” logic of how this directive works by implementing the onContentScroll method:

import { Directive, AfterViewInit, ElementRef, Renderer2, Input } from '@angular/core';
import { DomController } from '@ionic/angular';

@Directive({
	selector: '[parallaxHeader]',
	host: {
		'(ionScroll)': 'onContentScroll($event)'
	}
})
export class ParallaxHeaderDirective implements AfterViewInit {

	@Input('parallaxHeader') imagePath: string;
	@Input('parallaxHeight') parallaxHeight: number;

	private headerHeight: number;
	private header: HTMLDivElement;
  	private mainContent: HTMLDivElement;

	constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {

	}

	ngAfterViewInit(){

		this.headerHeight = this.parallaxHeight;
    	this.mainContent = this.element.nativeElement.querySelector('.main-content');

		this.domCtrl.write(() => {

			this.header = this.renderer.createElement('div');

			this.renderer.insertBefore(this.element.nativeElement, this.header, this.element.nativeElement.firstChild);
			
			this.renderer.setStyle(this.header, 'background-image', 'url(' + this.imagePath + ')');
			this.renderer.setStyle(this.header, 'height', this.headerHeight + 'px');
			this.renderer.setStyle(this.header, 'background-size', 'cover');

		});

  	}

	onContentScroll(ev){

	    this.domCtrl.read(() => {

	      let translateAmt, scaleAmt;
	  
	      // Already scrolled past the point at which the header image is visible
	      if(ev.detail.scrollTop > this.parallaxHeight){
	        return;
	      }

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

	      this.domCtrl.write(() => {
	        this.renderer.setStyle(this.header, 'transform', 'translate3d(0,'+translateAmt+'px,0) scale('+scaleAmt+','+scaleAmt+')');
	        this.renderer.setStyle(this.mainContent, 'transform', 'translate3d(0, '+(-ev.detail.scrollTop) + 'px, 0');
	      });

	    });

	}

}

We are doing three different things here:

  1. If the content area has already been scrolled more than the height of the parallax header we do nothing
  2. If the content area has been “overscrolled” (e.g. above the top of the content area) we scale the image up
  3. If the content area is scrolling down, we scale the image down

We apply a transform to the header to scale the image down in proportion to how far the content has scrolled down, and we also move the main content area up by transforming its y axis. The reason we transform the content area and move it up rather than just changing the height of the header area is because it is more performant to animate transform than it is to animate height (which will cause “layouts” in the browser rendering process).

7. Add the CSS

The main content area is transformed and moved on top of the parallax header area, so it is important that we give it a background to ensure that everything displays correctly.

Add the following style to src/app/home/home.page.scss:

.main-content {
	background-color: #fff;
	padding: 20px;
}

8. Use the Directive in a Template

Finally, we just need to make use of the directive in our template.

Modify your content area to reflect the following:

<ion-content [scrollEvents]="true" parallaxHeader="https://ununsplash.imgix.net/photo-1421091242698-34f6ad7fc088?fit=crop&fm=jpg&h=650&q=75&w=950" [parallaxHeight]="300">
	<div class="main-content">
		<h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2>
		<h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2>
		<h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2>
		<h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2>
		<h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2>
		<h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2><h2>Content</h2>
	</div>
</ion-content>

It is important that you make sure scrollEvents is set to true otherwise the content area will not emit any scroll events and your parallax directive will never trigger the onContentScroll method. As I mentioned before, it is also important to add your normal content inside of a <div> with a class of main-content.

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

Ionic 2 Parallax Header

Summary

With a bit of work upfront, we now have an easy way to add a parallax header effect to any of the <ion-content> areas in our application.

Check out my latest videos: