Ionic 3 custom sticky element on scroll

Ionic doesn't provide sticky elements by default. So I made one.

Problem

Recently I needed to make a sticky element for an app – where an element ‘sticks’ to the top of the screen whilst scrolling and doesn’t get hidden. Ionic doesn’t have any built in way to achieve this.

3 ways to do this I thought of were:

  • CSS only: doesn’t work in all browsers reliably I figured.
  • JavaScript tracking the scroll event and repositioning the element in a CSS fixed, absolute position: could cause a change in scroll height as element is moved in the DOM, causing flicker.
  • JavaScript tracking the scroll event and showing a copy of the original element, already in the correct fixed place: seems pretty good to me!

Demo here:

Plunker demo amazing sticky element ionic.

Note: you can do this with any element, not just the 'segment' element. I haven't tested this with multiple pages/elements, but I would expect the code to need further development for that use case. That's outside the scope of this requirement however!

Solution

1) Add a custom-sticky-element.js file to your project:

function loadStickyEl(mainContentContainerId, elementIdToTrack, elementIdToShowInstead) {

  function isVisibleY(el) {
    if (!el) { return; }
    var rect = el.getBoundingClientRect(), top = rect.top, height = rect.height, el = el.parentNode;

    // Check if bottom of the element is off the page
    if (rect.bottom < 0) {
      return false;
    }

    // Check its within the document viewport
    if (top > document.documentElement.clientHeight) {
      return false;
    }
    do {
      rect = el.getBoundingClientRect();
      if (top <= rect.bottom === false) {
        return false;
      }

      // Check if the element is out of view due to a container scrolling
      if ((top + height) <= rect.top) {
        return false;
      }
      el = el.parentNode;
    } while (el != document.body)
    return true;
  }

  function attachEvent(element, event, callbackFunction) {
    if (element.addEventListener) {
      element.addEventListener(event, callbackFunction, false);
    } else if (element.attachEvent) {
      element.attachEvent('on' + event, callbackFunction);
    }
  }

  function update() {
    var elementToTrack = document.getElementById(elementIdToTrack);
    if (!elementToTrack) { return; }

    var elementIdToTrackIsVisible = isVisibleY(elementToTrack);
    
    var elementToShowInstead = document.getElementById(elementIdToShowInstead);
    if (!elementToShowInstead) { return; }

    if (elementIdToTrackIsVisible) {
      elementToShowInstead.style.display = 'none';
    }
    else if (!$("#" + elementIdToShowInstead).is(":visible")) {
      $("#" + elementIdToShowInstead).slideDown(500);
    }
  }

  // get scrolling container + inner actual scrolling content container.
  var mainContentContainer = document.getElementById(mainContentContainerId);
  if (mainContentContainer) {
    var scrollContent = mainContentContainer.getElementsByClassName('scroll-content');
    if (!scrollContent) { return; }

    attachEvent(scrollContent[0], "scroll", update);
    attachEvent(window, "resize", update);
  }

}

2) Reference it in your index.html (NOT the one in /www, the one in /src.)

    <!-- For accounts page sticky-tabs -->
    <script src="jquery-3.3.1.min.js"></script>
    <script src="custom-sticky-element.js"></script>

Note: jQuery is NOT strictly required, see step 3 below.

3) Add jQuery to your index.html (optional)

I used this to simplify the sliding-in effect code + guarantee that it works cross-browser. Plus it’s less jarring than suddenly showing the sticky element.

If you don’t want to use jQuery to slide in the replacement element, just change the code to set the elementToShowInstead style to block.

4) Decorate your html page with the following bits:

Scroll-container Id (for code to get a reference to the inner scroll element – We do this because there might be several ion-content elements in the DOM if we’re on a nested page for example.

<ion-content id="scrollyContainer">

Element you want to track on scroll:

  <ion-segment id="stickThis" style="background-color: #F5F6F6">
    <ion-segment-button>
      Movies
    </ion-segment-button>
    <ion-segment-button>
      Tv Shows
    </ion-segment-button>
    <ion-segment-button>
      Other
    </ion-segment-button>      
  </ion-segment>

Element you want to show instead, when tracked one is out of view, e.g in the header:

<ion-header>
  <ion-navbar>
    <ion-title>Amazing sticky element demo</ion-title>
  </ion-navbar>
  
  <!-- this will be shown when the original is out of view -->
  <ion-segment id="stickThisCopy" style="display: none; background-color: #F5F6F6">
    <ion-segment-button>
      Movies
    </ion-segment-button>
    <ion-segment-button>
      Tv Shows
    </ion-segment-button>
    <ion-segment-button>
      Other
    </ion-segment-button>    
  </ion-segment>  
</ion-header>

5) Make your home.page.ts (or whatever it's actually called) code ngOnInit() run the startup function e.g.:

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

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


  constructor(public navController: NavController) { }


  ngOnInit() {
      window.loadStickyEl("scrollyContainer", "stickThis", "stickThisCopy");
  }
  
}

6) If you get a TypeScript error, add a customTypings.d.ts to your project like this:

/**
 * @description Add stuff here to make TypeScript not throw up.
 */
interface Window {
    loadStickyEl: any;
}

That should be it. Your ‘sticky’ element should now work fine.

Conclusion

This may not be the ‘best’ or ‘standard’ way of doing this, but it works and is easy to maintain and extend and that’s what counts at the end of the day.

Cheers.

D

Source: Stack Overflow for tracking scroll code.

Comments: