7

I am getting a date time value from asp.net mvc controller as "2014-08-31T00:00:00Z". When I bind this value to my angular-ui datepicker control it's state is showing as ng-invalid ng-invalid-date.

I am getting the date-format as well from the mvc controller so I am binding the date-format as well in my html.

When I am debugging the ui-bootstrap-tpls.js (latest version) file at line 1807

enter image description here

It's always coming as undefined. I have tried so many alternatives but I am unable to succeed. :(

javascript does't convert angular ui datepicker date to UTC correctly

So please give some thoughts and suggest me how can I solve this problem.

Thanks & Regards, N.Murali Krishna.

Community
  • 1
  • 1
Murali Krishna
  • 133
  • 1
  • 1
  • 6
  • Please can you share your code at least the Angular one. – michelem Oct 08 '15 at 13:52
  • Please see my answer posted below as the one posted by Chet is very heavy-handed and not the right way to go if you intend on adhering to proper Angular design. – icfantv Oct 08 '15 at 18:34
  • Please don't post screenshots of code. – Joel Coehoorn Oct 08 '15 at 18:41
  • Hi Joel,Since this is ui-bootstrap-tpls.js code and it is open source code so I don't think no body will object if we post open source code. – Murali Krishna Oct 09 '15 at 05:13
  • Joel is right. The correct way would be to link directly to the line of source code like so: https://github.com/angular-ui/bootstrap/blob/master/src/datepicker/datepicker.js#L799. – icfantv Oct 09 '15 at 17:56
  • 1
    Yeah, I wasn't referring to copyright at all. Screenshots are harder to work with for those who want to help you. We need you to post the text of the code, rather than an image. – Joel Coehoorn Nov 20 '15 at 15:01

7 Answers7

11

I had the same problem. The issue is that Angular is expecting an actual date object, not a string representation of the date. After doing a bunch of research I ended up adding a transformReponse to the $httpProvider which checks all string objects to see if they can be converted to a date, and if so actually converting them.

angular.module('test')
.config(['$httpProvider', function ($httpProvider) {

    // ISO 8601 Date Pattern: YYYY-mm-ddThh:MM:ss
    var dateMatchPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;

   var convertDates = function (obj) {
      for (var key in obj) {
         if (!obj.hasOwnProperty(key)) continue;

         var value = obj[key];
         var typeofValue = typeof (value);

         if (typeofValue === 'object') {
            // If it is an object, check within the object for dates.
            convertDates(value);
         } else if (typeofValue === 'string') {
            if (dateMatchPattern.test(value)) {
               obj[key] = new Date(value);
            }
         }
      }
   }

   $httpProvider.defaults.transformResponse.push(function (data) {
      if (typeof (data) === 'object') {
         convertDates(data);
      }

      return data;
   });
}])
Chet
  • 3,461
  • 1
  • 19
  • 24
  • Excellent. Jumping out of joy. I was struggling to fix this issue on my own since 2 days. Your solution solved the problem for me. Now we need to see when Angular-UI is going to fix this problem. For the moment I will use your code. – Murali Krishna Oct 08 '15 at 14:20
  • 2
    This solution is __VERY__ heavy-handed, is not flexible, and is not easily testable. It should not be the right solution for anyone interested in developing a system that follows good Angular coding practices. Please see my answer for more info, including how to rectify. – icfantv Oct 08 '15 at 18:35
6

A couple of notes here:

  1. First, the datepicker directive requires that the ng-model be a Date object. This is documented here.
  2. Second, the solution posted (and accepted) by Chet above is VERY heavy-handed as it takes EVERY date string received in an HTTP response and converts it to a Date object if it matches a hard-coded pattern. This is not flexible nor is it easily testable. It will not be the right solution for most people.

If, in your system, global date string conversion is the right behavior, the proper Angular design would be to create a service that does this for you. This leads me to...

We've (Angular UI Bootstrap) provided a mechanism for converting date strings into Date objects via the dateParser service. You can see the source code here. NB: this service name becomes deprecated and changed to uibDateParser with the 0.14.0 release.

