DateGallery week calendar tutorial

1. Introduction

In this tutorial we are going to use the DateGallery to create a week based calendar view. The calendar will show the agenda for a cinema.

That we are using vanilla JavaScript which requires us to write our own DOM manipulation inside of a subscriber. When using a reactive framework such as Angular or React, this is not necessary, as these frameworks handle synchronizing with the DOM for us. See Usage with Frameworks for more information.

To get started with the tutorial open and fork this StackBlitz. By using the sandbox you do not have to worry to much about setting up an entire development environment.

2. Files overview

In the sandbox you will see a couple of files, most of them setup the dev environment, but these are the relevant files containing the code:

  1. index.html contains the HTML for our week based calendar.
  2. calendar.js this file is going to contain our week calendar implementation.
  3. calendar.css it contains all styles for the calendar.
  4. form.js the logic for the birthday form.
  5. modal.js contains code to open up a model dialog.
  6. formatters.js contains various Date formatters.
  7. events.js contains utility functions that are about events.
  8. utils.js contains utilities functions.

3. Goals

We want our month calendar to do the following things:

  1. We want to be able to navigate to the next and previous month.
  2. We want to show all movies as events within the calendar
  3. We want to be able to view the poster of the movie.

4. DateGallery setup

Currently the screen is empty except for two buttons to move to the next and previous dates, so lets bring it to life!

We will start by creating an instance of the DateGallery in the mode "week":

In the file called "calendar.js" add the following at the bottom of the file, but keep the imports as is:

const dateGallery = new window.uiloosDateGallery.DateGallery(
  {
    mode: 'week'
  },
  (dateGallery) => {
    // subscriber function
  }
);

We use window.uiloosDateGallery variable which comes from using the pre-minified builds. You can also install "uiloos" via the "package.json" instead.

The first argument to the DateGallery is a configuration object called DateGalleryConfig. It allows you to setup the DateGallery, in our case we only set the mode to "week".

The "week" mode will give each frame 7 days which is exactly what we need.

The second argument is a subscriber function, it will get called whenever the DateGallery changes. It receives the DateGallery as the first argument, and the event that caused the change as the second argument.

5. Setting the title

Lets set the title of the week calendar first, it should be the month and year for that week.

First we need a reference to the title element, in "calendar.js" add the following above below the imports:

const titleEl = document.querySelector('.calendar-title');

Now add the following to the subscriber function:

titleEl.textContent = monthYearFormatter.format(
  dateGallery.firstFrame.anchorDate
);

You should see the current month and year now!

To see changes you will have to press the "Refresh" button in the fake browser window.

The monthYearFormatter is a Intl.DateTimeFormat instance: which is the browsers native way to format human readable dates.

Lets make the next and previous navigation buttons work, add the following at the bottom of "calendar.js":

document.querySelector('.previous').onclick = () => {
  dateGallery.previous();
};

document.querySelector('.next').onclick = () => {
  dateGallery.next();
};

If you refresh the fake browser window you should now be able to go to the next and previous weeks. Note that it takes on average 4 clicks to go out of a month! So you must click it multiple times to see a change.

7. Day names

It is time now to start making our calendar week grid. The CSS setup a CSS grid for us with 7 columns one for each day. Lets start by adding the names of the day.

First we need to get a reference to the grid, add the following above the const titleEl definition:

const gridEl = document.querySelector('.calendar-week-grid');

Now in the subscriber function add this to the bottom:

dateGallery.firstFrame.dates.forEach((dateObj) => {
  const dayNameEl = document.createElement('button');
  dayNameEl.className = 'calendar-week-dayname';
  dayNameEl.innerHTML = `
    <time datetime="${dateObj.date.toISOString()}">
      ${weekDayFormatter.format(dateObj.date)}
    </time>
  `;

  /* 
    Add the day to the appropriate CSS grid column.
     +2 is needed becausea CSS grid starts at 1, and
    we want to keep the first column free for the 
    hour indicators.

    The getDay() on date returns the day of the week, 
    sunday = 0 and saturday = 6.
  */
  dayNameEl.style.gridColumn = dateObj.date.getDay() + 2;

  gridEl.appendChild(dayNameEl);
});

