How to create shared date picker component in Angular 4

Posted on by

Choosing the date is a common task in the web forms. There is a default input element with the type date but, unfortunately, it’s neither supported by all the browsers nor very handy to use.

In the first version of the Flowhaven, we were using the external date picker module to solve the issue. Recently, we migrated to the Angular4, and I have been looking for a date picker component for the new version.

I was able to find some date picker components, but most of them weren’t what I was looking. There were either overcomplicated or not customizable enough.

Eventually, I came up with an idea to build own reusable component for our shared module so that we could use it across the whole application

I also decided to write this tutorial to teach you how to build a simple, shareable date picker component for Angular4 applications.

Setup

As in my previous posts, I will be using Ng CLI for generating project files. I will also assume that we already have built a boilerplate project, so we have our main app module and app component.

One of the best practices for large-scale Angular applications is to divide its parts logically into separate, independent modules, as well as to have one shared module which will contain all the shared components, directives, filters, and services.

At Flowhaven, we are following this rule, and in this tutorial, we will start with creating the shared module, empty date picker component and making it available for the rest of the app.

Run ng g module shared in the console to generate a new module.

Then we need to execute ng g component shared/components/datepicker.

Ng CLI will automatically import newly created class to the SharedModule, as well as add it to its declarations. One thing that we have to perform by ourselves is to add this component to the module’s exports.


import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DatepickerComponent } from './components/datepicker/datepicker.component';

@NgModule({
  imports: [
    CommonModule,
    FormsModule
  ],
  exports: [
    DatepickerComponent
  ],
  declarations: [
    DatepickerComponent
  ]
})
export class SharedModule { }

Now every other module which has SharedModule imported will have access to the exported component.

There is only one manual task left, import module to the root app. To do so, we need to open an AppModule file and include SharedModule to the imports.


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { SharedModule } from './shared/shared.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    BrowserModule,
    SharedModule
  ],
  declarations: [
    AppComponent
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule { }

Ok, we are ready with a basic setup and can start working on the date picker itself.

Building the skeleton

First of all, let’s take a look on the mockup to have a better understanding of how our component should look.

Angular 4 date picker component mockup

It is just a text input field with a floating label. Once a user clicks on it or navigates via tab, the calendar appears.

The user can navigate between the months and choose the desired date.

Clicking outside will close the calendar as well as selecting a day.

We also need to notify the parent component and send the result to it when the user selects a date.

I have already prepared CSS styles for it, and here is the HTML template skeleton which we are going to use to understand the structure better.


<div>
  <input type="text" />
  <label>{{ label }}</label>
  <div>
    <div>
      <i class="icon-fh-left"></i>
    </div>
    <div>
      <span>{{ months[month] }}</span>
      <span>{{ year }}</span>
    </div>
    <div>
      <i class="icon-fh-right"></i>
    </div>
  </div>
  <div>
    <span>Sun</span>
    <span>Mon</span>
    <span>Tue</span>
    <span>Wed</span>
    <span>Thu</span>
    <span>Fri</span>
    <span>Sat</span>
  </div>
  <div>
    <span *ngFor="let day of days">{{ day }}</span>
  </div>
</div>

I didn’t include any CSS classes and styles in the example because the primary goal is to focus on the functional part.

Show / Hide Calendar

Let’s start with importing all the stuff we might need. We need Input decorator for the dynamic label and pre-selected value. Also, import the Output decorator and EventEmitter class from the core library. We would need it for communication with the parent component.

Inside the component, we are going to use the following values: “date”, “month”, “months”, “year”, “days”, “result”, and “showCalendar.”


import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

export class DatepickerComponent implements OnInit {
  date: Date = new Date();
  months = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
    'August', 'September', 'October', 'November', 'December'];
  month: number;
  year:  number;
  days:  number[];
  result: string;
  showCalendar: boolean = false;
}

ShowCalendar is self-explanatory, it will define whenever we are showing or hiding the calendar.

We are going to create two functions for opening and closing the calendar.

Create a following method “onShowCalendar” which accepts an event as a parameter. Notice that I’m using stop propagation. We will return to it a little bit later.


onShowCalendar(e: Event) {
  e.stopPropagation();
  this.showCalendar = true;
}

Now we need to update the template and attach this method to click and focus events of our input element.


<input type="text"
  (click)="onShowCalendar($event)"
  (focus)="onShowCalendar($event)" />

Let’s deal with the closing. Create the “onCloseCalendar” method. It should only close the calendar if it is open.


onCloseCalendar(e: Event) {
  if (this.showCalendar) {
    this.showCalendar = false;
    this.update.emit(this.result);
  }
  return;
}

But where should we use it?

