A Quick Guide to Implement a Cross-Browser AngularJS Pull-to-refresh Directive

What made me do this?

As a newbie in frontend development, I have to admit, a few months ago I couldn’t even imagine myself writing my own UI component. In those good days, I just carelessly included other, ready-to-use components and never thought about why and how they really work. Sometimes these plugins caused me some headaches but I always managed to avoid going down to the lower pits of their implementation. Needless to say, this cloudless state didn’t really last too long.

One day a quite reasonable and seemingly simple request arrived in our office. One of our customers raised the question whether it is possible to have a pull to refresh feature in his web app. Our answer was quick, self-confident and achingly unconsidered contemplating at this distance of time. „Yes, it’s easily attainable”- we answered, after ascertaining that there are plenty of implemented open-source pull to refresh directives available all over the Internet.

I assume, the end of the story is quite predictable, considering the title of this blog-post. Soon I ended up testing dozens of pull-to-refresh directives, one after another, always running into some unforeseen bugs. Either they did not work with all the required browsers, or they were not able to handle content with dynamic sizes. After all of these efforts I came to a desperate but ambitious decision. I decided to write my own pull-to-refresh directive, combining the existing ones I found and tested before.

text

The most important criteria

According to my previous experiences with the components I tried, I collected the most important requirements that can be imposed upon a directive of this type:

During implementation, this list helped me a lot. While testing, it always reminded me of the most critical user cases. I had to solve plenty of tricky bugs to meet these requirements, and in the next paragraphs, I will share you some tips about how I solved these.

The basic architecture

Basically the task is simple. First of all, we need to be able to register the state of the scroll-able element. Is it scrolled to the top, or even “overscrolled”? I decided to achieve this by using element.scrollTop attibute. It is very important that it works only with a fixed size container, that doesn’t grow with the growth of it’s content. To secure responsibility, I preferred using a fixed position container instead of setting the exact height (pulltorefresh-frame).My implementation looks like this:

<div class="pulltorefresh-frame">
      <div class="scrollablecontainer">
         <ng-transclude></ng-transclude>
      </div>
</div>
.scrollablecontainer {
    height: 100%;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
}
 
.pulltorefresh-frame {
    position: absolute;
    left: 0;
    right: 0;
    top: 100px;
    bottom: 0;
    padding-top:10px;
    padding-left:10px;
    padding-right:10px;
    padding-bottom:0px;
}

The directive should check all the scroll events happening on the list. To achieve this, the best tools are javascript event handlers, such as touchstart, touchmove, touchend. It is highly recommended to use the link option to register DOM listeners as well as to update the view. It is executed after the template has been cloned and is where directive logic will be put.

The directive has an updateFn parameter as an input. This is the parent controller’s function, which is called when a refresh is needed. It also has a refreshInProgress flag, so that animations and other view manipulations can be bound to the value of this variable.

Summarizing these, the directive should look like the following:

angular
   .module('app.core')
   .directive('pullToRefresh', pullToRefresh);
  
