Scrolling based on an element's calculated position with Angular and Ionic

February 6, 2018, 9:00 am Categories:

Categories

In the following tutorial I'm going to take you through developing a very basic Ionic application that demonstrates how to use Ionic's built-in scrolling methods, along with help from Angular, to navigate to sections of a page based on the calculated position of the requested DOM element.

It's fairly simple but kind of fun and, although relatively basic, it's the type of functionality that nonetheless does come in handy for those real world app development scenarios.

What we'll be developing

Our Ionic application is relatively simple and consists of a Single Page Application (SPA) that includes a basic navigation system in the page footer to allow the user to scroll between the different sections of the page:

Ionic Scroll application

Along the way we'll be making use of Angular's DOM handling methods as well as scrolling functionality provided by Ionic's content component API.

Getting started

As always fire up your system CLI (this assumes you have installed the Ionic CLI and your software environment is fully configured for Ionic application development), navigate to a location on your computer where you store your digital projects and create the following Ionic project named ionic-scrolla:

ionic start ionic-scrolla blank

As we're not installing any Ionic Native/Apache Cordova plugins or third-party libraries that's all we need in order to set the stage for developing the SPA functionality.

Onwards we go!

The scrolling logic

As this is a Single Page Application (SPA) all of the scrolling logic is going to be written within the HomePage component - starting with importing the necessary modules:

import { Component, ElementRef, ViewChild } from '@angular/core';
import { Content, NavController } from 'ionic-angular';

As we are using the default Angular framework for this project we'll be making use of the ViewChild module to create links to elements in the component view template with the following template reference variables inside the opening of the HomePage class declaration:

@ViewChild(Content) content : Content;
@ViewChild('panel1') panel1 : ElementRef;
@ViewChild('panel2') panel2 : ElementRef;
@ViewChild('panel3') panel3 : ElementRef;
@ViewChild('panel4') panel4 : ElementRef;
@ViewChild('panel5') panel5 : ElementRef;

We then follow this by defining public properties to refer to these page elements (which will be subsequently used to manage interacting with those 'pages' via Ionic's scroll functionality):

/**
 * @name panelPos1
 * @type {number}
 * @public
 * @description     Object for storing the y-axis position of the first 'page' 
 */
public panelPos1 : number;



/**
 * @name panelPos2
 * @type {number}
 * @public
 * @description     Object for storing the y-axis position of the second 'page' 
 */
public panelPos2 : number;



/**
 * @name panelPos3
 * @type {number}
 * @public
 * @description     Object for storing the y-axis position of the third 'page' 
 */
public panelPos3 : number;



/**
 * @name panelPos4
 * @type {number}
 * @public
 * @description     Object for storing the y-axis position of the fourth 'page' 
 */
public panelPos4 : number;



/**
 * @name panelPos5
 * @type {number}
 * @public
 * @description     Object for storing the y-axis position of the fifth 'page' 
 */
public panelPos5 : number;



/**
 * @name panels
 * @type {number}
 * @public
 * @description     Array for storing each 'page' (which will be created dynamically
 *                  inside the template view) 
 */
public panels 	  : any     = [1,2,3,4,5];

Within the ionViewDidLoad method we programmatically determine the top position of each of these 'pages' using the top property of Angular's getBoundingClientRect() method.

Each calculated position is assigned to a respective property like so (we'll use these, a little later on in the script, to manage 'page' scrolling with):

/**
 * Determine the y-axis position for each 'page' once the view has loaded
 * and assign these values to properties for later reference
 *
 * @public
 * @method ionViewDidLoad 
 * @return {none}
 */
ionViewDidLoad() : void
{
   this.panelPos1 = this.panel1.nativeElement.getBoundingClientRect().top,
   this.panelPos2 = this.panel2.nativeElement.getBoundingClientRect().top,
   this.panelPos3 = this.panel3.nativeElement.getBoundingClientRect().top,
   this.panelPos4 = this.panel4.nativeElement.getBoundingClientRect().top,
   this.panelPos5 = this.panel5.nativeElement.getBoundingClientRect().top;
}

We then make use of Ionic's Content component API which provides the following scroll related methods to help manage and control the scrollable content area:

  • scrollTo(x, y, duration) - Allows scrolling to the specified position
  • scrollTop(duration) - Allows scrolling to the top of the content component
  • scrollBottom(duration) - Allows scrolling to the bottom of the content component

These are respectively implemented within the HomePage component class via the following methods:

/**
 * Scrolls to a specified point on the x/y axes
 *
 * @public
 * @method scrollTo 
 * @param x            {Number}         The amount of pixels to scroll on the x axis
 * @param y            {Number}         The amount of pixels to scroll on the y axis
 * @param duration     {Number}         The scroll duration (in milliseconds)
 * @return {none}
 */
scrollTo(x         : number, 
         y         : number, 
         duration  : number) : void
{
   this.content.scrollTo(x, y, duration);
}




/**
 * Scrolls to the top of the content component area
 *
 * @public
 * @method scrollTop 
 * @return {none}
 */
scrollToTop() : void
{
   this.content.scrollToTop(750);
}




/**
 * Scrolls to the bottom of the content component area
 *
 * @public
 * @method scrollTop 
 * @return {none}
 */
scrollToBottom()
{
   this.content.scrollToBottom(750);
}

We additionally define a method named scrollToPanel which, as the name implies, handles scrolling to a selected 'page'.

This will be triggered from the template when the user interacts with one of the numbered 'page' buttons:

/**
 * Scrolls to the specified 'page'
 *
 * @public
 * @method scrollToPanel 
 * @param num            {Number}         The number of the 'page' to scroll to
 * @return {none}
 */
scrollToPanel(num : number) : void
{
   switch(num)
   {
      case 1:
         this.scrollTo(0, this.panelPos1, 750);
      break;


      case 2:
         this.scrollTo(0, this.panelPos2, 750);
      break;


      case 3:
         this.scrollTo(0, this.panelPos3, 750);
      break;


      case 4:
         this.scrollTo(0, this.panelPos4, 750);
      break;


      case 5:
         this.scrollTo(0, this.panelPos5, 750);
      break;
   }      
}

In full then our completed component logic - located within the ionic-scrolla/src/pages/home/home.ts class - should look like the following (comments included):

import { Component, ElementRef, ViewChild } from '@angular/core';
import { Content, NavController } from 'ionic-angular';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
   // Retrieve all template reference variables
   @ViewChild(Content) content : Content;
   @ViewChild('panel1') panel1 : ElementRef;
   @ViewChild('panel2') panel2 : ElementRef;
   @ViewChild('panel3') panel3 : ElementRef;
   @ViewChild('panel4') panel4 : ElementRef;
   @ViewChild('panel5') panel5 : ElementRef;




   /**
    * @name panelPos1
    * @type {number}
    * @public
    * @description     Object for storing the y-axis position of the first 'page' 
    */
   public panelPos1 : number;




   /**
    * @name panelPos2
    * @type {number}
    * @public
    * @description     Object for storing the y-axis position of the second 'page' 
    */
   public panelPos2 : number;




   /**
    * @name panelPos3
    * @type {number}
    * @public
    * @description     Object for storing the y-axis position of the third 'page' 
    */
   public panelPos3 : number;




   /**
    * @name panelPos4
    * @type {number}
    * @public
    * @description     Object for storing the y-axis position of the fourth 'page' 
    */
   public panelPos4 : number;




   /**
    * @name panelPos5
    * @type {number}
    * @public
    * @description     Object for storing the y-axis position of the fifth 'page' 
    */
   public panelPos5 : number;




   /**
    * @name panels
    * @type {number}
    * @public
    * @description     Array for storing each 'page' (which will be created dynamically
                       inside the template view) 
    */
   public panels 	  : any     = [1,2,3,4,5];
   



   constructor(public navCtrl: NavController) 
   {  }




   /**
    * Determine the y-axis position for each 'page' once the view has loaded
    * and assign these values to properties for later reference
    *
    * @public
    * @method ionViewDidLoad 
    * @return {none}
    */
   ionViewDidLoad() : void
   {
      this.panelPos1 = this.panel1.nativeElement.getBoundingClientRect().top,
      this.panelPos2 = this.panel2.nativeElement.getBoundingClientRect().top,
      this.panelPos3 = this.panel3.nativeElement.getBoundingClientRect().top,
      this.panelPos4 = this.panel4.nativeElement.getBoundingClientRect().top,
      this.panelPos5 = this.panel5.nativeElement.getBoundingClientRect().top;
   }




   /**
    * Scrolls to a specified point on the x/y axes
    *
    * @public
    * @method scrollTo 
    * @param x            {Number}         The amount of pixels to scroll on the x axis
    * @param y            {Number}         The amount of pixels to scroll on the y axis
    * @param duration     {Number}         The scroll duration (in milliseconds)
    * @return {none}
    */
   scrollTo(x         : number, 
            y         : number, 
            duration  : number) : void
   {
      this.content.scrollTo(x, y, duration);
   }




   /**
    * Scrolls to the specified 'page'
    *
    * @public
    * @method scrollToPanel 
    * @param num            {Number}         The number of the 'page' to scroll to
    * @return {none}
    */
   scrollToPanel(num : number) : void
   {
      switch(num)
      {
         case 1:
           this.scrollTo(0, this.panelPos1, 750);
         break;


         case 2:
           this.scrollTo(0, this.panelPos2, 750);
         break;


         case 3:
           this.scrollTo(0, this.panelPos3, 750);
         break;


         case 4:
           this.scrollTo(0, this.panelPos4, 750);
         break;


         case 5:
           this.scrollTo(0, this.panelPos5, 750);
         break;
      }
      
   }




   /**
    * Scrolls to the top of the content component area
    *
    * @public
    * @method scrollTop 
    * @return {none}
    */
   scrollToTop() : void
   {
      this.content.scrollToTop(750);
   }




   /**
    * Scrolls to the bottom of the content component area
    *
    * @public
    * @method scrollTop 
    * @return {none}
    */
   scrollToBottom(): void
   {
      this.content.scrollToBottom(750);
   }


}

The component HTML

The component markup is relatively simple and can be broken into 2 parts:

  • The individual 'pages' (or sections) of the SPA (with template reference variables assigned to them to act as Angular 'hooks' which allows the component class to interact with these)
  • The scroll related buttons for navigating those 'pages'

The only 'complex' part of this template is the dynamic generation of the scroll buttons for each 'page':

<button 
   *ngFor="let panel of panels; let i = index" 
   ion-button 
   (click)="scrollToPanel(i + 1)">[ {{ i + 1 }} ]</button>

As you can see this isn't really complex at all!

This simply consists of an Angular *ngFor directive that iterates through the panels array and assigns a click event to each dynamically generated button to trigger the scrollToPanel method (supplying the current iteration's index value - plus one - which will match a certain 'page' in the SPA).

In full then the markup for the ionic-scrolla/src/pages/home/home.html view template should look like the following:

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

<ion-content padding>
  

  <!-- Template reference variable #panel1 allows the
       first 'page' to be managed by the component class -->
  <div 
     class="panel blue" 
     #panel1>
     <h2>01</h2>
  </div>


  <!-- Template reference variable #panel2 allows the
       second 'page' to be managed by the component class -->
  <div 
     class="panel red" 
     #panel2>
     <h2>02</h2>
  </div>


  <!-- Template reference variable #panel3 allows the
       third 'page' to be managed by the component class -->
  <div 
     class="panel green" 
     #panel3>
     <h2>03</h2>
  </div>


  <!-- Template reference variable #panel4 allows the
       fourth 'page' to be managed by the component class -->
  <div 
     class="panel yellow" 
     #panel4>
     <h2>04</h2>
  </div>


  <!-- Template reference variable #panel5 allows the
       fifth 'page' to be managed by the component class -->
  <div 
     class="panel orange" 
     #panel5>
  	 <h2>05</h2>
  </div>


</ion-content>

<ion-footer no-border>
   <ion-toolbar>
     <ion-buttons>

        <!-- Scroll to top of page -->
     	<button 
           ion-button 
           (click)="scrollToTop()">Top</button>


        <!-- Programmatically generate the individual 'page' buttons -->
     	<button 
           *ngFor="let panel of panels; let i = index" 
           ion-button 
           (click)="scrollToPanel(i + 1)">[ {{ i + 1 }} ]</button>


        <!-- Scroll to bottom of page -->
     	<button 
           ion-button 
           (click)="scrollToBottom()">Bottom</button>
     </ion-buttons>
   </ion-toolbar>
</ion-footer>

Fairly straightforward markup right?

Now all we need is to supply the styling for the DOM elements in our ionic-scrolla view template.

The component styling

Our component style rules - as you've probably guessed by now - are relatively simple and will be located in the ionic-scrolla/src/pages/home/home.scss file.

Within this stylesheet we're going to define the generic style rule for all 'pages' and then separate rules for how we want each individual 'page' to appear - like so:

page-home {


   .panel {
      position: relative;
      margin: 0;

      h2 {
         font-size: 3rem;
   	 position: absolute;
   	 bottom : 1em;
   	 right: 1em;
   	 color: #ffffff;
   	 font-family: Verdana;
      }
   }

   .red {
      height: 350px;
      background-color: #c13f3f;
   }

   .blue {
      height: 450px;
      background-color: #5681a9;
   }

   .green {
      height: 400px;
      background-color: #61884c;
   }

   .yellow {
      height: 600px;
      background-color: #dfd644;
   }

   .orange {
      height: 800px;
      background-color: #dfa744;
   }

}

Notice how I've set each 'page' to a different fixed height? This is deliberate to demonstrate that the scrolling logic will work regardless of the height of individual elements.

It also helps to simulate scenarios where certain pages will be larger than others due to their respective content and features.

Testing

With our style rules added to the component's stylesheet all that remains now is to run the ionic serve command from the system CLI like so:

ionic serve

Assuming that there are no errors in coding you should see the following application being launched in your system web browser which you can then interact with via the scroll buttons in the page footer:

Ionic Scroll application

In summary

Adding the ability to dynamically scroll to a page element based on its calculated position is relatively simple thanks to Ionic's built-in scroll methods and Angular's NativeElement methods (in particular the getBoundingClientRect() method).

Although this is a relatively basic application that we've built it does demonstrate how page elements can be scrolled to based on their calculated position on the Y-axis (which is a useful technique for functionality such as scrolling to the last posted thread in a forum or the most recent message in a chat based app).

I'll explore further scroll related functionality that we can implement with Ionic applications in future tutorials.

If you enjoyed this tutorial then feel free to share your thoughts, reactions and suggestions in the comments section below.

If you liked what was written here then please consider signing up to my FREE mailing list to stay updated on further articles and e-books that I have in the pipeline.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top