Build Angular Calendar Component Yourself - No UI Libraries Needed
In this post, you will learn how to create an interactive calendar in Angular without using any additional libraries. Here's how it will look:
We have a calendar where we can select any date, jump to the previous and next months, go to today, and see the meetings planned for that specific day.
We won't use any UI libraries to create this project, but we will need a library to work with dates in JavaScript.
The default date handling in JavaScript is really painful and not flexible.
A library called Luxon is a popular solution for working with dates in a comfortable and efficient manner.
DateTime.now().setZone('America/New_York').minus({weeks:1}).endOf('day').toISO();
We can write our code like this. Even if you've never worked with Luxon, you can understand what we are doing here. First, we get the current date. Then, we set the timezone, subtract one week from this date, and get the end of the day. The code is extremely readable, easy to support, and it will help us a lot in creating an interactive calendar with Angular.
Project and dependencies
I already generated an empty Angular application. Now we must install Luxon.
npm i luxon
npm i @types/luxon -D
It is not enough to just install Luxon. We also want to cover all functions with data types, so we installed types for this library.
Now let's look at our code.
import { CalendarComponent } from './calendar/calendar.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, CalendarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {}
Our AppComponent
is completely empty, but we imported our calendar component into it.
<calendar/>
In the HTML, we simply render this component.
// src/app/calendar/calendar.component.ts
import { CalendarComponent } from './calendar/calendar.component';
@Component({
selector: 'calendar',
standalone: true,
templateUrl: './calendar.component.html',
styleUrl: './calendar.component.css',
})
export class CalendarComponent {
}
Our CalendarComponent
is completely empty.
<!-- src/app/calendar/calendar.component.css -->
.calendar-container {
display: flex;
}
.calendar {
padding-right: 10px;
min-width: 384px;
}
.calendar-headline {
display: flex;
margin-bottom: 10px;
justify-content: space-between;
padding: 0 20px;
}
.calendar-headline-month {
font-weight: 600;
}
.calendar-headline-controls {
display: flex;
}
.calendar-headline-control {
cursor: pointer;
}
.calendar-headline-control:hover {
text-decoration: underline;
}
.calendar-headline-control-today {
margin: 0 10px;
}
.calendar-weeks-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
font-size: 12px;
margin-bottom: 10px;
color: rgb(81, 81, 81);
}
.calendar-weeks-grid-cell {
text-align: center;
}
.calendar-grid-cell-inactive {
color: #9e9e9e;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
border-top: 1px solid rgb(190, 190, 190);
border-left: 1px solid rgb(190, 190, 190);
}
.calendar-grid-cell {
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgb(190, 190, 190);
border-right: 1px solid rgb(190, 190, 190);
cursor: pointer;
}
.calendar-grid-cell:hover {
background: #9ba3bf;
}
.calendar-grid-cell-active {
background: #9ba3bf;
}
.schedule {
margin-left: 10px;
}
.schedule-headline {
font-weight: 600;
margin-bottom: 30px;
}
Our HTML is completely empty, but in the CSS file, I prepared lots of styling that we will use for our calendar. As always, you can download the source code of the project at the end of the post.
Preparing State
Now we are ready to implement our project. First, we need several states for our application. Here I want to heavily use signals as it is much easier to write code with them.
First, we need a reference to today.
export class CalendarComponent {
today: Signal<DateTime> = signal(DateTime.local());
}
Here we created an Angular signal with a Luxon date inside. It allows us to access a variety of properties and functions on the variable.
Another thing that we need is the first date of the month. Why do we need that? We want to render the whole month on the screen, and it will be much simpler if we know the first day of the month.
export class CalendarComponent {
...
firstDayOfActiveMonth: WritableSignal<DateTime> = signal(
this.today().startOf('month'),
);
}
The first important thing is that we created a WritableSignal
. It allows us not only to read its value but also to update it. By using startOf('month')
we can get the correct date in relation to today
.
The next thing that we need to prepare is an array of day names in a week. We can also use Luxon for this.
export class CalendarComponent {
...
weekDays: Signal<string[]> = signal(Info.weekdays('short'));
}
It's a signal where we use Info.weekdays to get a short format of names like this:
['Mon', 'Tue', 'Wed', ...]
Now we must prepare data for the month grid. It should be an array of Luxon dates which includes the start and end of the weeks. Again, Luxon can help tremendously here.
export class CalendarComponent {
...
daysOfMonth: Signal<DateTime[]> = computed(() => {
return Interval.fromDateTimes(
this.firstDayOfActiveMonth().startOf('week'),
this.firstDayOfActiveMonth().endOf('month').endOf('week'),
)
.splitBy({ day: 1 })
});
}
Here we used computed
to create a signal which is based on the firstDayOfActiveMonth
. We get an interval between the start and end of the month and split it by 1 day.
As you can see, it returns an array of intervals which is mostly what we need, as we can read the start date from each interval.
export class CalendarComponent {
...
daysOfMonth: Signal<DateTime[]> = computed(() => {
return Interval.fromDateTimes(
this.firstDayOfActiveMonth().startOf('week'),
this.firstDayOfActiveMonth().endOf('month').endOf('week'),
)
.splitBy({ day: 1 })
.map((d) => {
if (d.start === null) {
throw new Error('Wrong dates');
}
return d.start;
});
});
}
This will get us an array of Luxon dates that we can render now. As you can see, Luxon allows us to generate such an array in just 3 lines of code.
Rendering Markup
Let's begin with some rendering.
<div class="calendar-container">
<div class="calendar">
<div class="calendar-headline">
<div class="calendar-headline-month">
{{ firstDayOfActiveMonth().monthShort }},
{{ firstDayOfActiveMonth().year }}
</div>
<div class="calendar-headline-controls">
<div class="calendar-headline-control">
«
</div>
<div
class="calendar-headline-control calendar-headline-control-today"
>
Today
</div>
<div class="calendar-headline-control">»</div>
</div>
</div>
</div>
</div>
Here is some markup for the headline of the calendar. We render the month name, year, and controls.
Now, let's render a list of weekdays and a grid for the month.
<div class="calendar-container">
<div class="calendar">
<div class="calendar-headline">
...
</div>
<div class="calendar-weeks-grid">
@for (weekDay of weekDays(); track $index) {
<div class="calendar-weeks-grid-cell">{{ weekDay }}</div>
}
</div>
<div class="calendar-grid">
@for (dayOfMonth of daysOfMonth(); track $index) {
<div class="calendar-grid-cell">
{{ dayOfMonth.day }}
</div>
}
</div>
</div>
</div>
Here, we mapped through weekDays
and daysOfMonth
to render the entire UI of the calendar.
As you can see, the calendar is rendered correctly.
Adding Meetings
The next feature we need to implement is rendering a list of meetings on the right side of our calendar. To achieve this, we first need to pass the meeting information to our calendar component.
To do that, it makes sense to create an interface for our meeting.
// src/app/calendar/meetings.interface.ts
export interface Meetings {
[key: string]: string[];
}
Let's see what our meetings can look like.
// src/app/app.component.ts
export class AppComponent {
meetings: Meetings = {
'2024-04-05': ['Dring Coffee', 'Learn React', 'Sleep'],
'2024-04-06': ['Dring Coffee', 'Learn Angular', 'Sleep'],
};
}
We store our meetings as an object where the key is a short date and the value is a list of strings. Now we can accept them in our component as an input signal.
export class CalendarComponent {
...
meetings: InputSignal<Meetings> = input.required();
}
We also need to create a signal for the active day (the day we clicked on) and find the list of meetings for that day.
export class CalendarComponent {
...
meetings: InputSignal<Meetings> = input.required();
activeDay: WritableSignal<DateTime | null> = signal(null);
activeDayMeetings: Signal<string[]> = computed(() => {
const activeDay = this.activeDay();
if (activeDay === null) {
return [];
}
const activeDayISO = activeDay.toISODate();
if (!activeDayISO) {
return [];
}
return this.meetings()[activeDayISO] ?? [];
});
}
Our activeDay
is null
by default. Based on this, we create a computed signal, activeDayMeetings
. We try to get meetings for the specific date, and if it doesn't work, we get an empty array. With this setup, we have all the necessary information to render the list of meetings.
<div class="calendar-container">
<div class="calendar">
...
</div>
<div class="schedule">
<div class="schedule-headline">
@if (activeDay(); as activeDay) {
<div>{{ activeDay.toLocaleString(DATE_MED) }}</div>
} @else {
<div>Please select a day</div>
}
</div>
<div>
@if (activeDay() && activeDayMeetings().length === 0) {
<div>No Planned Meetings today</div>
}
@if (activeDay() && activeDayMeetings().length > 0) {
@for (meeting of activeDayMeetings(); track $index) {
<div>{{ meeting }}</div>
}
}
</div>
</div>
</div>
If there is no activeDay
, we render a label saying "Please select a day". Once a day is selected, we render at least a headline. If there are any meetings for this day, we render them as a list; otherwise, we render "No Planned Meetings Today".
The meetings list functionality is complete, but we still need to implement a click event to set the activeDay
.
<div class="calendar-grid">
@for (dayOfMonth of daysOfMonth(); track $index) {
<div class="calendar-grid-cell" (click)="activeDay.set(dayOfMonth)">
{{ dayOfMonth.day }}
</div>
}
</div>
We use activeDay.set
to update the state with the clicked day.
Now you can see that a day can be selected, and we render the correct information accordingly.
Highlighting dates
One more feature we're missing is highlighting the active day.
<div class="calendar-grid">
@for (dayOfMonth of daysOfMonth(); track $index) {
<div
[ngClass]="{
'calendar-grid-cell': true,
'calendar-grid-cell-active':
activeDay()?.toISODate() === dayOfMonth.toISODate(),
'calendar-grid-cell-inactive':
dayOfMonth.month !== firstDayOfActiveMonth().month
}"
(click)="activeDay.set(dayOfMonth)"
>
{{ dayOfMonth.day }}
</div>
}
</div>
Now, instead of applying a single class to our cell, we use ngClass
to concatenate them dynamically. By default, we set calendar-grid-cell
for all cells. If the day is not in the current month, we add calendar-grid-cell-inactive
. When the day is active, we add calendar-grid-cell-active
.
As you can see, now the active day is correctly highlighted.
Navigation
The last feature we're missing is arrow navigation and jumping to today. To implement this, we need to create several functions to change firstDayOfActiveMonth
. These functions will handle navigating to the previous and next months, as well as jumping to today's date.
export class CalendarComponent {
...
goToPreviousMonth(): void {
this.firstDayOfActiveMonth.set(
this.firstDayOfActiveMonth().minus({ month: 1 }),
);
}
goToNextMonth(): void {
this.firstDayOfActiveMonth.set(
this.firstDayOfActiveMonth().plus({ month: 1 }),
);
}
goToToday(): void {
this.firstDayOfActiveMonth.set(this.today().startOf('month'));
}
}
Here, we use Luxon to subtract or add a month to the firstDayOfActiveMonth
. Now, we just need to add click events to trigger these functions.
<div class="calendar-headline-controls">
<div class="calendar-headline-control" (click)="goToPreviousMonth()">
«
</div>
<div
class="calendar-headline-control calendar-headline-control-today"
(click)="goToToday()"
>
Today
</div>
<div class="calendar-headline-control" (click)="goToNextMonth()">»</div>
</div>
We can navigate through the calendar by clicking on our headline controls.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done