unction pullToRefresh() {
   return {
      templateUrl: 'directives/progressbar.html',
      transclude: true,
      scope: {
         updateFn: '&',
         refreshingInProgress: '=?',
      },
      link: function (scope, element) {
  
         element.bind("touchend", onTouchEnd);
         element.bind("touchmove", onTouchMove);
         element.bind("touchstart", onTouchStart);
 
         function onTouchStart(event) {...}
         function onTouchMove(event) {...}
         function onTouchEnd(event) {...}
   }
};

The heart of the directive

I think it is very useful to summarize the exact tasks of these event handlers.

text

function onTouchStart(event) {
   this.slideBeginY = event.changedTouches[0].pageY;
}
function onTouchMove(event) {
   var windowHeight = window.innerHeight;
   this.currentMousePosition = event.changedTouches[0].clientY;
   if(!this.reachedTheTop) {
      var allowUp = (document.getElementsByClassName('scrollablecontainer')[0].scrollTop > 1);
      var up = (event.changedTouches[0].pageY > this.slideBeginY);
      var down = (event.changedTouches[0].pageY < this.slideBeginY);
      this.slideBeginY = event.changedTouches[0].pageY;
      if((up && !allowUp) && !down) {
         this.reachedTheTop = true;
         this.refreshStartPosition = this.currentMousePosition;
      }
   } else {
      progressBarUpdate(this.refreshStartPosition, this.currentMousePosition, windowHeight);
   }
}
function onTouchEnd(e) {
   if(this.reachedTheTop) {
      this.reachedTheTop = false;
      if(isRefreshNeeded(this.currentMousePosition, this.refreshStartPosition)) {
         progressBarToRefreshPosition();
         scope.updateFn().then(function () {
            progressBarRestore();
         });
      } else {
 
         progressBarRestore();
      }
   }
}
 
function isRefreshNeeded(currentMousePosition,refreshStartPosition) {
   var w = window.innerHeight;
   return (currentMousePosition - refreshStartPosition) > w * scope.requiredPercentage;
}

Finally! We successfully achieved to handle pull attempts! We all can take a breath after surviving all the terrible ordeals while fighting against strange mobile browser behaviors and terrible challenges. Great! But if you have ever dealt with frontend development, you should already know that a very important issue has remained uncovered…

Mirror, mirror, on the wall, what the hell happened?!

Please note, that web development is a unique island in the endless seas of IT sectors where beauty and design DOES matter. In case of UI components, it is extremely important to give proper, easy-to-understand feedback. Sprinkling this with a pinch of design, our brand new pull-to-refresh component becomes a useful tool of mobile websites. Though I don’t want to constitute any limit on the creativity of the web designer living in you, here are some tips and examples showing you how to manage view manipulation.

In my example project, I chose a pullable loading bar, inspired by the native Android solution:

text text text

To add the loading-bar to the directive, you need to extend the directive’s template, for example like this:

<div class="pulltorefresh-frame">
      <div class="progress-bar-container">
         <div class="pie">
            <div class="clip1">
               <div class="slice1"></div>
            </div>
            <div class="clip2">
               <div class="slice2"></div>
            </div>
            <div class="cssload-container">
               <div class="cssload-speeding-wheel spinner" ng-hide="!home.refreshing"></div>
               <i class="glyphicon glyphicon-repeat reloadicon" ng-hide="home.refreshing"></i>
            </div>
         </div>
      </div>
      <div class="scrollablecontainer">
         <ng-transclude></ng-transclude>
      </div>
</div>

The view has to be manipulated according the state of the pull attempt. With the help of the event handlers, we can calculate and update the values defining the state of the progress bar. All the magic happens in the mysterious progressBarUpdate() function, that has been mentioned before. The propreggBarResore function also manipulates the view, when the pull attempt is over:

function progressBarUpdate(currentMousePosition, windowHeight) {
    // calculate values:
   var pulledDown = currentMousePosition - refreshStartPosition;
   var percentage = getPercentage(currentMousePosition, windowHeight);
 
   // set the transitions:
   setProgressBarPosition(pulledDown);
   setProgressBarColorAndOpacity(percentage);
   setProgressAngle(percentage);
}
  
function progressBarRestore() {
   scope.refreshingInProgress = false;
   setProgressBarPosition(0);
   setProgressBarColorAndOpacity(0);
   setProgressAngle(0);
}

There are plenty of possibilities to manipulate view, and as it is a quick review, let me introduce you the simplest one. This function changes the opacity of the progress bar depending on the parameters of the touch move:

function setProgressBarColorAndOpacity(percentage) {
   //progress-bar-icon opacity and color
   document.getElementsByClassName("reloadicon")[0].style.opacity = percentage / 100;
   if(percentage / 100 >= 1) {
      document.getElementsByClassName("reloadicon")[0].style.color = scope.barColor;
   } else {
      document.getElementsByClassName("reloadicon")[0].style.color = "#333";
   }
}

Final steps – the more configurable the better

The big questions of life usually force us to make tough decisions. What if I have a well-designed, pretty website with all the beautiful shades of yellow, and I find a pull to refresh directive blazing in blue? Things get quite difficult at this point, because it’s common sense that these two colors simply hate each other. On the other hand it is quite wasteful to omit a well-functioning directive for such silly reasons. To rescue people from the painful hours of diving in the pits of the implementation, and trying to override the content of css and javascript files, it’s very useful to make as many things configurable as possible.

In this case I decided to make the color of the progress bar, the required pull strength and the header height configurable. This way, the directive has three additional inputs. We also have to put a configuration function in the link function where the default values of the inputs can be set, and the effect of the inputs can prevail. All in all, following some little changes, the implementation will take it’s final shape:

angular
   .module('app')
   .directive('pullToRefresh', pullToRefresh);
 
function pullToRefresh() {
   return {
      templateUrl: 'directives/progressbar.html',
      transclude: true,
      scope: {
         updateFn: '&',
         refreshingInProgress: '=?',
         barColor: '=?',
         requiredPercentage: '=?',
         frameTop: '=?'
      },
      link: function (scope, element) {
         configuration();
 
         element.bind("touchend", onTouchEnd);
         element.bind("touchmove", onTouchMove);
         element.bind("touchstart", onTouchStart);
 
         function configuration() {
            scope.requiredPercentage = angular.isDefined(scope.requiredPercentage) ? scope.requiredPercentage : 0.25;
            scope.barColor = angular.isDefined(scope.barColor) ? scope.barColor : "#127ba3";
            scope.frameTop = angular.isDefined(scope.frameTop) ? scope.frameTop : 100;
            configureView();
 
         };
 
         function configureView() {
            document.getElementsByClassName('home-frame')[0].style.top = scope.frameTop + 'px';
            document.getElementsByClassName('slice1')[0].style.borderColor = scope.barColor;
            document.getElementsByClassName('slice2')[0].style.borderColor = scope.barColor;
            document.getElementsByClassName('cssload-speeding-wheel')[0].style.borderLeftColor = scope.barColor;
            document.getElementsByClassName('cssload-speeding-wheel')[0].style.borderRightColor = scope.barColor;
         }
 
         function onTouchEnd(e) {
            ...
         }
 
         function onMouseStart(event) {
           ...
         }
 
         function onTouchStart(event) {
            ...
         }
      }
   }
};

Summary

Finally… blast off! Our brand new pull to refresh component is ready to be released into the endless fields of the World Wide Web. I hope you found it useful, and feel free to add any observations. I myself had a very good time accomplishing this little challenge, and continue to get a large portion of self-confidence when facing problems like this.

text

You can find the whole project on https://github.com/scsenge/pull-to-refresh (GitHub) or check it out in action on https://plnkr.co/edit/czMKiz (Plunkr).

member photo

She is into working with Angular 2 on frontends and Scala on backends. (And anything else that helps her make beautifully sleek web apps.)

Latest post by Csenge Sóti

Monday Morning with a Content-Security-Policy HTTP Response Header