3

I'm creating a validation directive in angular and I need to add a tooltip to the element the directive is bound to.

Reading thru the web I found this solution setting a high priority and terminal to the directive, but since I'm using ngModel this doesn't work for me. This is what I'm doing right now:

return {
        restrict: 'A',
        require: 'ngModel',
        replace: false,
        terminal: true,
        priority: 1000,
        scope: {
            model: '=ngModel',
            initialValidity: '=initialValidity',
            validCallback: '&',
            invalidCallback: '&'
        },
        compile: function compile(element, attrs) {
            element.attr('tooltip', '{{validationMessage}');
            element.removeAttr("validator");
            return {
                post: function postLink(scope, element) {
                  $compile(element)(scope);
                }
            };
        },
}

But it's not working for me. It throws the following error:

Error: [$compile:ctreq] Controller 'ngModel', required by directive 'validator', can't be found!

This is the HTML where I'm using the directive:

<input id="username" name="username" data-ng-model="user.username" type="text" class="form-control" validator="required, backendWatchUsername" placeholder="johndoe" tabindex="1" >

Any ideas on how can I solve this?

Thanks.

2
  • Can we see your HTML where you use the directive?
    – floribon
    Commented Feb 5, 2015 at 19:45
  • Hi @floribon added the html to the question. Thanks Commented Feb 5, 2015 at 19:48

2 Answers 2

5

The reason is because of the combination of your directive priority with terminal option. It means that ngModel directive will not render at all. Since your directive priority (1000) is greater than ng-model's(0) and presence of terminal option will not render any other directive with lower priority (than 1000). So some possible options are :

  • remove the terminal option from your directive or
  • reduce the priority of your directive to 0 or -1 (to be less than or equal to ngModel) or
  • remove ng-model requirement from the directive and possibly use a 2-way binding say ngModel:"=" (based on what suits your requirement).
  • Instead of adding tooltip attribute and recompiling the element, you could use transclusion in your directive and have a directive template.

terminal - If set to true then the current priority will be the last set of directives which will execute (any directives at the current priority will still execute as the order of execution on same priority is undefined). Note that expressions and other directives used in the directive's template will also be excluded from execution.

demo

