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.
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.
#calendar-component {
position: relative;
max-width: 250px;
margin: 0 auto;
input {
font: normal 15px sans-serif;
border: 1px solid #313b3f;
outline: none;
padding: 8px;
border-radius: 5px;
width: 100%;
color: #313b3f;
box-sizing: border-box;
}
.calendar {
box-shadow: 2px 5px 6px 2px rgba(0, 0, 0, 0.2);
width: 250px;
padding-bottom: 15px;
border: 1px solid #313b3f;
background-color: white;
position: absolute;
right: 0;
top: 40px;
border-radius: 4px;
z-index: 1;
.all-days-skeleton {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
justify-content: center;
font: normal 14px sans-serif;
> div {
width: 31px;
height: 25px;
display: flex;
border: 1px solid transparent;
cursor: pointer;
.day-value {margin: auto;}
}
}
}
nav {
font: normal 15px sans-serif;
display: grid;
background-color: #313b3f;
color: white;
padding: 5px 0 6px 0;
grid-template-columns: repeat(2, 30px);
grid-column-gap: 5px;
position: relative;
justify-content: center;
.close-calendar {
background-color: white;
width: 20px;
height: 20px;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 5px;
color: black;
border-radius: 4px;
display: flex;
cursor: pointer;
span {margin: auto;}
}
.search {
position: absolute;
background-color: white;
border-radius: 3px;
padding: 4px 4px 4px 4px;
cursor: pointer;
right: 20px;
top: 3px;
color: #313b3f;
text-transform: uppercase;
font: bold 11px sans-serif;
}
.month, .year {
ul {
display: grid;
grid-template-columns: repeat(3, 33.3%);
grid-row-gap: 2px;
grid-column-gap: 1px;
justify-content: center;
align-items: center;
position: absolute;
background-color: #457b9d;
padding: 5px;
top: 28px;
left: 0;
list-style-type: none;
width: 240px;
text-align: center;
z-index: 1;
overflow-y: auto;
height: 200px;
li {
cursor: pointer;
transition: background-color 0.25s;
padding: 8px;
box-sizing: border-box;
border-radius: 4px;
&:hover {
background-color: #03658c;
}
}
}
}
.month, .year {cursor: pointer;}
}
.weekdays {
margin-top: 10px;
ul {
display: grid;
grid-template-columns: repeat(7, auto);
justify-content: center;
padding: 0;
grid-column-gap: 13px;
transform: translateX(-2px);
li {
list-style-type: none;
font: bold 14px sans-serif;
padding: 0 5px;
}
}
}
}
.has-value:hover, .current-date {
background-color: #03658c;
border-radius: 4px;
color: white;
transform: scale(0.9);
}
.future-date {
background-color: rgb(204, 51, 0);
color: white !important;
}
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!
+ There are no comments
Add yours