ReasonReact practice -  calendar from scratch
Tags: reason, rescript, react, javascript
Published: 2020-09-22 14:11:57

There are only a few examples of how to write web apps in Reason/rescript yet so I'd like to make my small contribution here. I'm going to show you how to implement a calendar component in ReasonReact from scratch, starting from setup the empty project and finishing on a simple calendar component that:

* Displays the current month initially
* Contains the navigation allowing to switch between months
* Is configurable (allows to set the first day of the week)
* Supports internationalization
* Looks nice

The plan is to build the component using the following dependencies:

* date-fns, to make all the calculations and display names of days and months in the selected language
* tailwindcss, to make the component looking good

Let's start!

<img src="https://media.giphy.com/media/26vIfTD9z3GcBJba0/giphy.gif" />

# Setup the empty project
I'm new in the Reason ecosystem, but I have a lot of experience in JavaScript and React development, so to feel more confident, I'm going to start a new `create-react-app` application and just add Reason to it.

```bash
yarn create react-app reason-calendar
cd reason-calendar
```

_More about how to create an empty app using create-react-app you can find here: [https://create-react-app.dev/docs/getting-started/#yarn](https://create-react-app.dev/docs/getting-started/#yarn)_

Now let's remove all generated files, we don't need them:

```bash
cd src
rm App.css App.js App.test.js setupTests.js logo.svg index.css serviceWorker.js
```
Now open the `index.js` file and make it as simple as possible:

```jsx
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <React.StrictMode>
    <div>Hello Reason</div>
  </React.StrictMode>,
  document.getElementById('root')
);
```

Run `yarn start` in the terminal to check if it's working.

Now it's time to set up ReasonReact. Again, nothing fancy here, I just suggest doing exactly the same what the ReasonReact docs ask you to do [here](https://reasonml.github.io/reason-react/docs/en/installation).

```bash
cd .. # go to the root directory of your project
yarn add bs-platform --dev --exact
yarn add reason-react --exact
```

…and update the package.json:

```json
"scripts": {
  "re:build": "bsb -make-world -clean-world",
  "re:watch": "bsb -make-world -clean-world -w"
}
```

…and create the bsconfig.json:

```json
{
  "name": "reason-calendar",
  "reason": { "react-jsx": 3 },
  "bsc-flags": ["-bs-super-errors"],
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "es6",
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "bs-dependencies": [
    "reason-react"
  ],
  "ppx-flags": [],
  "refmt": 3
}
```

And that's actually it. You can check the repo here:

```bash
git clone git@github.com:luski/reason-calendar-tutorial.git --branch step-1
```

## What we will make

I believe it's high time to define what exactly we are going to implement. The following image shows a calendar from one of the mockups that I was supposed to implement the other day. Let's use it and try to implement it:

<a href="https://imgbb.com/"><img src="https://i.ibb.co/9WJdXNJ/calendar.png" alt="calendar" border="0"></a>

As you can see, the component presents a month view. It's in polish, but no worries, we are going to support internationalization. As you can see, in the header we have arrows for switching between months and the MONTH/YEAR header.

Below, there is a list of days of a week and in the middle - day numbers. The current day has an accent color and days not belonging to the currently rendered month are grayed out.

## The plan

Let's start by splitting the task into subtasks. Here's my proposal:

1. Make a UI with dummy data
2. Implement the logic
3. Style the UI with tailwindcss

# UI with dummy data

Let's start with the dummy data. It would help us with defining the data structure. The simplest what comes to my mind is to define two functions: one returning a list of day names (for displaying the header row) and another returning a list of lists of days with metadata (for displaying weeks). Here's the code:

```ReasonML
/* MockData.re */
type dayOfMonth = {
  number: int,
  isToday: bool,
  isForeign: bool,
};

let daysOfMonth = [|
  [|
    {number: 31, isToday: false, isForeign: true},
    {number: 1, isToday: false, isForeign: false},
    {number: 2, isToday: false, isForeign: false},
    {number: 3, isToday: false, isForeign: false},
    {number: 4, isToday: false, isForeign: false},
    {number: 5, isToday: false, isForeign: false},
    {number: 6, isToday: false, isForeign: false},
  |],
  [|
    {number: 7, isToday: false, isForeign: false},
    {number: 8, isToday: false, isForeign: false},
    {number: 9, isToday: false, isForeign: false},
    {number: 10, isToday: false, isForeign: false},
    {number: 11, isToday: true, isForeign: false},
    {number: 12, isToday: false, isForeign: false},
    {number: 13, isToday: false, isForeign: false},
  |],
  [|
    {number: 14, isToday: false, isForeign: false},
    {number: 15, isToday: false, isForeign: false},
    {number: 16, isToday: false, isForeign: false},
    {number: 17, isToday: false, isForeign: false},
    {number: 18, isToday: false, isForeign: false},
    {number: 19, isToday: false, isForeign: false},
    {number: 20, isToday: false, isForeign: false},
  |],
  [|
    {number: 21, isToday: false, isForeign: false},
    {number: 22, isToday: false, isForeign: false},
    {number: 23, isToday: false, isForeign: false},
    {number: 24, isToday: false, isForeign: false},
    {number: 25, isToday: false, isForeign: false},
    {number: 26, isToday: false, isForeign: false},
    {number: 27, isToday: false, isForeign: false},
  |],
  [|
    {number: 28, isToday: false, isForeign: false},
    {number: 29, isToday: false, isForeign: false},
    {number: 30, isToday: false, isForeign: false},
    {number: 31, isToday: false, isForeign: false},
    {number: 1, isToday: false, isForeign: true},
    {number: 2, isToday: false, isForeign: true},
    {number: 3, isToday: false, isForeign: true},
  |],
|];

let daysOfWeek = [|"sun", "mon", "tue", "wed", "thu", "fri", "sat"|];
```

```reasonml
/* CalendarView.re */
let daysOfWeek = () => MockData.daysOfWeek;
let daysOfMonth = () => MockData.daysOfMonth;

[@react.component]
let make = () => {
  <div>
    // Here goes the calendar UI
  </div>;
};
```

Such a data structure allows us to build the UI very straightforward. The calendar is a table with a header and rows for each week. Besides, above the table we have a row with arrows and MONTH/YEAR title. Here's how I implemented it. It's rather self-explanatory, looks almost like a pure React.

```reasonml
/* CalendarView.re */
[@react.component]
let make = () => {
  <div>
    <button> {React.string("<")} </button>
    <span> {React.string("September / 2020")} </span>
    <button> {React.string(">")} </button>
    <table>
      <thead>
        <tr>
          {daysOfWeek()
           ->Belt.Array.map((day: string) =>
               <th key=day> {React.string(day)} </th>
             )
           ->React.array}
        </tr>
      </thead>
      <tbody>
        {daysOfMonth()
         ->Belt.Array.mapWithIndex((weekNumber, days) =>
             <tr key={string_of_int(weekNumber)}>
               {days
                ->Belt.Array.map(({number, isToday, isForeign}) =>
                    <td key={string_of_int(number)}>
                      {number->string_of_int->React.string}
                    </td>
                  )
                ->React.array}
             </tr>
           )
         ->React.array}
      </tbody>
    </table>
  </div>;
};
```

Now we can add our sketch to the main app to see how it looks. Open the index.js and import the component:

```jsx
import React from "react";
import ReactDOM from "react-dom";
import { make as Calendar } from "./CalendarView.bs";

ReactDOM.render(
  <React.StrictMode>
    <div>
      <Calendar />
    </div>
  </React.StrictMode>,
  document.getElementById("root")
);
```
As the listing shows, importing the ReasonReact components to JS code is almost as simple as importing React components. We must only remember that they are visible as `make` field.

Here's the result:

<a href="https://imgbb.com/"><img src="https://i.ibb.co/j3qDF1V/calendar-sketch.png" alt="calendar-sketch" border="0"></a>

Repo for this step:

```bash
git clone git@github.com:luski/reason-calendar-tutorial.git --branch step-2
```

# Logic implementation

Let's make it work. We are going to use the date-fns, so first, we need to add it to the project:

```bash
yarn add date-fns
```

Also, we need to bind this library. There are at least two binding libraries, but none of them fully worked for me, so let's just bind the functions that we need. I'd suggest starting with the JS version of the code and later transform it into Reason. Actually this is how the docs recommend [doing](https://rescript-lang.org/docs/manual/latest/converting-from-js).

```js
import {format, addDays, startOfWeek} from 'date-fns';

const daysOfWeek = () => {
  const days = [0, 1, 2, 3, 4, 5, 6];
  const firstDayOfWeek = startOfWeek(new Date());
  
  return days.map(
    dayNumber => format(addDays(firstDayOfWeek, dayNumber), 'EE')
  );
}
```

The above function returns a list of day names starting from the first day of a week, it would be (for english): `['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']`.

So first we need to bind the `format`, `addDays`, and `startOfWeek` from `date-fns`:

```reasonml
module DateFns = {
  type date = Js.Date.t;

  [@bs.module "date-fns"] external format: (date, string) => string = "format";
  [@bs.module "date-fns"] external addDays: (date, int) => date = "addDays";
  [@bs.module "date-fns"] external startOfWeek: date => date = "startOfWeek";
};
```

And implementation, pretty similar to the JS version, isn't it?

```reasonml
let daysOfWeek = () => {
  open DateFns;
  open Belt.Array;

  let days = [|0, 1, 2, 3, 4, 5, 6|];
  let firstDayOfWeek = startOfWeek(Js.Date.make());

  days->map(dayNumber =>
    firstDayOfWeek->addDays(dayNumber)->format(_, "EE")
  );
};
```

And finally, we can solve the main challenge of the task - implement a function generating weeks of the selected month. Again, let's start with the JS implementation:

```js
import {startOfWeek, startOfMonth, addDays, isSameDay} from 'date-fns';

function daysOfMonth(date) {
  // first day that appears in the month view.
  // It's a first day of the first week of the month.
  const firstDayOfWeek = startOfWeek(startOfMonth(date));

  return generateWeeks(date.getMonth(), firstDayOfWeek, []);
}

function generateWeeks(monthNum, firstDayOfWeek, weeksAccumulator) {
  const week = [0, 1, 2, 3, 4, 5, 6]  // days of week
    .map(dayNumber => addDays(firstDayOfWeek, dayNumber)) // days of week transformed into dates
    .map(date => createDateOfWeek(monthNumber, date)); // dates transformed into dayOfWeek objects

  if (week[6].isForeign) {
     // last generated day is not belonging to the current month so we stop the recursion
    return [...weeksAccumulator, week];
  } else {
    // otherwise we generate next week
    return generateWeeks(monthNum, addDays(firstDayOfWeek, 7), [...weeksAccumulator, week]);
  }
}

function createDateOfWeek(monthNumber, date) {
  return {
    number: date.getDate(),
    isToday: isSameDay(date, new Date()),
    isForeign: date.getMonth() === monthNumber
  };
}
```

The code can look a bit unusual for a JS programmer because instead of a simple loop I used recurrency. I think it's a more natural way of executing loop in functional programming because we don't need any temporal mutable variables.

The first function, `daysOfMonth` takes some date and generates an array of month weeks represented as an array of objects defined in `MockData.re`  - ` dayOfMonth`. I hope the code is self-explanatory and doesn't need any additional comments. So let's change it to Reason.

First, we need to update our bindings:

```reasonml
module DateFns = {
  type date = Js.Date.t;

  [@bs.module "date-fns"] external format: (date, string) => string = "format";
  [@bs.module "date-fns"] external addDays: (date, int) => date = "addDays";
  [@bs.module "date-fns"]
  external addMonths: (date, int) => date = "addMonths";
  [@bs.module "date-fns"] external startOfWeek: date => date = "startOfWeek";
  [@bs.module "date-fns"] external startOfMonth: date => date = "startOfMonth";
  [@bs.module "date-fns"] external isSameDay: (date, date) => bool = "isSameDay";
};
```

Next, we just rewrite the code:

```reasonml
type dayOfMonth = {
  number: int,
  isToday: bool,
  isForeign: bool,
};

let daysOfWeek = () => {
  open DateFns;
  open Belt.Array;

  let days = [|0, 1, 2, 3, 4, 5, 6|];
  let firstDayOfWeek = startOfWeek(Js.Date.make());

  days->map(dayNumber =>
    firstDayOfWeek->addDays(dayNumber)->format("EE")
  );
};

let createDateOfWeek = (monthNumber, date) => {
  number: Js.Date.getDate(date)->int_of_float,
  isToday: DateFns.isSameDay(date, Js.Date.make()),
  isForeign: Js.Date.getMonth(date) !== monthNumber,
};

let rec generateWeeks = (monthNum, startingDay, weeks) => {
  open Belt.Array;
  open DateFns;

  let week =
    [|0, 1, 2, 3, 4, 5, 6|]
    ->map(addDays(startingDay))
    ->map(createDateOfWeek(monthNum));

  if (!getUnsafe(week, 6).isForeign) {
    generateWeeks(
      monthNum,
      startingDay->addDays(7),
      concat(weeks, [|week|]),
    );
  } else {
    concat(weeks, [|week|]);
  };
};

let daysOfMonth = date => {
  open DateFns;

  let firstDayOfWeek = date->startOfMonth->startOfWeek;
  generateWeeks(Js.Date.getMonth(date), firstDayOfWeek, [||]);
};
```

Notice I moved the type declaration from `MockData.re` to `CalendarView.re` because we just implemented all functions from the `MockData.re` and now we can delete this file 🎉

Notice also that the `generateWeeks` function has the `rec` keyword. Recursive functions must be marked with this keyword.

And here are the fruits of our work:
![preview](https://i.ibb.co/r7CBV18/captured.gif)

The full code of this step:
```bash
git clone git@github.com:luski/reason-calendar-tutorial.git --branch step-3
```

## Internationalization support

By internationalization of the Calendar, I mean adding support for other languages than english and configuring the starting day of the week because in many countries Monday is the starting day.

Luckily `date-fns` provides all these features, we only need to use them.

The `format` function from `date-fns` takes as optional argument the configuration object. This object can contain field named `locale` and it is used to display results of the function is the desired language. Here's how to do it in JavaScript:

```js
import pl from 'date-fns/locale/pl';
import {format} from 'date-fns';

///...

const polishText = format(new Date(), 'EE', {locale: pl});
```

If we want to mark a different day than Sunday as the first day of a week, we need to pass an extra configuration object to function `startOfWeek`. Again, here's how to do it in JavaScript:

```js
import {startOfWeek, startOfMonth} from 'date-fns';

const lastMonday = startOfWeek(new Date(), {weekStartsOn: 1});
```

Let's change our code!

First, the bindings. We need to use a `locale` object of unknown type (it's not really unknown, but we don't care what's inside). We can get instances of this type by importing them from `date-fns/locale` package. Let's then introduce a new module inside `DateFns` and name it `Locale`. For convenience let's add two instances of `Locale` inside, for instance `en-US` and `pl`:

```reasonml
module DateFns = {
  // ...
  module Locale = {
    type t;
    [@bs.module "date-fns/locale/pl"] external pl: t = "default";
    [@bs.module "date-fns/locale/en-US"] external en: t = "default";
  };

  type formatOptions = {locale: Locale.t};

  [@bs.module "date-fns"]
  external format: (date, string, formatOptions) => string = "format";
  // ...
};
```

What is worth to mention, I defined `type t` inside the `Locale` module. The name is taken by the convention. Usually, modules representing a type have this type defined inside and its name is `t`. As you noticed, this type is only a declaration, we don't know what is inside! It's fine because this type reprensents an object imported from `date-fns`, we don't care what's inside since we use it only to pass it to external functions. Reason doesn't care as well what's inside so we won't get any compilation errors there.

But we will see a compilation error anyway, because we added the second argument to the `format` function and we didn't change calls of that function yet, so let's do it now!

I'd like to allow external users of the Calendar to specify the location and if they don't do it, I'd use the 'us-US' as default. So first, we need to add an optional property to the component:

```reasonml
[@react.component]
let make = (~locale=?) => {
  let localeValue = Belt.Option.getWithDefault(locale, DateFns.Locale.en);
 //... rest of implementation
```
And now, having the `localeValue` inside the component we only need to pass it to all occurrences of the `DateFns.format`, for example here's the changed `daysOfWeek`:

```reasonml
let daysOfWeek = (locale) => {
  open DateFns;
  open Belt.Array;

  let days = [|0, 1, 2, 3, 4, 5, 6|];
  let firstDayOfWeek =
    startOfWeek(Js.Date.make());

  days->map(dayNumber =>
    firstDayOfWeek->addDays(dayNumber)->format("EE", {locale: locale})
  );
};
```

The same process must be done with the starting day of week. First, bindings:

```reasonml
module DateFns = {
  // ...
  type weekStartsOnOptions = {weekStartsOn: int};

  external startOfWeek: (date, weekStartsOnOptions) => date = "startOfWeek";
  // ...
};
```

Next, let's introduce another optional property to the component:

```reasonml
[@react.component]
let make = (~locale=?, ~weekStartsOn=?) => {
  // ...
  let weekStartsOnValue = Belt.Option.getWithDefault(weekStartsOn, 0);

// ...
```

Finally, let's pass `weekStartOnValue` to functions using `startOfWeek`. 

Voilà, the component is ready. To test it, we must pass the custom configuration in index.js:

```jsx
import React from "react";
import ReactDOM from "react-dom";
import { pl } from "date-fns/locale";

import { make as Calendar } from "./CalendarView.bs";

ReactDOM.render(
  <React.StrictMode>
    <div>
      <Calendar locale={pl} weekStartsOn={1} />
    </div>
  </React.StrictMode>,
  document.getElementById("root")
);
```

Here's how it works:

![Internationalization](https://i.ibb.co/744Shsx/captured-1.gif)

You can check the code here:

```bash
git clone git@github.com:luski/reason-calendar-tutorial.git --branch step-4
```

# Styling with tailwindcss

## Add tailwind to your project

Regardless of whether we write the app in React or ReasonReact, the final output is a plain javascript code. When we realize that, it becomes clear that using any external tools working with React will work also with ReasonReact. Therefore, to integrate tailwindcss with our project we just need to follow the [guide](https://tailwindcss.com/docs/installation) on tailwincss.

Let's then do what the getting started page suggests doing:

1. install the dependencies
    
    ```bash
    yarn add --dev tailwindcss postcss-cli
    ```
    
2. create the base css file, name it (for instance) `tailwind.css` and store in `/src/styles` folder:
    `tailwind.css:`
    ```css
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    ```
3. create the configuration file `tailwind.config.js`
    ```bash
    npx tailwindcss init
    ```
4. create scripts for building styles in `package.json`
    ```bash
    "scripts": {
      "build:css": "postcss src/styles/tailwind.css -o src/styles/style.css",
      "watch:css": "postcss src/styles/tailwind.css -o src/styles/style.css -w"
    }
    ```
5. create `postcss.config.js` file:
    ```js
    module.exports = {
     plugins: [require("tailwindcss"), require("autoprefixer")],
   };
    ```

Now if you run `yarn run build:css` the `app.css` file will appear in `/src/styles`. It will be build from `tailwind.css` using configuration from `tailwind.config.js`. If you run `yarn run watch:css` the `app.css` will be rebuilt every time when you change something in the `app.css`.

So let's call `yarn run build:css` to create the `styles/style.css` and include this file in the main file of the app: `index.js`:

```jsx
import "./styles/style.css";
```

## Configure

To make the calendar look like on the graphics, we need to add the right color palette and the right font to the tailwind. Here's the updated `tailwind.config.js`:

```js
module.exports = {
  theme: {
    fontFamily: {
      lato: "Lato, sans-serif",
    },
    colors: {
      black: "#000000",
      white: "#FFFFFF",
      yellow: "#f2cb80",
      gray: {
        100: "#191c1e",
        900: "#808182"
      },
    }
  },
}
```

There is a lot of ways of adding the font to the project. I decided to add the `link` tag to the header of the `index.html` file:

```html
    <link
      href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap"
      rel="stylesheet"
    />
```

Now we are all good and can finally do the coding 😁

## Using tailwind

Using tailwind is not rocket science, we're just adding classes to elements. Instead of printing my all changes, that you can find in the repo, I'd like to give you several tips that I used in this project.

### VSCode plugin for tailwind

If you're using VSCode, I strongly suggest installing the [tailwind intellisense](https://github.com/tailwindcss/intellisense.git) plugin. Look how nice it works:

![plugin](https://i.ibb.co/tYwCP60/Peek-2020-09-22-15-08.gif)

### `re-classnames` library

I deliberately ignored the `isForeign` and `isToday` flags that can be found in our data model. It's because those flags are used only to change the decoration of the calendar days and now it's high time for handling it.

According to the graphics design, the regular days should be white, foreign days – gray, and today – yellow. In other words, the regular days get the class name `text-white`, foreign: `text-gray-900`, today: `text-yellow`.

For setting classnames dependent on some state I like using the `re-classnames` utility - similarly like I use `classnames` utility in JavaScript.

So let's add the library:
```bash
yarn add re-classnames
```

We must also add it to the `bsconfig.json` to make it visible by Reason:
```json
"bs-dependencies": [
  "reason-react",
  "re-classnames"
],
```
And here's how to implement the coloring of days with this library:

```reasonml
<td
  key={string_of_int(number)}
  className=Cn.(
      "text-white"->on(!isToday && !isForeign)
    + "text-yellow"->on(isToday)
    + "text-gray-900"->on(isForeign)
  )>
  {number->string_of_int->React.string}
</td>
```

You'll probably notice the warning `44`. The `re-classnames` [docs](https://github.com/MinimaHQ/re-classnames#warning-44) suggest to turn it off, so let's do it by adding the following text to `bsconfig.json`:

```json
"warnings": {
  "number": "-44"
}
```

Well, that's all that I wanted to share regarding the tailwindcss. Here's the repo of the complete solution of our task:

```bash
git clone git@github.com:luski/reason-calendar-tutorial.git --branch step-5
```

And look at the demo. Looks good, doesn't it?

![Demo](https://i.ibb.co/2gXjX7m/demo.gif)


# Summary

I've written this post because I'm learning rescript and the calendar component was a widget that I really had to implement. I learned a couple of new things during the implementation so I decided to share my knowledge hoping that it can be helpful for other newcomers.

I just realized that the implementation of this small widget covers a lot of rescript and ReasonReact crucial elements. We learned how to:

1. Write functional React components with internal state and optional parameters
2. Import ReasonReact components to JS code
3. Interop with JS libraries - by using `date-fns`
4. Write recursive functions
5. Set up a new project from scratch
6. And maybe other things that I don't remember now? 😉

It was my first blog post, so please let me know what you think about it and share it if you think it's somehow valuable.