简体   繁体   中英

Angular - Requiring ngModel and using it in controller of custom directive, not link function

Can anybody tell me if it's possible to require and use ngModel inside the controller of a custom Angular directive. I'm trying to stay away from the link function. I see most examples use the link function but I'm thinking there must be some way to use it inside a directive controller? Or is it only accessible in a link function? The one way I have seen to do it, as show below, gives me undefined. I'm not sure if there is another way?? I'm trying to validate the component and have the invalid class be set on the error object.

//directive
angular.module('myApp', [])
  .directive('validator', function (){
    return {
      restrict: 'E',
      require: {
           ngModelCtrl: 'ngModel',
           formCtrl: '?^form'
      },
      replace: true,
      templateUrl: 'view.html',
      scope: {},
      controllerAs: 'ctrl',
      bindToController: {
         rows: '=',
         onSelected: '&?' //passsed selected row outside component
         typedText: '&?' //text typed into input passed outside so developer can create a custom filter, overriding the auto
         textFiltered: '@?' //text return from the custom filter
         ngRequired: "=?" //default false, when set to true the component needs to validate that something was selected on blur. The selection is not put into the input element all the time so it can't validate based on whether or not something is in the input element itself. I need to validate inside the controller where I can see if 'this.ngModel' (selectedRow - not passed through scope) is undefined or not.
      },
      controller: ["$scope", "$element", function ($scope, $element){
         var ctrl = this;
         ctrl.rowWasSelected;

         //called when a user clicks the dropdown to select an item
          ctrl.rowSelected = function (row){
               ctrl.rowWasSelected = true;
               ctrl.searchText = row.name; //place the name property of the dropdown data into ng-model in the input element
          }

         ctrl.$onInit = $onInit;
         function $onInit (){
             ctrl.ngModelCtrl.$validators.invalidInput = validate;            
          }

        function validate (modelValue, viewValue) {
             var inputField = ctrl.formCtrl.name;
             var ddField = ctrl.formCtrl.listData;

             inputField.$setValidity('invalidInput', ddField.$touched && ctrl.rowWasSelected);

            return true;
          }          
       }];
   }
});

//template
<form name="validatorForm" novalidate>
  <div class="form-group" ng-class="{ng-invalid:validatorForm.name.$error.invalid}">
     <label for="name">Names</label>
     <input type="name" class="form-control" name="name" placeholder="Your name" ng-change="typedText(text)" ng-model="ctrl.textFiltered" ng-blur="ctrl.validate()" ng-required="ctrl.ngRequired">
  </div>
  <ul ng-show="show list as toggled on and off" name="listData" required>
    <li ng-repeat="row in ctrl.rows" ng-click="ctrl.rowSelected({selected: row}) filterBy:'ctrl.textFiltered' ng-class="{'active':row === ctrl.ngModel}">{{row}}<li>
  </ul>
</form>

//html
<validator
   rows="[{name:'tim', city:'town', state:'state', zip: 34343}]"
   on-selected="ctrl.doSomethingWithSelectedRow(selected)"
   typed-text="ctrl.manualFilter(text)"
   text-filtered="ctrl.textReturnedFromManualFilter"
   ng-required="true">
</validator>

Here is the code refactored a bit (Note: you need to be using the latest Angular for some of this). After rereading your question I am not sure what exactly you are having trouble with (whether it is how to use required in the directive definition object or how to use ngRequired attribute or something else). Note that with the code below you do not need $scope:

angular.module('myApp', []);
angular.module('myApp').directive('validator', validator);

function validator (){
    return {
        restrict: 'E',
        require: {
            ngModelCtrl: 'ngModel'
        },
        replace: true,
        templateUrl: 'view.html',
        scope: {}, //this controls the kind of scope. Only use {} if you want an isolated scope.
        controllerAs: 'ctrl',
        bindToController: {
            rows: '=',
            onSelected: '&?', //passsed selected row outside component
            typedText: '&?', //text typed into input passed outside so developer can create a custom filter, overriding the auto
            textFiltered: '@?', //text return from the custom filter
            ngRequired: "=?" //default false, when set to true the component needs to validate that something was selected on blur. The selection is not put into the input element all the time so it can't validate based on whether or not something is in the input element itself. I need to validate inside the controller where I can see if 'this.ngModel' (selectedRow - not passed through scope) is undefined or not.
        },
        controller: 'validatorController'
    }
}

//usually do this in a new file

angular.module('myApp').controller('validatorController', validatorController);
validatorController.$inject = ['$element'];

function validatorController($element){
    var ctrl = this;

    //controller methods
    ctrl.validate = validate;

    ctrl.$onInit = $onInit; //angular will execute this after all conrollers have been initialized, only safe to use bound values (through bindToController) in the $onInit function.

    function $onInit() {
        if(ctrl.ngRequired)
            ctrl.ngModelCtrl.$validators.myCustomRequiredValidator = validate;
    }



    //don't worry about setting the invalid class etc. Angular will do that for you if one if the functions on its $validators object fails
    function validate (modelValue, viewValue){
        //validate the input element, if invalid add the class ng-invalid to the .form-group in the template
        //return true or false depending on if row was selected from dropdown
        return rowWasSelected !== undefined
    }
}   

