How to create an angularJs wrapper directive for a ui-bootstrap datepicker?

JavascriptJqueryAngularjsAngularjs DirectiveAngular Ui-Bootstrap

Javascript Problem Overview


I am using the ui.bootstrap.datepicker directive to display some date field. However most of the time I need the same setup: I want it to come along with a popup and a popup button and also I want German names for the texts. That does create the same code for the button and the texts and the formatting over and over again, so I wrote my own directive to prevent myself from repeating myself.

Here is a plunkr with my directive. However I seem to be doing it wrong. If you choose a date with the date picker using the "Date 1" datepicker that does not use my directive everything works fine. I'd expect the same for Date 2, but instead of displaying the date according to the template I supplied in the input field (or any other value I expected) it displays the .toString() representation of the date object (e.g. Fri Apr 03 2015 00:00:00 GMT+0200 (CEST)).

Here is my directive:

angular.module('ui.bootstrap.demo').directive('myDatepicker', function($compile) {
  var controllerName = 'dateEditCtrl';
  return {
      restrict: 'A',
      require: '?ngModel',
      scope: true,
      link: function(scope, element) {
          var wrapper = angular.element(
              '<div class="input-group">' +
                '<span class="input-group-btn">' +
                  '<button type="button" class="btn btn-default" ng-click="' + controllerName + '.openPopup($event)"><i class="glyphicon glyphicon-calendar"></i></button>' +
                '</span>' +
              '</div>');

          function setAttributeIfNotExists(name, value) {
              var oldValue = element.attr(name);
              if (!angular.isDefined(oldValue) || oldValue === false) {
                  element.attr(name, value);
              }
          }
          setAttributeIfNotExists('type', 'text');
          setAttributeIfNotExists('is-open', controllerName + '.popupOpen');
          setAttributeIfNotExists('datepicker-popup', 'dd.MM.yyyy');
          setAttributeIfNotExists('close-text', 'Schließen');
          setAttributeIfNotExists('clear-text', 'Löschen');
          setAttributeIfNotExists('current-text', 'Heute');
          element.addClass('form-control');
          element.removeAttr('my-datepicker');

          element.after(wrapper);
          wrapper.prepend(element);
          $compile(wrapper)(scope);

          scope.$on('$destroy', function () {
              wrapper.after(element);
              wrapper.remove();
          });
      },
      controller: function() {
          this.popupOpen = false;
          this.openPopup = function($event) {
              $event.preventDefault();
              $event.stopPropagation();
              this.popupOpen = true;
          };
      },
      controllerAs: controllerName
  };
});

And that's how I use it:

<input my-datepicker="" type="text" ng-model="container.two" id="myDP" />

(Concept was inspired from this answer)

I am using angular 1.3 (the plunker is on 1.2 because I just forked the plunker from the angular-ui-bootstrap datepicker documentation). I hope this does not make any difference.

Why is the text output in my input wrong and how is it done correctly?

Update

In the meantime I made a little progress. After reading more about the details about compile and link, in this plunkr I use the compile function rather than the link function to do my DOM manipulation. I am still a little confused by this excerpt from the docs:

> Note: The template instance and the link instance may be different objects if the template has been cloned. For this reason it is not safe to do anything other than DOM transformations that apply to all cloned DOM nodes within the compile function. Specifically, DOM listener registration should be done in a linking function rather than in a compile function.

Especially I wonder what is meant with "that apply to all cloned DOM nodes". I originally thought this means "that apply to all clones of the DOM template" but that does not seem to be the case.

Anyhow: My new compile version works fine in chromium. In Firefox I need to first select a date using a date picker and after that everything works fine (the problem with Firefox solved itself if I change undefined to null (plunkr) in the date parser of the date picker). So this isn't the latest thing either. And additionally I use ng-model2 instead of ng-model which I rename during compile. If I do not do this everything is still broken. Still no idea why.

Javascript Solutions


