Tag: calendar

A Calendar in Three Lines of CSS

This article has no byline and is on a website that is even more weirdly specific than this one is, but I appreciate the trick here. A seven-column grid makes for a calendar layout pretty quick. You can let the days (grid items) fall onto it naturally, except kick the first day over to the correct first column with grid-column-start.

Thoughts:

  • I’d go with an <ol> rather than a <ul> just because it seems like days are definitely ordered.
  • The days as-a-list don’t really bother me since maybe that makes semantic sense to the content of the calendar (assuming it has some)
  • But… seeing the titles of the days-of-the-week as the first items in the same list feels weird. Almost like that should be a separate list or something.
  • Or maybe it should all just be a <table> since it’s sort of tabular data (it stands to reason you might want to cross-reference and look at all Thursdays or whatever).

Anyway, the placement trickery is fun.

Here’s another (similar) approach from our collection of CSS Grid starter templates.

Direct Link to ArticlePermalink


The post A Calendar in Three Lines of CSS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,

Web Performance Calendar

The Web Performance Calendar just started up again this year. The first two posts so far are about, well, performance! First up, Rick Viscomi writes about the mythical “fast” web page:

How you approach measuring a web page’s performance can tell you whether it’s built for speed or whether it feels fast. We call them lab and field tools. Lab tools are the microscopes that inspect a page for all possible points of friction. Field tools are the binoculars that give you an overview of how users are experiencing the page.

This to me suggests that field tools are the future of performance monitoring. But Rick’s post goes into a lot more depth beyond that.

Secondly, Matt Hobbs wrote about the font-display CSS property and how it affects the performance of our sites:

If you’re purely talking about perceived performance and connection speed isn’t an issue then I’d say font-display: swap is best. When used, this setting renders the fallback font almost instantly. Allowing a user to start reading page content while the webfont continues to load in the background. Once loaded the font is swapped in accordingly. On a fast, stable connection this usually happens very quickly.

My recommendation here would be to care deeply about the layout shift if you use this property. Once the font loads and is swapped out you could create a big shift of text moving all about over the place. I once shipped a change to a site with this property without minding the layout shift and users complained a lot.

It was a good lesson learned: folks sure care about performance even if they don’t say that out loud. They care deeply about “cumulative layout shift” too.

Direct Link to ArticlePermalink


The post Web Performance Calendar appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

Let’s Make a Vue-Powered Monthly Calendar

Have you ever seen a calendar on a webpage and thought, how the heck did they did that? For something like that, it might be natural to reach for a plugin, or even an embedded Google Calendar, but it’s actually a lot more straightforward to make one than you might think. Especially when we use the component-driven power of Vue.

I’ve set up a demo over at CodeSandbox so you can see what we’re aiming for, but it’s always a good idea to spell out what we’re trying to do:

  • Create a month view grid that displays the days of the current month
  • Display dates from the previous and next months to so the grid is always full
  • Indicate the current date
  • Show the name of the currently selected month
  • Navigate to the previous and next month
  • Allow the user to navigate back to the current month with a single click

Oh, and we’ll build this as a single page application that fetches calendar dates from Day.js, a super light utility library.

Step 1: Start with the basic markup

We’re going to jump straight into templates. If you’re new to Vue, Sarah’s introduction series is a nice place to start. It’s also worth noting that I’ll be linking to the Vue 2 docs throughout this post. Vue 3 is currently in beta and the docs for it are subject to change.

Let’s start with creating a basic template for our calendar. We can outline our markup as three layers where we have:

  • A section for the calendar header. This will show components with the currently selected month and the elements responsible for paginating between months.
  • A section for the calendar grid header. A table header that holds a list containing the days of the week, starting with Monday.
  • The calendar grid. You know, each day in the current month, represented as a square in the grid.

Let’s write this up in a file called CalendarMonth.vue. This will be our main component.

<!-- CalendarMonth.vue --> <template>   <!-- Parent container for the calendar month -->   <div class="calendar-month">           <!-- The calendar header -->     <div class="calendar-month-header"       <!-- Month name -->       <CalendarDateIndicator />       <!-- Pagination -->       <CalendarDateSelector />     </div>      <!-- Calendar grid header -->     <CalendarWeekdays />      <!-- Calendar grid -->     <ol class="days-grid">       <CalendarMonthDayItem />     </ol>   </div> </template>

Now that we have some markup to work with, let’s go one step further and create required components.

Step 2: Header components

In our header we have two components:

  • CalendarDateIndicator shows the currently selected month.
  • CalendarDateSelector is responsible for paginating between months.

Let’s start with CalendarDateIndicator. This component will accept a selectedDate property which is a Day.js object that will format the current date properly and show it to the user.

<!-- CalendarDateIndicator.vue --> <template>   <div class="calendar-date-indicator">{{ selectedMonth }}</div> </template>  <script> export default {   props: {     selectedDate: {       type: Object,       required: true     }   },    computed: {     selectedMonth() {       return this.selectedDate.format("MMMM YYYY");     }   } }; </script>

That was easy. Let’s go and create the pagination component that lets us navigate between months. It will contain three elements responsible for selecting the previous, current and next month. We’ll add an event listener on those that fires the appropriate method when the element is clicked.

<!-- CalendarDateSelector.vue --> <template>   <div class="calendar-date-selector">     <span @click="selectPrevious">﹤</span>     <span @click="selectCurrent">Today</span>     <span @click="selectNext">﹥</span>   </div> </template>

Then, in the script section, we will set up two props that the component will accept:

  • currentDate allows us to come back to current month when the “Today” button is clicked.
  • selectedDate tells us what month is currently selected.

We will also define methods responsible for calculating the new selected date based on the currently selected date using the subtract and add methods from Day.js. Each method will also $ emit an event to the parent component with the newly selected month. This allows us to keep the value of selected date in one place — which will be our CalendarMonth.vue component — and pass it down to all child components (i.e. header, calendar grid).

// CalendarDateSelector.vue <script> import dayjs from "dayjs";  export default {   name: "CalendarDateSelector",    props: {     currentDate: {       type: String,       required: true     },      selectedDate: {       type: Object,       required: true     }   },    methods: {     selectPrevious() {       let newSelectedDate = dayjs(this.selectedDate).subtract(1, "month");       this.$ emit("dateSelected", newSelectedDate);     },      selectCurrent() {       let newSelectedDate = dayjs(this.currentDate);       this.$ emit("dateSelected", newSelectedDate);     },      selectNext() {       let newSelectedDate = dayjs(this.selectedDate).add(1, "month");       this.$ emit("dateSelected", newSelectedDate);     }   } }; </script>

Now, let’s go back to the CalendarMonth.vue component and use our newly created components.

To use them we first need to import and register the components, also we need to create the values that will be passed as props to those components:

  • today properly formats today’s date and is used as a value for the “Today” pagination button.
  • selectedDate is the  currently selected date (set to today’s date by default).

The last thing we need to do before we can render the components is create a method that’s responsible for changing the value of selectedDate. This method will be fired when the event from the pagination component is received.

// CalendarMonth.vue <script> import dayjs from "dayjs"; import CalendarDateIndicator from "./CalendarDateIndicator"; import CalendarDateSelector from "./CalendarDateSelector";  export default {   components: {     CalendarDateIndicator,     CalendarDateSelector   },    data() {     return {       selectedDate: dayjs(),       today: dayjs().format("YYYY-MM-DD")     };   },    methods: {     selectDate(newSelectedDate) {       this.selectedDate = newSelectedDate;     }   } }; </script>

Now we have everything we need to render our calendar header:

