Angular Date Picker

Custom Angular date picker that's easy to customize and integrate into any project.

I created this Angular date picker to have as a standalone component. A feature to easily add into projects as needed. I arrived at this decision after taking unnecessary time to configure everything that Material needs. I’ve done this several times—for a date picker. Much of what the library supplies can be developed as custom features. Custom features vs needing to conform to the library’s demands. The TypeScript file below has a number of moving parts. Once we get past the first function however, it’s not as complex.

Git Repo

How it Works

  • calendarVisible is the variable that determines if the component is open or closed. Whether it’s in the DOM, or removed.
  • d is the date object.
  • Manually declare weekdays, months, and years.
  • currentDay, monthIndex, and year are standard date methods.
  • firstDay and lastDay holds the first and last day of each month respectively.
  • monthsMenu determines visibility of the months when the current month is clicked.
  • yearsMenu the same as monthsMenu.
  • In the calculateStartEndDate fn, we loop over 42 indices. With seven days in a week multiplied by six potential weeks in a month, there’s 42 possible days. One month may begin on a Saturday, end on Sunday—spanning six weeks. If the first day of the month is greater than one of the 42, add empty cells. Then push those values to the array.
  • The firstLastDays fn gets the first and last day of the month respectively, then passes them into the previous fn.
  • selectDay(), selectYear(), and selectMonth() are similar.
  • showMonths() toggle the months menu while closing the years menu.
  • checkForFutureDate() flags to the user, “Hey, you’re selecting a future date”.
  • This is clearly the brains of the Angular date picker. Moderately complex, yet easier than maintaining Material in my opinion.
import { Component, OnInit } from '@angular/core';

@Component({
  selector: '[app-calendar]',
  templateUrl: './date-picker.component.html',
  styleUrls: ['./date-picker.component.scss'],
})
export class DatepickerComponent implements OnInit {
  calendarVisible: boolean = false;
  d: any = new Date();
  weekdays: readonly string[] = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
  months: readonly string[] = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  years: readonly number[] = [2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014,
                  2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023];
  
  currentDay: any = this.d.getDate();
  monthIndex: any = this.d.getMonth();
  year: any = this.d.getFullYear();
  firstDay: any;
  lastDay: any;
  monthsMenu: boolean = false;
  yearsMenu: boolean = false;
  daySpan: any[] = [];
  selectedDate: number;
  currentDate: number;

  ngOnInit() { this.firstLastDays(); }

  // Calculate First/ Last Days of the Month
  calculateStartEndDate(firstDayOfMonth: number, lastDayOfMonth: number) {
    this.daySpan = [];
    let dayIndex = 1,
        emptyCells = 0;
    
    for (let i = 0; i < 42; i++) {
      if (firstDayOfMonth > i) emptyCells++;
      this.daySpan.push(
        {value: i >= firstDayOfMonth && i < emptyCells + lastDayOfMonth ? dayIndex++ : null}
      );
    }
  }

  // Determine First/Last Days
  firstLastDays() {
    this.firstDay = new Date(this.year, this.monthIndex, 1);
    this.lastDay = new Date(this.year, this.monthIndex + 1, 0);
    this.calculateStartEndDate(this.firstDay.getDay(), this.lastDay.getDate());
  }

  // Select Year
  selectYear(i: number) {
    this.year = i;
    this.firstLastDays();
    this.checkForFutureDate(this.year, this.monthIndex, this.currentDay);
  }

  // Select Month
  selectMonth(i: number) {
    this.monthIndex = i;
    this.firstLastDays();
    this.checkForFutureDate(this.year, this.monthIndex + 1, this.currentDay);
  }

  // Select Day
  selectDay(i: number) {
    this.currentDay = i;
    this.currentDay = this.currentDay.value;
    this.checkForFutureDate(this.year, this.monthIndex, this.currentDay);
  }

  openCalendar() {
    this.calendarVisible = true;
  }

  closeCalendar() {
    this.calendarVisible = false;
  }

  showMonths() {
    this.monthsMenu = !this.monthsMenu;
    this.yearsMenu = false;
  }

  showYears() {
    this.yearsMenu = !this.yearsMenu;
    this.monthsMenu = false;
  }

  checkForFutureDate(y: number, m: number, d: number) {
    this.selectedDate = new Date(y, m, d).getTime();
    this.currentDate = new Date().getTime();
    this.currentDate > this.selectedDate ? '' : console.log('...future date');
  }
}

Angular Date Picker Markup

The only semi unrecognizable part of the input should be the value. Which simply lays out the date, already retrieved. monthsMenu is visible only if it’s open, then loops over the months array. As does yearsMenu. weekdays are constant. showMonths() opens the months menu. selectDay() at the end passes the day as an argument. In the TypeScript, the incoming day is then set to currentDay. checkForFutureDate() fires which creates a new date.

<div id="calendar-component">
  <input
    type="text" placeholder="Select a date..."
    (focus)="openCalendar()"
    [ngClass]="{ 'future-date': selectedDate > currentDate }"
    value="{{this.months[this.monthIndex] + ' ' + this.currentDay + ', ' + this.year}}"
  />
  <div *ngIf="calendarVisible" class="calendar">
    <nav>
      <div class="close-calendar" (click)="closeCalendar()">
        <span>✕</span>
      </div>
      <div class="month" (click)="showMonths()">
        <span>{{ months[monthIndex] }}</span>
        <ul *ngIf="monthsMenu">
          <li
            *ngFor="let month of months;
            let i = index"
            (click)="selectMonth(i)">{{ month }}</li>
        </ul>
      </div>
      <div class="year" (click)="showYears()">
        <span>{{ year }}</span>
        <ul *ngIf="yearsMenu">
          <li
            *ngFor="let year of years;
            let i = index"
            (click)="selectYear(year)">{{ year }}</li>
        </ul>
      </div>
    </nav>
    <div class="weekdays">
      <ul>
        <li
          *ngFor="let day of weekdays">{{ day }}</li>
      </ul>
    </div>
    <div class="all-days-skeleton">
      <div
        *ngFor="let day of daySpan;
        let i = index" (click)="selectDay(day)"
        [ngClass]="{'has-value': day.value != null,'current-date': day.value === currentDay}">
          <div class="day-value">{{ day.value }}</div>
      </div>
    </div>
  </div>
</div>

Create the Styling

Styling is what it is. Nothing overly complicated here if you know CSS. The nesting aspect is a feature of SCSS and allows for wonderful organization of styling blocks. Only caveat is updating said styles in media queries requires the same ordering of nested elements. If > div looks foreign to you, it’s simply referencing all divs that are first descendants. Grandchildren are not captured.

Wrapping Up Angular Date Picker

If you get stuck, check out the live demo. I really hope these explanations helped you understand this component. My objective here, as always is to give something back to the developer community. Development is tough. And yes, there’s some great learning resources available in every medium. Other times we find code that’s either outdated, not compatible with our environment, or simply doesn’t work. My goal here is provide solutions and features that are regularly updated and improved. Hope I’ve succeeded in providing a valuable Angular date picker!

Be the first to comment

Leave a Reply

Your email address will not be published.


*