cho.sh
Creating Calendar in JavaScript

Creating Calendar in JavaScript

Warning

Over a year old. May be outdated

A calendar in plain JavaScript, no libraries. This project is based on my internship at Woowa Bros, a unicorn food-delivery startup in Seoul, back in .

Show me the code first.

GitHub - anaclumos/calendar.js: Vanilla JS Calendar

Show me the demo first.

Goals

  • Use functional programming* over object-oriented programming.
  • No DOM manipulation after init. This philosophy is based on React (or any Single Page Application library). DOM manipulation can be highly confusing if 30 different codes try to edit the same thing, so we will rerender the components if we need to edit something.

💡 Don't fix it. Buy a new one.

That's rerendering in front-end, in one line.

Stack

  • JavaScript Date Object
  • CSS display: grid will be useful.

Basic Idea

  • There will be a global displayDate object that represents the displaying month.
  • navigator.js will change this displayDate object, and trigger renderCalendar() function with displayDate as an argument.
  • renderCalendar() will rerender with the calendar.

Before anything, prettier!

helps write clean and neat code with automatic formatting.

json
// `.prettierrc`
{
  "semi": false,
  "singleQuote": true,
  "arrowParens": "always",
  "tabWidth": 2,
  "useTabs": false,
  "printWidth": 60,
  "trailingComma": "es5",
  "endOfLine": "lf",
  "bracketSpacing": true
}

Now throw in some HTML.

html
<!-- `index.html` -->
<!doctype html>
<html lang="en">
  <head>
    <title>JavaScript Calendar</title>
  </head>
  <body>
    <div id="navigator"></div>
    <div id="calendar"></div>
  </body>
  <script>
    // code for rendering
  </script>
</html>

I generated this boilerplate with VS Code.

Then trick VS Code to read JS String as HTML Tags.

Vanilla JavaScript means no JSX-style highlighting. Our HTML will live in JavaScript strings, with no highlighting or Intellisense. So, a function that tricks VS Code into reading the string as HTML.

js
// `util.js`
const html = (s, ...args) => s.map((ss, i) => `${ss}${args[i] || ""}`).join("");

to be added - screenshot of highlighting

calendar.js

Then we connect calendar.js and index.html.

html
<!-- `index.html` -->
<script src="calendar.js"></script>

Defining constants will help before writing renderCalendar().

js
// `calendar.js`
const NUMBER_OF_DAYS_IN_WEEK = 7;
const NAME_OF_DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
const LONG_NAME_OF_DAYS = [
  "Sunday",
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
];
const ACTUAL_TODAY = new Date();

NUMBER_OF_DAYS_IN_WEEK kills the magic number. A random 7 in code can be tough to decipher. A constant like this increases maintainability.

js
for (let d = 0; d < NUMBER_OF_DAYS_IN_WEEK; d++) {
  // do something
}

If there was a random 7, who knows if we are iterating through the number of Harry Potter Books?

This code block will be the baseline for our calendar generation. We will pass in the HTML target and a day object. today is the month on screen and comes from navigator.js. Navigator gives the real date for this month and the first of the month for other months.

js
// `calendar.js`
const renderCalendar = ($target, today) => {
  let html = getCalendarHTML(today);
  // minify html
  html = html.replace(/\n/g, "");
  // replace multiple spaces with single space
  html = html.replace(/\s{2,}/g, " ");
  $target.innerHTML = html;
};

Now, we need four different Date objects for displaying the calendar. We could've used fewer, but that's up to the implementation. Cutting date objects here would buy a tiny bit of performance and make the code much harder to follow, so four objects feels like a fair middle ground.

Four Date objects we need

  • The last day of last month: needed to highlight last month's weekend and display the correct date for last month's row.
  • The first day of this month: needed to highlight this month's weekend and figure out how many days of last month we need to render.
  • The last day of this month: needed for rendering this month with iteration.
  • The first day of next month: needed to highlight the weekend of next month.

I made a function that would process these four dates when inputted a specific Date.

js
// `calendar.js`
const processDate = (day) => {
  const month = day.getMonth();
  const year = day.getFullYear();
  return {
    lastMonthLastDate: new Date(year, month, 0),
    thisMonthFirstDate: new Date(year, month, 1),
    thisMonthLastDate: new Date(year, month + 1, 0),
    nextMonthFirstDate: new Date(year, month + 1, 1),
  };
};

The function bundles these four dates into an object and returns it. It takes a Date object as its argument, and in this calendar that's the Date for "today."

js
const processDate = (day) => {
  const date = day.getDate();
  const month = day.getMonth();
  const year = day.getFullYear();
  return {
    lastMonthLastDate: new Date(year, month, 0),
    thisMonthFirstDate: new Date(year, month, 1),
    thisMonthLastDate: new Date(year, month + 1, 0),
    nextMonthFirstDate: new Date(year, month + 1, 1),
  };
};

