Thu. Mar 28th, 2024

Sometimes you need to have a calendar for showing information in your web application. After looking around at different implementations and finding none that work. I decided to create my own. This Calendar is not mobile optimized, For that, I created an Agenda that shows a single day at a time, and used my *onSize directive to switch between the two when needed.

So to begin with I create a DTO to represent the data, of the event.

export class EventData {
    id: number;
    title: string;
    desc: string;
    startDate: Date;
    endDate: Date;
    createdBy: string;
    createdAt: Date;
    type: string;
    color: string;
}

Now to use internally for each day, a new DTO to represent the day, and the events that occur on that day.

import {EventData} from "./event-data.dto";

export class EventDay {

    date: number;
    month: number;
    year: number;

    events: EventData[];
}

Now the next thing was to create a component that would show the individual day on the calendar. For that I created the <app-calendar-control-day>. It has one input, eventDay that takes an EventDay from above.

import {Component, Input, OnInit} from '@angular/core';
import {EventDay} from "../../dto/calendar/event-day.dto";
import {EventData} from "../../dto/calendar/event-data.dto";

@Component({
  selector: 'app-calendar-control-day',
  templateUrl: './calendar-day.component.html',
  styleUrls: ['./calendar-day.component.scss']
})
export class CalendarDayComponent implements OnInit {
  set eventDay(value: EventDay) {
    this._eventDay = value;
    this.checkIfToday();
  }

  @Input('eventDay') private _eventDay:EventDay;

  isToday:boolean = false;

  constructor() { }

  ngOnInit(): void {
    this.checkIfToday();
  }

  checkIfToday(){
    let today = new Date();
    if( today.getFullYear() == this._eventDay.year
        && today.getMonth() == this._eventDay.month
        && today.getDate() == this._eventDay.date) {
      this.isToday = true;
    } else {
      this.isToday = false;
    }
  }
}

Here is the HTML for this component.

<table class="day-table">
	<tbody>
	<tr>
		<td>
			<span (click)="viewDayAgenda(_eventDay)" class="apply-pointer" [class.today]="isToday">{{_eventDay.date}}</span>
		</td>
	</tr>
	<tr *ngFor="let event of _eventDay.events" style="background-color:{{event.color}}"  class="apply-pointer " (click)="viewEvent(event)">
		<td class="day-row">
			{{event.startDate | date:'hh:mm a'}} {{event.title}}
		</td>
	</tr>
	</tbody>
</table>

And the SCSS that goes along with it.

.day-table {
  width: 100%;
}

.day-row {
  border: solid 1px;
  border-radius: 5px;
}

.today {
  color:  red;
}

Now that let’s get tot he main component of this. This lays out the creates the EventDay’s data to display.

import {AfterViewInit, Component, EventEmitter, HostListener, Input, OnChanges, OnInit, Output} from '@angular/core';
import {animate, style, transition, trigger} from '@angular/animations';
import {EventData} from "../../dto/calendar/event-data.dto";
import {EventDay} from "../../dto/calendar/event-day.dto";
import {CalendarService} from "../../services/calendar.service";

