DEV Community

Cover image for Angular: How to build a full screen calendar like Outlook
Ricky Stam
Ricky Stam

Posted on • Updated on

Angular: How to build a full screen calendar like Outlook

In an Angular project a while back I needed to display a full screen calendar like the one in outlook. So as a good lazy developer I start looking on the web for a NPM package that could do the job.
To my surprise I didn't find anything that could cover my needs 100% so I went on and built one!

This is the end result:

P.S.: Please be kind with me, HTML and CSS are not my strong suit.

Here is the coding story of how I did:

1st let's have our Angular component

This is our starting point an Angular component and an Array that will hold the days that our calendar will display

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
  public calendar: CalendarDay[] = []; 

}

2nd let's see how the CalendarDay class looks like

export class CalendarDay {
  public date: Date;
  public title: string;
  public isPastDate: boolean;
  public isToday: boolean;

  constructor(d: Date) {
    this.date = d;
    this.isPastDate = d.setHours(0, 0, 0, 0) < new Date().setHours(0, 0, 0, 0);
    this.isToday = d.setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0);
  }

}

Let explain the constructor a little bit.

  this.isPastDate = d.setHours(0, 0, 0, 0) < new Date().setHours(0, 0, 0, 0);
  this.isToday = d.setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0);

I set property isPastDate so my calendar knows how to display or disable past dates and isToday property so UI knows how to draw the today's date.

The reason I use .setHours(0,0,0,0) is cause I want to be sure I'm comparing the beginning of the day and hours don't matter.

3rd let's populate our calendar with the needed days

I have comments in my code that explain the logic.

  ngOnInit(): void {
    // here we initialize the calendar
    this.generateCalendarDays();
  }

  private generateCalendarDays(): void {
    // we reset our calendar every time
    this.calendar = [];

    // we set the date 
    let day: Date = new Date();

    // here we find the first day that our calendar will start from
    // it would be the last Monday of the previous month
    let startingDateOfCalendar = this.getStartDateForCalendar(day);

    // dateToAdd is an intermediate variable that will get increased
    // in the following for loop
    let dateToAdd = startingDateOfCalendar;

    // ok since we have our starting date then we get the next 41 days 
    // that we need to add in our calendar array
    // 41 cause our calendar will show 6 weeks and MATH say that
    // 6 weeks * 7 days = 42!!
    for (var i = 0; i < 42; i++) {
      this.calendar.push(new CalendarDay(new Date(dateToAdd)));
      dateToAdd = new Date(dateToAdd.setDate(dateToAdd.getDate() + 1));
    }
  }

  private getStartDateForCalendar(selectedDate: Date){
    // for the day we selected let's get the previous month last day
    let lastDayOfPreviousMonth = new Date(selectedDate.setDate(0));

    // start by setting the starting date of the calendar same as the last day of previous month
    let startingDateOfCalendar: Date = lastDayOfPreviousMonth;

    // but since we actually want to find the last Monday of previous month
    // we will start going back in days intil we encounter our last Monday of previous month
    if (startingDateOfCalendar.getDay() != 1) {
      do {
        startingDateOfCalendar = new Date(startingDateOfCalendar.setDate(startingDateOfCalendar.getDate() - 1));
      } while (startingDateOfCalendar.getDay() != 1);
    }

    return startingDateOfCalendar;
  }

4th let's add some HTML and CSS to actually start displaying our calendar

In the HTML you will see that I'm using a pipe named chunk I'll explain the use of it and the code in a bit

<table class='calendar-table' *ngIf="calendar">
  <thead>
    <tr>
      <th>Monday</th>
      <th>Tuesday</th>
      <th>Wednesday</th>
      <th>Thursday</th>
      <th>Friday</th>
      <th>Saturday</th>
      <th>Sunday</th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let row of calendar | chunk: 7; let i = index">
      <td class="calendar-day" [ngClass]="{'past-date': c.isPastDate, 'today': c.isToday}" *ngFor="let c of row; let j = index">
        <div class="calendar-day-header" [ngClass]="{'blue-date': c.isToday}"><strong>{{c.date.getDate()}}</strong> <strong *ngIf="c.isToday || (i==0 && j==0) || (c.date.getDate() == 1)"> {{monthNames[c.date.getMonth()]}}</strong></div>
      </td>
    </tr>
  </tbody>
</table>
.calendar-table {
  border-collapse: collapse;
  width: 100%;
  max-width: 100%;
  margin-bottom: 1rem;
  border: 1px solid #dee2e6;
  background-color: #fff;
}

.calendar-table thead th {
  vertical-align: bottom;
  border-bottom: 2px solid #dee2e6;
  width: 14.2%;
}

.calendar-table td, .calendar-table th {
  border: 1px solid #dee2e6;
}

.calendar-table td, .calendar-table th {
  padding: .75rem;
  vertical-align: top;
  border-top: 1px solid #dee2e6;
}

