Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validation error message doesn't appear for custom DatePicker Angular Formly field

I'm trying to use Angular Datetime Picker as a Angular Formly input type. I've got it working such that I can edit and set a value that is correctly added to the binded model.

However, I can't get the validation error messages to display like on the regular input fields.

JS Bin with what I've got so far. As you can see the red color doesn't appear when you exit the field, only when you try to submit. And the error message never shows up.

Formly Config:

formlyConfigProvider.setType({
  name: 'datepicker',
  templateUrl: "custom-template.html",
  overwriteOk: true,
  wrapper: ['bootstrapHasError'],
  defaultOptions: function defaultOptions(options) {
    return {
      templateOptions: {
        validation: {
          show: true
        }
      }
    };
  }
});

formlyConfigProvider.setWrapper({
  name: 'validation',
  types: ['input', 'datepicker'],
  templateUrl: 'error-messages.html'
});

Fields

vm.fields = [
  {
    key: 'text',
    type: 'input',
    templateOptions: {
      label: 'Text',
      placeholder: 'Write something',
      required: true
    },
  },
  {
    key: 'date',
    type: 'datepicker',
    templateOptions: {
      label: 'Date',
      placeholder: 'Pick a date',
      required: true
    },
  }
];

Templates

<script type="text/ng-template" id="custom-template.html">
  <div class="form-group">

      <label class="control-label" for="{{::id}}">{{to.label}} {{to.required ? '*' : ''}}</label>
      <div class="dropdown">
          <a class="dropdown-toggle" id="dropdown-{{options.key}}" role="button" data-toggle="dropdown">
          <div class="input-group">
              <input id="{{::id}}" name="{{::id}}" type="text" data-date-time-input="YYYY-MM-DD" class="form-control" data-ng-model="model[options.key]"><span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
          </div>
        </a>
        <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
          <datetimepicker
              data-ng-model="model[options.key]"
              data-datetimepicker-config="{ dropdownSelector: '#dropdown-' + options.key, minView: 'day', startView: 'year', modelType: 'YYYY-MM-DDTHH:mm:ssZ'}"/>
        </ul>
      </div>
  </div>

</script>

<script type="text/ng-template" id="error-messages.html">
  <formly-transclude></formly-transclude>
  <div ng-messages="fc.$error" ng-if="form.$submitted || options.formControl.$touched" class="error-messages">
    <div ng-message="{{ ::name }}" ng-repeat="(name, message) in ::options.validation.messages" class="message">{{ message(fc.$viewValue, fc.$modelValue, this)}}</div>
  </div>
</script>
like image 241
Oskar Persson Avatar asked Dec 15 '16 13:12

Oskar Persson


1 Answers

After investigating this deeper, I can see the control you used Angular Datetime Picker is not fully compatible with Angular Formly.

This is because of the reason that it's overwriting the AngularJS's ngModelController.$render() method and hence not setting the value for $touched like other input controls.

Another reason in your code is, the config and the template error-messages.html are treating the custom control as single element with fc.$touched, fc.$error and fc.$viewValue whereas DatePicker is rendering as group of elements (array).

To get rid of all these issues, you can have a custom directive to set $touched as below,

app.directive('setTouched', function MainCtrl() {
    return {
      restrict: 'A', // only activate on element attribute
      require: '?ngModel', // get a hold of NgModelController
      link: function(scope, element, attrs, ngModel) {
         if (!ngModel) return; // do nothing if no ng-model
         element.on('blur', function() {
            var modelControllers = scope.$eval(attrs.setTouched);
            if(angular.isArray(modelControllers)) {
              angular.forEach(modelControllers, function(modelCntrl) {
                modelCntrl.$setTouched();
              });
            }            
         });
      }
    };
  });

And in custom-template.html,

<div class="input-group">
   <input set-touched="options.formControl" id="{{::id}}" name="{{::id}}" type="text" data-date-time-input="YYYY-MM-DD" class="form-control" data-ng-model="model['date1']"><span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
</div>

And add fc[0].$touched in below configuration to take care of array of fields,

app.run(function run(formlyConfig, formlyValidationMessages) {
    formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'form.$submitted || fc.$touched || fc[0].$touched';
    formlyValidationMessages.addStringMessage('required', 'This field is required');
});

And also add below section in error-messages.html to take care of array of fields,

<div ng-messages="fc[0].$error" ng-if="form.$submitted || options.formControl[0].$touched" class="error-messages">
    <div ng-message="{{ ::name }}" ng-repeat="(name, message) in ::options.validation.messages" class="message">{{ message(fc[0].$viewValue, fc[0].$modelValue, this)}}</div>
</div>

This changes will fix the issue.

As you can see a bit of design issue with the error message which is displayed further down,