2-2. Create getCalendarHTML

Now let's actually draw the calendar. getCalendarHTML returns the calendar's contents as HTML. It gets a bit bulky, so here's the skeleton first.

js
const getCalendarHTML = () => {
  let today = new Date();
  let {
    lastMonthLastDate,
    thisMonthFirstDate,
    thisMonthLastDate,
    nextMonthFirstDate,
  } = processDate(today);
  let calendarContents = [];

  // ...

  return calendarContents.join("");
};

First, a row at the top for the days of the week. The const from earlier keeps the magic number out.

js
for (let d = 0; d < NUMBER_OF_DAYS_IN_WEEK; d++) {
  calendarContents.push(
    html`<div class="${NAME_OF_DAYS[d]} calendar-cell">
      ${NAME_OF_DAYS[d]}
    </div>`,
  );
}

Then last month. If this month starts on a Wednesday, this loop fills in last month's Sunday, Monday, and Tuesday. Sundays get the sun class.

js
for (let d = 0; d < thisMonthFirstDate.getDay(); d++) {
  calendarContents.push(
    html`<div
      class="
          ${d % 7 === 0 ? "sun" : ""}
          calendar-cell
          past-month
        "
    >
      ${lastMonthLastDate.getMonth() + 1}/${
        lastMonthLastDate.getDate() - thisMonthFirstDate.getDay() + d
      }
    </div>`,
  );
}

This month works the same way. Today gets the today class and a "today" label, and Saturdays and Sundays get sat and sun.

js
for (let d = 0; d < thisMonthLastDate.getDate(); d++) {
  calendarContents.push(
    html`<div
      class="
          ${today.getDate() === d + 1 ? "today" : ""}
          ${(thisMonthFirstDate.getDay() + d) % 7 === 0 ? "sun" : ""}
          ${(thisMonthFirstDate.getDay() + d) % 7 === 6 ? "sat" : ""}
          calendar-cell
          this-month
        "
    >
      ${d + 1} ${today.getDate() === d + 1 ? " today" : ""}
    </div>`,
  );
}

Finally, draw the days of the next month in the remaining cells.

js
let nextMonthDaysToRender = 7 - (calendarContents.length % 7);

for (let d = 0; d < nextMonthDaysToRender; d++) {
  calendarContents.push(
    html`<div
      class="
          ${(nextMonthFirstDate.getDay() + d) % 7 === 6 ? "sat" : ""}
          calendar-cell
          next-month
        "
    >
      ${nextMonthFirstDate.getMonth() + 1}/${d + 1}
    </div>`,
  );
}

3. Writing CSS

3-1. Using display: grid

display: grid lays an element's children out neatly in a grid, like a table.

  • grid-template-columns sets how the columns are laid out. 1fr means one fraction, and it's written 7 times, so you get 7 columns of equal width.
  • grid-template-rows sets row sizes. There's only one 3rem here, so only the first row is 3rem.
  • grid-auto-rows sets the size of every row after that. It says 6rem, so the rest are 6rem.

The rest is plain styling.

css
#App {
  /* grid */
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  grid-template-rows: 3rem;
  grid-auto-rows: 6rem;

  /* style */
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  border: 1px solid black;
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
}
  • When you draw a table, you want every cell wrapped in the same border, like Excel. But sometimes only the outermost cells end up with thinner lines, because in HTML terms the border only goes on th and td.
  • I like to put n px on every cell border and n px on the table border. That gives you a uniform 2n px border everywhere.
css
.calendar-cell {
  border: 1px solid black;
  padding: 0.5rem;
}

3-2. Highlighting Saturdays, Sundays, and today

css
.past-month,
.next-month {
  color: gray;
}

.sun {
  color: red;
}

.sat {
  color: blue;
}

.past-month.sun {
  color: pink;
}

.next-month.sat {
  color: lightblue;
}

.today {
  color: #e5732f;
}

I felt that

  • At first I got a little lost hooking up JS to "initialize" the calendar. I had attached renderCalendar at the top of body. The DOM runs top to bottom, so renderCalendar ran before the #App div existed and couldn't find it.
  • I also forgot how to get what I'd built in JS onto the screen. The answer was simple. Grab the app with querySelector in the JS that plays the role of index.js, and set its innerHTML.
  • The Woowa Tech Camp project had magic numbers. This time I took them out for readability.
  • The Woowa Tech Camp project was object-oriented JavaScript (a Singleton, to be precise). This time it's small functions.
  • I tried to use ES6+ syntax, like template literals with backticks and destructuring what processDate returns. Mostly let and const, too.
  • I wish I could have made getCalendarHTML a bit shorter.
Backlinks1