Solution 1 - Javascript

To be honest, I'm not quite sure why it's caused and what's causing your date to be "toString-ed" before showing it in the input.

However, I did find places to restructure your directive, and remove much unnecessary code, such as $compile service, attributes changes, scope inheritance, require in the directive, etc.. I used isolated scope, since I don't think every directive usage should know the parent scope as this might cause vicious bugs going forward. This is my changed directive:

angular.module('ui.bootstrap.demo').directive('myDatepicker', function() {
  return {
      restrict: 'A',
      scope: {
          model: "=",
          format: "@",
          options: "=datepickerOptions",
          myid: "@"
      },
      templateUrl: 'datepicker-template.html',
      link: function(scope, element) {
          scope.popupOpen = false;
          scope.openPopup = function($event) {
              $event.preventDefault();
              $event.stopPropagation();
              scope.popupOpen = true;
          };
          
          scope.open = function($event) {
            $event.preventDefault();
            $event.stopPropagation();
            scope.opened = true;
          };

      }
  };
});

And your HTML usage becomes:

<div my-datepicker model="container.two" 
                   datepicker-options="dateOptions" 
                   format="{{format}}"  
                   myid="myDP">
</div>

Edit: Added the id as a parameter to the directive. Plunker has been updated.

Plunker

Solution 2 - Javascript

Your directive will work when you add these 2 lines to your directive definition:

return {
    priority: 1,
    terminal: true,
    ...
 }

This has to do with the order in which directives are executed.

So in your code

<input my-datepicker="" type="text" ng-model="container.two" id="myDP" />

There are two directives: ngModel and myDatepicker. With priority you can make your own directive execute before ngModel does.

Solution 3 - Javascript

I think the answer from @omri-aharon is the best, but I'd like to point out some improvements that haven't been mentioned here:

##Updated Plunkr

You can use the config to uniformly set your options such as the format and text options as follows:

angular.module('ui.bootstrap.demo', ['ui.bootstrap'])
.config(function (datepickerConfig, datepickerPopupConfig) {
  datepickerConfig.formatYear='yy';
  datepickerConfig.startingDay = 1;
  datepickerConfig.showWeeks = false;
  datepickerPopupConfig.datepickerPopup = "shortDate";
  datepickerPopupConfig.currentText = "Heute";
  datepickerPopupConfig.clearText = "Löschen";
  datepickerPopupConfig.closeText = "Schließen";
});

I find this to be clearer and easier to update. This also allows you to vastly simplify the directive, template and markup.

###Custom Directive

angular.module('ui.bootstrap.demo').directive('myDatepicker', function() {
  return {
      restrict: 'E',
      scope: {
          model: "=",
          myid: "@"
      },
      templateUrl: 'datepicker-template.html',
      link: function(scope, element) {
          scope.popupOpen = false;
          scope.openPopup = function($event) {
              $event.preventDefault();
              $event.stopPropagation();
              scope.popupOpen = true;
          };
          
          scope.open = function($event) {
            $event.preventDefault();
            $event.stopPropagation();
            scope.opened = true;
          };

      }
  };
});

###Template

<div class="row">
    <div class="col-md-6">
        <p class="input-group">
          <input type="text" class="form-control" id="{{myid}}" datepicker-popup ng-model="model" is-open="opened" ng-required="true"  />
          <span class="input-group-btn">
            <button type="button" class="btn btn-default" ng-click="open($event)"><i class="glyphicon glyphicon-calendar"></i></button>
          </span>
        </p>
    </div>
</div> 

###How to Use It

<my-datepicker model="some.model" myid="someid"></my-datepicker>

Further, if you want to enforce the use of a German locale formatting, you can add angular-locale_de.js. This ensures uniformity in the use of date constants like 'shortDate' and forces the use of German month and day names.

Solution 4 - Javascript

Here is my monkey patch of your plunker,

http://plnkr.co/edit/9Up2QeHTpPvey6jd4ntJ?p=preview

