Search code examples
angularjsangularjs-directiveangularjs-scopeangular-ui-bootstrap

In Angular JS, is there a way to watch for changes to the DOM without using scope.watch?


I am attempting to create an angularjs bootstrap accordion that scrolls the accordion to the top when opened.

These solutions are close to what I would like to do:

However, they use a timeout or scope watches. I would like to avoid using these unless absolutely necessary.

Is there a way to accomplish this without using $watch or setTimeout?

Here is a plunk of what i am trying to do, this is using the $watch: https://plnkr.co/edit/XQpUdrdjqaCGom4L9yIJ

app.directive( 'scrollTop', scrollTop );

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

function link( scope, element ) {
    scope.collapsing = false;
    var jqElement = $( element) ;
    scope.$watch( function() {
        return jqElement.find( '.panel-collapse' ).hasClass( 'collapsing' );
    }, function( status ) {
        if ( scope.collapsing && !status ) {
            if ( jqElement.hasClass( 'panel-open' ) ) {
                $( 'html,body' ).animate({
                    scrollTop: jqElement.offset().top - 30
                }, 500 );
            }
        }
        scope.collapsing = status;
    } );
}

Solution

  • I have found a way to do this from the controller.

    I added a function that is triggered on ng-click to report the is-open status of the accordion.

    Using the component lifecycle hook $doCheck I was able to watch for changes to the state of vm.isOpen. $doCheck runs on the end of every digest cycle, so I did not need to set a $scope.watch or $timeOut

    The $doCheck runs essentially the same code as the directive in the question

    app.controller('homeController', function($state, $element, sections, $transitions) {
      var vm = this;
    
      vm.$onInit = function() {
        vm.sections = sections.getSections();
      };
    
      function updateOpenStatus() {
        vm.collapsing = false;
        vm.isOpen = vm.sections.some(function(item) {
          return item.isOpen;
        });
      }
    
      vm.$doCheck = function() {
        if (vm.isOpen) {
          var elem = $element.find('.panel-collapse');
          var status = elem.hasClass('collapsing');
          if (vm.collapsing && !status) {
            var parentElem = elem.closest('.panel-open');
            if (elem.parent().hasClass('panel-open')) {
              $('html,body')
                .stop()
                .animate({
                  scrollTop: parentElem.offset().top - 52
                }, 'fast');
            }
          }
          vm.collapsing = status;
        }
      };
    });
    

    I updated the uib-accordion to call the function in the controller

    <uib-accordion>
      <div heading="Section Title" is-open="section.isOpen" ng-repeat="section in vm.sections" scroll-top uib-accordion-group>
        <uib-accordion-heading>
          <div ng-class="{isOpen: section.isOpen}" ng-click="vm.toggleOpen()">
            <h3>{{section.sectionTitle}}</h3>
          </div>
        </uib-accordion-heading>
        <div class="clearfix">
          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
          in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
        </div>
      </div>
    </uib-accordion>
    

    Updated Plnkr: https://plnkr.co/edit/5EqDfmVOa0hzFfaQqdI0?p=preview