You can change the custom-template.html as below by removing the div wrapper <div class="form-group">,

<script type="text/ng-template" id="custom-template.html">
    <label class="control-label" for="{{::id}}"
           uib-popover="{{options.templateOptions.desc}}"
           popover-trigger="mouseenter"
           popover-placement="top-left"
           popover-popup-delay="500"
           popover-append-to-body="true">{{to.label}} {{to.required ? '*' : ''}}</label>

          <div class="dropdown">
              <a class="dropdown-toggle" id="dropdown-{{options.key}}" role="button" data-toggle="dropdown">
              <div class="input-group">
                  <input set-touched="options.formControl" id="{{::id}}" name="{{::id}}" type="text" data-date-time-input="YYYY-MM-DD" class="form-control" data-ng-model="model['date1']"><span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
              </div>
            </a>
            <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
              <datetimepicker
                  data-ng-model="model[options.key]"
                  data-datetimepicker-config="{ dropdownSelector: '#dropdown-' + options.key, minView: 'day', startView: 'year', modelType: 'YYYY-MM-DDTHH:mm:ssZ'}"/>
            </ul>
          </div>

    </script>

I have updated your JSBin with these changes.

Snippet:

/* global angular */
(function() {
  
  'use strict';

  var app = angular.module('formlyExample', ['formly', 'formlyBootstrap', 'ngAnimate', 'ngMessages', 'ui.bootstrap.datetimepicker', 'ui.dateTimeInput'], function config(formlyConfigProvider) {
    
    formlyConfigProvider.setType({
        name: 'datepicker',
        templateUrl: "custom-template.html",
        overwriteOk: true,
        wrapper: ['bootstrapHasError'],
        defaultOptions: function defaultOptions(options) {
            return {
                templateOptions: {
                    validation: {
                        show: true
                    }
                }
            };
        }
    });
    
    formlyConfigProvider.setWrapper({
      name: 'validation',
      types: ['input', 'datepicker'],
      templateUrl: 'error-messages.html'
    });

  });
  
  
  
  app.run(function run(formlyConfig, formlyValidationMessages) {
    formlyConfig.extras.errorExistsAndShouldBeVisibleExpression = 'form.$submitted || fc.$touched || fc[0].$touched';
    formlyValidationMessages.addStringMessage('required', 'This field is required');
  });
  

  app.directive('setTouched', function MainCtrl() {
    return {
      restrict: 'A', // only activate on element attribute
      require: '?ngModel', // get a hold of NgModelController
      link: function(scope, element, attrs, ngModel) {
         if (!ngModel) return; // do nothing if no ng-model
         element.on('blur', function() {
            var modelControllers = scope.$eval(attrs.setTouched);
            if(angular.isArray(modelControllers)) {
              angular.forEach(modelControllers, function(modelCntrl) {
                modelCntrl.$setTouched();
              });
            }            
         });
      }
    };
  });
  

  app.controller('MainCtrl', function MainCtrl(formlyVersion) {
    var vm = this;
    
    vm.onSubmit = onSubmit;
    vm.model = {};
    vm.options = {};
        vm.env = {
      angularVersion: angular.version.full,
      formlyVersion: formlyVersion
    };
    
    vm.fields = [
      {
        key: 'text',
        type: 'input',
        templateOptions: {
          label: 'Text',
          placeholder: 'Write something',
          required: true
        },
      },
      {
        key: 'moretext',
        type: 'input',
        templateOptions: {
          label: 'More Text',
          placeholder: 'Write something else',
        },
      },
      {
        key: 'date',
        type: 'datepicker',
        templateOptions: {
          label: 'Date',
          placeholder: 'Pick a date',
          required: true
        },
      }
    ];
    

    vm.originalFields = angular.copy(vm.fields);

    // function definition
    function onSubmit() {
      if (vm.form.$valid) {
        vm.options.updateInitialValue();
        alert(JSON.stringify(vm.model), null, 2);
      }
    }
  });

})();
body {
  margin: 20px
}

.formly-field {
  margin-bottom: 30px;
}

.error-messages {
  position: relative;
}

.error-messages, .message {
  opacity: 1;
  transition: .3s linear all;
}

.message {
  font-size: .8em;
  position: absolute;
  width: 100%;
  color: #a94442;
  margin-top: 4px;
}

.error-messages.ng-enter.ng-enter-active,
.message.ng-enter.ng-enter-active {
  opacity: 1;
  top: 0;
}

.error-messages.ng-enter,
.message.ng-enter {
  opacity: 0;
  top: -10px;
}

.error-messages.ng-leave,
.message.ng-leave {
  opacity: 1;
  top: 0;
}