<!-- CalendarMonth.vue --> <template>   <div class="calendar-month">     <div class="calendar-month-header">       <CalendarDateIndicator         :selected-date="selectedDate"         class="calendar-month-header-selected-month"       />       <CalendarDateSelector         :current-date="today"         :selected-date="selectedDate"         @dateSelected="selectDate"       />     </div>   </div> </template>

This is a good spot to stop and see what we have so far. Our calendar header is doing everything we want, so let’s move forward and create components for our calendar grid.

Step 3: Calendar grid components

Here, again, we have two components:

  • CalendarWeekdays shows the names of the weekdays.
  • CalendarMonthDayItem represents a single day in the calendar.

The CalendarWeekdays component contains a list that iterates through the weekday labels (using the v-for directive) and renders that label for each weekday. In the script section, we need to define our weekdays and create a computed property to make it available in the template and cache the result to prevent us from having to re-calculate it in the future.

// CalendarWeekdays.vue <template>   <ol class="day-of-week">     <li       v-for="weekday in weekdays"       :key="weekday"     >       {{ weekday }}     </li>   </ol> </template> 
 <script> const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];  export default {   name: 'CalendarWeekdays',    computed: {     weekdays() {       return WEEKDAYS     }   } } </script>

Next is CalendarMonthDayItem. It’s a list item that receives a day property that is an object, and a boolean prop, isToday, that allows us to style the list item to indicate that it’s the current date. We also have one computed property that formats the received day object to our desired date format (D, or the numeric day of the month).

// CalendarMonthDayItem.vue <template>   <li     class="calendar-day"     :class="{       'calendar-day--not-current': !isCurrentMonth,       'calendar-day--today': isToday     }"   >     <span>{{ label }}</span>   </li> </template> 
 <script> import dayjs from "dayjs";  export default {   name: "CalendarMonthDayItem",    props: {     day: {       type: Object,       required: true     },      isCurrentMonth: {       type: Boolean,       default: false     },      isToday: {       type: Boolean,       default: false     }   },    computed: {     label() {       return dayjs(this.day.date).format("D");     }   } }; </script>

OK, now that we have these two components, let’s see how we can add them to our CalendarMonth component.

We first need to import and register them. We also need to create a computed property that will return an array of objects representing our days. Each day contains a date property and isCurrentMonth property.