After a refresh you should now see the names of the day inside of a column.

Lets try to understand what we have done, by understanding the DateGallery a little better:

A DateGallery shows a number of frames, a frame (represented by the class DateGalleryFrame) is an array containing DateGalleryDate's, which in turn represents a "date".

The idea is that based on the mode of the DateGallery the frames contain different amount of dates. When the mode is "day" a frame contains one day, when the mode is year a frame contain 365 days.

Since our mode is "week" 7 days are contained in a frame. Since we have not configured the amount of frames, the DateGallery will default to having a single frame.

We use the firstFrame shortcut to access the first frame instead of using frames[0]. This makes the code a little easier to follow.

Now we just loop over each date in the firstFrame.dates and render each date as a div. The date itself is rendered as a time for screen readers.

8. Hours

Lets add the hours of the day next: the first thing we need to do is some setup, add this below the const titleEl declaration:

const START_HOUR = 10;
const END_HOUR = 25;

const HEIGHT = (END_HOUR - START_HOUR - 1) * 60;

const calendarExampleEl = document.querySelector(
  '.calendar-week-example'
);
calendarExampleEl.style.setProperty('--height', HEIGHT);

The START_HOUR and END_HOUR are going to determine how many hours are going to be shown. The cinema is open from 10:00 to 24:00 hours. If you want to change the hours displayed you can modify these variables!

Next we calculate the HEIGHT of our week calendar and set it as a CSS variable for the .calendar-week-example element. This way the CSS knows how large we want our week calendar to be.

The 60 pixel height per hour is because a hour has 60 minutes. This way we map one minute to one pixel, and this makes creating our calendar much easier. Always base the height of an hour on 60! so 120, 180 etc etc.

Now that the groundwork is done, lets render the hours, add the following below gridEl.innerHTML = '':

// Render the hours on the left
for (let i = START_HOUR; i < END_HOUR; i++) {
  const hourEl = document.createElement('span');
  hourEl.className = 'calendar-week-hour';
  hourEl.ariaHidden = true;

  // + 3 To skip the first two rows, CSS grids start at 1!
  const row = (i - START_HOUR) * 60 + 3;
  // Each row is "minute" and we want it to span one hour.
  hourEl.style.gridRow = `${row} / ${row + 60}`;

  // Create a new Date, the "date" does not really matter
  const date = new Date();
  
  // but the time does: set the hour to the hour rendered
  date.setHours(i, 0, 0, 0);

  // Now format it to HH:MM format
  const time = timeFormatter.format(date);
  hourEl.textContent = time;

  // and add it to the grid.
  gridEl.appendChild(hourEl);
}

Et voilà: we now have hour indicators, and it is starting to look like something.

9. Events

No for the most difficult part: adding events. Lets first add events to our DateGallery's configuration:

Add the following below the mode configuration:

{
  mode: 'week',
  events: generateEvents()
},

What generateEvents() does is create some placeholder events, which are movie screenings. A DateGallery can contain as many events as you want.

An event is represented by a class called DateGalleryEvent. An event contains the start and end date of the event, but also knows whether or not an other event overlaps with itself. You can also add custom data on the event on the data property. Which in our case is the title of the movie, color and poster image.

What the DateGallery also does is add all events that belong on a DateGalleryDate and DateGalleryFrame. This makes it easy to loop over all events of a particular date.

Now to start rendering the events / movie screenings, we must first get a reference to the template element for events. Add the following code below the const titleEl declaration:

const eventTemplate = document.querySelector(
  '#calendar-week-event-template'
);

Take a look at the template in the "index.html" file so you can understand the HTML structure an event is going to have.

Lets add the events now. This is going to be a large chunk of code but try to read the comments to get a sense of what is going on. Below gridEl.appendChild(dayNameEl) add:

// Will be a subgrid for each day containing the events
const dayEl = document.createElement('ul');
dayEl.className = 'calendar-week-day-grid';

