In this tutorial, we are going to take a look at creating a directive that will allow us to apply a cool little enter/exit animation to items inside of an Ionic list. We will be able to define a simple CSS class to determine what the animated styles should look like, which might end up looking something like this:
NOTE: The actual end result is smoother than the GIF above looks
In this case, we are modifying the opacity
of the list items and applying a transform
as they enter or exit the list from the top or bottom of the page. You can apply whatever styles you like to this list (just by changing a simple CSS class), but keep in mind that animating anything other than opacity
and transform
will have a much greater negative impact on performance. In some circumstances, the performance may still be fine regardless, but just keep that in mind.
If you don’t know why animating height
is bad for performance and why animating transform
is better for performance, I would recommend watching High Performance Animations in Ionic.
Achieving this animation will rely on adding and removing a particular CSS class as the items come on to the screen (or leave), and to do that we will be using something called the Intersection Observer API.
Before We Get Started
Last updated for Ionic 4.0.0
This tutorial assumes you already have a basic level of understanding of Ionic & Angular. If you require more introductory level content on Ionic I would recommend checking out my book or the Ionic tutorials on my website.
Introducing the Intersection Observer
The Intersection Observer API is a new-ish API and it is the main reason I came up with this tutorial (I’ve wanted an excuse to use the Intersection Observer API for a while now). In short, the Intersection Observer API allows us to easily detect whether an element is currently within the browser’s viewport (i.e. you can see the element on screen). More specifically, the Intersection Observer API allows us to detect/measure the intersection of an element and an ancestor element (with the viewport being the default ancestor if nothing else is specified).
Common examples of using the Intersection Observer API include things like lazy loading images (where we only want to load images as they come on screen), or infinite scrolling (where we only want to load more items as we get towards the bottom of a list).
The basic use of the Intersection Observer API looks like this:
// Create the observerthis.observer =newIntersectionObserver((entries)=>{
entries.forEach((entry: any)=>{if(entry.isIntersecting){// do something if intersecting}else{// do something if not intersecting}})});// Use the observer on specific elementsthis.items.forEach(item =>{this.observer.observe(item.nativeElement);});
We first create a new instance of an IntersectionObserver
which provides a callback function. This function is triggered whenever an element being observed enters or exists the viewport (assuming the viewport is being used). We are provided with an array of entries
which will contain any of these elements.
This provides us with some information about the intersection. We can see if isIntersecting
is true which indicates that the element is visible, but other information is also available like the intersectionRatio
which indicates how much of the element is visible.
Once our observer is set up with a callback, we then tell that observer to observe
any items that we want to watch. In this example, we are watching a list of items. I’d recommend taking a look further into the API, but now that we are armed with some basic knowledge, let’s use that to build our directive.
1. Create an AnimateItems Directive
First, we will create a new directive by running the following command:
ionic g directive directives/AnimateItems
You will need to set this directive up appropriately in your application in order to be able to use it (e.g. by adding it to the module for the page you want to use it on). If you are not aware of how you should include directives/components in your application, I would recommend watching: Using Custom Components on Multiple Pages.
With that directive created, let’s add the code.
Modify src/app/directives/animate-items.directive.ts to reflect the following:
import{ Directive, ContentChildren, QueryList, ElementRef, AfterViewInit, Renderer2 }from'@angular/core';import{ IonItem }from'@ionic/angular';
@Directive({
selector:'[appAnimateItems]'})exportclassAnimateItemsDirectiveimplementsAfterViewInit{private observer: IntersectionObserver;
@ContentChildren(IonItem,{read: ElementRef}) items: QueryList<ElementRef>;constructor(private renderer: Renderer2){}ngAfterViewInit(){this.observer =newIntersectionObserver((entries)=>{
entries.forEach((entry: any)=>{if(!entry.isIntersecting){this.renderer.addClass(entry.target,'exit-enter-styles');}else{this.renderer.removeClass(entry.target,'exit-enter-styles');}})}, threshold:0.5});this.items.forEach(item =>{this.observer.observe(item.nativeElement);});}}
The basic idea is that we want to be able to attach this directive to an <ion-list>
like this:
<ion-listappAnimateItems><ion-item*ngFor="let item of items"></ion-item></ion-list>
and this will handle applying the animation automatically for us. For the animation to work, we need to apply (and remove) a specific CSS class to each of the <ion-item>
elements as they enter or exit the screen. To do this, we need to get a reference to those elements, which we do with @ContentChildren
:
@ContentChildren(IonItem,{read: ElementRef}) items: QueryList<ElementRef>;
Since this directive will be attached to an IonList
, we will be able to grab a reference to all of the IonItem
elements projected inside of that list by using @ContentChildren
. We specifically want a reference to the actual element (not the IonItem
) itself, so we supply read: ElementRef
. This will return us a list of IonItem
elements as a QueryList
which will be stored on the items
member variable.
In the ngAfterViewInit
function we set up our observer. This looks almost identical to the example we just discussed, with a couple of differences. First, we are using the Renderer
to apply or remove a class called exit-enter-styles
based on whether or not the isIntersecting
property is true
or false
.
The other important difference is that we supply a threshold
option to the observer. If the observer is triggered when an element goes off-screen, we can’t really achieve much by animating an item that is already off-screen. Instead, by supplying a threshold
of 0.5
the observer will trigger when an item is either 50%
on the screen or 50%
off the screen (which is the same thing, but I’m talking in the context of entering or exiting the screen). This means that the class will be applied just as the item is about to leave or enter the screen (rather than after it has already left).
2. Define the Styles
Our directive is pretty much done at this point, but we still need to define the styles for the animation. Assuming that you wanted to add an animation to a list on the home page, you might do something like this.
Modify src/app/home/home.page.scss to reflect the following:
ion-item{transition: .3s ease-in-out;}.exit-enter-styles{opacity: 0;transform:translate3d(20px, 0, 0);}
It is important to apply the transition
property so that the changes in CSS styles are animated rather than just being instantly applied. Otherwise, you can just set up whatever kind of styles you want inside of the exit-enter-styles
, but again, make sure you keep performance in mind as I mentioned earlier.
3. Add the Directive to an Ionic List
Finally, we will just need to apply the directive to an Ionic list. If you want to recreate the example I used, just follow these steps.
Modify src/app/home/home.page.ts to reflect the following:
import{ Component }from'@angular/core';
@Component({
selector:'app-home',
templateUrl:'home.page.html',
styleUrls:['home.page.scss'],})exportclassHomePage{public messages;constructor(){this.messages =newArray(100).fill({title:'Hello'});}}
This will create an array with 100
elements prefilled with some dummy data.
Modify src/app/home/home.page.html to reflect the following
<ion-header><ion-toolbarcolor="tertiary"><ion-title>
List Animation
</ion-title></ion-toolbar></ion-header><ion-content><ion-listlines="none"appAnimateItems><ion-item*ngFor="let message of messages"><h2>{{ message.title }}</h2></ion-item></ion-list></ion-content>
Summary
Although the logic for our animation is somewhat complex, we can now easily apply it to any list simply by adding the appAnimateItems
directive to it and defining the styles we want to apply. You could even take this further by making the directive more configurable (e.g. you could add an @Input
to the directive that allows you to specify the name of the class to be applied).
The Intersection Observer API is a fantastic addition to the web, and it transforms the previously difficult and expensive task of detecting intersecting/visible elements and makes it relatively simple.