【このご時世にjQuery!?】まさかのjQuery+webpack+babelでマンスリーカレンダーを作ってみた

## このReactのコード、jQueryで書いたらどうなるんだろう…
ふと、Reactを書いていてそんなことを思いました。

ここ最近Reactも主流になりつつあり、仕事でもReactを書くことが増えてきました。
Reactはstateやpropsを更新することによりviewを自動で更新してくれるのが超便利な点です。（あまり詳しくはないのですが、AngularやVueもそういうものだと思っています。）

それをもし、未だにjQueryでよろしくやっていたら、どのようにコードを書いていくのだろうと、なんか、ふと思いました。

jQueryにはどれだけお世話になったのだろう。。。
やっぱりjQueryで書かれたソースを見ると、どこか昔の恋人を思い出すような、そんな気持ちになる方も多いのではないでしょうか？

`animate()`でどれだけテンションが上がったことか…
`$.ajax()`でどれだけ感動したことか…

jQueryは、私たちのWeb制作を十二分に支えてくれて、可能性を最大限に広げてくれました。

ならば！es2015も主流になり始めた今！
その環境でjQueryを最大限に活用して、Reactで書いたほうが絶対に楽な「マンスリーカレンダー」を作っていきながら、過去をなぞっていこうではありませんか！！

**webpackとbabelを使って、jQueryを書くぞ！！！**

## マンスリーカレンダーのイメージ
以下のような簡易的なものを作っていきます。jQueryとの甘酸っぱい過去をなぞりたいのでCSSなんて書いている余裕はありません。`<table>`にあたっているデフォルトのCSSを信用します。

![](https://qiita-image-store.s3.amazonaws.com/0/213205/62f0b7bb-ffbd-80ab-9835-41cf6142f9a8.png)


年と月をキャプションとして表示し、その左右に前の月、次の月に行けるように`＜`と`＞`でボタンを作っています。

曜日が表示されているところは`<thead>`となります。`<thead>`の中身は見出しなので特に変わる必要はないですね。
それに対して`<tbody>`は、`＜`と`＞`を押すたびにDOMが書き換わらないといけないですね。

この`<tbody>`の箇所が絶対にReactで書いたほうが楽なところです。本当にこれをjQueryで書くなんてどうかしてるぜ！

## `calendar`パッケージを使う
カレンダーを作るにしても、「どの曜日から始まって、月に何日あるのか」というのをイチからプログラムを書いていたら大変で仕方ありません。
今回の目標はあくまでもjQueryとの甘い過去をなぞることなので、[calendar](https://www.npmjs.com/package/calendar)というパッケージを使います。

```javascript
import { Calendar } from 'calendar';

const monthlyCalendar = new Calendar().monthDays(2018, 7);

console.log(monthlyCalendar);
```

と書くと

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

というArrayが返ってくるので、これを使ってカレンダーの見た目にしていきます。便利！

## 開発環境の構築
使ったnodeのバージョンは`v8.7.0`で、npmのバージョンは`5.5.1`です。（アップデートサボりました…）

フォルダ構成は以下のようにしています。

```
.
├── .babelrc
├── package-lock.json
├── package.json
├── public
│   ├── bundle.js
│   └── index.html
├── src
│   └── index.js
└── webpack.config.develop.babel.js
```

以下より各設定ファイルの紹介をしていきます。

### package.json
```json:package.json
{
  "name": "jquery-calendar",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --mode development --hot --inline --config webpack.config.develop.babel.js",
    "dev-build": "webpack --mode development --config webpack.config.develop.babel.js"
  },
  "keywords": [],
  "author": "rhirayamaaan",
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.4",
    "babel-preset-es2015": "^6.24.1",
    "webpack": "^4.14.0",
    "webpack-cli": "^3.0.8",
    "webpack-dev-server": "^3.1.4"
  },
  "dependencies": {
    "calendar": "^0.1.0",
    "jquery": "^3.3.1",
    "moment": "^2.22.2",
    "moment-timezone": "^0.5.21"
  }
}
```

今回は年月日を扱うのでDateオブジェクトが必要になります。
dependenciesにインストールしている`moment`というのはDateをとても扱いやすくしているものなので、使用していこうと思います。
momentの使い方は[こちら](https://qiita.com/osakanafish/items/5ef636bbcb2c3ef94953)を参考にしました。
また、タイムゾーンも設定しようと思ったので、`moment-timezone`もインストールしています。

jQueryはせっかくwebpackで実装するので一緒にパックしちゃおうと思いdependenciesでインストールしてあります。

`script`には`dev`と`dev-build`の二つを用意しています。
本来であれば`build`を用意して、本番用のminify化したjsを出力するのが正しいですが、
今回はjQueryだとどういうコードになるのかを遊び半分で書くだけなので`dev`だけでいいかなと思った次第です。＾＾

また`webpack dev server`を使って保存するたびにページをリロードする場合、ファイルを保存しても仮想的にしかbundle.jsを吐き出してくれない（実ファイルは変更されない）ため、ファイルをちゃんと吐き出してもらうために`dev-build`を用意しています。


### .babelrc
```json:.babelrc
{
  "presets": ["es2015"]
}
```

es2015で書いていくので、その指定をここに記します。
babel系のパッケージをdevDependenciesにインストールしているので、この設定ファイルを用意すればes2015でjsを書けるようになります。

### webpack.config.develop.babel.js
```javascript:webpack.config.develop.babel.js
import webpack from 'webpack';

export default {
  // ビルドの起点となるファイルの設定
  entry: ['./src/index.js'],
  // 出力されるファイルの設定
  output: {
    publicPath: '/',
    path: `${__dirname}/public/`, // 出力先のパス
    filename: 'bundle.js', // 出力先のファイル名
  },
  // ローカルサーバの設定
  devServer: {
    contentBase: './public',
    hot: true,
    inline: true,
    historyApiFallback: true,
  },
  resolve: {
    // importする拡張子の指定
    extensions: [
      '.js',
    ],
  },
  // ソースマップの設定
  devtool: 'inline-source-map',
  // loaderの設定
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
    ],
  },
  plugins: [
    // hotモードに必要なプラグイン
    new webpack.HotModuleReplacementPlugin(),
  ],
};
```

ファイル名に`babel`とつけているのでes2015で書くことができます。


## 実際にjQueryを書く！
環境も整ったので、いい感じにjQueryをes2015で書いていこうと思います＾＾

### 下準備
まずはHTMLがないと始まらないので適当に書きます。

```html:public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>jquery-calendar</title>
</head>
<body>
<div id="wrapper">
</div>
<script src="./bundle.js"></script>
</body>
</html>
```

webpack.config.develop.babel.jsでbundle.jsの吐き出し場所publicにしてあるので`<script>`を使って相対パスで読み込みます。
jQueryはbundle.jsに混ぜ込んでしまうので指定しません。

一応、`<div id="wrapper">`を作っています。この中にカレンダーのDOMをJSで生成していこうと思います。

### index.jsの作成
webpack.config.develop.babel.jsに、entryの箇所を`['./src/index.js']`と記述したので、実際に`src/index.js`を作成します。

```javascript:src/index.js
import $ from 'jquery';
import { Calendar } from 'calendar';
import moment from 'moment-timezone';

class CreateCalendar {
  constructor() {
    return this;
  }
}
```

今回は、jQuery、calendar、momentを使用するので、それぞれを最初にimportしています。

その後にカレンダーを生成するためのプログラムを書いていくわけですが、せっかくes2015で書いているのでclassで書いてやろうじゃねーかと思った次第です＾＾

### HTMLの定義
Reactであれば`render()`の中にHTML風にタグを記述していけばそれをレンダリングしてくれますが、jQueryとなるとそうはいきません。
`$()`でDOMをちまちま生成していかなければならないわけですが、せっかくclassで書こうとしているので、必要なHTMLの文字列を定義しておこうと思います。
インスタンスを生成する際にわざわざHTMLの文字列を定義する必要もないと感じたので、staticで定義しちゃいます。

```javascript:src/index.js
// ...省略

class CreateCalendar {
  
  static get ROOT_HTML() {
    return '<table />';
  }

  static get HEADER_HTML() {
    return `
      <thead>
        <tr>
          <th>日</th>
          <th>月</th>
          <th>火</th>
          <th>水</th>
          <th>木</th>
          <th>金</th>
          <th>土</th>
        </tr>
      </thead>
    `;
  }

  static get BODY_HTML() {
    return '<tbody />';
  }

  static get BODY_ROW_HTML() {
    return '<tr />';
  }

  static get BODY_CELL_HTML() {
    return '<td />';
  }

  static get CAPTION_HTML() {
    return '<caption />';
  }

  static get TITLE_HTML() {
    return '<span />';
  }

  static get PREV_BUTTON_HTML() {
    return '<span>＜</span>';
  }

  static get NEXT_BUTTON_HTML() {
    return '<span>＞</span>';
  }

  constructor() {
    return this;
  }
}
```

これで`CreateCalendar.ROOT_HTML`という形でHTMLの文字列を取得できるようになりました。

### 初期値をセット
コンストラクタが実行された時点で、ある程度初期値を設定しておきたいのでそのプログラムを書きます。

```javascript:src/index.js
// ...省略

class CreateCalendar {
  
  // ...省略

  constructor($target = $('body')) {
    this.nowDate = moment().tz('Asia/Tokyo');
    this.$target = $target;
    return this;
  }
}
```

`new CreateCalendar()`とするときに、どのDOMに対してカレンダーを出力するのかを定義したいよなと思い、`new CreateCalendar($('#wrapper'))`とできるように引数を設けました。
エラーとか書くのめんどくさいなとも思い、初期値に`$('body')`を指定できるようにしています。

また、`new CreateCalendar()`が実行されたときのDateオブジェクトと、`$target`を参照できるようにクラス直下の変数に保存しています。

### カレンダーデータをセット
カレンダーデータも立派な初期値ですが、カレンダーデータは月を移動することでデータが変わるので、何度もセットされることが予想されます。
なので、`setCalendarData`という関数を用意した上で、それを`constructor()`で叩いてあげます。

```javascript:src/index.js
// ...省略

class CreateCalendar {
  
  // ...省略

  constructor($target = $('body')) {
    this.nowDate = moment().tz('Asia/Tokyo');
    this.$target = $target;

    // カレンダーデータをセット
    this.setCalendarData();

    return this;
  }

  setCalendarData() {
    this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
  }
}
```

### render用の関数を作成
Reactみたいに、実際にDOMをレンダリングする関数を作成します。
ただ、`render()`の中にダラダラと書くと見づらくなるので、今回は4つの関数を用意します。

- `createCaptionDOM()`：見出しと`＜`と`＞`のボタン部分のDOMを生成する関数
- `createHeadDOM()`；曜日が記述された`<thead>`部分のDOMを生成する関数
- `createBodyDOM()`：日付が表示される`<tbody>`部分のDOMを生成する関数
- `render()`：すべてをwrapした`<table>`を生成し、`$target`に追加する関数

```javascript:src/index.js
// ...省略

class CreateCalendar {
  
  // ...省略

  constructor($target = $('body')) {
    // ...省略

    // render実行
    this.render();

    return this;
  }

  setCalendarData() {
    // ...省略
  }

  // captionのDOM生成
  createCaptionDOM() {
    const $caption = $(CreateCalendar.CAPTION_HTML);
    const $prev = $(CreateCalendar.PREV_BUTTON_HTML);
    const $next = $(CreateCalendar.NEXT_BUTTON_HTML);

    $caption
      .append($prev)
      .append($(CreateCalendar.TITLE_HTML).text(`${this.nowDate.year()}年${this.nowDate.month() + 1}月`))
      .append($next);

    return $caption;
  }

  // theadのDOM生成
  createHeadDOM() {
    return $(CreateCalendar.HEADER_HTML);
  }

  // tbodyのDOM生成
  createBodyDOM() {
    const $body = $(CreateCalendar.BODY_HTML);

    this.calendarData.forEach(week => {
      const $row = $(CreateCalendar.BODY_ROW_HTML);

      $body.append($row);

      week.forEach(day => {
        const $cell = $(CreateCalendar.BODY_CELL_HTML);
        // `0`の場合はその月の日付ではないので`day`は表示しない
        if (day !== 0) $cell.text(day);
        $row.append($cell);
      });
    });

    return $body;
  }

  // DOM生成
  render() {
    // 中身削除
    this.$target.empty();

    const $calendar = $(CreateCalendar.ROOT_HTML);

    $calendar
      .append(this.createCaptionDOM())
      .append(this.createHeadDOM())
      .append(this.createBodyDOM());

    $calendar.appendTo(this.$target);
  }
}

// インスタンス生成
new CreateCalendar($('#wrapper'));
```

`this.render()`が最終的な出力を担うようにプログラムが書けました。
ちゃんとレンダリングされるように`this.render()`を`constructor`で実行するように変更を入れています。

また、クラスは作ったらちゃんとインスタンスを生成してあげないといけないので、最後の行に`new CreateCalendar($('#wrapper'));`を追加しています。

おそらく、これでカレンダー自体は表示されるようになったと思います。

ここから、前後の月に移動できるようにしていきましょう。

### ボタンをクリックしたら月を移動できるようにする
`createCaptionDOM`で、`$prev`と`$next`を作成しているので、このDOMにclickイベントを付与していきます。

`$prev`がクリックされた場合は、前の月が表示されてほしいので`this.calendarData`の中身が前の月のものにならなければなりません。
カレンダーデータを書き換えるということは`setCalendarDate()`が実行されないといけません。

```javascript
setCalendarData() {
  this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
}
```

`setCalendarData()`を抜き出してみました。
`setCalendarData()`は、`this.nowDate`を基にデータを生成し`this.calendarData`に格納する関数なので、クリックされたら`this.nowDate`を更新し、その後に`setCalendarDate()`が実行されればいい感じになりそうですね！

```javascript:src/index.js
// ...省略

class CreateCalendar {
  
  // ...省略

  // clickイベント名
  static get CLICK_EVENT_NAME() {
    return 'click.CreateCalendar';
  }

  constructor($target = $('body')) {
    // ...省略
  }

  setCalendarData() {
    this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
    // データの更新はここで完了するのでここでレンダリング
    this.render();
  }

  // 前の月へ移動（クリックイベント時に実行）
  goToPrevMonth() {
    this.nowDate = this.nowDate.add(-1, 'month');
    this.setCalendarData();
  }

  // 次の月へ移動（クリックイベント時に実行）
  goToNextMonth() {
    this.nowDate = this.nowDate.add(1, 'month');
    this.setCalendarData();
  }

  createCaptionDOM() {
    const $caption = $(CreateCalendar.CAPTION_HTML);
    const $prev = $(CreateCalendar.PREV_BUTTON_HTML);
    const $next = $(CreateCalendar.NEXT_BUTTON_HTML);

    // イベントセット
    $prev.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToPrevMonth());
    $next.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToNextMonth());

    $caption
      .append($prev)
      .append($(CreateCalendar.TITLE_HTML).text(`${this.nowDate.year()}年${this.nowDate.month() + 1}月`))
      .append($next);

    return $caption;
  }

  createHeadDOM() {
    // ...省略
  }

  createBodyDOM() {
    // ...省略
  }

  render() {
    // ...省略
  }
}

new CreateCalendar($('#wrapper'));
```

まず、クリックされたときに実行される`goToPrevMonth()`と`goToNextMonth()`を用意しています。
momentを利用しているので、「1ヶ月前」と「1ヶ月後」を`.add()`を使って簡単に`nowDate`を生成することができます。
momentによって帰ってきた日付のオブジェクトを`this.nowDate`に格納して、その直後に`setCalendarData()`を実行すれば、勝手に`this.calendarData`も更新されるようになります。

Reactの場合は`this.state`が`this.setState()`によって更新されると、勝手に`this.render()`が実行されますが、今はただclassを作っているだけなので`this.calendarData`を更新した後に自分で`this.render()`を叩かなければなりません。
なので`setCalendarDate()`に`this.render()`を追加しています。
（本来であれば`setState()`を作成すればよいのですが、ちょっとめんどくさいなーと思ってやめておきました…笑）

また、今回はjQueryで実装しているのでイベントの付与には`.on()`を使うことができます。
なので、ちゃんとjQueryの機能を最大限に使おうと思い、イベントに名前空間を付けるためにstaticで`CLICK_EVENT_NAME`というのを用意し、`click.CreateCalendar`という名前空間付きのクリックイベント名を用意しました。

## 完成したプログラム
部分ごとに紹介してきたので、全体のコードをもう一度見てみましょう。

```javascript:src/index.js
import $ from 'jquery';
import { Calendar } from 'calendar';
import moment from 'moment-timezone';

class CreateCalendar {

  static get ROOT_HTML() {
    return '<table />';
  }

  static get HEADER_HTML() {
    return `
      <thead>
        <tr>
          <th>日</th>
          <th>月</th>
          <th>火</th>
          <th>水</th>
          <th>木</th>
          <th>金</th>
          <th>土</th>
        </tr>
      </thead>
    `;
  }

  static get BODY_HTML() {
    return '<tbody />';
  }

  static get BODY_ROW_HTML() {
    return '<tr />';
  }

  static get BODY_CELL_HTML() {
    return '<td />';
  }

  static get CAPTION_HTML() {
    return '<caption />';
  }

  static get TITLE_HTML() {
    return '<span />';
  }

  static get PREV_BUTTON_HTML() {
    return '<span>＜</span>';
  }

  static get NEXT_BUTTON_HTML() {
    return '<span>＞</span>';
  }

  static get CLICK_EVENT_NAME() {
    return 'click.CreateCalendar';
  }

  constructor($target = $('body')) {
    this.nowDate = moment().tz('Asia/Tokyo');
    this.$target = $target;

    this.setCalendarData();

    this.render();

    return this;
  }

  setCalendarData() {
    this.calendarData = new Calendar().monthDays(this.nowDate.year(), this.nowDate.month())
    this.render();
  }

  goToPrevMonth() {
    this.nowDate = this.nowDate.add(-1, 'month');
    this.setCalendarData();
  }

  goToNextMonth() {
    this.nowDate = this.nowDate.add(1, 'month');
    this.setCalendarData();
  }

  createCaptionDOM() {
    const $caption = $(CreateCalendar.CAPTION_HTML);
    const $prev = $(CreateCalendar.PREV_BUTTON_HTML);
    const $next = $(CreateCalendar.NEXT_BUTTON_HTML);

    $prev.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToPrevMonth());
    $next.on(CreateCalendar.CLICK_EVENT_NAME, () => this.goToNextMonth());

    $caption
      .append($prev)
      .append($(CreateCalendar.TITLE_HTML).text(`${this.nowDate.year()}年${this.nowDate.month() + 1}月`))
      .append($next);

    return $caption;
  }

  createHeadDOM() {
    return $(CreateCalendar.HEADER_HTML);
  }

  createBodyDOM() {
    const $body = $(CreateCalendar.BODY_HTML);

    this.calendarData.forEach(week => {
      const $row = $(CreateCalendar.BODY_ROW_HTML);

      $body.append($row);

      week.forEach(day => {
        const $cell = $(CreateCalendar.BODY_CELL_HTML);
        if (day !== 0) $cell.text(day);
        $row.append($cell);
      });
    });

    return $body;
  }

  render() {
    this.$target.empty();

    const $calendar = $(CreateCalendar.ROOT_HTML);

    $calendar
      .append(this.createCaptionDOM())
      .append(this.createHeadDOM())
      .append(this.createBodyDOM());

    $calendar.appendTo(this.$target);
  }
}

new CreateCalendar($('#wrapper'));
```

**なっっっげ！！！ 読みづらっ！！！**


## jQueryはDOM構造の構築には本当に向かない
もちろんやる前からわかってはいましたが、やっぱり、DOMの構造が全然わからないのは辛いですね＞＜
バッククォートが出たことによって、JS内にHTML文字列をインデント付きでかけるようにはなりましたが、結局は`.append()`等で構造を作らなければならないので分かりづらくなってきます。

```javascript
createBodyDOM() {
  const $body = $(CreateCalendar.BODY_HTML);

  this.calendarData.forEach(week => {
    const $row = $(CreateCalendar.BODY_ROW_HTML);

    $body.append($row);

    week.forEach(day => {
      const $cell = $(CreateCalendar.BODY_CELL_HTML);
      if (day !== 0) $cell.text(day);
      $row.append($cell);
    });
  });

  return $body;
}
```

`createBodyDOM()`を抜き出して見ましたが、これは本当に分かりづらいですね。。。
おそらくReactなら以下のようにかけるはずです。（仮で書いているのでミスってたら指摘してください＞＜）

```jsx
createBodyDOM() {
  return (
    <tbody>
      {this.calendarData.map((week, weekIndex) => (
        <tr key={weekIndex}>{
          week.map((day, dayIndex) => (
            <td key={dayIndex}>{day}</td>
          )) 
        }</tr>
      ))}
    </tbody>
  )
}
```

HTMLにロジックが入ってきているような感じなので見づらいなーと思う方もいるかも知れないですが、やっぱりなにより、一気にreturnできちゃうあたりがいいなーと個人的には思います。
jQueryだとどうしても`forEach`（または`$.each()`）を使わないといけないので、受け口となる変数を作ってあげなきゃいけなかったりと、考えることが地味に多いのが辛いところです。


## やっぱり、Reactが最高で、jQueryがダメなのか？
Reactはデータ構造に合わせて、勝手にDOMを生成したり削除したりするのが売りのフレームワークです。
それに対してjQueryは、存在するDOMに対して様々な挙動を付与しやすいのが売りのフレームワークです。

変数に格納されているObjectを変えるだけで、勝手にviewが変わるなんてjQueryで書いている場合は想像もつかない世界です。
しかし、Reactを書いているときに「親のDOMを取得して、親の高さの半分のサイズにしたい」なんてことをやりたいときには、`ref`でDOMを返してクラスの変数にぶち込んで、`compoenntDidMount`でクラスの変数から引っ張り出して`.closest()`で親を取得して`.clientHeight()`で高さをようやく取得して…みたいなことをするわけですが、jQueryだったら`$('.self').closest('.parent').height()`で終わっちゃうわけです。

また、例えば、ちょっとしたランディングページを作りましょうみたいなときに、わざわざnpmでいろいろパッケージをインストールして、webpackのconfig書いて、、、みたいなことをするのか？と言われると、答えはNoだと思います。
Sass使いたいからちょっとgulpでも使おうかな？という感じでnpmは使うかもですが、わざわざSPAにしてやろうみたいなことは滅多にやらないと思います。

`<script src="./js/jQuery.min.js"></script>`と書いて、どんどん`$`使って、ガンガンDOMアクセスして、さっとリリースしちゃえばいいわけです。

えーーーー今更jQueryなんて使うのーーーーみたいな感じもありますが、やっぱりJavaScriptをそのまま書くより、jQueryで書いたほうがそれだけで実装力があがりますし、やっぱりjQueryはすごいなぁと思いました！
初心者なんて絶対jQueryから入った方がいいと思いました！
`animate()`あるし、やっぱりビュンビュン動くもの作りたいし、できたらテンションあがりますもんね！

いろいろ作ってから考えてみて、**やっぱりReactも最高だし、jQueryも最高だなと思いました！**

そして、もしjQueryでカレンダーを作るなら、**jQuery UIか、他の使いやすそうなjQueryプラグインを使おうと思いました！！！**

以上です！