// + 2: grids start at 1, and the hour indicators are on 1.
dayEl.style.gridColumn = dateObj.date.getDay() + 2;

/* 
  Events can overlap with each other and when this happens 
  we want to display the events next to each other.
  
  What the `calculateEventColumns` function does is determine 
  which CSS column each event should have, so that they 
  are neatly packed together visually.

  You should read the code for `calculateEventColumns` to 
  see how the algorithm works.
*/
const eventColumns = calculateEventColumns(dateObj);

dateObj.events.forEach((event) => {
  // Clones the <template> fragment.
  const eventEl = clone(eventTemplate);

  // Create a button with the correct color for the movie.
  const buttonEl = eventEl.querySelector('button');
  buttonEl.style.backgroundColor = event.data.color;
  
  // yiq calculates whether black or white text is
  // more visible based on the background color.
  buttonEl.style.color = yiq(event.data.color);

  // Empty for now we will do this next.
  buttonEl.onclick = () => {
    
  };

  // Get the <i> element and set the title and content
  // to the name of the movie.
  const titleEl = eventEl.querySelector('i');
  titleEl.title = event.data.title;
  titleEl.textContent = event.data.title;

  // Next set the start and end times to HH:MM
  const [startTimeEl, endTimeEl] = Array.from(
    eventEl.querySelectorAll('b')
  );
  startTimeEl.textContent = timeFormatter.format(
    event.startDate
  );
  endTimeEl.textContent = timeFormatter.format(
    event.endDate
  );

  // Calculate where to put the event vertically. 
  // This is of course based on which hour the 
  // week calendar stars. In our case 10:00 hours.
  const start = getMinutesSinceStart(event.startDate, START_HOUR);
  const end = getMinutesSinceStart(event.endDate, START_HOUR);

  // + 2 because CSS grids start at 1, and the first 
  // row contains the week day names.
  eventEl.style.gridRow = `${start + 2} / ${end + 2}`;

  // Finally place the event in the day column
  // as was calculated by `calculateEventColumns`.
  eventEl.style.gridColumn = eventColumns.get(event);

  dayEl.appendChild(eventEl);
});

gridEl.appendChild(dayEl);

We now have a pretty nice looking week calendar, built on a CSS grid.

10. Posters

Finally just so the movies can be clicked, lets fill in the onclick of the buttonEl:

buttonEl.onclick = () => {
  openDialog(`
    <div class="poster">
      <img 
        src="${event.data.image}" 
        alt="Poster for the movie '${event.data.title}'" 
      />
    </div>
  `);
};

When you click a movie screening now the poster should be displayed in a modal.

This concludes this tutorial.

11. What you have learned

  1. That the subscriber receives all events that take place on the DateGallery, and that in the subscriber you must sync the DOM with what occurred.
  2. That a events are represented by the DateGalleryEvent class.
  3. That you can get all events for the DateGalleryFrame, DateGalleryDate or DateGallery.
  4. That firstFrame is a shortcut to the first frame in the frames array.
  5. That you can move between frames using the next() and previous() methods.

12. Further reading

  1. Read through the API of the DateGallery, DateGalleryFrame, DateGalleryEvent, and DateGalleryDate.
  2. You can also create datepickers using the DateGallery. The tutorial for a datepicker shows you how.
  3. When using vanilla JavaScript you must handle DOM manipulation yourself, contrast this with the examples that use a framework.

13. Full code

This is the finished code for this tutorial, for reference.

A week calendar is one thing but how about a calendar with day, week, year and month modes. Take a look a the reference implementation of a calendar in vanilla JS.

It has a bazillion more features:

  1. Drag and drop events from one day to the next.
  2. Increasing the duration of an event by dragging the edges of an event.
  3. Switch between year, month, week and day calendars.
  4. Adding events by clicking on empty places in the calendar.
  5. Editing existing events, and the ability to remove them.
Usage with Frameworks
Learn how to use the DateGallery in combination with frameworks such as Angular and Svelte.