.calendar-day {
  height: 12vh;
  max-height: 12vh;
  cursor: pointer;
}

.calendar-items-wrapper {
  margin-left: -10px;
  margin-right: -10px;
  overflow-y: auto;
  max-height: calc(100% - 20px);
}

.calendar-day.past-date {
  background-color: rgb(248, 248, 248);
}

.calendar-day:hover {
  background-color: rgb(248, 248, 248);
}

.blue-date {
  color: rgb(16, 110, 190);
}

5th now it's time to explain and show the code for the chunk pipe

Since our calendar array has 42 elements but we want to show 7 elements in each row the chunk pipe will create an array with 6 arrays inside one array for each week.

@Pipe({
  name: 'chunk'
})
export class ChunkPipe implements PipeTransform {

  transform(calendarDaysArray: any, chunkSize: number): any {
    let calendarDays = [];
    let weekDays = [];

    calendarDaysArray.map((day,index) => {
        weekDays.push(day);
        // here we need to use ++ in front of the variable else index increase 
        //will happen after the evaluation but we need it to happen BEFORE
        if (++index % chunkSize  === 0) {
          calendarDays.push(weekDays);
          weekDays = [];
        }
    });
    return calendarDays;
  }
}

This post was written with love ❀️

Top comments (19)

Collapse
 
bryannativel profile image
bryanNativel

Thanks for your solution

Collapse
 
gencatcree profile image
Genji

Good job!

Collapse
 
rickystam profile image
Ricky Stam

Thank you :)

Collapse
 
herrigor profile image
herrigor

This piece of code here is so cool

weekDays.push(days)
if (++index % chunkSize === 0) {
calendarDays.push(weekDays);
weekDays = [];
}

Collapse
 
rickystam profile image
Ricky Stam

Thank you herrigor, I hope you find it useful :)

Collapse
 
donhmorris profile image
Don Morris

Looks good man

Collapse
 
rickystam profile image
Ricky Stam

Thank you Don, glad you like it!

Collapse
 
santoshyadavdev profile image
Santosh Yadav

This is amazing πŸŽ‰πŸŽ‰

Collapse
 
thisdotmedia_staff profile image
This Dot Media

You did great Ricky!

Collapse
 
rickystam profile image
Ricky Stam

Many thanks :)

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Very nice

Collapse
 
surajkurade profile image
surajKurade

Hi Ricky. It's awesome. Just wanted to know why we use map instead of foreach in pipe. Am new in angular

Collapse
 
rickystam profile image
Ricky Stam • Edited

Hi surajKurade!

For this example it wouldn't make any difference to be honest. Map and ForEach both iterate the array and apply the given function, their difference is that ForEach doesn't return a value just iterates the array and executes the given function on the elements but map will return a new array with the modified array.

Usually people are using map when they want to achieve immutability (they don't want to modify their array but to return a modified copy)

Example:

let nums = [1,2,3];
let result = nums.forEach(n => n*2); // here the result variable will be undefined
let result = nums.map(n => n*2) // here the result variable will be [2, 4, 6]

Hope this helps!

Collapse
 
surajkurade profile image
surajKurade

Thanks Ricky πŸ‘

Collapse
 
kahiko profile image
Kahiko

Thank you for your post it was very helpful. I added a method to determine the exact number of days for the number day in the calendar instead of 42 in generateCalendarDays and thought I would pass it on:
/**

  • Calculates the number of calendar days in a given month. *
  • @param {Date} day - The date used to determine the month.
  • @return {number} The number of calendar days in the specified month. */ private getNumberOfCalendarDays(day: Date): number { const mYear = day.getFullYear(); const mMonth = day.getMonth()+ 1; const mFristDay = new Date(mYear, mMonth, 1).getDay(); const mLastDay = new Date(mYear, mMonth + 1, 0).getDay(); const mDaysFromLastMonth = mFristDay - 1 == -1 ? 6 : mFristDay - 1; const mDaysFromNextMonth = 6 - mLastDay == -1 ? 0 : 6 - mLastDay; const mDaysInMonth = new Date(mYear, mMonth + 1, 0).getDate(); return (mDaysFromLastMonth + mDaysInMonth + mDaysFromNextMonth) + 1; }
Collapse
 
harigovindyadv profile image
hari govind yadav

Great help!! Thanks :-)

Collapse
 
pkhussain profile image
pkhussain

Hi Ricky, Can we create room and meeting booking in your good calendar ?

Collapse
 
rickystam profile image
Ricky Stam

Hi @pkhussain sure you can use it as you like :) Just keep in mind that this is just a POC and might have bugs, I've never tested it thoroughly, so use with causion!

Collapse
 
nadiagizdova profile image
Nadia Gizdova • Edited

Hi there! I'm looking at your implementation but It currently seems like the left and right arrows are skipping/incorrectly setting the months. Do you know why this might be happening?