icfantv
  • 4,523
  • 7
  • 36
  • 53
  • Hi icfantv, Can you please let me know is there any possibility for me to convert a model value to Date object directly, if we change any thing in JSONSerailization setting do we get the date as java date object instead of string. If possible can you please provide us a plnkr sample how to use the uibDateParser. – Murali Krishna Oct 09 '15 at 05:38
  • I have done the changes in my code as mentioned by you, then I am getting the following error in console. **"[ngModel:datefmt] Expected `2014-08-31T00:00:00Z` to be a date"**. I have modified the input type from text to date as well in my html code. – Murali Krishna Oct 09 '15 at 05:49
  • I think you may have misunderstood and I apologize if I didn't explain it well enough. The value of the model object referred to by the `ng-model` directive needs to be a JS `Date` object. You don't need to touch the input type. And in fact, some browsers still do not support ``. – icfantv Oct 09 '15 at 17:50
  • Also, to answer your first question - yes, you can use the `dateParser` service as an example for how to do formatted date conversion. I would actually recommend you not using our service directly but rather create your own (using ours as an example) because ours is not documented specifically because it's meant for internal use only. I.e., we could change how it works and it might break your code if you use it directly. – icfantv Oct 09 '15 at 17:54
  • 3
    @icfantv I understand you guys did this design choice to close a lot of bugs (just read the bug reports), but it defeated the transparent two-way data binding with json that was the reason I used the library in the first place. – Marcos Brigante Dec 08 '15 at 16:52
  • @MarcosBrigante, not sure i follow you. what do you mean by transparent, two-way binding with json? – icfantv Dec 08 '15 at 17:15
  • We exchange a lot of dates with backend services using strings, and we edit those dates directly on $resource objects properties using the datepicker. No need for parsing, no need for defining additional models on my scope as dates and watching them for changes. No custom directives. The binding was bidirectional, so when displaying, editing, or postbacking the changes to the server everything just worked. The need for defining date typed models just put the burden of those transformations on client code. @chet solution tries to bring that transparency back, but it is too heavy as you noticed. – Marcos Brigante Dec 08 '15 at 17:42
  • 1
    Gotcha. Unfortunately, I don't have a better solution for you here other than the one I already suggested. We have decided to expose the $dateParser service (i.e., document it and support it) for use by devs. The only other thing I can think of is wrapping our `uib-datepicker` directive with a custom one and passing in the date string, leaving it to the directive to convert to the `Date` object necessary for the `uib-datepicker` directive. This is probably a better (and significantly faster) solution than an HTTP interceptor. HTH. – icfantv Dec 08 '15 at 18:08
  • @icfantv Could you point to an example code where uibDateParser is used? I'm trying the following and it fails silently: $scope.dateOfBirth = $uibDateParser.parse('05/12/2004', 'dd/MM/yyyy') – saravana_pc Apr 21 '17 at 13:46
  • @saravana_pc, looks like the docs are good, but the demo is incomplete. That service is simply an angular service so you can debug it like any other service. what have you tried so far? i should also point out that we are no longer actively maintaining that project as all the maintainers have moved to angular 2/4. – icfantv Apr 22 '17 at 15:43
6

This is best solution i found so far:

.directive('uibDatepickerPopup', function (dateFilter, uibDateParser, uibDatepickerPopupConfig) {
    return {
        restrict: 'A',
        priority: 1,
        require: 'ngModel',
        link: function (scope, element, attr, ngModel) {
            var dateFormat = attr.uibDatepickerPopup || uibDatepickerPopupConfig.datepickerPopup;
            ngModel.$validators.date = function(modelValue, viewValue) {
                var value = viewValue || modelValue;

                if (!attr.ngRequired && !value) {
                    return true;
                }

                if (angular.isNumber(value)) {
                    value = new Date(value);
                }
                if (!value) {
                    return true;
                } else if (angular.isDate(value) && !isNaN(value)) {
                    return true;
                } else if (angular.isString(value)) {
                    var date = uibDateParser.parse(value, dateFormat);
                    return !isNaN(date);
                } else {
                    return false;
                }
            };
        }
    };
});
2

I don't have enough reputation to comment under Chet's answer but thanks so much this solution worked for me as well! I don't know your Github handle to tag you in the issue but I submitted a Github issue under Angular UI to help others find the solution and hopefully get Angular UI guys to look into it. Thanks again!

https://github.com/angular-ui/bootstrap/issues/4554

  • This is not a bug in Angular UI Bootstrap as the `datepicker` directive is functioning as documented and designed. It requires that the `ng-model` be a `Date` object. Please see my answer in this SO article for the proper way to solve your issue. – icfantv Oct 08 '15 at 18:37
