Bootstrap data-spy="affix" not working on Angular View change
Asked Answered
T

5

16

I'm trying to figure out why my affix-ed panel isn't staying put when I change my Angular view.

I've added the affix property directly to the panel on the first page (details), and left it in the data-spy only in the second page (flights).

In a full blown web version, if I refresh the flights page, suddenly the affix kicks in, and stays put when I scroll, but doesn't if I just navigate to the page using Angular.

It looks like the affix isn't being added to the class by Bootstrap when I navigate between views.

HTML:

<div class="panel panel-primary mySidebar" id="sidebar" 
    data-spy="affix" data-offset-top="0" data-offset-bottom="200">

CSS:

.mySidebar.affix {
    position: fixed;
    top: 250px;  
}

.mySidebar.affix-bottom {
    position: absolute;
    top: auto;
    bottom: 450px;  
}

Here's a Plunker..

http://plnkr.co/edit/S0Bc50?p=preview

I found a similair question here:

Twitter Bootstrap: Affix not triggering in single page application

But I couldn't figure out how to apply that to my problem here...

Any help would be great!

Terricolous answered 13/5, 2014 at 18:28 Comment(5)
Have you considered using github.com/angular-ui/bootstrap ?Uncoil
I haven't actually... What does this offer to help? Cheers!Terricolous
@Uncoil Angular UI Bootstrap doesn't appear to have an Affix directiveLashaunda
But Angular UI Utils has a Scrollfix directiveLashaunda
You can also just trigger it from component. It works I tried getbootstrap.com/docs/3.4/javascript/#via-javascript-5Clovis
L
13

Angular UI Utils (not Angular UI Bootstrap, as mentioned in the comments) has a Scrollfix directive that can be used in place of Affix. I had the same problem as you with Affix not working after changing views. I tried adding Angular UI Scrollfix to my app and so far it looks like it will meet my needs.

Here is a helpful Stack Overflow post I found with more explanation and examples: Angular ui-utils scrollfix.

Lashaunda answered 7/8, 2014 at 21:59 Comment(0)
B
8

If you don't want to use UI Bootstrap or UI Utils in favor of the plain old Bootstrap 3 affix, this affix directive wrapper is working good for me on my single page angular app. I'm using the AngularUI Router.

Apply the affix the first time the directive is loaded. Then clear it on $stateChagneSuccess with this: $element.removeData('bs.affix').removeClass('affix affix-top affix-bottom'); as seen here: Resetting / changing the offset of bootstrap affix, and then re-apply it. Let me know if anything I'm doing here is painfully ugly, though I like the simplicity and I like leveraging the default Bootstrap JS lib.

myApp.directive('affix', ['$rootScope', '$timeout', function($rootScope, $timeout) {
    return {
        link: function($scope, $element, $attrs) {

            function applyAffix() {
                $timeout(function() {                   
                    $element.affix({ offset: { top: $attrs.affix } });
                });
            }

            $rootScope.$on('$stateChangeSuccess', function() {
                $element.removeData('bs.affix').removeClass('affix affix-top affix-bottom');
                applyAffix();
            });

            applyAffix();

        }
    };
}]);
Bailiff answered 21/1, 2015 at 22:31 Comment(0)
A
4

I prefer to hook on the original data-spy="scroll" bootstrap bindings - for better migration from a clickdummy to production - and apply the original bootstrap functionality to it, because Angular-UI is (at least for me) an unacceptable mess.

(function () {
  'use strict';

  angular
    .module('app.core')
    .directive('spy', spy);


  function spy() {
    return {
      restrict: 'A',
      link: link
    };

    function link(scope, element, attrs) {

      // Dom ready
      $(function () {
        if (attrs.spy === 'affix') {
          $(element).affix({
            offset: {
              top: attrs.offsetTop || 10
              // bottom: attrs.offsetBottom || 10
            }
          });
        }
      });
    }
  }
})();
Agrestic answered 2/10, 2015 at 9:21 Comment(0)
Y
3

This is an old question, but I recently stumbled upon it when rewriting my website as an AngularJS app. I was also using the Bootstrap Affix plugin (including updating active class on scroll), and was unable to find a plugin that worked quite like what I was looking for, so I wrote my own directive.

See it in action: http://bobbograph.com/#/api

Note: The value provided to the directive is the top offset.

The JavaScript:

https://github.com/robertmesserle/Bobbograph/blob/v3/www/js/site.js#L102

app.directive('rmAffix', function ($window) {
  var body = document.body,
      win = document.defaultView,
      docElem = document.documentElement,
      isBoxModel = (function () {
        var box = document.createElement('div'),
            isBoxModel;
        box.style.paddingLeft = box.style.width = "1px";
        body.appendChild(box);
        isBoxModel = box.offsetWidth == 2;
        body.removeChild(box);
        return isBoxModel;
      })();
  function getTop (element) {
    var box = element.getBoundingClientRect(),
        clientTop  = docElem.clientTop  || body.clientTop  || 0,
        scrollTop  = win.pageYOffset || isBoxModel && docElem.scrollTop || body.scrollTop;
    return box.top + scrollTop - clientTop;
  }
  return function ($scope, $element, $attrs) {
    var offset = $scope.$eval($attrs.rmAffix),
        elemTop = getTop($element[0]),
        $links = $element.find('li'),
        linkTops = [];
    angular.forEach($links, function ($link) {
      var $a = angular.element($link).find('a'),
          href = $a.attr('href').substr(1),
          target = document.getElementById(href),
          elemTop = getTop(target);
      linkTops.push(elemTop);
      $a.on('click', function (event) {
        window.scrollTo(0, elemTop - offset);
        event.preventDefault();
      });
    });
    angular.element($window).on('scroll', function (event) {
      var top = window.pageYOffset,
          index;
      if (top > elemTop - offset) $element.addClass('affix');
      else $element.removeClass('affix');
      //-- remove selected class from all links
      $links.removeClass('active');
      //-- find the closest element
      for (index = 1; index < linkTops.length; index++) {
        if (linkTops[index] - 100 > top) break;
      }
      angular.element($links[index - 1]).addClass('active');
    });
  };
});

The HTML (Jade)

https://github.com/robertmesserle/Bobbograph/blob/v3/www/api.jade#L288

ul.nav.nav-stacked.nav-pills(rm-affix=70)
  li.active: a( href="#basic" ) Basic Usage
  li: a( href="#options"   ) Options
  li: a( href="#data"      ) Data
  li: a( href="#padding"   ) Padding
Yanina answered 14/10, 2014 at 16:35 Comment(0)
L
2

It doesn't work because the specific events are not fired on which affix is bound to. In a Single Page Application as in your case the page is loaded only once so any logic that is bound to page loaded and similar events fail. Things will work fine if you reload the page.

The solution is to use angularized way. One of the comments suggest using angular-strap a try as it has an affix directive for the exact same purpose.

I came across a similar situation and ended up using MacGyver It works really well and is simple to use.

Lungan answered 29/6, 2014 at 18:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.