Build a simple Vue calendar without webpack
Tags: javascript, vue, tutorial
Published: 2020-08-26 01:03:42

I needed a calendar (or a date picker) for a product I'm working on. In the past, I would have always reached for a library.

A calendar isn't usually something I want to maintain myself and the features in a library typically cover many different use cases. However, in this case, I wanted to see what I could build in a couple hours.

So, let's build a calendar with Vue. We'll use a single HTML file. We won't use webpack or babel. We won't need to compile or install anything at all.

Here's what the end product will look like.

{% twitter 1297192294895026182 %}

We'll start with this template. It contains everything we'll be using to get the job done.

- [tailwind](https://tailwindcss.com): a utility-first CSS framework
- [vue](https://vuejs.org): a JavaScript framework for building user interfaces
- [date-fns](https://date-fns.org): a date utility library
- [lodash](https://lodash.com/): a utility library

```html
<!DOCTYPE html>
<html>

<head>
  <title>Calendar.vue</title>
  <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
  <script src="https://unpkg.com/vue"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.30.1/date_fns.min.js"
    integrity="sha512-F+u8eWHrfY8Xw9BLzZ8rG/0wIvs0y+JyRJrXjp3VjtFPylAEEGwKbua5Ip/oiVhaTDaDs4eU2Xtsxjs/9ag2bQ=="
    crossorigin="anonymous"></script>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.20/lodash.min.js"></script>
</head>

<body class="py-12">
  <div class="container mx-auto flex justify-center">
    <div id="calendar" class="w-1/3 rounded border">
      &nbsp;
    </div>
  </div>

  <script>
    var app = new Vue({
      el: '#calendar'
    })
  </script>
</body>

</html>
```

To start, it's helpful to think about how a calendar can be represented in a data structure. This is how I thought it could look.

```javascript
var august2020 = [
  [null, null, null, null, null, null, 1],
  [2, 3, 4, 5, 6, 7, 8],
  [9, 10, 11, 12, 13, 14, 15],
  [16, 17, 18, 19, 20, 21, 22],
  [23, 24, 25, 26, 27, 28, 29],
  [30, 31, null, null, null, null, null]
]
```

I wanted a data structure that could easily map to DOM elements. If we look at `august2020`, we can see how each array could be a row in the calendar and each value inside the array could map to a `div`. `null` values would be an empty `div`.

The challenge is that we need to build this data structure anytime the month is changed. A month can start and end on any day of the week, so we need to come up with an algorithm.

Let's get started.

We're going to need three functions. One to get the first day of the month, one to get the last day of the month, and one to get the number of days in the month. We'll use those functions to determine where to start drawing our calendar. In other words, how many empty elements do we need before we draw 1 and how many empty elements do we need after we draw 31.

```javascript
var app = new Vue({
  el: '#calendar',
  data: {
    // August 1, 2020
    date: new Date(2020, 7, 1)
  },
  computed: {
    startOfMonth: function() {
      return dateFns.startOfMonth(this.date)
    },
    endOfMonth: function() {
      return dateFns.endOfMonth(this.date)
    },
    daysInMonth: function() {
      return dateFns.getDaysInMonth(this.date)
    }
  }
})
```

The `dateFns` object is provided by date-fns. The functions we're calling do what you'd expect them to do (awesome, right?).

Once we have the beginning and the end of the month, we have enough to build the `august2020` data structure shown above. What we'll be building are the weeks of August 2020. We'll use the weeks to display the month of August 2020 on the page.

```vue
<body class="py-12">
  <div class="container mx-auto flex justify-center">
    <div id="calendar" class="w-1/3 rounded border">
      <section>
        <ul v-for="(week, weekIndex) in weeks" :key="weekIndex" class="border-t">
          <li>
            <ul class="flex">
              <li v-for="(day, dayIndex) in week" :key="`${weekIndex}-${dayIndex}`" class="flex-1 p-1 text-center border-r last:border-r-0">
                {{day && day.getDate()}}
              </li>
            </ul>
          </li>
        </ul>
      </section>
    </div>
  </div>

  <script>
    var app = new Vue({
      el: '#calendar',
      data: {
        // August 1, 2020
        date: new Date(2020, 7, 1)
      },
      computed: {
        startOfMonth: function() {
          return dateFns.startOfMonth(this.date)
        },
        endOfMonth: function() {
          return dateFns.endOfMonth(this.date)
        },
        daysInMonth: function() {
          return dateFns.getDaysInMonth(this.date)
        },
        weeks: function() {
          var firstDayOfWeek = dateFns.getDay(this.startOfMonth)

          var days = []

          for (var i = 0; i < firstDayOfWeek; i++) {
            days.push(null)
          }

          for (var i = 1; i <= this.daysInMonth; i++) {
            days.push(dateFns.setDate(this.date, i))
          }

          var lastDayOfWeek = dateFns.getDay(this.endOfMonth)
          for (var i = 0; i < 6 - lastDayOfWeek; i++) {
            days.push(null)
          }

          return _.chunk(days, 7)
        }
      }
    })
  </script>
</body>
```

The result of the above code looks like this. Let's break it down.

![A basic calendar built with Vue for August 2020](https://dev-to-uploads.s3.amazonaws.com/i/0ggr93twdwh2jwly62pm.png)

Once we determine the first day of the week (August 2020 starts on a Saturday), we can figure out how many blanks we need. In this case, we need six blanks. A loop that goes from 0 to 5 will give us just that. At this point, the `days` array looks like this: `[null, null, null, null, null, null]`.

```javascript
for (var i = 0; i < firstDayOfWeek; i++) {
  days.push(null)
}
```

At this point, we can add days 1-31 since we know there are 31 days in August. A loop that goes from 1 to 31 can get the job done. Instead of using integers, we push in `Date` objects. We use `dateFns.setDate(this.date, i)` to get a new date object set to the value of `i`. This will come in handy for other purposes.

After this look executes, `days` is an array with 6 null values followed by the integers 1 to 31.

```javascript
for (var i = 1; i <= this.daysInMonth; i++) {
  days.push(dateFns.setDate(this.date, i))
}
```

Finally, once we're done with that, we can use the last day of the month to determine how many blanks we need to fill in the rest of the calendar. August 2020 ends on a Monday, so we need to fill in five blanks to finish. A loop that goes from 0 to 4 is here to help.

```javascript
for (var i = 0; i < 6 - lastDayOfWeek; i++) {
  days.push(null)
}
```

Now, let's add some labels. To do that, we need a little HTML and a formatting function. We want to display August 2020 at the top with labels for each day of the week.

We'll add the following to our Vue template directly above the `<section>` for displaying the weeks and days.

```vue
<section class="flex">
  <h2 class="flex-grow text-center text-lg font-bold">{{month}}</h2>
</section>
<ul class="flex bg-gray-100 border-t text-gray-600">
  <li class="flex-1 text-center border-r last:border-r-0">S</li>
  <li class="flex-1 text-center border-r last:border-r-0">M</li>
  <li class="flex-1 text-center border-r last:border-r-0">T</li>
  <li class="flex-1 text-center border-r last:border-r-0">W</li>
  <li class="flex-1 text-center border-r last:border-r-0">R</li>
  <li class="flex-1 text-center border-r last:border-r-0">F</li>
  <li class="flex-1 text-center border-r last:border-r-0">S</li>
</ul>
```

And the function for `month` is a simple one as well. Again, `dateFns` is provided by date-fns and we use its format function. `'MMMM YYYY'` just says give us a string representation of the date that looks like August 2020.

```javascript
function() {
  return dateFns.format(this.date, 'MMMM YYYY')
}
```

Doing that gets us to this point. Here's what the calendar looks like n ow.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hjw5l1augdssv5b0zfwp.png)

The last thing we need to do is allow ourselves to navigate to the previous and next months. This is the fun part. Everything we've done so far is setting us up to use Vue's most powerful feature: reactivity.

Whenever `date` changes, Vue will re-use the code we already wrote and draw a new calendar based on the new date. So if we change the date to be July 2020, we'll get a July 2020 calendar drawn for us. Let's do it.

First, we need a couple links to click on. We need a link that says "Previous" and a link that says "Next". Something like this will do just fine.

```vue
<section class="flex">
  <a class="ml-4 flex-shrink cursor-pointer text-gray-800 underline">Previous</a>
  <h2 class="flex-grow text-center text-lg font-bold">{{month}}</h2>
  <a class="ml-4 flex-shrink cursor-pointer text-gray-800 underline">Next</a>
</section>
```

As you might be able to tell, these links don't do anything. We don't have them hooked up to Vue at all, but that's easy enough - let's do it. We'll need a method and then tell the links to call that method when clicked. The previous link will decrement the month by 1 and the next link will increment the month by 1.

```javascript
// ...
methods: {
  changeMonth: function(by) {
    this.date = dateFns.addMonths(this.date, by)
  }
}
// ...
```

```vue
<section class="flex">
  <a v-on:click="changeMonth(-1)" class="ml-4 flex-shrink cursor-pointer text-gray-800 underline">Previous</a>
  <h2 class="flex-grow text-center text-lg font-bold">{{month}}</h2>
  <a v-on:click="changeMonth(1)" class="ml-4 flex-shrink cursor-pointer text-gray-800 underline">Next</a>
</section>
```

When we click on the previous link, Vue calls our function with -1 as the argument. Then our function adds -1 months to the current date (which is August 1, 2020). This tells Vue to re-render our component with July 2020 as the current date. We're done.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/q1njjr5a66x7vv5geyfo.png)

[If you'd like to view the full version, check it out on JSFiddle.](https://jsfiddle.net/rdcv3h96/) Feel free to copy and paste this to your local machine and play around with it.

If you found this helpful, [follow me on Twitter](https://twitter.com/derekjhopper).