Basically what I did was to change your model, which is a date, to return formatted string using a directive

.directive('dateFormat', function (dateFilter) {
  return {
    require:'^ngModel',
    restrict:'A',
    link:function (scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function (viewValue) {
        viewValue.toString = function() {
          return dateFilter(this, attrs.dateFormat);
        };
        return viewValue;
      });
    }
  };
});

You need to pass date-format attribute for your input tag.

If I were you, I would not go that far to make a complex directive. I would simply add a <datepicker> appended to your input tag with the same ng-model, and control show/hide with a button. You may experiment your option starting from my plunker

Solution 5 - Javascript

If creating the directive is a convenience to add the attributes you can have the 2 directives on the original input:

<input my-datepicker="" datepicker-popup="{{ format }}" type="text" ng-model="container.two" id="myDP" />

Then avoid the multiple isolate scopes by changing scope: true to scope: false in the myDatepicker directive.

This works and I think it's preferable to creating a further directive to change the date input to the desired format:

http://plnkr.co/edit/23QJ0tjPy4zN16Sa7svB?p=preview

Why you adding the attribute from within the directive causes this issue I have no idea, it's almost like you have 2 date-pickers on the same input, one with your format and one with default that get's applied after.

Solution 6 - Javascript

Use moment.js with ui-bootstrap datepicker component to create the directive to provide a comprehensive set of patterns for date time formats. You can accept any time format within the isolated scope.

Solution 7 - Javascript

If anyone is interested in a Typescript implementation (loosely based on @jme11's code):

Directive:

'use strict';

export class DatePickerDirective implements angular.IDirective {
    restrict = 'E';
    scope={
        model: "=",
        myid: "@"
    };
    template = require('../../templates/datepicker.tpl.html');

    link = function (scope, element) {
        scope.altInputFormats = ['M!/d!/yyyy', 'yyyy-M!-d!'];
        scope.popupOpen = false;
        scope.openPopup = function ($event) {
            $event.preventDefault();
            $event.stopPropagation();
            scope.popupOpen = true;
        };

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

    public static Factory() : angular.IDirectiveFactory {
        return () => new DatePickerDirective();
    }
}

angular.module('...').directive('datepicker', DatePickerDirective.Factory())

Template:

<p class="input-group">
    <input type="text" class="form-control" id="{{myid}}"
           uib-datepicker-popup="MM/dd/yyyy" model-view-value="true"
           ng-model="model" ng-model-options="{ getterSetter: true, updateOn: 'blur' }"
           close-text="Close" alt-input-formats="altInputFormats"
           is-open="opened" ng-required="true"/><span class="input-group-btn"><button type="button" class="btn btn-default" ng-click="open($event)"><i
        class="glyphicon glyphicon-calendar"></i></button>
          </span>
</p>

Usage:

<datepicker model="vm.FinishDate" myid="txtFinishDate"></datepicker>

Solution 8 - Javascript

I have tried to make this work (somewhat hack), which might not be exactly what you want, just some rough ideas. So you still need to tweak it a little bit. The plunker is:

`http://plnkr.co/edit/aNiL2wFz4S0WPti3w1VG?p=preview'

Basically, I changed the directive scope, and also add watch for scope var container.two.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionyankeeView Question on Stackoverflow
Solution 1 - JavascriptOmri AharonView Answer on Stackoverflow
Solution 2 - JavascriptSander_PView Answer on Stackoverflow
Solution 3 - Javascriptjme11View Answer on Stackoverflow
Solution 4 - JavascriptallenhwkimView Answer on Stackoverflow
Solution 5 - JavascriptgonkanView Answer on Stackoverflow
Solution 6 - JavascriptAditya SinghView Answer on Stackoverflow
Solution 7 - JavascriptAdam PlocherView Answer on Stackoverflow
Solution 8 - JavascriptABOSView Answer on Stackoverflow