DEV Community

loading...
Cover image for Angular: How to build a full screen calendar like Outlook

Angular: How to build a full screen calendar like Outlook

rickystam profile image Ricky Stam Updated on ・4 min read

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 ❤️

Discussion (13)

pic
Editor guide
Collapse
gencatcree profile image
Genji

Good job!

Collapse
rickystam profile image
Ricky Stam Author

Thank you :)

Collapse
donhmorris profile image
Don Morris

Looks good man

Collapse
rickystam profile image
Ricky Stam Author

Thank you Don, glad you like it!

Collapse
santoshyadav198613 profile image
Santosh Yadav

This is amazing 🎉🎉

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 Author

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

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 Author • 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
thisdotmedia_staff profile image
This Dot Media

You did great Ricky!

Collapse
rickystam profile image
Ricky Stam Author

Many thanks :)

Collapse
spock123 profile image