Angular2でレスポンシブなカレンダーを作った

この記事は[IS17er Advent Calendar](http://www.adventar.org/calendars/1632)17日目の記事として書かれたものです。
16日目の記事は[こちら](http://yukke42.hatenablog.com/entry/2016/12/16/221900)。

## はじめに
この記事はAngular2を知らない人をも読者の対象としているので、説明はいいから早くコードが見たい！という方は下の方にいってください。

#### カレンダーの特徴
1. レスポンシブ対応
2. railsのAPIなどから取得したjson（下みたいなやつ）から自動でカレンダーを生成![Screen Shot 2016-12-17 at 6.32.04 PM.png](https://qiita-image-store.s3.amazonaws.com/0/140269/b9b344a2-1af0-b318-62a0-a91e38b7f218.png)
3. 本日の日付をハイライト

#### 完成図

![Screen Shot 2016-12-17 at 6.26.57 PM.png](https://qiita-image-store.s3.amazonaws.com/0/140269/ddab565f-d709-dd2d-6c8e-bdaf2b627f9a.png)
![Screen Shot 2016-12-17 at 6.27.08 PM.png](https://qiita-image-store.s3.amazonaws.com/0/140269/7d270ae1-a1d4-b516-f51f-60c9d3cce50a.png)


## Angular2とは
Angular2とは**コンポーネント指向**ウェブアプリケーションのフレームワークです。
コンポーネント指向とは、それぞれの画面やセクションをコンポーネントとして管理するというスタイルのことをいいます。こうすることで、同じ部分の繰り返し（たとえば列挙や表の実装によく現れます）を最小限に抑えたDRY (Don't Repeat Yourself) に従ったコードがかけます。DRYに従ったコードは定数の変更などに強く、ウェブページの保守性を高めます。実際、今回のケースでは元となるjsonファイルを変更するだけでそれに対応したカレンダーが自動で生成されます。WebApp (Module)とComponentの関係は、Androidで例えるとちょうどActivityとFragmentの関係に似ています。

公式ドキュメントは[こちら](https://angular.io/)です。Angular2は今年秋に安定版がリリースされたばかりで、それまでのrc版などとは仕様が大きく変わっています。したがって、コードを書く際には公式ドキュメントを参照することをおすすめします。

## Angular2を書き始めるには
Angular2を使用するには環境構築が必要です。公式ドキュメントにもその方法がありますが、[angular-cli](https://cli.angular.io/)を使えばrailsのscaffoldのような感覚で必要なファイルを自動生成してくれたりするので簡単に使うことができます（railsと違って、ファイル名やファイルの配置に制約はとくにありません）。

したがって、以下に貼り付けているhtmlファイルなどをそのままコピーしてブラウザで表示してもうまく表示できないので注意してください。
また、以下に貼り付けているファイルは、WebAppの多々のファイルのうち今回のカレンダーの実装でとくに重要なファイルであり、動かすために必要なファイルは他にもたくさんあります。

## tsファイル
Angular2でModuleやComponentなどを記述する際に使用される言語はtypescriptとよばれるスクリプト言語です。typescriptはjavascriptの上位互換で、最終的にはコンパイルされてjavascriptになります。

## カレンダーの内容となるデータ
実際のアプリケーションではAPIから取得したjsonをobjectに変換したものをデータとして使いますが、ここではtypescriptに慣れるために以下のようなファイルを用意し、これをデータとして用いることにしましょう。整理すると、APIから取得したjsonは以下の`TEMP_SCHEDULE`に変換されるのです。（実はこの記事は先週書いたものなのですが、その段階ではまだrailsのサーバーを作っていなく、従って以下のようなファイルを用意していました）。こちらのほうが後の繰り返し分などを理解しやすいと思います。

```javascript:TEMP_SCHEDULE.ts
export const TEMP_SCHEDULE = {
  month: 12,
  events: [
    {
      date: 1,
      day: 4,
      name: `日比谷`,
      start: 13,
      end: 16,
      remark: `2面`
    },
    {
      date: 2,
      day: 5,
      name: `日比谷`,
      start: 9,
      end: 11,
      remark: null
    },
    ... (省略) ...
    {
      date: 30,
      day: 5,
      name: `松戸練`,
      start: null,
      end: null,
      remark: null
    },
    {
      date: 31,
      day: 6,
      name: `ブランク`,
      start: null,
      end: null,
      remark: null
    }
  ]
}
```

## schedule.component.tsの編集
ここでは、```schedule.html```で使う変数を準備します。具体的には、```TEMP_SCHEDULE.ts```からいい具合にデータを取り出してやり、```month```, ```weeks```, ```weekdays```, ```today```という変数を作ってやります。
先に変数の解説をしておきます。
```month``` : カレンダーの月です。
```weeks``` : ```weeks[i][j]```はi番目の週のj番目の日にあるイベント内容を格納します。
たとえば12/1日が木曜日から始まる場合、```weeks[0][i]```（iは0から3まで）には```null```を入れます。
```weekdays``` : 日本語の曜日名を格納します。
```today`` : 今日の月日を格納します。

```javascript:schedule.component.ts
import { Component, OnInit } from '@angular/core';
import { Schedule, Event, Today } from '../../../schema';
import { TEMP_SCHEDULE } from '../../../values';

@Component({
  selector: 'schedule',
  templateUrl: './schedule.html'
})

export class ScheduleComponent implements OnInit {
  public month: number;
  public weeks: Event[][] = [];
  public weekdays: string[];
  public today: Today;

  ngOnInit() {
    let self = this;
    let schedule = TEMP_SCHEDULE;
    self.month = schedule.month;
    self.weekdays = [`日`, `月`, `火`, `水`, `木`, `金`, `土`];

    let today = new Date();
    self.today = {
      month: today.getMonth() + 1,
      date: today.getDate()
    };

    let oneWeek: Event[] = [];
    for (let _i = 0; _i < schedule.events[0].day; _i++) {
      oneWeek.push(null);
    }
    for (let event of schedule.events) {
      oneWeek.push(event);
      if (oneWeek.length === 7) {
        self.weeks.push(oneWeek);
        oneWeek = [];
      }
    }
    if (oneWeek.length !== 0) {
      while (oneWeek.length !== 7) {
        oneWeek.push(null);
      }
      self.weeks.push(oneWeek);
    }
  }
}
```

落ち着いて読めば簡単なコードですね。さて、いよいよHTMLの編集です。

## schedule.htmlの編集
見てください！Angular2ではこんなにソースコードが短くなります！
上に貼り付けた画像のカレンダーのhtmlはたったのこれだけなんです！
曜日を表示するのにPipeを使っていますが、コードは省略します。

Angular2を知らない人のために解説すると
*ngForは要素ごとに繰り返す命令、*ngIfはその部分を表示するかを条件指定できます。さらにngClassは条件にマッチするときのみ指定したスタイルを適用させることができます。

```html:schedule.html
<section class="schedule-section">
  <div class="container">
    <h2 class=title>{{ month }}月の練習日程</h2>
    <div class="calendar">
      <ul class="weekdays">
        <li *ngFor="let weekday of weekdays">
          {{ weekday }}
        </li>
      </ul>

      <ul *ngFor="let week of weeks">
        <li *ngFor="let event of week" class="event">
          <div *ngIf="!!event" class="event-content" [ngClass]="{today: month === today.month && event.date === today.date}">
            <span class="date" [ngClass]="{sun: event.day === 0, sat: event.day === 6}">{{ event.date }}</span>
            <div class="description">
              <span class="day" [ngClass]="{sun: event.day === 0, sat: event.day === 6}">{{ event.day | toDay }}</span>
              <span class="name">{{ event.name }}</span>
              <span *ngIf="!!event.start && !!event.end" class="time">{{ event.start }} - {{ event.end }}</span>
              <span *ngIf="!!event.remark" class="remark">{{ event.remark }}</span>
            </div>
          </div>
        </li>
      </ul>
    </div>
  </div>
</section>
```

## スタイルの実装
スタイルはAngular2とあまり関係がないですね。いつもどおりの作業です。今回は関数使用が可能なlessでスタイルを指定します。
.clearfix()の中身やコア（@から始まる部分）の詳細は省略します。

```css:schedule.less
.schedule-section {
  .clearfix();

  h2 {
    color: @gray-darken-2;
    font-size: @font-size-xxlarge;
    padding: @padding-xxlarge 0 @padding-normal 0;
  }

  .calendar {
    padding: 0 0 @padding-xxlarge 0;
    overflow: hidden;

    ul {
      width: 100%;
    }

    li {
      display: block;
      float: left;
      width: 100% / 7.0;
      height: 80px;
      border: 1px solid @blue-gray-lighten-4;
      margin: 0 -1px -1px 0;
    }

    ul.weekdays {
      height: 40px;
    }

    ul.weekdays li {
      background: @blue-gray-lighten-4;
      text-align: center;
      height: 40px;
      border: 1px solid @blue-gray-lighten-4;
      padding: @padding-normal 0;
      margin: 0 -1px -1px 0;
    }

    .event {
      background: @white;
    }

    .event:hover {
      background: @gray-lighten-1;
    }

    .date {
      font-size: @font-size-small;
      width: 24px;
      padding: 3px;
      text-align: center;
      color: @black;
      float: left;
    }

    .today {
      background: @red-lighten-4;
      height: 100%;
    }

    .sun {
      color: @red;
    }

    .sat {
      color: @blue;
    }

    .description {
      clear: both;
      font-size: @font-size-small;
      padding: 0 0 0 @padding-normal;

      .day {
        display: none;
      }

      .name {
        font-weight: 500;
      }
    }
  }
}

@media screen and (max-width: @tablet-portrait-width) {
  .schedule-section {
    .calendar {
      li {
        border: none !important;
        height: auto !important;
        width: 100% !important;
        margin: 0 !important;
      }

      .weekdays {
        display: none;
      }

      .event:hover {
        background: @white;
      }

      .event-content {
        border-bottom: 1px solid @blue-gray-lighten-4;
        padding: 4px 0 1px 0;
      }

      .date {
        padding: 2px;
      }

      .description {
        display: inline;
        padding: 0;

        .day {
          display: inline !important;
        }

        span {
          padding: @padding-small @padding-small;
        }
      }
    }
  }
}
```

以上で目的のカレンダーが完成します！お疲れさまでした！