.error-messages.ng-leave-active,
.message.ng-leave-active {
  opacity: 0;
  top: -10px;
}
<!DOCTYPE html>
<html>

  <head>
    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    
    <!-- Twitter bootstrap -->
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.css" rel="stylesheet">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    
    <!-- apiCheck is used by formly to validate its api -->
    <script src="//npmcdn.com/api-check@latest/dist/api-check.js"></script>
    <!-- This is the latest version of angular (at the time this template was created) -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>

    <!-- This is the latest version of formly core. -->
    <script src="//npmcdn.com/angular-formly@latest/dist/formly.js"></script>
    <!-- This is the latest version of formly bootstrap templates -->
    <script src="//npmcdn.com/angular-formly-templates-bootstrap@latest/dist/angular-formly-templates-bootstrap.js"></script>

        <script src="https://rawgit.com/angular/bower-angular-messages/v1.4.4/angular-messages.js"></script>
    <script src="https://rawgit.com/angular/bower-angular-animate/v1.4.4/angular-animate.js"></script>
    
    <!-- Moment -->
    <script src="https://cdn.rawgit.com/moment/moment/develop/min/moment-with-locales.min.js"></script>
    
    <!-- Datetime picker -->
    <script type="text/javascript" src="https://cdn.rawgit.com/dalelotts/angular-bootstrap-datetimepicker/master/src/js/datetimepicker.js"></script>
    <script type="text/javascript" src="https://cdn.rawgit.com/dalelotts/angular-bootstrap-datetimepicker/master/src/js/datetimepicker.templates.js"></script>
    <link href="https://cdn.rawgit.com/dalelotts/angular-bootstrap-datetimepicker/master/src/css/datetimepicker.css" rel="stylesheet">
    <script type="text/javascript" src="https://cdn.rawgit.com/dalelotts/angular-date-time-input/master/src/dateTimeInput.js"></script>
    
   
    <title>Angular Formly Example</title>
  </head>

  <body ng-app="formlyExample" ng-controller="MainCtrl as vm">
    <div>
      <form ng-submit="vm.onSubmit()" name="vm.form" novalidate>
        <formly-form model="vm.model" fields="vm.fields" options="vm.options" form="vm.form">
          <button type="submit" class="btn btn-primary submit-button">Submit</button>
          <button type="button" class="btn btn-default" ng-click="vm.options.resetModel()">Reset</button>
        </formly-form>
      </form>
      <hr />
      <h2>Model</h2>
      <pre>{{vm.model | json}}</pre>
      <h2>Fields <small>(note, functions are not shown)</small></h2>
      <pre>{{vm.originalFields | json}}</pre>
      <h2>Form</h2>
      <pre>{{vm.form | json}}</pre>
    </div>

    <!-- Put custom templates here -->
    <script type="text/ng-template" id="custom-template.html">
    <label class="control-label" for="{{::id}}"
           uib-popover="{{options.templateOptions.desc}}"
           popover-trigger="mouseenter"
           popover-placement="top-left"
           popover-popup-delay="500"
           popover-append-to-body="true">{{to.label}} {{to.required ? '*' : ''}}</label>

          <div class="dropdown">
              <a class="dropdown-toggle" id="dropdown-{{options.key}}" role="button" data-toggle="dropdown">
              <div class="input-group">
                  <input set-touched="options.formControl" id="{{::id}}" name="{{::id}}" type="text" data-date-time-input="YYYY-MM-DD" class="form-control" data-ng-model="model['date1']"><span class="input-group-addon"><i class="glyphicon glyphicon-calendar"></i></span>
              </div>
            </a>
            <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
              <datetimepicker
                  data-ng-model="model[options.key]"
                  data-datetimepicker-config="{ dropdownSelector: '#dropdown-' + options.key, minView: 'day', startView: 'year', modelType: 'YYYY-MM-DDTHH:mm:ssZ'}"/>
            </ul>
          </div>
    </script>
    
    <script type="text/ng-template" id="error-messages.html">
      <formly-transclude></formly-transclude>
      <div ng-messages="fc.$error" ng-if="form.$submitted || options.formControl.$touched" class="error-messages">
        <div ng-message="{{ ::name }}" ng-repeat="(name, message) in ::options.validation.messages" class="message">{{ message(fc.$viewValue, fc.$modelValue, this)}}</div>
      </div>
      <div ng-messages="fc[0].$error" ng-if="form.$submitted || options.formControl[0].$touched" class="error-messages">
        <div ng-message="{{ ::name }}" ng-repeat="(name, message) in ::options.validation.messages" class="message">{{ message(fc[0].$viewValue, fc[0].$modelValue, this)}}</div>
      </div>
    </script>

  </body>

</html>
like image 92
Aruna Avatar answered Oct 17 '22 01:10

Aruna