angular.module('app', []).directive('validator', function($compile) {
  return {
    restrict: 'A',
    require: 'ngModel',
    replace: false,
    terminal: true,

    scope: {
      model: '=ngModel',
      initialValidity: '=initialValidity',
      validCallback: '&',
      invalidCallback: '&'
    },
    compile: function compile(element, attrs) {
      element.attr('tooltip', '{{validationMessage}');
      element.removeAttr("validator");
      return {
        post: function postLink(scope, element) {

          $compile(element)(scope);

        }
      };
    },
  }
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
  <input validator ng-model="test">
</div>

As explained in my comments you do not need to recompile the element and all these stuffs, just set up an element and append it after the target element (in your specific case, the input).

Here is a modified version of validation directive (i have not implemented any validation specifics which i believe you should be able to wire up easily).

So what you need is to set up custom trigger for tooltip which you can do by using the $tooltipprovider. So set up an event pair when you want to show/hide tooltip.

.config(function($tooltipProvider){
    $tooltipProvider.setTriggers({'show-validation':'hide-validation'});
});

And now in your directive just set up your tooltip element as you like with tooltip attributes on it. compile only the tooltip element and append it after the target element (you can manage positioning with css ofcourse). And when you have validation failure, just get the tooltip element reference (which is reference to the tooltip element, instead of copying the reference you could as well select every time using the selector) and do $tooltipEl.triggerHandler('show-validation') and to hide it $tooltipEl.triggerHandler('show-validation').

Sample Implementation which shows the tooltip after 2 sec and hides it after 5 sec (since validation is not in the scope of this question you should be able to wire it up):

.directive('validator', function($compile, $timeout){

  var tooltiptemplate = '<span class="validation" tooltip="{{validationMessage}}" tooltip-trigger="show-validation" tooltip-placement="bottom"></span>';
  var tooltipEvents = {true:'show-validation', false:'hide-validation'};

  return {
        restrict: 'A',
        require: 'ngModel',
        replace: false,
        priority: 1000,
        scope: {
            model: '=ngModel',
            initialValidity: '=initialValidity',
            validCallback: '&',
            invalidCallback: '&'
        },
        compile: function compile(element, attrs) {


            return {
                post: function postLink(scope, element) {

                  var $tooltipEl= getTooltip();


                  init();

                  function init(){
                   scope.$on('$destroy', destroy);
                   scope.validationMessage ="Whoops!!!";

                   $timeout(function(){
                    toggleValidationMessage(true);
                   },2000);

                   $timeout(function(){
                     toggleValidationMessage(false);
                   },5000);
                 }

                 function toggleValidationMessage(show){
                   $tooltipEl.triggerHandler(tooltipEvents[show]);
                 }



                 function getTooltip(){
                     var elm = $compile(angular.element(tooltiptemplate))(scope);
                     element.after(elm);
                     return elm;
                 }

                 function destroy(){
                    $tooltipEl= null;
                 }

                }
            };
        },
  }

});

Plnkr

Inline Demo

var app = angular.module('plunker', ['ui.bootstrap']);

app.controller('MainCtrl', function($scope) {
  $scope.user = {
    username: 'jack'
  };
}).directive('validator', function($compile, $timeout) {

  var tooltiptemplate = '<span class="validation" tooltip="{{model}}" tooltip-trigger="show-validation" tooltip-placement="bottom"></span>';
  var tooltipEvents = {
    true: 'show-validation',
    false: 'hide-validation'
  };

  return {
    restrict: 'A',
    require: 'ngModel',
    replace: false,
    priority: 1000,
    scope: {
      model: '=ngModel',
      initialValidity: '=initialValidity',
      validCallback: '&',
      invalidCallback: '&'
    },
    compile: function compile(element, attrs) {


      return {
        post: function postLink(scope, element) {

          var $tooltipEl = getTooltip();


          init();

          function init() {
            scope.$on('$destroy', destroy);
            scope.validationMessage = "Whoops!!!";

            $timeout(function() {
              toggleValidationMessage(true);
            }, 2000);

            $timeout(function() {
              toggleValidationMessage(false);
            }, 5000);
          }

          function toggleValidationMessage(show) {
            $tooltipEl.triggerHandler(tooltipEvents[show]);
          }



          function getTooltip() {
            var elm = $compile(angular.element(tooltiptemplate))(scope);
            element.after(elm);
            return elm;
          }

          function destroy() {
            elm = null;
          }

        }
      };
    },
  }

}).config(function($tooltipProvider) {
  $tooltipProvider.setTriggers({
    'show-validation': 'hide-validation'
  });
});
/* Put your css in here */

.validation {
  display: block;
}
<!DOCTYPE html>
<html ng-app="plunker">

<head>
  <meta charset="utf-8" />
  <title>AngularJS Plunker</title>
  <link data-require="[email protected].*" data-semver="3.1.1" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
  <script>
    document.write('<base href="' + document.location + '" />');
  </script>

  <script data-require="[email protected]" src="https://code.angularjs.org/1.3.12/angular.js" data-semver="1.3.12"></script>
  <script data-require="ui-bootstrap@*" data-semver="0.12.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.min.js"></script>

</head>

<body ng-controller="MainCtrl">
  <br/>
  <br/>{{user.username}}
  <input id="username" name="username" data-ng-model="user.username" type="text" class="form-control" validator="required, backendWatchUsername" placeholder="johndoe" tabindex="1">

</body>

</html>

14
  • @RichardGonzálezAlberto i have pasted the possible options in my answer
    – PSL
    Commented Feb 5, 2015 at 19:52
  • 1
    is it better to define scope attribute ="ngModel" or use required ngModel's controller in link function?
    – lujcon
    Commented Feb 5, 2015 at 19:56
  • @PSL thanks for your response but I already tried all of those, that's why I got to the idea of increasing the priority. If I do #1 or #2 then what I'm trying to accomplish doesn't work. And I can't do #3. Commented Feb 5, 2015 at 19:56
  • @lujcon Yes that is an option too
    – PSL
    Commented Feb 5, 2015 at 19:57
  • @RichardGonzalez What do you mean by you already tried all of those? what is the issue? When you increase the priority and use a terminal option, you need to be aware (as explained in my answer). Why do you need terminal option anyways.
    – PSL
    Commented Feb 5, 2015 at 19:57
0

You should not create a new isolated scope in your directive: this will mess up with the others directives (and in this case will not share ngModel).

return {
    restrict: 'A',
    require: 'ngModel',
    compile: function compile(element, attrs) {
        element.attr('tooltip', '{{validationMessage}');
        element.removeAttr("validator");
        return {
            post: function postLink(scope, element) {
              $compile(element)(scope);
            }
        };
    },
}

I invite you to check the Angular-UI library and especially how they have implemented their ui.validate directive: http://angular-ui.github.io/ui-utils/

4
  • this doesn't fix the major problem I'm having which is that the directive looses functionality once I remove it. But thanks for your response. Commented Feb 5, 2015 at 20:50
  • What do you mean once you remove it?
    – floribon
    Commented Feb 5, 2015 at 21:42
  • when you do this: element.removeAttr("validator"); you are removing the directive so it won't end in an infinite loop. But this also removes the functionality of the directive. Commented Feb 5, 2015 at 21:49
  • Oh wow I didn't notice that! This is a very very bad idea. The compile method is only called once anyway, and if it doesn't you have another problem. At least check is the directive has already been called by adding some data or class or anything to your element, but don't do that
    – floribon
    Commented Feb 5, 2015 at 22:10

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.