1

I have created a complex form element to avoid code duplication. However I can't make it to behave same as normal input field.

HTML

<input name="first" ng-model="ctrl.first" type="text" required is-number />
<complex-input name="second" ng-model="ctrl.second" required></complex-input>

JS/ng

// main input directive
app.directive("complexInput", function(){
  return {
    require: "^?ngModel",
    scope: {
      passedModel: '=ngModel'
    },
    template: "<div><input ng-model='passedModel' is-number type='text' child-element /></div>",
    link: function(scope, elem, attr, modelCtrl) {
      angular.extend(modelCtrl.$validators, scope.childValidators || {});
    }
  }
});

// is number validator
app.directive('isNumber', function(){
  return {
    require: "^?ngModel",
    link: function(scope, elem, attr, modelCtrl) {

      modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
        var value = modelValue || viewValue;
        return !isNaN(value);
      };     
    }
  }
});

// hacky directive to pass back validators from child field
app.directive('childElement', function(){
  return {
    require: "^?ngModel",
    priority: 10,
    link: function(scope, elem, attr, modelCtrl) {
      if (!modelCtrl) return;

      scope.childValidators = modelCtrl.$validators;
    }
  }
});

When I run it content of both fields errors is following.

On init:

First: {"required":true}
Second: {"required":true}

If I enter string:

First: {"isNumber":true}
Second: {**"required":true**,"isNumber":true}

If I enter number:

First: {}
Second: {}

I would expect both input and complex-inputto behave same. Problem is obviously that is-number validation on inner input is blocking model on outer complex-input so it's value is not set, unless you enter number.

What am I doing wrong? Is there a nicer/cleaner way to do this and possibly avoid the ugly childElement directive?

Please find test plnkr here: https://plnkr.co/edit/Flw03Je1O45wpY0wf8om

  • UPDATE: Complex input is not a simple wrapper for input. In reality in can have multiple inputs that together compile a single value.
Miroslav Jonas
  • 5,407
  • 1
  • 27
  • 41

2 Answers2

0

Solution was to add ng-model-options='{ allowInvalid: true }' on inner input. This forces inner input to update model even if it's invalid.

However more nicer solution would be to pass entire model from child elements to parent directive and then iterate through their $validators.

app.directive('childElement', function(){
  return {
    require: "^?ngModel",
    priority: 10,
    link: function(scope, elem, attr, modelCtrl) {
      if (!modelCtrl) return;

      scope.childModels.push(modelCtrl);
    }
  }
});

Complex input here has two inputs that combined give a final value. First one has to be number but it's not required, while the second one is required.

app.directive("complexInput", function(){
  function preLink(scope, elem, attr, modelCtrl) {
      // create local scope value
      scope.inner1 = "";
      scope.inner2 = "";

      scope.childModels = [];
  }

  // do some nice mapping of inner errors
  function mapKeys(index, validator) {
    return validator + "." + index;
  }

  function postLink(scope, elem, attr, modelCtrl) {
      // copy value on change to passedModel
      // could be complex state
      scope.$watch('inner1', function(value){
        scope.passedModel = scope.inner1 + scope.inner2;
      });
      scope.$watch('inner2', function(value){
        scope.passedModel = scope.inner1 + scope.inner2;
      });

      scope.childModels.forEach(function(childModel, index){
        childModel.$viewChangeListeners.push(function(){

          Object.keys(childModel.$validators).forEach(function(validatorKey){
            modelCtrl.$setValidity(mapKeys(index, validatorKey), !childModel.$error[validatorKey]);
          });
        });
      });
  }

  return {
    require: "^?ngModel",
    scope: {
      passedModel: '=ngModel'
    },
    template: "<div><input ng-model='inner1' is-number type='text' child-element /><br/><input ng-model='inner2' required type='text' child-element /></div>",
    compile: function(element, attributes){  
      return { pre: preLink, post: postLink };
    }
  }
});
Miroslav Jonas
  • 5,407
  • 1
  • 27
  • 41
0

You can solve both problems (the childElement and the correct validation) by letting complex-element be only a wrapper around the real input field.

To do this :

  • The complex-element directive has to use something else than name, for example "input-name"
  • The input in the complex-element directive template need to use that name
  • You need to pass from complex-element to the input field whatever else you need (validations, events etc..)

For example, the following is your code modified and works as you expect :

var app = angular.module("myApp", []);

app.controller("MyCtrl", function($scope){
  var vm = this;

  vm.first = "";
  vm.second = "";
});

app.directive("complexInput", function(){
  return {
    require: "^?ngModel",
    scope: {
      passedModel: '=ngModel',
      name: '@inputName'
    },
    template: "<div><input ng-model='passedModel' name='{{name}}' is-number type='text'/ required></div>",
    link: function(scope, elem, attr, modelCtrl) {
      angular.extend(modelCtrl.$validators, scope.childValidators || {});
    }
  }
});

app.directive('isNumber', function(){
  return {
    require: "^?ngModel",
    link: function(scope, elem, attr, modelCtrl) {

      modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
        var value = modelValue || viewValue;
        return !isNaN(value);
      };     
    }
  }
});

HTML

<p>COMPLEX:<br/><complex-input required input-name="second" ng-model="ctrl.second"></complex-input></p>

See the plunker here : https://plnkr.co/edit/R8rJr53Cdo2kWAA7zwJA

Simone Gianni
  • 11,426
  • 40
  • 49
  • Thanks, but unfortunately this doesn't really work as instead of one input I might have two inputs inside the directive that together compose the new value – Miroslav Jonas Apr 08 '16 at 10:04