// CalendarMonth.vue <script> import dayjs from "dayjs"; import CalendarMonthDayItem from "./CalendarMonthDayItem"; import CalendarWeekdays from "./CalendarWeekdays"; 
 export default {   name: "CalendarMonth",    components: {     // ...     CalendarMonthDayItem,     CalendarWeekdays   },    computed: {     days() {       return [         { date: "2020-06-29", isCurrentMonth: false },         { date: "2020-06-30", isCurrentMonth: false },         { date: "2020-07-01", isCurrentMonth: true },         { date: "2020-07-02", isCurrentMonth: true },         // ...         { date: "2020-07-31", isCurrentMonth: true },         { date: "2020-08-01", isCurrentMonth: false },         { date: "2020-08-02", isCurrentMonth: false }       ];     }   } }; </script>

Then, in the template, we can render our components. Again, we use the v-for directive to render the required number of day elements.

<!-- CalendarMonth.vue --> <template>   <div class="calendar-month">     <div class="calendar-month-header">       // ...     </div>      <CalendarWeekdays/>      <ol class="days-grid">       <CalendarMonthDayItem         v-for="day in days"         :key="day.date"         :day="day"         :is-today="day.date === today"       />     </ol>   </div> </template>

OK, things are starting to look good now. Have a look at where we are. It looks nice but, as you probably noticed, the template only contains static data at the moment. The month is hardcoded as July and the day numbers are hardcoded as well. We will change that by calculating what date should be shown on a specific month. Let’s dive into the code!

Step 4: Setting up current month calendar

Let’s think how we can calculate the date that should be shown on a specific month. That’s where Day.js really comes into play. It provides all the data we need to properly place dates on the correct days of the week for a given month using real calendar data. It allows us to get and set anything from the start date of a month to all the date formatting options we need to display the data.

We will:

  • Get the current month
  • Calculate where the days should be placed (weekdays)
  • Calculate the days for displaying dates from the previous and next months
  • Put all of the days together in a single array

We already have Day.js imported in our CalendarMonth component. We’re also going to lean on a couple of Day.js plugins for help. WeekDay helps us set the first day of the week. Some prefer Sunday as the first day of the week. Other prefer Monday. Heck, in some cases, it makes sense to start with Friday. We’re going to start with Monday.

The WeekOfYear plugin returns the numeric value for the current week out of all weeks in the year. There are 52 weeks in a year, so we’d say that the week starting January 1 is the the first week of the year, and so on.

Here’s what we put into CalendarMonth.vue to put all of that to use:

// CalendarMonth.vue <script> import dayjs from "dayjs"; import weekday from "dayjs/plugin/weekday"; import weekOfYear from "dayjs/plugin/weekOfYear"; // ... 
 dayjs.extend(weekday); dayjs.extend(weekOfYear); // ...

That was pretty straightforward but now the real fun starts as we will now play with the calendar grid. Let’s stop for a second a think what we really need to do to get that right.

First, we want the date numbers to fall in the correct weekday columns. For example, July 1, 2020, is on a Wednesday. That’s where the date numbering should start.

If the first of the month falls on Wednesday, then that means we’ll have empty grid items for Monday and Tuesday in the first week. The last day of the month is July 31, which falls on a Friday. That means Saturday and Sunday will be empty in the last week of the grid. We want to fill those with the trailing and leading dates of the previous and next months, respectively, so that the calendar grid is always full.

Calendar grid showing the first two and last two days highlighted in red, indicated they are coming from the previous and next months.

Adding dates for the current month

To add the days of the current month to the grid, we need to know how many days exist in the current month. We can get that using the daysInMonth method provided by Day.js. Let’s create a computed property for that.

// CalendarMonth.vue computed: {   // ...   numberOfDaysInMonth() {       return dayjs(this.selectedDate).daysInMonth();   } }

When we know that, we create an empty array with a length that’s equal to number of days in the current month. Then we map() that array and create a day object for each one. The object we create has an arbitrary structure, so you can add other properties if you need them.

In this example, though, we need a date property that will be used to check if a particular date is the current day. We’ll also return a isCurrentMonth value that checks whether the date is in the current month or outside of it. If it is outside the current month, we will style those so folks know they are outside the range of the current month.

// CalendarMonth.vue computed: {   // ...   currentMonthDays() {     return [...Array(this.numberOfDaysInMonth)].map((day, index) => {       return {         date: dayjs(`$ {this.year}-$ {this.month}-$ {index + 1}`).format("YYYY-MM-DD")         isCurrentMonth: true       };     });   }, }

Adding dates from the previous month

To get dates from the previous month to display in the current month, we need to check what the weekday of the first day is in selected month. That’s where we can use the WeekDay plugin for Day.js. Let’s create a helper method for that.

// CalendarMonth.vue methods: {   // ...   getWeekday(date) {     return dayjs(date).weekday();   }, }

Then, based on that, we need to check which day was the last Monday in the previous month. We need that value to know how many days from the previous month should be visible in the current month view. We can get that by subtracting the weekday value from the first day of the current month. For example, if first day of the month is Wednesday, we need to subtract three days to get last Monday of the previous month. Having that value allows us to create an array of day objects starting from the last Monday of the previous month through the end of that month.

// CalendarMonth.vue computed: {   // ...   previousMonthDays() {     const firstDayOfTheMonthWeekday = this.getWeekday(this.currentMonthDays[0].date);     const previousMonth = dayjs(`$ {this.year}-$ {this.month}-01`).subtract(1, "month");     const previousMonthLastMondayDayOfMonth = dayjs(this.currentMonthDays[0].date).subtract(firstDayOfTheMonthWeekday - 1, "day").date();      // Cover first day of the month being sunday      (firstDayOfTheMonthWeekday === 0)     const visibleNumberOfDaysFromPreviousMonth = firstDayOfTheMonthWeekday ? firstDayOfTheMonthWeekday - 1 : 6;     return [...Array(visibleNumberOfDaysFromPreviousMonth)].map((day, index) = {       return {         date: dayjs(`$ {previousMonth.year()}-$ {previousMonth.month() + 1}-$ {previousMonthLastMondayDayOfMonth + index}`).format("YYYY-MM-DD"),         isCurrentMonth: false       };     });   } }

Adding dates from the next month

Now, let’s do the reverse and calculate which days we need from the next month to fill in the grid for the current month. Fortunately, we can use the same helper we just created for the previous month calculation. The difference is that we will calculate how many days from the next month should be visible by subtracting that weekday numeric value from seven.

So, for example, if the last day of the month is Saturday, we need to subtract one day from seven to construct an array of dates needed from next month (Sunday).

// CalendarMonth.vue computed: {   // ...   nextMonthDays() {     const lastDayOfTheMonthWeekday = this.getWeekday(`$ {this.year}-$ {this.month}-$ {this.currentMonthDays.length}`);     const nextMonth = dayjs(`$ {this.year}-$ {this.month}-01`).add(1, "month");     const visibleNumberOfDaysFromNextMonth = lastDayOfTheMonthWeekday ? 7 - lastDayOfTheMonthWeekday : lastDayOfTheMonthWeekday;      return [...Array(visibleNumberOfDaysFromNextMonth)].map((day, index) => {       return {         date: dayjs(`$ {nextMonth.year()}-$ {nextMonth.month() + 1}-$ {index + 1}`).format("YYYY-MM-DD"),         isCurrentMonth: false       };     });   } }

OK, we know how to create all days we need, so let’s use them and merge all days into a single array of all the days we want to show in the current month, including filler dates from the previous and next months.

// CalendarMonth.vue computed: {   // ...   days() {     return [       ...this.previousMonthDays,       ...this.currentMonthDays,       ...this.nextMonthDays     ];   }, }

 Voilà, there we have it! Check out the final demo to see everything put together.


The post Let’s Make a Vue-Powered Monthly Calendar appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

How to Make a Monthly Calendar With Real Data

Have you ever seen a calendar on a webpage and thought, how the heck did they did that? For something like that, it might be natural to reach for a plugin, or even an embedded Google Calendar, but it’s actually a lot more straightforward to make one than you might think and only requires the trifecta of HTML, CSS and JavaScript. Let’s make one together!

I’ve set up a demo over at CodeSandbox so you can see what we’re aiming for.

Let’s first identify some requirements for what the calendar should do. It should:

  • Display a month grid for a given month
  • Display dates from the previous and next months to so the grid is always full
  • Indicate current date
  • Show the name of the currently selected month
  • Navigate to the previous and next month
  • Allow the user to navigate back to current month with a single click

Oh, and we’ll build this as a single page application that fetches calendar dates from Day.js, a super light utility library.

We’re going to shy away from choosing a specific framework to keep things easy. For this setup, I’m using Parcel for package management so I can write in Babel, bundle things up, and manage the one and only dependency for the project. Check out the package.json file over at CodeSandbox for specifics.

Step 1: Start with the basic markup and styles

Let’s start with creating a basic template for our calendar. This doesn’t need to be anything fancy. But it also should be done without resorting to tables.

We can outline our markup as three layers where we have:

  • A section for the calendar header. This will show the currently selected month and the elements responsible for paginating between months.
  • A section for the calendar grid header. Again, we’re not reaching form tables, but this would be sort of like a table header that holds a list containing the days of the week, starting with Monday.
  • The calendar grid. You know, each day in the current month, represented as a square in the grid.

Let’s write this up in a file called index.js. This can go inside a src folder in the project folder. We will indeed have an index.html file in the project root that imports our work, but the primary markup will live in the JavaScript file.

<!-- index.js --> document.getElementById("app").innerHTML = ` <!-- Parent container for the calendar month --> <div class="calendar-month">   <!-- The calendar header -->   <section class="calendar-month-header">     <!-- Month name -->     <div       id="selected-month"       class="calendar-month-header-selected-month"     >       July 2020     </div> 
     <!-- Pagination -->     <div class="calendar-month-header-selectors">       <span id="previous-month-selector"><</span>       <span id="present-month-selector">Today</span>       <span id="next-month-selector">></span>     </div>   </section>      <!-- Calendar grid header -->   <ol     id="days-of-week"     class="day-of-week"   >     <li>Mon</li>     ...     <li>Sun</li>   </ol> 
   <!-- Calendar grid -->   <ol     id="calendar-days"     class="date-grid"   >     <li class="calendar-day">       <span>         1       </span>       ...       <span>         29       </span>     </li>   </ol> </div> `;

Let’s go ahead and import this file into that index.html file that lives in the root directory of the project. Nothing special happening here. It’s merely HTML boilerplate with an element that’s targeted by our app and registers our index.js file.

<!DOCTYPE html> <html>   <head>     <title>Parcel Sandbox</title>     <meta charset="UTF-8" />   </head>    <body>     <div id="app"></div>      <script src="src/index.js"></script>   </body> </html>

Now that we have some markup to work with, let’s style it up a bit so we have a good visual to start with. Specifically, we’re going to:

  • Position the elements using flexbox
  • Create a calendar frame using CSS grid
  • Position the labels within the cells

First up, let’s create a new styles.css file in the same src folder where we have index.js and drop this in:

body {   --grey-100: #e4e9f0;   --grey-200: #cfd7e3;   --grey-300: #b5c0cd;   --grey-800: #3e4e63;   --grid-gap: 1px;   --day-label-size: 20px; }  .calendar-month {   position: relative;   /* Color of the day cell borders */   background-color: var(--grey-200);   border: solid 1px var(--grey-200); } 
 /* Month indicator and selectors positioning */ .calendar-month-header {   display: flex;   justify-content: space-between;   background-color: #fff;   padding: 10px; } 
 /* Month indicator */ .calendar-month-header-selected-month {   font-size: 24px;   font-weight: 600; } 
 /* Month selectors positioning */ .calendar-month-header-selectors {   display: flex;   align-items: center;   justify-content: space-between;   width: 80px; } 
 .calendar-month-header-selectors > * {   cursor: pointer; } 
 /* | Mon | Tue | Wed | Thu | Fri | Sat | Sun | */ .day-of-week {   color: var(--grey-800);   font-size: 18px;   background-color: #fff;   padding-bottom: 5px;   padding-top: 10px; } 
 .day-of-week, .days-grid {   /* 7 equal columns for weekdays and days cells */   display: grid;   grid-template-columns: repeat(7, 1fr); } 
 .day-of-week > * {   /* Position the weekday label within the cell */   text-align: right;   padding-right: 5px; } 
 .days-grid {   height: 100%;   position: relative;   /* Show border between the days */   grid-column-gap: var(--grid-gap);   grid-row-gap: var(--grid-gap);   border-top: solid 1px var(--grey-200); } 
 .calendar-day {   position: relative;   min-height: 100px;   font-size: 16px;   background-color: #fff;   color: var(--grey-800);   padding: 5px; } 
 /* Position the day label within the day cell */ .calendar-day > span {   display: flex;   justify-content: center;   align-items: center;   position: absolute;   right: 2px;   width: var(--day-label-size);   height: var(--day-label-size); }

The key part that sets up our grid is this:

.day-of-week, .days-grid {   /* 7 equal columns for weekdays and days cells */   display: grid;   grid-template-columns: repeat(7, 1fr); }

Notice that both the calendar grid header and the calendar grid itself are using CSS grid to lay things out. We know there will always be seven days in a week, so that allows us to use the repeat() function to create seven columns that are proportional to one another. We’re also declaring a min-height of 100px on each date of the calendar to make sure the rows are consistent.

We need to hook these styles up with the markup, so let’s add this to the top of our index.js file:

import "./styles.css";

This is a good spot to stop and see what we have so far.

Step 2: Setting up current month calendar

As you probably noticed, the template only contains static data at the moment. The month is hardcoded as July and the day numbers are hardcoded as well. That’s where Day.js comes into play. It provides all the data we need to properly place dates on the correct days of the week for a given month using real calendar data. It allows us to get and set anything from the start date of a month to all the date formatting options we need to display the data.

We will:

  • Get the current month
  • Calculate where the days should be placed (weekdays)
  • Calculate the days for displaying dates from the previous and next months
  • Put all of the days together in a single array

First, we need to import Day.js and remove all static HTML (selected month, weekdays and days). We’ll do that by adding this to our index.js file right above where we imported the styles:

import dayjs from "dayjs";

We’re also going to lean on a couple of Day.js plugins for help. WeekDay helps us set the first day of the week. Some prefer Sunday as the first day of the week. Other prefer Monday. Heck, in some cases, it makes sense to start with Friday. We’re going to start with Monday.

The weekOfYear plugin returns the numeric value for the current week out of all weeks in the year. There are 52 weeks in a year, so we’d say that the week starting January 1 is the the first week of the year, and so on.

So here what we put into index.js right after our import statements:

const weekday = require("dayjs/plugin/weekday"); const weekOfYear = require("dayjs/plugin/weekOfYear"); 
 dayjs.extend(weekday); dayjs.extend(weekOfYear);

Once we strip out the hardocded calendar values, Here’s what we have in index.js so far:

import dayjs from "dayjs"; import "./styles.css"; const weekday = require("dayjs/plugin/weekday"); const weekOfYear = require("dayjs/plugin/weekOfYear"); 
 dayjs.extend(weekday); dayjs.extend(weekOfYear); 
 document.getElementById("app").innerHTML = ` <div class="calendar-month">   <section class="calendar-month-header">     <div       id="selected-month"       class="calendar-month-header-selected-month"     >     </div>     <div class="calendar-month-header-selectors">       <span id="previous-month-selector"><</span>       <span id="present-month-selector">Today</span>       <span id="next-month-selector">></span>     </div>   </section>      <ul     id="days-of-week"     class="day-of-week"   >   </ul>   <ul     id="calendar-days"     class="days-grid"   >   </ul> </div> `;

Now let’s set few constants. Specifically, we want to construct an array of days of the weeks (i.e. Monday, Tuesday, Wednesday, etc.):

const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

Then, we want to fetch the current year and set in in YYYY format:

const INITIAL_YEAR = dayjs().format("YYYY");

And we want to set the current month as the starting point when initially loading the calendar, where M formats the month as a numeric value (e.g. January equals 1):

const INITIAL_MONTH = dayjs().format("M");

Let’s go and populate our calendar grid header with the days of the week. First we grab the proper element (#days-of-week), then we iterate through our WEEKDAYS array, creating a list item element for each item in the array while setting the name for each one:

// Select the calendar grid header element const daysOfWeekElement = document.getElementById("days-of-week"); 
 // Loop through the array of weekdays WEEKDAYS.forEach(weekday => {   // For each item in the array, make a list item element   const weekDayElement = document.createElement("li");   // Append a child element inside the list item...   daysOfWeekElement.appendChild(weekDayElement);   /// ...that contains the value in the array   weekDayElement.innerText = weekday; });

Step 3: Creating the calendar grid

That was pretty straightforward but now the real fun starts as we will now play with the calendar grid. Let’s stop for a second a think what we really need to do to get that right.

First, we want the date numbers to fall in the correct weekday columns. For example, July 1, 2020 is on a Wednesday. That’s where the date numbering should start.

If the first of the month falls on Wednesday, then that means we’ll have empty grid items for Monday and Tuesday in the first week. The last day of the month is July 31, which falls on a Friday. That means Saturday and Sunday will be empty in the last week of the grid. We went to fill those with the trailing and leading dates of the previous and next months, respectively, so that the calendar grid is always full.

Create days for the current month

To add the days for the current month to the grid, we need to know how many days exist in the current month. We can get that using the daysInMonth method provided by Day.js. Let’s create a helper method for that.

function getNumberOfDaysInMonth(year, month) {   return dayjs(`$ {year}-$ {month}-01`).daysInMonth() }

When we know that, we create an empty array with a length that’s equal to number of days in the current month. Then we map() that array and create a day object for each one. The object we create has an arbitrary structure, so you can add other properties if you need them.

In this example, though, we need a date property that will be used to check if a particular date is the current day. We’ll also return a dayOfMonth property that acts as the label (e.g. 1, 2, 3 and so on). isCurrentMonth checks whether the date is in the current month or outside of it. If it is outside the current month, we will style those so folks know they are outside the range of the current month.

function createDaysForCurrentMonth(year, month) {   return [...Array(getNumberOfDaysInMonth(year, month))].map((day, index) => {     return {       date: dayjs(`$ {year}-$ {month}-$ {index + 1}`).format("YYYY-MM-DD"),       dayOfMonth: index + 1,       isCurrentMonth: true     };   }); }

Add dates from the previous month to the calendar grid

To get dates from the previous month to display in the current month, we need to check what is the weekday of the first day in selected month. That’s where we can use the WeekDay plugin for Day.js. Let’s create a helper method for that.

function getWeekday(date) {   return dayjs(date).weekday() }

Then, based on that, we need to check which day was the last Monday in the previous month. We need that value to know how many days from the previous month should be visible in the current month view. We can get that by subtracting the weekday value from the first day of the current month. For example, if first day of the month is Wednesday, we need to subtract 3 days to get last Monday of the previous month. Having that value allows us to create an array of day objects starting from the last Monday of the previous month through the end of that month.

function createDaysForPreviousMonth(year, month) {   const firstDayOfTheMonthWeekday = getWeekday(currentMonthDays[0].date); 
   const previousMonth = dayjs(`$ {year}-$ {month}-01`).subtract(1, "month");      const previousMonthLastMondayDayOfMonth = dayjs(     currentMonthDays[0].date   ).subtract(firstDayOfTheMonthWeekday - 1, "day").date(); 
   // Account for first day of the month on  a Sunday (firstDayOfTheMonthWeekday === 0)   const visibleNumberOfDaysFromPreviousMonth = firstDayOfTheMonthWeekday ? firstDayOfTheMonthWeekday - 1 : 6 
   return [...Array(visibleNumberOfDaysFromPreviousMonth)].map((day, index) => {         return {       date: dayjs(         `$ {previousMonth.year()}-$ {previousMonth.month() + 1}-$ {previousMonthLastMondayDayOfMonth + index}`       ).format("YYYY-MM-DD"),       dayOfMonth: previousMonthLastMondayDayOfMonth + index,       isCurrentMonth: false     };   }); }

Add dates from the next month to the calendar grid

Now, let’s do the reverse and calculate which days we need from the next month to fill in the grid for the current month. Fortunately, we can use the same helper we just created for the previous month calculation. The difference is that we will calculate how many days from the next month should be visible by subtracting that weekday numeric value from 7.

So, for example, if the last day of the month is Saturday, we need to subtract 1 day from 7 to construct an array of dates needed from next month (Sunday).

function createDaysForNextMonth(year, month) {   const lastDayOfTheMonthWeekday = getWeekday(`$ {year}-$ {month}-$ {currentMonthDays.length}`) 
   const visibleNumberOfDaysFromNextMonth = lastDayOfTheMonthWeekday ? 7 - lastDayOfTheMonthWeekday : lastDayOfTheMonthWeekday 
   return [...Array(visibleNumberOfDaysFromNextMonth)].map((day, index) => {     return {       date: dayjs(`$ {year}-$ {Number(month) + 1}-$ {index + 1}`).format("YYYY-MM-DD"),       dayOfMonth: index + 1,       isCurrentMonth: false     }   }) }

OK, we know how to create all days we need, let’s use the methods we just created and then merge all days into a single array of all the days we want to show in the current month, including filler dates from the previous and next months.

let currentMonthDays = createDaysForCurrentMonth(INITIAL_YEAR, INITIAL_MONTH) let previousMonthDays = createDaysForPreviousMonth(INITIAL_YEAR, INITIAL_MONTH, currentMonthDays[0]) let nextMonthDays = createDaysForNextMonth(INITIAL_YEAR, INITIAL_MONTH) 
 let days = [...this.previousMonthDays, ...this.currentMonthDays, ...this.nextMonthDays]

Here’s everything we just covered put together in index.js:

// Same as before ... 
 const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const INITIAL_YEAR = dayjs().format("YYYY"); const INITIAL_MONTH = dayjs().format("M"); const daysOfWeekElement = document.getElementById("days-of-week"); 
 // Add weekdays to calendar header WEEKDAYS.forEach(weekday => {   const weekDayElement = document.createElement("li");   daysOfWeekElement.appendChild(weekDayElement);   weekDayElement.innerText = weekday; }); 
 let currentMonthDays = createDaysForCurrentMonth(INITIAL_YEAR, INITIAL_MONTH); let previousMonthDays = createDaysForPreviousMonth(INITIAL_YEAR, INITIAL_MONTH); let nextMonthDays = createDaysForNextMonth(INITIAL_YEAR, INITIAL_MONTH); let days = [...previousMonthDays, ...currentMonthDays, ...nextMonthDays]; 
 console.log(days); 
 function getNumberOfDaysInMonth(year, month) {   return dayjs(`$ {year}-$ {month}-01`).daysInMonth(); } 
 function createDaysForCurrentMonth(year, month) {   return [...Array(getNumberOfDaysInMonth(year, month))].map((day, index) => {     return {       date: dayjs(`$ {year}-$ {month}-$ {index + 1}`).format("YYYY-MM-DD"),       dayOfMonth: index + 1,       isCurrentMonth: true     };   }); } 
 function createDaysForPreviousMonth(year, month) {   const firstDayOfTheMonthWeekday = getWeekday(currentMonthDays[0].date);   const previousMonth = dayjs(`$ {year}-$ {month}-01`).subtract(1, "month");   const previousMonthLastMondayDayOfMonth = dayjs(currentMonthDays[0].date)     .subtract(firstDayOfTheMonthWeekday - 1, "day")     .date();   // Cover first day of the month being sunday (firstDayOfTheMonthWeekday === 0)   const visibleNumberOfDaysFromPreviousMonth = firstDayOfTheMonthWeekday     ? firstDayOfTheMonthWeekday - 1     : 6; 
   return [...Array(visibleNumberOfDaysFromPreviousMonth)].map((day, index) => {     return {       date: dayjs(         `$ {previousMonth.year()}-$ {previousMonth.month() +           1}-$ {previousMonthLastMondayDayOfMonth + index}`       ).format("YYYY-MM-DD"),       dayOfMonth: previousMonthLastMondayDayOfMonth + index,       isCurrentMonth: false     };   }); }  function createDaysForNextMonth(year, month) {   const lastDayOfTheMonthWeekday = getWeekday(     `$ {year}-$ {month}-$ {currentMonthDays.length}`   );   const nextMonth = dayjs(`$ {year}-$ {month}-01`).add(1, "month");   const visibleNumberOfDaysFromNextMonth = lastDayOfTheMonthWeekday     ? 7 - lastDayOfTheMonthWeekday     : lastDayOfTheMonthWeekday;   return [...Array(visibleNumberOfDaysFromNextMonth)].map((day, index) => {     return {       date: dayjs(         `$ {nextMonth.year()}-$ {nextMonth.month() + 1}-$ {index + 1}`       ).format("YYYY-MM-DD"),       dayOfMonth: index + 1,       isCurrentMonth: false     };   }); }  function getWeekday(date) {   return dayjs(date).weekday(); }

Step 4: Show calendar dates

OK, so we have the basic markup for our calendar, the data we need to display dates from the current month, plus dates from the previous and next month to fill in empty grid items. Now we need to append the dates to the calendar!

We already have a container for the calendar grid, #calendar-days. Let’s grab that element.

const calendarDaysElement = document.getElementById("calendar-days");

Now, let’s create a function that will append a day to our calendar view.

function appendDay(day, calendarDaysElement) {   const dayElement = document.createElement("li");   const dayElementClassList = dayElement.classList; 
   // Generic calendar day class   dayElementClassList.add("calendar-day"); 
   // Container for day of month number   const dayOfMonthElement = document.createElement("span"); 
   // Content   dayOfMonthElement.innerText = day.dayOfMonth; 
   // Add an extra class to differentiate current month days from prev/next month days   if (!day.isCurrentMonth) {     dayElementClassList.add("calendar-day--not-current");   } 
   // Append the element to the container element   dayElement.appendChild(dayOfMonthElement);   calendarDaysElement.appendChild(dayElement); }

Notice that we’re tossing in a check for the dates that  are coming from the previous and next months so that we can add a class to style them differently from the dates in the current month:

.calendar-day--not-current {   background-color: var(--grey-100);   color: var(--grey-300); }

That’s it! Our calendar should now look as we wanted.

Step 5: Select current month

What we have so far is pretty nice, but we want the user to be able to paginate from month-to-month forwards and backwards in time, starting from the current month. We have most of the logic in place, so all we really need to do is to add a click listener to the pagination buttons that re-runs the days calculation and re-draws the calendar with updated data.

Before we begin, let’s define variables for dates that are in the current month, previous month, and next month so we can reference them throughout the code.

let currentMonthDays; let previousMonthDays; let nextMonthDays;

Now, let’s create a method that will be responsible for re-calculating the calendar days and re-rendering the calendar when paginating to another month. We will call that function createCalendar. This method will accept two attributes — year and month  — and based on that, the calendar will re-render with new data and without a new page load.

The method will replace the header content to always show the selected month label.

function createCalendar(year = INITIAL_YEAR, month = INITIAL_MONTH) {   document.getElementById("selected-month").innerText = dayjs(     new Date(year, month - 1)   ).format("MMMM YYYY"); 
   // ...

Then it will grab the calendar days container and remove all existing days.

// ... 
   const calendarDaysElement = document.getElementById("calendar-days");   removeAllDayElements(calendarDaysElement); 
   // ...

When the calendar is cleared, it will calculate new days that should be displayed using the methods we created before.

//... 
 currentMonthDays = createDaysForCurrentMonth(   year,   month,   dayjs(`$ {year}-$ {month}-01`).daysInMonth() ); 
 previousMonthDays = createDaysForPreviousMonth(year, month); 
 nextMonthDays = createDaysForNextMonth(year, month); 
 const days = [...previousMonthDays, ...currentMonthDays, ...nextMonthDays]; 
 // ...

And, finally, it will append a day element for each day.

// ... days.forEach(day => {   appendDay(day, calendarDaysElement); });

There is one piece of logic still missing: a removeAllDayElements method that clears the existing calendar. This method takes the first calendar day element, removes it, and replaces it with another one. From there, it will run the logic in a loop until all of the elements are removed.

function removeAllDayElements(calendarDaysElement) {   let first = calendarDaysElement.firstElementChild; 
   while (first) {     first.remove();     first = calendarDaysElement.firstElementChild;   } }

Now we can reuse that logic when we want to change the month. Recall the first step when we created a static template for our component. We added these elements:

<div class="calendar-month-header-selectors">   <span id="previous-month-selector"><</span>   <span id="present-month-selector">Today</span>   <span id="next-month-selector">></span> </div>

These are the controls for paginating between months. To change it, we need to store the currently selected month. Let’s create a variable to keep track of what that is and set its initial value to the present month.

let selectedMonth = dayjs(new Date(INITIAL_YEAR, INITIAL_MONTH - 1, 1));

Now, to make the selectors work, we need a bit of JavaScript. To make it more readable, we will create another method called initMonthSelectors and we will keep the logic there. This method will add event listeners to the selector elements. It will listen for click events and update the value of selectedMonth to the name of the newly selected month before running the createCalendar method with proper year and month values.

function initMonthSelectors() {   document   .getElementById("previous-month-selector")   .addEventListener("click", function() {     selectedMonth = dayjs(selectedMonth).subtract(1, "month");     createCalendar(selectedMonth.format("YYYY"), selectedMonth.format("M"));   }); 
   document   .getElementById("present-month-selector")   .addEventListener("click", function() {     selectedMonth = dayjs(new Date(INITIAL_YEAR, INITIAL_MONTH - 1, 1));     createCalendar(selectedMonth.format("YYYY"), selectedMonth.format("M"));   }); 
   document   .getElementById("next-month-selector")   .addEventListener("click", function() {     selectedMonth = dayjs(selectedMonth).add(1, "month");     createCalendar(selectedMonth.format("YYYY"), selectedMonth.format("M"));   }); }

That’s it! Our calendar is ready. While that’s great and all, it would be even nicer if we could mark the current date so it stands out from the rest. That shouldn’t be very hard. We are already styling days that are not in the selected month, so let’s do similar thing to that.

We’ll create a variable that’s set for today:

const TODAY = dayjs().format("YYYY-MM-DD");

Then, in the appendDay method where we apply a class for dates outside the current month, we have to add another check to see if the element is today’s date. If it is, we’ll add a class to that element:

function appendDay(day, calendarDaysElement) {   // ...   if (day.date === TODAY) {     dayElementClassList.add("calendar-day--today");   } }

Now we can style it!

.calendar-day--today {   padding-top: 4px; } 
 .calendar-day--today > div {   color: #fff;   border-radius: 9999px;   background-color: var(--grey-800); }

Voilà, there we have it! Check out the final demo to see everything put together.


The post How to Make a Monthly Calendar With Real Data appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Hey, let’s create a functional calendar app with the JAMstack

Hey, let’s create a functional calendar app with the JAMstack

I’ve always wondered how dynamic scheduling worked so I decided to do extensive research, learn new things, and write about the technical part of the journey. It’s only fair to warn you: everything I cover here is three weeks of research condensed into a single article. Even though it’s beginner-friendly, it’s a healthy amount of reading. So, please, pull up a chair, sit down and let’s have an adventure.

My plan was to build something that looked like Google Calendar but only demonstrate three core features:

  1. List all existing events on a calendar
  2. Create new events
  3. Schedule and email notification based on the date chosen during creation. The schedule should run some code to email the user when the time is right.

Pretty, right? Make it to the end of the article, because this is what we’ll make.

A calendar month view with a pop-up form for creating a new event as an overlay.

The only knowledge I had about asking my code to run at a later or deferred time was CRON jobs. The easiest way to use a CRON job is to statically define a job in your code. This is ad hoc — statically means that I cannot simply schedule an event like Google Calendar and easily have it update my CRON code. If you are experienced with writing CRON triggers, you feel my pain. If you’re not, you are lucky you might never have to use CRON this way.

To elaborate more on my frustration, I needed to trigger a schedule based on a payload of HTTP requests. The dates and information about this schedule would be passed in through the HTTP request. This means there’s no way to know things like the scheduled date beforehand.

We (my colleagues and I) figured out a way to make this work and — with the help of Sarah Drasner’s article on Durable Functions — I understood what I needed learn (and unlearn for that matter). You will learn about everything I worked in this article, from event creation to email scheduling to calendar listings. Here is a video of the app in action:

You might notice the subtle delay. This has nothing to do with the execution timing of the schedule or running the code. I am testing with a free SendGrid account which I suspect have some form of latency. You can confirm this by testing the serverless function responsible without sending emails. You would notice that the code runs at exactly the scheduled time.

Tools and architecture

Here are the three fundamental units of this project:

  1. React Frontend: Calendar UI, including the UI to create, update or delete events.
  2. 8Base GraphQL: A back-end database layer for the app. This is where we will store, read and update our date from. The fun part is you won’t write any code for this back end.
  3. Durable Functions: Durable functions are kind of Serverless Functions that have the power of remembering their state from previous executions. This is what replaces CRON jobs and solves the ad hoc problem we described earlier.

See the Pen
durable-func1
by Chris Nwamba (@codebeast)
on CodePen.

The rest of this post will have three major sections based on the three units we saw above. We will take them one after the other, build them out, test them, and even deploy the work. Before we get on with that, let’s setup using a starter project I made to get us started.

Project Repo

Getting Started

You can set up this project in different ways — either as a full-stack project with the three units in one project or as a standalone project with each unit living in it’s own root. Well, I went with the first because it’s more concise, easier to teach, and manageable since it’s one project.

The app will be a create-react-app project and I made a starter for us to lower the barrier to set up. It comes with supplementary code and logic that we don’t need to explain since they are out of the scope of the article. The following are set up for us:

  1. Calendar component
  2. Modal and popover components for presenting event forms
  3. Event form component
  4. Some GraphQL logic to query and mutate data
  5. A Durable Serverless Function scaffold where we will write the schedulers

Tip: Each existing file that we care about has a comment block at the top of the document. The comment block tells you what is currently happening in the code file and a to-do section that describes what we are required to do next.

Start by cloning the starter form Github:

git clone -b starter --single-branch https://github.com/christiannwamba/calendar-app.git

Install the npm dependencies described in the root package.json file as well as the serverless package.json:

npm install

Orchestrated Durable Functions for scheduling

There are two words we need to get out of the way first before we can understand what this term is — orchestration and durable.

Orchestration was originally used to describe an assembly of well-coordinated events, actions, etc. It is heavily borrowed in computing to describe a smooth coordination of computer systems. The key word is coordinate. We need to put two or more units of a system together in a coordinated way.

Durable is used to describe anything that has the outstanding feature of lasting longer.

Put system coordination and long lasting together, and you get Durable Functions. This is the most powerful feature if Azure’s Serverless Function. Durable Functions based in what we now know have these two features:

  1. They can be used to assemble the execution of two or more functions and coordinate them so race conditions do not occur (orchestration).
  2. Durable Functions remember things. This is what makes it so powerful. It breaks the number one rule of HTTP: stateless. Durable functions keep their state intact no matter how long they have to wait. Create a schedule for 1,000,000 years into the future and a durable function will execute after one million years while remembering the parameters that were passed to it on the day of trigger. That means Durable Functions are stateful.

These durability features unlock a new realm of opportunities for serverless functions and that is why we are exploring one of those features today. I highly recommend Sarah’s article one more time for a visualized version of some of the possible use cases of Durable Functions.

I also made a visual representation of the behavior of the Durable Functions we will be writing today. Take this as an animated architectural diagram:

Shows the touch-points of a serverless system.

A data mutation from an external system (8Base) triggers the orchestration by calling the HTTP Trigger. The trigger then calls the orchestration function which schedules an event. When the time for execution is due, the orchestration function is called again but this time skips the orchestration and calls the activity function. The activity function is the action performer. This is the actual thing that happens e.g. “send email notification”.

Create orchestrated Durable Functions

Let me walk you through creating functions using VS Code. You need two things:

  1. An Azure account
  2. VS Code

Once you have both setup, you need to tie them together. You can do this using a VS Code extension and a Node CLI tool. Start with installing the CLItool:

 npm install -g azure-functions-core-tools  # OR  brew tap azure/functions brew install azure-functions-core-tools 

Next, install the Azure Function extension to have VS Code tied to Functions on Azure. You can read more about setting up Azure Functions from my previous article.


Now that you have all the setup done, let’s get into creating these functions. The functions we will be creating will map to the following folders.

Folder Function
schedule Durable HTTP Trigger
scheduleOrchestrator Durable Orchestration
sendEmail Durable Activity

Start with the trigger.

  1. Click on the Azure extension icon and follow the image below to create the schedule function

    Shows the interface steps going from Browse to JavaScript to Durable Functions HTTP start to naming the function schedule.
  2. Since this is the first function, we chose the folder icon to create a function project. The icon after that creates a single function (not a project).
  3. Click Browse and create a serverless folder inside the project. Select the new serverless folder.
  4. Select JavaScript as the language. If TypeScript (or any other language) is your jam, please feel free.
  5. Select Durable Functions HTTP starter. This is the trigger.
  6. Name the first function as schedule

Next, create the orchestrator. Instead of creating a function project, create a function instead.

  1. Click on the function icon:

  2. Select Durable Functions orchestrator.
  3. Give it a name, scheduleOrchestrator and hit Enter.
  4. You will be asked to select a storage account. Orchestrator uses storage to preserve the state of a function-in-process.
  5. Select a subscription in your Azure account. In my case, I chose the free trial subscription.
  6. Follow the few remaining steps to create a storage account.

Finally, repeat the previous step to create an Activity. This time, the following should be different:

  • Select Durable Functions activity.
  • Name it sendEmail.
  • No storage account will be needed.

Scheduling with a durable HTTP trigger

The code in serverless/schedule/index.js does not need to be touched. This is what it looks like originally when the function is scaffolded using VS Code or the CLI tool.

const df = require("durable-functions"); module.exports = async function (context, req) {   const client = df.getClient(context);   const instanceId = await client.startNew(req.params.functionName, undefined, req.body);   context.log(`Started orchestration with ID = '$ {instanceId}'.`);   return client.createCheckStatusResponse(context.bindingData.req, instanceId); };

What is happening here?

  1. We’re creating a durable function on the client side that is based on the context of the request.
  2. We’re calling the orchestrator using the client’s startNew() function. The orchestrator function name is passed as the first argument to startNew() via the params object. A req.body is also passed to startNew() as third argument which is forwarded to the orchestrator.
  3. Finally, we return a set of data that can be used to check the status of the orchestrator function, or even cancel the process before it’s complete.

The URL to call the above function would look like this:

http://localhost:7071/api/orchestrators/{functionName}

Where functionName is the name passed to startNew. In our case, it should be:

//localhost:7071/api/orchestrators/scheduleOrchestrator

It’s also good to know that you can change how this URL looks.

Orchestrating with a Durable Orchestrator

The HTTP trigger startNew call calls a function based on the name we pass to it. That name corresponds to the name of the function and folder that holds the orchestration logic. The serverless/scheduleOrchestrator/index.js file exports a Durable Function. Replace the content with the following:

const df = require("durable-functions"); module.exports = df.orchestrator(function* (context) {   const input = context.df.getInput()   // TODO -- 1      // TODO -- 2 });

The orchestrator function retrieves the request body from the HTTP trigger using context.df.getInput().

Replace TODO -- 1 with the following line of code which might happen to be the most significant thing in this entire demo:

yield context.df.createTimer(new Date(input.startAt))

What this line does use the Durable Function to create a timer based on the date passed in from the request body via the HTTP trigger.

When this function executes and gets here, it will trigger the timer and bail temporarily. When the schedule is due, it will come back, skip this line and call the following line which you should use in place of TODO -- 2.

return yield context.df.callActivity('sendEmail', input);

The function would call the activity function to send an email. We are also passing a payload as the second argument.

This is what the completed function would look like:

const df = require("durable-functions");  module.exports = df.orchestrator(function* (context) {   const input = context.df.getInput()        yield context.df.createTimer(new Date(input.startAt))        return yield context.df.callActivity('sendEmail', input); });

Sending email with a durable activity

When a schedule is due, the orchestrator comes back to call the activity. The activity file lives in serverless/sendEmail/index.js. Replace what’s in there with the following:

const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(process.env['SENDGRID_API_KEY']);  module.exports = async function(context) {   // TODO -- 1   const msg = {}   // TODO -- 2   return msg; };

It currently imports SendGrid’s mailer and sets the API key. You can get an API Key by following these instructions.

I am setting the key in an environmental variable to keep my credentials safe. You can safely store yours the same way by creating a SENDGRID_API_KEY key in serverless/local.settings.json with your SendGrid key as the value:

{   "IsEncrypted": false,   "Values": {     "AzureWebJobsStorage": "<<AzureWebJobsStorage>",     "FUNCTIONS_WORKER_RUNTIME": "node",     "SENDGRID_API_KEY": "<<SENDGRID_API_KEY>"   } }

Replace TODO -- 1 with the following line:

const { email, title, startAt, description } = context.bindings.payload;

This pulls out the event information from the input from the orchestrator function. The input is attached to context.bindings. payload can be anything you name it so go to serverless/sendEmail/function.json and change the name value to payload:

{   "bindings": [     {       "name": "payload",       "type": "activityTrigger",       "direction": "in"     }   ] }

Next, update TODO -- 2 with the following block to send an email:

const msg = {   to: email,   from: { email: 'chris@codebeast.dev', name: 'Codebeast Calendar' },   subject: `Event: $ Hey, let’s create a functional calendar app with the JAMstack`,   html: `<h4>$ Hey, let’s create a functional calendar app with the JAMstack @ $ {startAt}</h4> <p>$ {description}</p>` }; sgMail.send(msg);  return msg;

Here is the complete version:

const sgMail = require('@sendgrid/mail'); sgMail.setApiKey(process.env['SENDGRID_API_KEY']);  module.exports = async function(context) {   const { email, title, startAt, description } = context.bindings.payload;   const msg = {     to: email,     from: { email: 'chris@codebeast.dev', name: 'Codebeast Calendar' },     subject: `Event: $ Hey, let’s create a functional calendar app with the JAMstack`,     html: `<h4>$ Hey, let’s create a functional calendar app with the JAMstack @ $ {startAt}</h4> <p>$ {description}</p>`   };   sgMail.send(msg);    return msg; };

Deploying functions to Azure

Deploying functions to Azure is easy. It’s merely a click away from the VS Code editor. Click on the circled icon to deploy and get a deploy URL:

Still with me this far in? You’re making great progress! It’s totally OK to take a break here, nap, stretch or get some rest. I definitely did while writing this post.

Data and GraphQL layer with 8Base

My easiest description and understanding of 8Base is “Firebase for GraphQL.” 8Base is a database layer for any kind of app you can think of and the most interesting aspect of it is that it’s based on GraphQL.

The best way to describe where 8Base fits in your stack is to paint a picture of a scenario.

Imagine you are a freelance developer with a small-to-medium scale contract to build an e-commerce store for a client. Your core skills are on the web so you are not very comfortable on the back end. though you can write a bit of Node.

Unfortunately, e-commerce requires managing inventories, order management, managing purchases, managing authentication and identity, etc. “Manage” at a fundamental level just means data CRUD and data access.

Instead of the redundant and boring process of creating, reading, updating, deleting, and managing access for entities in our backend code, what if we could describe these business requirements in a UI? What if we can create tables that allow us to configure CRUD operations, auth and access? What if we had such help and only focus on building frontend code and writing queries? Everything we just described is tackled by 8Base

Here is an architecture of a back-end-less app that relies on 8Base as it’s data layer:

Create an 8Base table for events storage and retrieval

The first thing we need to do before creating a table is to create an account. Once you have an account, create a workspace that holds all the tables and logic for a given project.

Next, create a table, name the table Events and fill out the table fields.

We need to configure access levels. Right now, there’s nothing to hide from each user, so we can just turn on all access to the Events table we created:

Setting up Auth is super simple with 8base because it integrates with Auth0. If you have entities that need to be protected or want to extend our example to use auth, please go wild.

Finally, grab your endpoint URL for use in the React app:

Testing GraphQL Queries and mutation in the playground

Just to be sure that we are ready to take the URL to the wild and start building the client, let’s first test the API with a GraphQL playground and see if the setup is fine. Click on the explorer.

Paste the following query in the editor.

query {   eventsList {     count     items {       id       title       startAt       endAt       description       allDay       email     }   } }

I created some test data through the 8base UI and I get the result back when I run they query:

You can explore the entire database using the schema document on the right end of the explore page.

Calendar and event form interface

The third (and last) unit of our project is the React App which builds the user interfaces. There are four major components making up the UI and they include:

  1. Calendar: A calendar UI that lists all the existing events
  2. Event Modal: A React modal that renders EventForm component to create a component
  3. Event Popover: Popover UI to read a single event, update event using EventForm or delete event
  4. Event Form: HTML form for creating new event

Before we dive right into the calendar component, we need to setup React Apollo client. The React Apollo provider empowers you with tools to query a GraphQL data source using React patterns. The original provider allows you to use higher order components or render props to query and mutate data. We will be using a wrapper to the original provider that allows you query and mutate using React Hooks.

In src/index.js, import the React Apollo Hooks and the 8base client in TODO -- 1:

import { ApolloProvider } from 'react-apollo-hooks'; import { EightBaseApolloClient } from '@8base/apollo-client';

At TODO -- 2, configure the client with the endpoint URL we got in the 8base setup stage:

const URI = 'https://api.8base.com/cjvuk51i0000701s0hvvcbnxg';  const apolloClient = new EightBaseApolloClient({   uri: URI,   withAuth: false });

Use this client to wrap the entire App tree with the provider on TODO -- 3:

ReactDOM.render(   <ApolloProvider client={apolloClient}>     <App />   </ApolloProvider>,   document.getElementById('root') );

Showing events on the calendar

The Calendar component is rendered inside the App component and the imports BigCalendar component from npm. Then :

  1. We render Calendar with a list of events.
  2. We give Calendar a custom popover (EventPopover) component that will be used to edit events.
  3. We render a modal (EventModal) that will be used to create new events.

The only thing we need to update is the list of events. Instead of using the static array of events, we want to query 8base for all store events.

Replace TODO -- 1 with the following line:

const { data, error, loading } = useQuery(EVENTS_QUERY);

Import the useQuery library from npm and the EVENTS_QUERY at the beginning of the file:

import { useQuery } from 'react-apollo-hooks'; import { EVENTS_QUERY } from '../../queries';

EVENTS_QUERY is exactly the same query we tested in 8base explorer. It lives in src/queries and looks like this:

export const EVENTS_QUERY = gql`   query {     eventsList {       count       items {         id         ...       }     }   } `;

Let’s add a simple error and loading handler on TODO -- 2:

if (error) return console.log(error);   if (loading)     return (       <div className="calendar">         <p>Loading...</p>       </div>     );

Notice that the Calendar component uses the EventPopover component to render a custom event. You can also observe that the Calendar component file renders EventModal as well. Both components have been setup for you, and their only responsibility is to render EventForm.

Create, update and delete events with the event form component

The component in src/components/Event/EventForm.js renders a form. The form is used to create, edit or delete an event. At TODO -- 1, import useCreateUpdateMutation and useDeleteMutation:

import {useCreateUpdateMutation, useDeleteMutation} from './eventMutationHooks'
  • useCreateUpdateMutation: This mutation either creates or updates an event depending on whether the event already existed.
  • useDeleteMutation: This mutation deletes an existing event.

A call to any of these functions returns another function. The function returned can then serve as an even handler.

Now, go ahead and replace TODO -- 2 with a call to both functions:

const createUpdateEvent = useCreateUpdateMutation(   payload,   event,   eventExists,   () => closeModal() ); const deleteEvent = useDeleteMutation(event, () => closeModal());

These are custom hooks I wrote to wrap the useMutation exposed by React Apollo Hooks. Each hook creates a mutation and passes mutation variable to the useMutation query. The blocks that look like the following in src/components/Event/eventMutationHooks.js are the most important parts:

useMutation(mutationType, {   variables: {     data   },   update: (cache, { data }) => {     const { eventsList } = cache.readQuery({       query: EVENTS_QUERY     });     cache.writeQuery({       query: EVENTS_QUERY,       data: {         eventsList: transformCacheUpdateData(eventsList, data)       }     });     //..   } });

Call the Durable Function HTTP trigger from 8Base

We have spent quite some time in building the serverless structure, data storage and UI layers of our calendar app. To recap, the UI sends data to 8base for storage, 8base saves data and triggers the Durable Function HTTP trigger, the HTTP trigger kicks in orchestration and the rest is history. Currently, we are saving data with mutation but we are not calling the serverless function anywhere in 8base.

8base allows you to write custom logic which is what makes it very powerful and extensible. Custom logic are simple functions that are called based on actions performed on the 8base database. For example, we can set up a logic to be called every time a mutation occurs on a table. Let’s create one that is called when an event is created.

Start by installing the 8base CLI:

npm install -g 8base

On the calendar app project run the following command to create a starter logic:

8base init 8base

8base init command creates a new 8base logic project. You can pass it a directory name which in this case we are naming the 8base logic folder, 8base — don’t get it twisted.

Trigger scheduling logic

Delete everything in 8base/src and create a triggerSchedule.js file in the src folder. Once you have done that, drop in the following into the file:

const fetch = require('node-fetch');  module.exports = async event => {   const res = await fetch('<HTTP Trigger URL>', {     method: 'POST',     body: JSON.stringify(event.data),     headers: { 'Content-Type': 'application/json' }   })   const json = await res.json();   console.log(event, json)   return json; };

The information about the GraphQL mutation is available on the event object as data.

Replace <HTTP Trigger URL> with the URL you got after deploying your function. You can get the URL by going to the function in your Azure URL and click “Copy URL.”

You also need to install the node-fetch module, which will grab the data from the API:

npm install --save node-fetch

8base logic configuration

The next thing to do is tell 8base what exact mutation or query that needs to trigger this logic. In our case, a create mutation on the Events table. You can describe this information in the 8base.yml file:

functions:   triggerSchedule:     handler:       code: src/triggerSchedule.js     type: trigger.after     operation: Events.create

In a sense, this is saying, when a create mutation happens on the Events table, please call src/triggerSchedule.js after the mutation has occurred.

We want to deploy all the things

Before anything can be deployed, we need to login into the 8Base account, which we can do via command line:

8base login

Then, let’s run the deploy command to send and set up the app logic in your workspace instance.

8base deploy

Testing the entire flow

To see the app in all its glory, click on one of the days of the calendar. You should get the event modal containing the form. Fill that out and put a future start date so we trigger a notification. Try a date more than 2-5 mins from the current time because I haven’t been able to trigger a notification any faster than that.

Yay, go check your email! The email should have arrived thanks to SendGrid. Now we have an app that allows us to create events and get notified with the details of the event submission.

The post Hey, let’s create a functional calendar app with the JAMstack appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]