In my AngularJS app, I have a template that contains a <textarea>
and a submit button. A <div>
(which could just as easily be a <form>
) wraps both of those elements and has a controller. The controller utilizes a $resource service to POST
the value from the <textarea>
to a REST API when the submit button is clicked.
Template:
<div ng-controller="MyController as vm">
<textarea ng-model="vm.text" rows="5"></textarea>
<button ng-click="vm.save()">Save</button>
</div>
Controller:
angular.module('myApp')
.controller('MyController', MyController);
MyController.$inject = ['myRestApiResource'];
function MyController(myRestApiResource) {
var vm = this;
vm.save = function() {
var params = [];
var postData = {text: vm.text};
myRestApiResource.save(params, postData)
.then(function(res) { /* success handler */ })
.catch(function(res) { /* error handler */ });
};
}
When the resource save()
method's $promise is rejected, the response data (in some cases) contains an error.position
property that refers to the string index where a syntax error was detected in vm.text
.
In those cases, I focus on the <textarea>
element and set the selection range from the error.position
to the next non-word character. I have a function for this, and it all works fine.
My question is:
Should the function call that sets the selection range on the <textarea>
(after the promise is rejected) be performed in...
- the controller
- a custom directive added to the textarea
- somewhere else
...and why?
This is a question about separation of concerns.
The controller seems like the most sensible place, but it is my understanding that controllers should not perform DOM manipulation...and this would require the use of the $element
service (or jQuery) for doing just that.
A custom directive would need to subscribe to an event broadcast by the controller, and then set its own focus and selection range. This separates the concerns, but according to Angular Best Practices $broadcast
and $on
should only be used for "events that are relevant globally across the entire app".
I suppose the controller and directive could communicate via another service, but that just seems like over-engineering -- something I am notorious for and trying hard to avoid here! :-)
Thanks in advance!
Solution
For completeness, here is the custom directive I wrote based on @kristin-fritsch 's answer:
Directive
angular.module('myApp')
.directive('selectionRange', selectionRangeDirective);
function selectionRangeDirective() {
return {
restrict: 'A',
scope: {
selectionRange: '='
},
link: function(scope, iElement) {
var element = iElement[0];
if (element.setSelectionRange && typeof element.setSelectionRange === 'function') {
scope.$watch('selectionRange', function(range) {
if (range && typeof range.start === 'number' && range.start > -1 && range.start < iElement.val().length) {
element.focus();
element.setSelectionRange(range.start, range.end);
}
});
}
}
};
}
To implement it, I added the directive attribute to my textarea:
<textarea ng-model="vm.text" rows="5" selection-range="vm.selection"></textarea>
...and set the value of vm.selection
to an object with start
and end
properties in my controller, when the promise is rejected.
myRestApiResource.save(params, postData)
.then(function(res) { /* success handler */ })
.catch(function(res) {
var error = res.data.error;
if (error && error.position) {
var start = error.position;
var end = start + vm.text.substr(start).search(/(\W|$)/);
vm.selection = {start: start, end: end};
}
});