50

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.

Community
  • 1
  • 1
yankee
  • 38,872
  • 15
  • 103
  • 162
  • This has me absolutely stumped! If you open the plunker and put a breakpoint on line 1541 of ui-bootstrap-tpls-0.12.1.js, and then choose a date from the custom directive datepicker, for a split second, the date is correct in the textbox, and then it is overwritten by the toString version when you stop debugging. – TwitchBronBron Apr 13 '15 at 18:16
  • Just wanted to mention that if I use your latest Plunkr ng-model works fine for me in all my browsers: Safari, Chrome, Firefox. The only things I changed were to replace ng-model2 with ng-model and commented out the set-if-not-set bit for ng-model2. You might want to test it again. – jme11 Apr 16 '15 at 00:49
  • @jme11: I tested it again. But at least my Firefox (37.0.1) refuses any input in this field that would make it temporarily invalid. – yankee Apr 16 '15 at 08:59
  • I came across the same date ISO string issue with UI picker, was able to work around this by using a model getter setter to convert the date string to a date object. – Chris Gunawardena Apr 19 '15 at 09:09

8 Answers8

17

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

Omri Aharon
  • 16,959
  • 5
  • 40
  • 58
  • The problem with that solution is that I loose the ability to overwrite the default values I set in my directive. Additionally the `id` field is not set on the `` element, but that would be good so that I can reference it from a ` – yankee Apr 12 '15 at 15:22
  • @yankee Those are all easily solvable problems. All you need to do is just add another attribute and get it on the isolated scope. I'll update in a sec to show how to do it for the `id`. – Omri Aharon Apr 12 '15 at 15:26
  • @yankee You can see update. The thing is your directive can be as flexible as you want. You can parameterize it endlessly. – Omri Aharon Apr 12 '15 at 15:29
  • The text on "Selected date 2 is:" is simply not showing anything. – ABOS Apr 12 '15 at 16:02
  • @ABOS Thank you, that's much better. It was hard coded variable that got left in the template. Now it's fixed. – Omri Aharon Apr 12 '15 at 16:12
  • No problem. One more point is your approach is a standard approach from angular ui datepicker, while OP is asking about wrapper of an existing directive. Even your plunker works, it still does not answer OP's original question. IMHO, a deep understanding of what happened in this case would be more interesting. – ABOS Apr 12 '15 at 16:22
  • @ABOS I agree it would be interesting to know what caused the bug, but it still doesn't change the fact that a better directive structure can be used, as in my opinion it's better architecture. And still answers to the "and how is it done correctly?" part of the OP's question. – Omri Aharon Apr 12 '15 at 16:28
  • @yankee Just read a bit about the `gettSetter` option you wrote as I'm not familiar with Angular 1.4 and that option. I'm sure it can be accommodated the same way using a parameter, and/or in worst case - a directive that will wrap this one. – Omri Aharon Apr 12 '15 at 16:32
  • You added the id as `myid` attribute. That leads to problems: What if I don't specify an id at all? That would make `myid` and empty string, right? And if I have two of those this is a violation to the uniqueness of the ids. I guess I could solve this by watching the myid attribute and setting it to a random value if not set by the outside world. And I need to do this with all attributes for which I want to have overridable default values?! Another (less critical) problem is that usually my IDE warns me if a label refers to an id that does not exist, but IDE does not know about myid. – yankee Apr 12 '15 at 20:18
  • 1
    What Omri has done is wrap the original DatePicker Directive in his own Directive/Template, but the work horse is still the bootstrap-UI-DatePicker. I'm not sure what the advantage is, but in Omri's defense, @yankee, it's unclear what you want from your directive that he original does not provide... – Dave Alperovich Apr 12 '15 at 21:17
  • @yankee You don't have to specify the ID, you can emit that when you use the directive. What will happen is that the input will have an empty id attribute and that's it, there's no penalty of anything. If you even want to remove that, you can attach the `id` attribute in the link function if the passed `myid` parameter has a value, and otherwise do nothing and leave the input without `id` at all. – Omri Aharon Apr 12 '15 at 22:29
  • @DaveAlperovich: What I want from my directive that the original does not provide? 1. A number of default values, 2. automatic adding of a button that opens the popup. Code that I'd otherwise would need to copy&paste around everywhere, violating the DRY principle. – yankee Apr 15 '15 at 16:56
  • @yankee, I would fork the original repo and modify the original Directive / Controller for what you want. It's less trouble than trying to make the current one behave the way you like. – Dave Alperovich Apr 15 '15 at 17:01
  • @OmriAharon: Yes, your solution will work. But still unsolved are: `` I cannot tell validity checkers that my ` – yankee Apr 15 '15 at 17:03
  • @yankee My solution for the `label` will be a `dynamicLabel` directive that creates a `label` HTML element after a 0 `$timeout`, that is supposed to work. I've had a bit more than a year of pure Angular experience and I've tutored many devs on my project I've created from scratch, and their intuition is good enough to know `model` is `ng-model` and `myId` will be the `id`. Believe in them :-) I agree about the extra attributes, but it will give you a robust directive with flexibility, you don't have to pass them all if you don't need them. – Omri Aharon Apr 15 '15 at 17:40
  • Following this thread, I would argue that the label element should be added to the directive. This eliminates the possibility that the id doesn't match the for attribute in the label and ensures accessibility. If you don't always want to display the label, I have updated my Plunkr http://plnkr.co/edit/NvdwXkPIBvLVzDadjEo0?p=preview to demonstrate how to use the Boostrap .sr-only class to hide the label when desired but still have the label readable by screen-readers. Further, I added a default value for the label itself, so that it doesn't have to be added to the markup. – jme11 Apr 15 '15 at 23:40
  • @jme11 Only one thing, the datepicker HTML directive declaration is (by accident?) using `id` attribute instead myid which causes an issue of uniqueness, and @yankee - you'd probably want the label to open the popup I assume ? So I shifted a bit to the `id` on the input and `{{id}}_button` on the button itself http://plnkr.co/edit/jevzBrCLeLiSNwe6KAk4?p=preview – Omri Aharon Apr 16 '15 at 06:48
  • 1
    @OmriAharon thanks for pointing out the id. I had intentionally changed it to id instead of myId to address some of the other concerns mentioned previously, but forgot to add element.removeAttr('id') from the link function. I updated it now. I do think your approach is clearest (and would ultimately be best for maintainability) even though it doesn't appear to be the best fit for the OP. Given the nature of this site though, it's sure to be an inspiration for others (as it clearly already has been based on the number of votes you've gotten). – jme11 Apr 17 '15 at 02:44
  • @jme11 Thanks, that's what matters :-) – Omri Aharon Apr 17 '15 at 07:31
9

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.

Sander_P
  • 1,787
  • 1
  • 13
  • 37
  • 1
    Brilliant! `terminal` was exactly what I was missing :-). (Well I do need to do some more testing, but adapted my [plunkr here](http://plnkr.co/edit/BHpJSswAE8GbRoWeTlqC) and it seems to work fine). – yankee Apr 16 '15 at 10:33
  • The terminal property tells Angular to skip all directives on that element that comes after it http://stackoverflow.com/questions/15266840/how-to-understand-the-terminal-of-directive – jediz Dec 16 '16 at 15:02
4

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.

jme11
  • 17,134
  • 2
  • 38
  • 48
2

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

allenhwkim
  • 27,270
  • 18
  • 89
  • 122
  • Your plunkr does not work. I can select a date using the datepicker alright, but I cannot use textual input. Firefox will just refuse to accept any inputs in the date2 field and chrome immediatly converts and display a toString() date representation as soon as I start typing a date. – yankee Apr 15 '15 at 16:52
0

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.

gonkan
  • 259
  • 1
  • 7
0

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.

Aditya Singh
  • 15,810
  • 15
  • 45
  • 67
0

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>
Adam Plocher
  • 13,994
  • 6
  • 46
  • 79
-1

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.

ABOS
  • 3,723
  • 3
  • 17
  • 23
  • Yes, I could watch my variables an continuously convert them to a string. But what is actually happening here? Why is that happening? – yankee Apr 10 '15 at 18:00