Here are a couple of snippets from Angular's docs on $compile:

If the require property is an object and bindToController is truthy, then the required controllers are bound to the controller using the keys of the require property. This binding occurs after all the controllers have been constructed but before $onInit is called.

and

Deprecation warning: although bindings for non-ES6 class controllers are currently bound to this before the controller constructor is called, this use is now deprecated. Please place initialization code that relies upon bindings inside a $onInit method on the controller, instead.

Again, make sure you are using the latest version of Angular or the above won't work. I can't remember exactly which part (I feel like it might be getting the require object keys auto-bound to the controller object), but I have definitely run into a nasty bug where the above wasn't working and I was using 1.4.6.

Second Edit: Just want to clear up a few things:

1) the .ng-invalid class will be applied to any input in an angular validated form that is invalid. For example, if there is a required attribute on an input and the input is empty, then the input will have an ng-invalid class. Additionally, it will have a class .ng-invalid-required. Every validation rule on the input gets its own ng-invalid class. You say you want to add a red border to an input after it has been blurred for the first time. The standard way to do this is to have a css rule like this:

.ng-invalid.ng-touched {
   border: 1px #f00 solid;
}

If you inspect a validated input you will see all kinds of angular classes. One of them is .ng-touched. A touched element is one that has been blurred at least once. If you wanted to ensure that validation is only applied on blur you could use ng-model-options directive.

2) $formatters are used to format a model value. Angular has two way data binding. That means that angular is $watching a model value and view value. If one of them changes angular executes a workflow to update the other one. The workflows are as follows:

view value changes -> $parsers -> $validators -> update model value model value changes -> $formatters -> update view value

The result of the work flow is populated into the other value. This means that if you want to change model value before showing it in the view (maybe you want to format a date) then you could do it in the $formatter. Then, you could do the opposite operation in a $parser as it travels back to the model. Of course, you should be cognizant of what is happening in the $parsers when you write your $validators because it is the parsed view value that gets validated before getting sent to the model.

3) Per the quote I added from the angular docs, it is clear that you should not use any logic that contains a value that has been bound to the controller by bindToController outside of $onInit. This includes ngModelCtrl. Note that you could place the logic in another function as long as you are sure that the other function will execute AFTER $onInit.

4) There are two things to consider here: Which control is having the error show up and where are you triggering the validation from. It sounds like you want to trigger it from the dropdown's workflow (ie after it has been blurred once). So, I suggest adding a validator to the dropdown. Now, you say you want to validate the input and not the dropdown. So, you can use $setValidity inside the validator. To ensure that the dropdown is always "valid" you can just return true from the validator. You say you want to only validate after blur. There are two ways to do that (off the top of my head). One is to use the ng-model-options that I mentioned above, the other is to test if the dropdown has been $touched in the validator. Here is some code using the second method:

function validate (modelValue, viewValue) {
    var inputField = ctrl.formCtrl.inputName, ddField = ctrl.formCtrl.ddName;

    inputField.$setValidity('validationName', ddField.$touched && rowSelectedCondition);
    return true;
}

You see, I am testing to see if the dropdown has been $touched (ie blurred) before I set the validity. There is a fundemental difference between these two approaches. Using ng-model-options basically defers the whole update workflow until blur. This means your model value will only get updated to match the view value after the input has been blurred. The second way (with $touched) will validate every time the viewValue changes but will only render the input invalid after the first blur.

The 'validationName' argument will just specify the class that is added if the input is invalid so in this case it will add two classes .ng-invalid (added to any invalid control) and .ng-invalid-validation-name.

In order to get access to the formCtrl you need to add another property to your require object (formCtrl: '^form')

The easiest way to get access to the information provided by ngModel in a custom directive is to set the scope to false. This should happen by default, but if you are working with multiple directive it can be helpful to set it expressly. This way, the directive will inherit the controller and controller alias as if it were completely native to the rest of the view.

The directive:

.directive('myValidator', function (){
return {
  restrict: 'E',
  replace: true,
  templateUrl: 'view.html',
  scope: false
  };
}

You don't have to change the template very much. Just make sure the ng-model="ctrl.name" is binding to something on your main controller, or whatever controller you are using for the rest of the view. You can move the validation function to the main controller, too. Or, to a service and inject into the controller, etc.

Using compile or link in a custom directive can make it much more versatile. But you are basically passing values for the directives, attributes, or html tags. ngModel is available, but you may not be using ctrl.user every time you use the custom directive. Compile or link let you set the value of ngModel each time you use the directive.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM