React Calendar in Drupal

In this article the Javascript framework React is used to build an application while the rest of the website functionality and data storage is provided by Drupal 8. This article focuses on a progressive approach provided by the Drupal contributed project Decoupled Blocks.  The React Calendar library is used to provide this functionality.

Decoupled Blocks

The Decoupled Blocks project provides a Javascript framework that allows blocks to be written in Angular, React or other frameworks. These Javascript frameworks are generally based on the Node.js package manager, npm, instead of being Composer based. Bridging these can take on different strategies. Decoupled Blocks provides a pdb_react module which provides a bridge but uses React 15.0.1. Unfortunately this makes it difficult to use other versions or update the version. So this article creates a custom module that follows the same approach but leaves the management of the Javascript framework to npm. The module machine name is react_custom and it has an info file.

name: React custom
type: module
description: 'Turns on the React framework'
package: PDB
core: '8.x'
dependencies:
  - pdb

Although the Javascript library gets added automatically, an error occurs when it is aggregated with the rest of the Javascript files for the website. So it needs to be explicitly added with aggregation turned off. There also is a CSS file generated that needs to be added along with a second CSS file to allow overrides. The order is important for the overrides to work.

react-calendar:
  version: 1.x
  js:
    components/react_calendar/dist/main.js: {preprocess: false}
  css:
    theme:
      components/react_calendar/dist/calendar.css: {}
      components/react_calendar/css/calendar_styles.css: {}

The components (apps) are set up as Drupal plugins. There are only two other files required to get the basic infrastructure for the components working. In src/Plugin/Block/ there is the file ReactCustomBlock.php.

<?php

namespace Drupal\react_custom\Plugin\Block;

use Drupal\pdb\Plugin\Block\PdbBlock;

/**
 * Exposes a React component as a block.
 *
 * @Block(
 *   id = "react_custom_component",
 *   admin_label = @Translation("React custom component"),
 *   deriver = "\Drupal\react_custom\Plugin\Derivative\ReactCustomBlockDeriver"
 * )
 */
class ReactCustomBlock extends PdbBlock {

  /**
   * {@inheritdoc}
   */
  public function build() {
    $info = $this->getComponentInfo();
    $machine_name = $info['machine_name'];

    $build = parent::build();
    $build['#allowed_tags'] = [$machine_name];
    $build['#markup'] = '<' . $machine_name . ' id="' . $machine_name . '"></' . $machine_name . '>';

    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function attachSettings(array $component) {
    $machine_name = $component['machine_name'];

    $attached = [];
    if (array_key_exists('path', $component)) {
      $attached['drupalSettings']['react-custom-apps'][$machine_name]['uri'] = '/' . $component['path'];
    }

    return $attached;
  }

  /**
   * {@inheritdoc}
   */
  public function attachLibraries(array $component) {
    $parent_libraries = parent::attachLibraries($component);

    $framework_libraries = [
      'react_custom/react-calendar'
    ];

    $libraries = [
      'library' => array_merge($parent_libraries, $framework_libraries),
    ];

    return $libraries;
  }

}

The last file is in src/Plugin/Derivative and called ReactCustomBlock Deriver.php.

<?php

namespace Drupal\react_custom\Plugin\Derivative;

use Drupal\pdb\Plugin\Derivative\PdbBlockDeriver;

/**
 * Derives block plugin definitions for React components.
 */
class ReactCustomBlockDeriver extends PdbBlockDeriver {

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $definitions = parent::getDerivativeDefinitions($base_plugin_definition);

    return array_filter($definitions, function (array $definition) {
      return $definition['info']['presentation'] == 'react_custom';
    });
  }

}

React Calendar

Now create a component for the Calendar. in the directory components/react_calendar/ place the calendar.info.yml file.

name: Calendar
machine_name: calendar
type: pdb
description: 'Calendar Block'
package: React
core: '8.x'
module_status: active
presentation: react_custom
add_js:
  footer:
    'dist/main.js': {}

There are two files that specify how to build the application. Start with the package.json file.

{
  "name": "calendar",
  "version": "1.0.0",
  "description": "",
  "main": "calendar.js",
  "scripts": {
    "build": "webpack --mode production",
    "build-dev": "webpack --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-proposal-class-properties": "^7.1.0",
    "@babel/plugin-transform-runtime": "^7.2.0",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "@babel/runtime": "^7.3.1",
    "axios": "^0.18.0",
    "babel-loader": "^8.0.5",
    "css-loader": "^3.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "path": "^0.12.7",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "terser-webpack-plugin": "^2.2.1",
    "webpack": "^4.29.0",
    "webpack-cli": "^3.2.1",
    "webpack-node-externals": "^1.7.2"
  },
  "dependencies": {
    "react-calendar": "^2.19.2"
  },
  "babel": {
    "presets": [
      "@babel/preset-env",
      "@babel/preset-react"
    ],
    "plugins": [
      "@babel/plugin-proposal-class-properties"
    ]
  }
}

The other file is the webpack.config.js file.

const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          { loader: 'css-loader', options: { url: false, sourceMap: true } },
        ],
      },
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader',
          options:{
            presets: ['@babel/preset-react']
          }
        }
      },
      { include: [
          path.resolve(__dirname, "src")
        ]
      }
    ]
  },
  optimization: {
    minimizer: [new TerserPlugin()],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "calendar.css"
    }),
  ],
};

The TerserPlugin is used to minimize the Javascript code. MiniCssExtractPlugin extracts the CSS style code and creates the calendar.css file. A separate CSS file because including it in the Javascript code can result in the CSS code getting loaded later and causing the application of the styles to be visible to the user which can be disconcerting. Babel is a Javascript transpiler that converts the React ES6 to ES5. The technically correct name for Javascript is ECMAScript. ES6 was adopted in 2015 and still is not completely supported by browsers. Since ES5 is widely supported by browsers Babel is used to allow the React code to run on a wide variety of browsers. There are other approaches to the webpack configuration that may provide benefits depending on the situation.

There is a css/calendar_styles.css file that can be used to customize the CSS for a particular website. The default width of the calendar is 350 pixels. To change this to as wide as the enclosing div allows then calendar_styles.css would be as follows.

.react-calendar {
  width: 100%;
}

Other things that can be included in this file are changing the colors to match the website.

A README.md file provides instructions.

# React Calendar

## Installation
- Download and install Decoupled Blocks https://www.drupal.org/project/pdb

## How to use block
- Run `npm install` inside the react_calendar root folder. You can compile the JSX with the commands `npm run build` or `npm run build-dev`
- Place the block "Calendar" into a region using block layout or any other block manager

Now create src and dist directories for the source code and compiled application.

Calendar

In the src directory create a file index.js. From the React Calendar documentation start with the example with a few modifications.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Calendar from 'react-calendar';

class CalendarBlock extends Component {
  state = {
    date: new Date(),
  }

  onChange = date => {
    this.setState({ date });
  };

  render() {
    return (
      <div>
        <Calendar
          onChange={this.onChange}
          value={this.state.date}
          calendarType="US"
        />
      </div>
    );
  }
}

If npm is not already installed on the computer then this will need to be done. Now from the react_calendar directory install the libraries with "npm install" and then build the application with "npm run build-dev" for development or "npm run build" for production. Now enable the React Custom and PDB modules and place the calendar block in the region where messages appear (usually the highlight or content top region).

A calendar is not very useful unless it is integrated with the data store in the Drupal backend. Start by enabling the RESTful Web Services and Serialization modules. Create a new view for content of a particular content type such as Articles and sorted by title. For REST export settings start with a path such as react/articles. Once the view is created change the path to /react/articles/%parameter. Under Advanced create a contextural filter for Created date. When the filter is not available set it to display "No results found." Set the access to view published content. Under format change from entity to fields. In the settings for the serializer choose JSON. Under the fields section change it so that the ID and Title fields are provided. For the pager change the setting to display all items. Modify the above as necessary for your website and the data to be displayed.

Now return to the index.js file in the src directory. In the render function there is the Calendar tag. Add the addClickDay parameter at the end.

        <Calendar
          onChange={this.onChange}
          value={this.state.date}
          calendarType="US"
          onClickDay={this.onClickDay}
        />

Then in the CalendarBlock class add the onClickDay function which uses the React fetch API.

  onClickDay = date => {
    var iso = date.toISOString();
    var arg = iso.substring(0, 4) + iso.substring(5, 7) + iso.substring(8, 10);
    fetch('/react/articles/' + arg)
      .then(response => response.json())
      .then(data => this.displayArticles(data));
  };

The last step is creating the displayArticles function in the CalendarBlock class. In this example the articles are to be displayed where system messages normally appear. For this website this is a div with an id of system-messages-block. This will need to be changed to wherever the output is to show up.

  displayArticles = function (data) {
    if (data.length == 0) {
      var messages = '<p>No articles found.</p>';
    }
    else {
      var messages = '';
      data.forEach(function (item, index) {
        messages += '<li><a href="/node/' + item.nid + '">' + item.title + '</a></li>';
      });
      messages = '<ul>' + messages + '</ul>';
    }
    messages = '<h3>Articles</h3>' + messages;
    document.getElementById('system-messages-block').innerHTML = messages;
  };

Once this is added, return to the react_calendar directory and rebuild the app. The you should be able to click on the date that this article was created and this article should appear in the status message area.

Categories