@Component({
    selector: 'app-calendar-control',
    templateUrl: './calendar.component.html',
    styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent {
    set startDate(value: Date) {
        this._startDate = value;

        this.showYear = this._startDate.getFullYear();
        this.showMonth = this._startDate.getMonth();

        this.lookupEventData();
        this.buildDays();
    }
    set startYear(value: number) {
        this._startYear = value;

        this.years = [];

        for(let i = this._startDate.getFullYear() + 1 ; i > this._startYear; i-- ) {
            this.years.push(i);
        }
    }
    @Input() dataSource: EventData[] = [];
    _startDate: Date = new Date();

    _startYear:number = 1900;

    years:number[] = [];
    showYear: number;
    showMonth: number;
    days:EventDay[] = [];
    events:EventData[] = [];

    constructor(private calendarService:CalendarService) {
        this.showYear = this._startDate.getFullYear();
        this.showMonth = this._startDate.getMonth();
        for(let i = this._startDate.getFullYear() + 1 ; i > this._startYear; i-- ) {
            this.years.push(i);
        }

        this.lookupEventData();
        this.buildDays();
    }

    previousMonth() {
        let lastDayOfPreviousMonth = new Date(this.showYear,this.showMonth,0);
        this.showYear = lastDayOfPreviousMonth.getFullYear();
        this.showMonth = lastDayOfPreviousMonth.getMonth();

        this.lookupEventData();
        this.buildDays();
    }

    nextMonth() {
        let firstDayOfNextMonth = new Date(this.showYear,this.showMonth + 1, 1);
        this.showYear = firstDayOfNextMonth.getFullYear();
        this.showMonth = firstDayOfNextMonth.getMonth();

        this.lookupEventData();

        this.buildDays();
    }

    today() {
        let today = new Date();
        this.showYear = today.getFullYear();
        this.showMonth = today.getMonth();

        this.lookupEventData();

        this.buildDays();
    }

    buildDays() {
        this.days = [];
        let firstDayOfCurrentMonth = new Date(this.showYear,this.showMonth,1);
        let lastDayOfPreviousMonth = new Date(this.showYear,this.showMonth,0);
        let lastDayOfCurrentMonth = new Date(this.showYear,this.showMonth+1,0);
        let firstDayOfNextMonth = new Date(this.showYear,this.showMonth + 1, 1);

        let firstDayPos = firstDayOfCurrentMonth.getDay();
        let lastDayPos = lastDayOfCurrentMonth.getDay();


        if(firstDayPos != 0) {
            let lastMonthDay = lastDayOfPreviousMonth;
            for(let i = firstDayPos - 1; i >= 0; i--) {
                let day = new EventDay();
                day.year = lastMonthDay.getFullYear();
                day.month = lastMonthDay.getMonth();
                day.date = lastMonthDay.getDate();

                day.events = this.getEventsByDay(day.year,day.month,day.date);

                lastMonthDay = new Date(lastMonthDay.getFullYear(),lastMonthDay.getMonth(),lastMonthDay.getDate()-1);
                this.days.unshift(day);
            }
        }
        for(let i = 1; i <= lastDayOfCurrentMonth.getDate(); i++) {
            let day = new EventDay();
            day.year = lastDayOfCurrentMonth.getFullYear();
            day.month = lastDayOfCurrentMonth.getMonth();
            day.date = i;

            day.events = this.getEventsByDay(day.year,day.month,day.date);

            this.days.push(day);
        }
        if(lastDayPos != 6) {
            for(let i = lastDayPos + 1; i <= 6; i++) {
                let day = new EventDay();
                day.year = firstDayOfNextMonth.getFullYear();
                day.month = firstDayOfNextMonth.getMonth();
                day.date = i - lastDayPos;

                day.events = this.getEventsByDay(day.year,day.month,day.date);

                this.days.push(day);
            }
        }
    }

    getEventsByDay(year:number, month:number,date:number):EventData[] {
        return this.events.filter(event => {
            event.startDate = new Date(event.startDate);
            event.endDate = new Date(event.endDate);
            return true;
        }).filter( event => {
            return event.startDate.getFullYear() == year;
        }).filter(event => {
            return event.startDate.getMonth() == month;
        }).filter(event => {
            return event.startDate.getDate() == date;
        }).sort((a,b) => {
            return a.startDate.getTime() - b.startDate.getTime();
        })
    }

    lookupEventData() {
        let firstDayOfPreviousMonth = new Date(this.showYear,this.showMonth - 1,1);
        let lastDayOfNextMonth = new Date(this.showYear,this.showMonth + 2,0);

        this.calendarService.getEventData(firstDayOfPreviousMonth,lastDayOfNextMonth).subscribe(events => {
            this.events = events;
            this.buildDays();
        })
    }
}

So a quick break down on this code. It has 2 inputs [startYear], and [startDate] startYear indicates what year the calendar should allow going back, by default I limit it to 1900 and up to one year in advance. startDate indicates a Javascript Date to show on initial load, by default it goes to today.

Methods previousMonth(), nextMonth(), and today() used to move the calendar displayed. These simply jump us in our dates views to the new date.

Then buildDays(), this calculates the dates that need to be shown on the calendar for a month. It shows the last days of the previous month and the first few days of the next month as needed to complete the calendar. Javascript Date object does the heavy lifting here.

Next to get the events for a day we have getEventsByDay() a filter that goes through all the of the events and pulls out the events that occur on specific date.

Finally lookupEventData() used to load additional data from the API for a given date range. By requesting the a 3 month range at a time, we can switch to previous/next month and show the calendar correctly while we load additional data.

<mat-card>
	<mat-grid-list cols="14" rowHeight="2:1">
		<mat-grid-tile><mat-icon (click)="previousMonth()" class="apply-pointer" matTooltip="{{'Previous Month' | translate}}">arrow_back_ios</mat-icon></mat-grid-tile>
		<mat-grid-tile colspan="12">
			<mat-form-field class="date-selector">
				<mat-select [(ngModel)]="showMonth" (selectionChange)="buildDays()">
					<mat-option [value]="0">{{'January' | translate }}</mat-option>
					<mat-option [value]="1">{{'February' | translate }}</mat-option>
					<mat-option [value]="2">{{'March' | translate }}</mat-option>
					<mat-option [value]="3">{{'April' | translate }}</mat-option>
					<mat-option [value]="4">{{'May' | translate }}</mat-option>
					<mat-option [value]="5">{{'June' | translate }}</mat-option>
					<mat-option [value]="6">{{'July' | translate }}</mat-option>
					<mat-option [value]="7">{{'August' | translate }}</mat-option>
					<mat-option [value]="8">{{'September' | translate }}</mat-option>
					<mat-option [value]="9">{{'October' | translate }}</mat-option>
					<mat-option [value]="10">{{'November' | translate }}</mat-option>
					<mat-option [value]="11">{{'December' | translate }}</mat-option>
				</mat-select>
			</mat-form-field>
			<mat-form-field class="date-selector">
				<mat-select [(ngModel)]="showYear" (selectionChange)="buildDays()">
					<mat-option *ngFor="let y of years" [value]="y" >{{ y }}
					</mat-option>
				</mat-select>
			</mat-form-field>
			<mat-icon (click)="today()" class="apply-pointer" matTooltip="{{'Today' | translate }}" >calendar_today</mat-icon>
		</mat-grid-tile>
		<mat-grid-tile><mat-icon (click)="nextMonth()" class="apply-pointer" matTooltip="{{'Next Month' | translate}}">arrow_forward_ios</mat-icon></mat-grid-tile>
	</mat-grid-list>
	<mat-grid-list cols="7" rowHeight="4:1" >
		<mat-grid-tile class="calendar-border">{{'Sunday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border">{{'Monday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border">{{'Tuesday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border">{{'Wednesday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border">{{'Thursday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border">{{'Friday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border">{{'Saturday'|translate}}</mat-grid-tile>
		<mat-grid-tile class="calendar-border" rowspan="3" *ngFor="let day of days"><app-calendar-control-day class="calendar-day" [eventDay]="day"></app-calendar-control-day> </mat-grid-tile>
	</mat-grid-list>
</mat-card>

Now to show the template for the Calendar component I keep it simple, using a mat-grid-list to display the calendar, and ngx-translate to translate the month and weekday names.

.date-selector {
  margin: 5px;
}

.calendar-border {
  border: solid 1px $accent;
}

.calendar-day {
  width: 100%;
  height: 100%;
}

Finally the SCSS for the component.

So what do you get after all of this? See below. Style it however you want. I have some enhancements that I plan to make and I will post them here when I make them.

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d