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.

4
  • 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.CommentedApr 13, 2015 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
    CommentedApr 16, 2015 at 0: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
    CommentedApr 16, 2015 at 8: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.CommentedApr 19, 2015 at 9:09

8 Answers 8

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

19
  • 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 <input> element, but that would be good so that I can reference it from a <label>. And if I e.g. want to use a getter/setter as model (using ng-model-options="{ getterSetter: true }") but do not want to do so globally that's going to be complicated...
    – yankee
    CommentedApr 12, 2015 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.CommentedApr 12, 2015 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.CommentedApr 12, 2015 at 15:29
  • 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...CommentedApr 12, 2015 at 21:17
  • 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
    CommentedApr 17, 2015 at 2:44
9
+300

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.

2
  • 1
    Brilliant! terminal was exactly what I was missing :-). (Well I do need to do some more testing, but adapted my plunkr here and it seems to work fine).
    – yankee
    CommentedApr 16, 2015 at 10:33
  • The terminal property tells Angular to skip all directives on that element that comes after it stackoverflow.com/questions/15266840/…
    – jediz
    CommentedDec 16, 2016 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.

    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

    1
    • 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
      CommentedApr 15, 2015 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.

      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.

        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> 
          -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.

          1
          • Yes, I could watch my variables an continuously convert them to a string. But what is actually happening here? Why is that happening?
            – yankee
            CommentedApr 10, 2015 at 18:00

          Start asking to get answers

          Find the answer to your question by asking.

          Ask question

          Explore related questions

          See similar questions with these tags.