0

I have a form with sections that are scrolled and lined up automatically when the user interacts with it. I would like to have all the logic defined in a directive but at the moment can't figure out how to get some of the DOM manipulation logic out of my controller. Most of the functionally can be attached to on scrolls, clicks or focus events but how do I get a function attached to my scope to trigger some DOM manipulation without having the DOM logic in my controller?

What I currently have is

$scope.scrollToNextSection = function(section){
    //DOM manipulation logic to scroll to next section.
}

Would it be valid for me to have

directiveDOMObject.scrollToNextSection = function(section){
    //DOM manipulation logic to scroll to next section.
}

and call it from my controller with

$scope.scrollToNextSection = function(section){
    directiveDOMObject.scrollToNextSection(section);
}

Is attaching a function to a DOM object like this ok so all my DOM manipulation can be contained in the directive? Is there a standard pattern for triggering DOM manipulation logic defined in a directive from a controller?

Adrian Brand
  • 20,384
  • 4
  • 39
  • 60

2 Answers2

1

HTML handles scrolling within the page using name anchors. <a name="sectionX"> and <a href="#sectionX"> These are getting heavily (mis)used in an SPA if you use a router.

The scope/controller does not know about the dom and cannot/shouldnot change it. The FAQ says:

DOM Manipulation

Stop trying to use jQuery to modify the DOM in controllers. Really. That includes adding elements, removing elements, retrieving their contents, showing and hiding them. Use built-in directives, or write your own where necessary, to do your DOM manipulation. See below about duplicating functionality.

Someone has written an ngScrollTo directive which keeps the logic in the view + directive. I haven't tried it out but it looks like the way to go.

See also See Anchor links in Angularjs? for alternative solutions.

Community
  • 1
  • 1
flup
  • 26,937
  • 7
  • 52
  • 74
0

Is attaching a function to a DOM object like this ok so all my DOM manipulation can be contained in the directive

The short answer here is no, not really. If the controller has business logic, then it shouldn't be concerned with what's going on in the DOM.

Is there a standard pattern for triggering DOM manipulation logic defined in a directive from a controller?

Not sure if they're standard, but they are a few ways. Their common theme is that the controller, that handles business logic either directly or via services, doesn't actually call the directive, or really know what's going on in the DOM / view. It just provides "hooks" in one form or another, so the directive can react appropriately.

The ways I know of are:

  1. React to changes of variable on the scope. So you can have a variable, like state

    <div scroll-listen-to="state"> .... </div>
    

    And a directive, scrollListenTo, with a scope + link function as follows:

    scope: {
      scrollListenTo: '='
    },
    link: function postLink(scope, iElement, iAttrs) {
       scope.$watch('scrollListenTo', function(newValue, oldValue) {
         // Do something, maybe with scrolling?
       });
    }
    
  2. React to events $broadcast from the controller. This sends the event to child scopes (and so scopes in directives within the sending scope). The name of this event can also be configurable. So, for example

    <div ng-controller="MyController">
      <input scroller-event="MyController::stateChanged" />
    </div>
    

    Then in MyController, at the appropriate point:

    $scope.$broadcast('MyController::stateChanged', 'someData');
    

    And in the directive:

    scope: {
      'eventName': '@scrollerEvent'
    },
    link: function postLink(scope, iElement, iAttrs) {
       scope.$on(scope.eventName, function(e, data) {
         // Do something the data
       });
    }
    
  3. React to events $emited from the controller. This is very similar to $broadcast, but the event is emitted upwards through the hierarchy. You can "wrap" several controllers and then they can send events to a directive that wraps them.

    <div scroller-event="MyController::stateChanged">
      <div ng-controller="MyController">
      </div> 
      <div ng-controller="MyController">
      </div> 
    </div>
    

    Then in MyController

    $scope.$emit('MyController::stateChanged', 'someData');
    

    In this case, you probably shouldn't use the scope parameter in the directive, as this would create an isolated scope, which in this case probably isn't desired. The directive could have something like

    link: function postLink(scope, iElement, iAttrs) {
       var eventName = iAttrs.scrollerEvent;
       scope.$on(eventName, function(e, data) {
         // Do something with the data, like scrolling.
       });
    }
    
  4. You say you're using a form. You could create a set of custom directives that interact, much like ngModel and ngForm interact. So, for example, you could have:

    <div scroller-container>
      <input scroll-on-focus />
      <input scroll-on-focus />
    </div>
    

    Then in the scrollOnFocus directive

    require: '^scrollerContainer',
    link: function(scope, iElement, iAttrs, scrollerContainerController) {
      iElement.on('focus', function() {
        scrollerContainerController.scrollTo(iElement);
      });
    }
    

    And in the scollerContainer directive, you must define scrollTo on its controller

    controller: function() {
      this.scrollTo = function(element) {
        // Some code that scrolls the container so the element is visible
      };
    }
    

I realise the above ways are not especially specific to your issue of scrolling: they are more generic, and to be honest, I'm not yet sure which to recommend in any given case.

Michal Charemza
  • 25,940
  • 14
  • 98
  • 165