2

To use formatted date string instead of Date object as ng-model for Angular Datepicker, you need to create a wrapper directive. The wrapper directive will parse your string into Date object and pass it to the datepicker. When you select a date, it is converted from Date back into string. Here is an example (Plunker):

(function () {
    'use strict';

    angular
        .module('myExample', ['ngAnimate', 'ngSanitize', 'ui.bootstrap'])
        .controller('MyController', MyController)
        .directive('myDatepicker', myDatepickerDirective);

    MyController.$inject = ['$scope'];

    function MyController ($scope) {
      $scope.dateFormat = 'dd MMMM yyyy';
      $scope.myDate = '30 Jun 2017';
    }

    myDatepickerDirective.$inject = ['uibDateParser', '$filter'];

    function myDatepickerDirective (uibDateParser, $filter) {
        return {
            restrict: 'E',
            scope: {
                name: '@',
                dateFormat: '@',
                ngModel: '='
            },
            required: 'ngModel',
            link: function (scope) {

                var isString = angular.isString(scope.ngModel) && scope.dateFormat;

                if (isString) {
                    scope.internalModel = uibDateParser.parse(scope.ngModel, scope.dateFormat);
                } else {
                    scope.internalModel = scope.ngModel;
                }

                scope.open = function (event) {
                    event.preventDefault();
                    event.stopPropagation();
                    scope.isOpen = true;
                };

                scope.change = function () {
                    if (isString) {
                        scope.ngModel = $filter('date')(scope.internalModel, scope.dateFormat);
                    } else {
                        scope.ngModel = scope.internalModel;
                    }
                };

            },
            template: [
                '<div class="input-group">',
                    '<input type="text" readonly="true" style="background:#fff" name="{{name}}" class="form-control" uib-datepicker-popup="{{dateFormat}}" ng-model="internalModel" is-open="isOpen" ng-click="open($event)" ng-change="change()">',
                    '<span class="input-group-btn">',
                        '<button class="btn btn-default" ng-click="open($event)">&nbsp;<i class="glyphicon glyphicon-calendar"></i>&nbsp;</button>',
                    '</span>',
                '</div>'
            ].join('')
        }
    }

})();
<!DOCTYPE html>
<html>

  <head>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular-animate.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular-sanitize.js"></script>
    <script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-2.5.0.js"></script>
    <script src="example.js"></script>
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  </head>

  <body ng-app="myExample">
    <div ng-controller="MyController">
      <p>
        Date format: {{dateFormat}}
      </p>
      <p>
        Value: {{myDate}}
      </p>
      <p>
        <my-datepicker ng-model="myDate" date-format="{{dateFormat}}"></my-datepicker>
      </p>
    </div>
  </body>

</html>
user147677
  • 471
  • 4
  • 7
1

Just wanted to add my solution to this stack.

app.directive('formatDate', function (dateFilter, uibDateParser, uibDatepickerPopupConfig) {
    return {
        restrict: 'A',
        priority: 1,
        require: 'ngModel',
        link: function (scope, element, attr, ngModel) {
            var dateFormat = attr.uibDatepickerPopup || uibDatepickerPopupConfig.datepickerPopup;
            ngModel.$validators.date = function(modelValue, viewValue) {
                var value = viewValue || modelValue;

                if (!attr.ngRequired && !value) {
                    return true;
                }

                if (angular.isNumber(value)) {
                    value = new Date(value);
                }
                if (!value) {
                    return true;
                } else if (angular.isDate(value) && !isNaN(value)) {
                    return true;
                } else if (angular.isString(value)) {
                    var date = new Date(value);
                    return !isNaN(date);
                } else {
                    return false;
                }
            }
            ngModel.$formatters.push(function(value) {
                return new Date(value);
            });
        }
    };
});
DJ Singh
  • 11
  • 1
  • Just added formatter to @user1839114. It took me sometime to figure out why the value was not showing after adding the validator. – DJ Singh Jul 02 '18 at 17:12
1

I solved the problem by converting string to valid javascript formt in directive:

see here: Datepicker-popup formatting not working when value set initially in scope

Morteza QorbanAlizade
  • 1,520
  • 2
  • 19
  • 35