Pages

Tuesday, 27 January 2015

Angularjs Directive for an Animated Highlight Slider

Recently I had to work on angularjs and one of the component that had to be reproduced was a slider to slide across to the selected element. This is an attempt to write out the same. Any suggestion for improvements is welcome.
The objective is to transition the slider from one heading to another.

A continuation for the below post with a simpler approach is available in Part 2 of this post

The directive can be seen in action here

A jsfiddle for the same is available here.

Components:
  • Define the Angular App
  • Define the HTML
  • Define the Angular Directive
  • Define the transition css
Assumption
  • Bootstrap is used for the Grid Layout.
  • All selectors are rendered on the same line.
HTML :

The code is al

<div  class='row' data-ng-init='moveSlider="slider1"'>
 <div class='col-xs-4 text-center'>
  <div>
   <span><a id='slider1' data-ng-click='moveSlider="slider1"'>Slider1</a></span>
  </div>
 </div>
 <div class='col-xs-4 text-center'>
  <div>
   <span><a id='slider2' data-ng-click='moveSlider="slider2"'>Slider2</a></span>
  </div>
 </div>
 <div class='col-xs-4 text-center'>
  <div>
   <span><a id='slider3' data-ng-click='moveSlider="slider3"'>Slider3</a></span>
  </div>
 </div>
</div>
<underline-slider slider-class='"sliderdownarrow"' selector='moveSlider' selector-map='{slider1 : 0,slider2 : 1,slider3 : 2}' duration='3' > 
 <div class='row visible-xs-block'>
  <div class='col-xs-4 text-center'>
   <div class='sliderdownarrow'  ></div>
  </div>
  <div class='col-xs-4 text-center'>
   <div class='sliderdownarrow'  ></div>
  </div>
  <div class='col-xs-4 text-center'>
   <div class='sliderdownarrow'  ></div>
  </div>
 </div>
</underline-slider>


As you can notice there are there are 3 headings/tabs named slider1, slider2 and slider3 that are enclosed in a a row (the rows have been created using bootstrap).
The directive is named underline-slider which needs the following attributes (all attributes are mandatory):
  • slider-class - This attribute is used to identify the element that needs to animate. Corresponding to each of the link/tab there is a element with this class within the directive as can be seen in the html.
  • selector - This should be an angular variable that is bound to the link. The change in this value triggers the directive to transition from one position to another.
  • selector-map - This should provide a map of all possible selector values and its position. First position starts with 0 similar to an array as it will be used to identify the elements to transition.
  • duration - The duration should be specified in seconds. The value should match the css transition period. This is used to ensure that at the end of the transition all styles are reverted and the slider corresponding to the current element is shown. This will ensure that the slider is placed within the div that has been provided for each of the tab ensuring that the slider is responsive to a page resize.
Note:

  • The link/tab relative to which the slider should animate should have the same id as the selector variable. In the above the anchor id and the model value match.
  • Initiate the tab/link that has to be highlighted on first load. In the above example it has been done with data-ng-init='moveSlider="slider1"'.
  • The duration value 3 matches the transition property of the animating class (sliderdownarrow).
  • Since we are animating on property left we should ensure that the slider-class defined in css contains the left set to a numeric value else the transition will not take effect.


Angular App:

angular.module('slider',[]);
Controller and Directive:
The complete source code is available in the following GIT

