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:
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?
});
}
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
});
}
React to events $emit
ed 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.
});
}
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.