Take the 2-minute tour ×
Stack Overflow is a question and answer site for professional and enthusiast programmers. It's 100% free, no registration required.

I'm fairly new to Angular JS and I'm trying to create a datepicker directive. It will not work like the Angular UI Bootstrap datepicker as it won't use a textbox but a full page calendar which you will be able to swipe through and click a day (which will update ngModel).

My plan was to have a single directive which has a renderMonth() function. This function would accept a month as a parameter and generate all the rows/days in an array which would then be bound to a template for the month.

My problem is that I can specify a template for the datepicker in my directive declaration but I don't know how to specify templates for the rows/days etc and load them in and bind them. I could do it using jQuery and lots of string concetenation, but that seems all wrong.

I have been reading the source code for the Angular UI Datepicker but as a newbie it makes very little sense to me. They have decoupled everything into many sub-directives (month directive, year directive etc.) and they have their own templates, but that's not what I want to do because my Angular skills aren't going to stretch to creating directives which communicate with each other. The Angular UI code is just way too complicated for me.

One thing I do like is that they use existing directives like ng-repeat in their templates and bind them to the array or rows/days. For me to do that I'd need to load the template in each time the renderMonth() is called and compile the template because it has existing directives in it.

So, basically my question is, does anybody have any examples of how I could write a import a template into my render function, compile it and ten bind it to the row/day data which is in my directives scope.

I'll be honest. I have no idea if I'm even speaking any sense. I'm just typing words which sound vaguely right.

ALl I need is for somebody to point me in the right direction. Thanks.

share|improve this question
    
Can you explain why you need a template for each month? I think my approach would instead to be to have a single template that is smart enough to put the days of the months into the right column. This could be done with a little bit of logic at the start of the directive's link function. If you don't use that approach, and you don't want sub-directives, you'll have to start looking at using $compile, which is more complicated (imo). –  Ed Hinchliffe Aug 9 '14 at 16:30
    
Sorry, I didn't mean that the template would be different for each month. Just one template for the datepicker (containing title, next/previous buttons) and a single template to bind to the currently visible month (e.g. right now it would render August on load and september when you click the next button. Just one template for the month though). –  jonhobbs Aug 9 '14 at 16:37
    
I don't think you will need to re-compile the template everytime the renderMonth() is called. Just change models in a scope, and that will reflect on the UI automatically. –  runTarm Aug 9 '14 at 16:40
    
OK, so I need to load the sub-template (monthTemplate) once when the directive is initalized, compile it, then use it each time a month is rendered. I think I'm clear on the plan but don't know how to do that :) –  jonhobbs Aug 9 '14 at 16:42
    
If you want to keep it as simple as possible, then just use one template, which has the month and controls in it. Let me see if I can mock something up and put it in an answer. –  Ed Hinchliffe Aug 9 '14 at 16:54

1 Answer 1

up vote 4 down vote accepted
+200

As discussed in the comments, I personally wouldn't worry too much about importing templates, the $compile function, or anything that complex for what is relatively simple markup.

I've created a (very) basic barebones plunker here, to demonstrate what my approach would be. I've used moment.js to deal with dates, because I am terrible with vanilla javascript date manipulation and find it very clumsy.

This is the template:

<div class="controls">
  <button ng-click="prevMonth()"><-</button>
  <span>{{selected.format('MMMM')}}</span>
  <button ng-click="nextMonth()">-></button>
</div>
<div class="month">
  <span class="day" ng-repeat="day in selected.days">
    {{day.number}}
  </span>
</div>

The template relies on the scope having a selected property which is a moment object. All it does is creates some buttons change the month and displays the month name at the top, then creates a span for each day in that month.

A tiny bit of CSS puts the days in rows of 7:

span.day{
  float: left;
  width: 25px;
}
span.day:nth-child(7n+1){
  clear:left;
}

This is the directive's link function:

link: function(scope, element, attributes){
  // Set the selection to now initially.
  scope.selected = moment();

  generateDaysArray = function(){
    // -- REMOVED FOR BREVITY -- //
    return days;
  }
  // Watch the month for changes and update the days array
  scope.$watch(
    function(){
      return scope.selected.month();
    },
    function(newVal, oldVal){
      scope.selected.days = generateDaysArray();
    }
  )
  // Control button actions
  scope.nextMonth = function(){
    scope.selected.add('month', 1)
  }
  scope.prevMonth = function(){
    scope.selected.subtract('month', 1)
  }
}

There is really nothing special there, but do note that the $watch command used takes advantage of the fact that you can pass a function instead of a string (though looking at it now you could just use 'selected.month()' as a string).

Generating the days array is slightly more complex if you want to align the dates with days of the week, which I started to do, but didn't finish. My plan was for each object in the array to have a isPreviousMonth property which would conditionally apply a different style using ng-class, and would look up the correct number as well, rather than just using a ~ for the offset days!

There is a lot more to do to finish this off - not least making the dates selectable and integrating with ngModel, but hopefully this example has given you an idea of one way to approach the problem of templating. There are many other ways you could do this, but I find this the most intuitive.

Extension

To address your question regarding having multiple months, I've expanded the plunker a bit.

I added an attribute to the directive:

<date-picker num-months="3"></date-picker>

And modified the template to ng-repeat certain parts depending on the number of months you specify in the attribute:

<div class="controls">
  <button ng-click="prevMonth()"><-</button>
  <span ng-repeat="month in months" class="month-title">
    {{month.moment.format('MMMM')}}
  </span>
  <button ng-click="nextMonth()">-></button>
</div>
<div class="month" ng-repeat="month in months">
  <span class="day" ng-repeat="day in month.days">
    {{day.number}}
  </span>
</div>

So this instead requires an array of months, each with 2 properties: a moment, and an array of days. I added some quick and dirty javascript to generate that, but I won't paste it here as there are almost certainly much neater ways of doing it!

Note that at present we're still watching scope.selected for changes in the month, which would lead to the (probably unwanted) behaviour that if you selected a date not in the first month, the calendar would rearrange itself. I think instead of using a $watch in this case, I'd just add to the click events to handle redrawing the calendar properly.

PS This addresses the cloning part but not the scrolling part. For scrolling, I would recommend adding one extra month on either end of the months array, then having an ng-show condition that means only the middle month(s) is shown. You can then take advantage of angular's inbuilt animations (this is a good reference) to scroll them in/out using css. If you want more info about that contact me through the chat I opened for this question.

share|improve this answer
1  
First of all, I want to say that this is one of the most comprehensive answers I've seen on SO so I'm going to throw you a couple of hundred points just as soon as I can add a bounty. I already have the ngModel part working and can easily do the day array generation as there's no Angular involved there. The one thing I'm still confused about is how I would clone the template to create the next month off screen so I can scroll it on from the right hand side. Presumably if I just jgLite .clone() it then it won't be 'Angularized'? –  jonhobbs Aug 10 '14 at 12:14
    
Thanks for the comments and bounty! See my extension to the post for cloning - and ignore the messy javascript! –  Ed Hinchliffe Aug 10 '14 at 16:48
    
This is a quick (and again hacky with the css) example of what I am getting at. It only animates one way, you'd have to modify the class on the element depending on which button was clicked to make it slide in different directions. –  Ed Hinchliffe Aug 10 '14 at 17:20

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.