angular.module('sliderapp').controller('sliderAppCtrl', ['$scope',
 function() {
 }
]);
angular.module('sliderapp').directive('underlineSlider',['$compile','$timeout',function($compile,$timeout){
return{
 restrict :'EA',
 scope : {
  sliderClass : '=',
  selector : '=',
  selectorMap : '=',
  sliderStyle : '=',
  duration : '='

 },
 link : function($scope,$element, $attrs){
  
  var children = $element;
  var parent = $element;
  var iter = 0;
  var sliderParent ;
  var sliderElements = [];
  var checkClass = function(el){
   angular.forEach(el,function(child){
    var childEl = angular.element(child);
    if(childEl.hasClass($scope.sliderClass)){
     childEl.addClass('ng-hide');
     sliderElements.push(childEl);
     return childEl;
    }
    
    if(!angular.isUndefined(child)){
     var children = angular.element(child).children();
     var retClass = checkClass(children);
    }
   });
  }
  checkClass(parent);
  
  var getPosition = function(elem){
       if (typeof elem == 'string' || elem instanceof String) {
         elem = document.getElementById(elem);
       } else {
         var elm = angular.element(elem);
         if ('undefined' == typeof(elm)) {
           elm = elem;
         }
       }
       if ('undefined' == typeof(elm)) {
         return {
           left: 0,
           top: 0
         };
       }
       var rawDom = elm[0];
       var _x = 0;
       var _y = 0;
       var body = document.documentElement || document.body;
       var scrollX = window.pageXOffset || body.scrollLeft;
       var scrollY = window.pageYOffset || body.scrollTop;
       var position = rawDom.getBoundingClientRect();
       return position;

  }

  //console.log(sliderEl);
  var slideCompleteFunc;
  var promise;
  $scope.$watch(function(){return $scope.selector},function(newValue,oldValue){
   var prevChange = angular.element(document.getElementById(oldValue));
   var curChange = angular.element(document.getElementById(newValue));
   var prevElPos = $scope.selectorMap[oldValue];
   var newElPos = $scope.selectorMap[newValue];
   //console.log(prev);
   //console.log(cur);

   var sliderPrevEl = sliderElements[prevElPos];
   var sliderNewEl = sliderElements[newElPos];

   if(oldValue == newValue){
    sliderPrevEl.removeClass('ng-hide');
    return;
   }
   
   var curElPos = getPosition(curChange);
   var prevElPos = getPosition(prevChange);
   var sliderPrevElPos = getPosition(sliderPrevEl);
   
   
   //$scope.$parent.$digest();
   
   console.log(sliderPrevElPos);
   var moveLeft = curElPos.left - prevElPos.left;
   console.log(moveLeft);
    sliderNewEl.attr('style','');
    //Style it with position Absolute and float left and width 100% in case the slider does not have it already
    //To ensure that the animation is not edgy the slider should be floated and the size should be controlled using the 
    //width of the parent
    sliderPrevEl.attr('style','left:'+moveLeft+'px;');
    //Finish execution 
    if(angular.isDefined(promise)){
     var promiseRet = $timeout.cancel(promise);
     if(promiseRet){
      slideCompleteFunc;
     }
    }

    slideCompleteFunc = function(prevElement,newElement){
     prevElement.removeClass('ng-hide');
     prevElement.addClass('ng-hide');
     prevElement.attr('style','');
     newElement.removeClass('ng-hide');

    }

    promise = $timeout(function(){
     slideCompleteFunc(sliderPrevEl,sliderNewEl);
     },
     $scope.duration * 1000);

  });

 }
};
}]);




CSS:

.sliderdownarrow {
  width: 100%;
  height: 4px;
  border: 2px solid;
  border-color: #9B9B9B;
  -webkit-transition: left 5s linear;
  left: 0;
  float: left;
  position: absolute;
  transition: left 3s linear; 
}
 
.sliderdownarrow::after {
    content: ' ';
    top: 100%;
    left: 50%;
    height: 0;
    width: 0;
    position: absolute;
    border-top: 5px solid #9B9B9B;
    border-right: 5px solid transparent;
    border-left: 5px solid transparent; 
}




2 comments:

  1. We never miss a single post from this useful website related to js and angularjs. After attending online Angularjs training, this site worked as a supplementary knowledge to our technical knowledge to the knowledge I gained from my instructors.

    ReplyDelete
  2. Thanks, glad to know that it helped.

    ReplyDelete