Each component decorator in Angular has an optional host property.

Specifies which DOM events a directive listens to via a set of (event) to method key-value pairs.

This way we can monitor global document events and execute our function once the user clicks anywhere on the document.


@Component({
  selector: 'fh-datepicker',
  templateUrl: './datepicker.component.html',
  host: {
    '(document:click)': 'onCloseCalendar($event)'
  }
})

That is why we used stop propagation to prevent this behavior when he clicks on the input field. We will also use it to navigate between months.

Change Month

We have all the data to start working on the further functionality.

Here comes the most exciting part, generation of day elements in the calendar and navigation between months.

I will use one method for both tasks. To simplify the array generation, I will use RamdaJS library so that I will have access to its range function.

The “updateMonth” function will accept two optional parameters: event and type.

We will run stop propagation method to prevent calendar from closing.

Then based on type, we will either decrease or increase the month value. After that, if month value is less than zero, we will reduce the year value and set month to December. If month value is bigger than eleven, we will set it to January and increase the year value.

After that, we can generate a date, count the number of days in the current month and the starting day of the week.

Since we always start from the Sunday, we need to add empty days as a prefix. Based on that, we can calculate the overall number of days including the blank values.

For instance, let’s say the starting day has index three meaning that the first day of the month is on Wednesday and the month has 30 days in total. This way the result will be 3 + 30 = 33 days with the following values: [undefined, undefined, undefined, 1, 2, 3 … 30].


updateMonth(e?: Event, type?: string) {
  if (e) e.stopPropagation();
  if (type === 'dec') this.month--;
  if (type === 'inc') this.month++;
  if (this.month < 0) {
    this.month = 11;
    this.year--;
  }
  if (this.month > 11) {
    this.month = 0;
    this.year++;
  }
  const date = new Date(this.year, this.month, 0);
  const days = date.getDate();
  const day  = date.getDay();
  const prefix = new Array(day);

  this.days = prefix.concat(range(1, days));
}

We also need to use ngOnInit lifecycle hook to set the initial values for date, month and year, as well as execute the updateMonth method for the first time to generate days array.

The date is the current date by default, but we can give an opportunity for a user to use predefined value.


@Input()
value:  string;

ngOnInit() {
  // Set date if default value is present
  if (this.value) this.date = new Date(this.value);
  this.month = this.date.getMonth();
  this.year  = this.date.getFullYear();
  this.updateMonth();
}

Now let’s update the template to render the days and attach a updateMonth function to the corresponding buttons.


<div>
  <i class="icon-fh-left"
    (click)="updateMonth($event, 'dec')"></i>
</div>
<div>
  <span>{{ months[month] }}</span>
  <span>{{ year }}</span>
</div>
<div>
  <i class="icon-fh-right"
    (click)="updateMonth($event, 'inc')"></i>
</div>

Select Day

Well done! We have almost finished our date picker component.

The only thing left is the actual selection of the day. Once the user selects the day, we need to generate a result out of that and send it to the parent component for processing.

Create a function called selectDay with the day as a parameter. We shouldn’t do anything if the value is empty because we might have a bunch of undefined values in our days array.

Also, we need to convert numbers, so one “1” become “01” and so on. I will create a simple pad function right here.

Once the day is selected, we need to update the date value, generate the result string and emit the update and send the result as an event object.


@Output()
update: EventEmitter = new EventEmitter();

selectDay(day: number) {
  if (!day) return;
  const pad = s => s.length < 2 ? 0 + s : s;
  this.date = new Date(this.year, this.month, day);
  this.result = `${ this.year }-${ pad(this.month + 1 + '') }-${ pad(day + '') }`;
  this.update.emit(this.result);
}

We also need to update the ngOnInit method and run a selectDay function there if the pre-defined value is present.


ngOnInit() {
  // Set date if default value is present
  if (this.value) this.date = new Date(this.value);
  this.month = this.date.getMonth();
  this.year  = this.date.getFullYear();
  // Select day if default value is present
  if (this.value) this.selectDay(this.date.getDate());
  this.updateMonth();
}

Conclusion

Congrats! You've just created a simple reusable date picker component. Now it is accessible from anywhere in your Angular4 application, and you can use it as so:


<fh-datepicker [value]="sample.date" label="Estimated Date"
  (update)="onDateUpdate($event)"><fh-datepicker>

Here you can find the final version of our code and try the datepicker in action.

Angular4 provides cool instruments to deal with such tasks. In this tutorial, I was trying to give a basic understanding of how to create reusable components, application structure, and and use such things as HostListener.

Remember that the best way to learn the language/framework is to get your hands dirty and try to build things by yourself from scratch.

Hope you have found this tutorial somewhat useful! Happy coding!