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

13,518

Solution 1

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 2

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 3

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

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

Share:
13,518
yankee
Author by

yankee

Updated on June 07, 2022

Comments

  • yankee
    yankee almost 2 years

    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.

  • yankee
    yankee about 9 years
    Yes, I could watch my variables an continuously convert them to a string. But what is actually happening here? Why is that happening?
  • yankee
    yankee about 9 years
    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...
  • Omri Aharon
    Omri Aharon about 9 years
    @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
    Omri Aharon about 9 years
    @yankee You can see update. The thing is your directive can be as flexible as you want. You can parameterize it endlessly.
  • ABOS
    ABOS about 9 years
    The text on "Selected date 2 is:" is simply not showing anything.
  • Omri Aharon
    Omri Aharon about 9 years
    @ABOS Thank you, that's much better. It was hard coded variable that got left in the template. Now it's fixed.
  • ABOS
    ABOS about 9 years
    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.
  • Omri Aharon
    Omri Aharon about 9 years
    @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
    Omri Aharon about 9 years
    @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.
  • yankee
    yankee about 9 years
    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.
  • Dave Alperovich
    Dave Alperovich about 9 years
    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...
  • Omri Aharon
    Omri Aharon about 9 years
    @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.
  • yankee
    yankee about 9 years
    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
    yankee about 9 years
    @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.
  • Dave Alperovich
    Dave Alperovich about 9 years
    @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.
  • yankee
    yankee about 9 years
    @OmriAharon: Yes, your solution will work. But still unsolved are: <label for="myId" /><input my-datepicker myid="myId" /> I cannot tell validity checkers that my <label> is not referencing a not existing id. Additionally I need to adapt my directive to include all possible attributes I need if new ones come up and I add a number of scope watches that do nothing but cost performance. Additionally the use of ng-model and id needs no documentation: Developers know those attributes. myid and model are different. It works, but it is not pretty...
  • Omri Aharon
    Omri Aharon about 9 years
    @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.
  • jme11
    jme11 about 9 years
    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 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.
  • Omri Aharon
    Omri Aharon about 9 years
    @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 plnkr.co/edit/jevzBrCLeLiSNwe6KAk4?p=preview
  • yankee
    yankee about 9 years
    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).
  • jme11
    jme11 about 9 years
    @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).
  • Omri Aharon
    Omri Aharon about 9 years
    @jme11 Thanks, that's what matters :-)
  • jediz
    jediz over 7 years
    The terminal property tells Angular to skip all directives on that element that comes after it stackoverflow.com/questions/15266840/…