Adrian Moisa
Adrian Moisa

Reputation: 4353

Manipulate external DOM (prevent text selection) while avoiding DOM access from directive controller

Scenario...

I'm building a date picker directive and untill now I managed to keep strict separation between template/controller concerns. The controller builds a month array full of day objects. The template uses ng-repeat="day in days" to render current month dates.

Currently I am building an interval selection method which is invoked by: ng-mousedown="startSelection(date.dayId);". While the click is pressed a range of dates is defined in the model as the selected dates, and highlighted in the template via a css class. On mouse button release the range is defined one last time in order to obtain the final date selection.

This works fine, however there is a small issue that needs to be tackled. While the mouse is pressed and startSelection does it's job also the browser highlights the text in the specific blue shade. I want to prevent this behavior by attaching a no-select css class to the body element while the mouse is pressed, and then removing it when selection sequence has ended.

(I choose the body element because if the cursor slides of the directive into the app body, text will be selected from everywhere in the page.)

.no-select { 
    -webkit-touch-callout:none; 
    -webkit-user-select:none; 
    -khtml-user-select:none; 
    -moz-user-select:none; 
    -ms-user-select:none; 
    user-select:none;
}

... and Question

And here's what troubles me: Since we are not supposed to manipulate DOM from the controller (especially external elements) in order to facilitate unit testing, what is the recommended way to do this operation? Are there any guidelines/best practices for fringe cases like this one? Is it overkill to create a service dedicated for text selection prevention? I guess that would best fit as a method into an utils service.

Directive controller:

app.directive("caDatePicker", function () {
    return {
        ...
        controller: function ($scope) {
            ...
            $scope.startSelection = function (dayId) {
                angular.element(document.querySelector('body')).addClass('no-select');
                ...
            };
            ...
        }
    }
}

Slightly different scenario: What if I add a class on the directive element <ca-date-picker></ca-date-picker> like in the following code sample. Is this ok practice? Does this infringe the controller-template separation of concerns guideline?

link: function (scope, element, attrs) {
    element.addClass("no-select");
    ...
}

Upvotes: 0

Views: 439

Answers (1)

Arno_Geismar
Arno_Geismar

Reputation: 2330

You could set a boolean in the controller and then in your directive you could watch a change on this model attribute and do your dom manipulation in the directive :

controller: function ($scope) {
    $scope.dayId = "";
    $scope.selectionStarted = false;
    $scope.startSelection = function (dayId) {
       $scope.dayId = dayId;
       $scope.selectionStarted = true;

    };
    ...
}

then in your directive :

$scope.$watch('selectionStarted', function(newValue, oldValue) {
          if(newValue) {
            angular.element(document.querySelector('body')).addClass('no-select');
          }
        });

Basically take this as a rule of thumb. Never ever manipulate your dom in your controllers ever. Your controller is the glue that controls the behaviour of your view and the directive is what depending on the status of your controller $scope variables is what manipulates the dom.

if your element that you want to manipulate is outside of your direcitve scope or controller scope. but an attribute of that controller is responsible for if or not the dom manipulation should be applied. You can do this :

in your template :

ng-class="noSelectBoolean ? 'no-select-css-class' : 'select-css-class'"

in your controller that is in scope of your template :

.controller(function($scope) {
     $scope.noSelectBoolean = false;
     $scope.$on('applyNoSelect', function(event, value) {
     $scope.noSelectBoolean = value;
     }
}

and lastly in your controller that decides if it should be applied or not :

.controller(function($scope, $rootScope){
    $scope.noSelect  = function(value){
     $rootScope.$broadcast('applyNoSelect', value);
   }
}

for more a more indepth explaination check out a previous answer I submitted :

Event broadcasting in angularJS

Upvotes: 1

Related Questions