Angular5 で bootstrap-datepickerを使う #serverless #adventcalendar

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Web アプリを開発しているときに、日付の選択をカレンダーで行いたいシーンがあると思います。いろいろな選択肢がありますが、今回は bootstrap-datepicker を使ってみます。また、bootstrap-datepicker で選択した日付情報は input 要素の value に設定されますが、この値を Angualr アプリケーションから使えるよう設定します。

バージョン情報

ライブラリ/フレームワーク名 バージョン 役割
Angular 5.1.1 Webアプリケーションの構築フレームワークとして使う
bootstrap 4.0.0-beta.2 CSS利用のため。Card UI が使いたかったためbetaですがver4を採用しています
jquery 3.2.1 bootstrap が要求します
popper.js 1.13.0 bootstrap が要求します
bootstrap-datepicker 1.7.1 日付の選択でカレンダーUIを適用するために利用しています

作業の流れ

  • Angular アプリケーションを作成する
  • ライブラリをインストールする
  • カレンダーUIを使うコンポーネントを作成する
  • カレンダーで選択した値がAngularアプリケーションで利用できることを確認する

Angular アプリケーションを作成する

ng new angular-datepicker
cd angular-datepicker
ng version

Angular CLI: 1.6.1
Node: 8.5.0
OS: darwin x64
Angular: 5.1.1
... animations, common, compiler, compiler-cli, core, forms
... http, language-service, platform-browser
... platform-browser-dynamic, router

@angular/cli: 1.6.1
@angular-devkit/build-optimizer: 0.0.36
@angular-devkit/core: 0.0.22
@angular-devkit/schematics: 0.0.42
@ngtools/json-schema: 1.1.0
@ngtools/webpack: 1.9.1
@schematics/angular: 0.1.11
@schematics/schematics: 0.0.11
typescript: 2.4.2
webpack: 3.10.0

ライブラリをインストールする

cd angular-datepicker
npm install --save bootstrap@4.0.0-beta.2
npm install --save jquery popper.js
npm install --save bootstrap-datepicker

.angular-cli.json を編集します。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "angular-datepicker"
  },
  "apps": [
    {
      "assets": [
        "favicon.ico",
        { "glob": "jquery.slim.min.js.map", "input": "../node_modules/jquery/dist/", "output": "./" },
        { "glob": "popper.min.js.map", "input": "../node_modules/popper.js/dist/umd/", "output": "./" },
        { "glob": "bootstrap.min.js.map", "input": "../node_modules/bootstrap/dist/js/", "output": "./" }
      ],
      "styles": [
        "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "../node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css",
        "styles.css"
      ],
      "scripts": [
        "../node_modules/jquery/dist/jquery.slim.min.js",
        "../node_modules/popper.js/dist/umd/popper.min.js",
        "../node_modules/bootstrap/dist/js/bootstrap.min.js",
        "../node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"
      ],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
  ],
  ...
}

これで、bootstrap-datepicker を利用する準備が整いました。

bootstrap-datepicker を利用するコンポーネントを作成

ng g component datepicker
<div class="container-fluid">
  <div class="card">
    <div class="card-header">
      <span>カレンダー入力</span>
    </div>
    <div class="card-body">
      <div class="form-group row">
        <label for="datepicker" class="col-sm-2 col-form-label">bootstrap-datepicker</label>
        <div id="datepicker" class="col-sm-4">
          <input type="text" class="form-control date" [(ngModel)]="date">
        </div>
        <div class="col-sm-6 align">
          <span> バインドされているモデルの値:{{date}} </span>
        </div>
      </div>
    </div>
  </div>
</div>
import {Component, OnInit} from '@angular/core';
import * as $ from 'jquery';
import 'bootstrap-datepicker';

@Component({
  selector: 'app-datepicker',
  templateUrl: './datepicker.component.html',
  styleUrls: ['./datepicker.component.css']
})
export class DatepickerComponent implements OnInit {

  date: string;

  constructor() {
  }

  ngOnInit() {
    this.date = this.formatDate(new Date());
    $('#datepicker .date').datepicker({
      format: 'yyyy-mm-dd'
    });
  }

  private formatDate(date) {
    let format = 'YYYY-MM-DD';
    format = format.replace(/YYYY/g, date.getFullYear());
    format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
    format = format.replace(/DD/g, ('0' + date.getDate()).slice(-2));
    if (format.match(/S/g)) {
      const milliSeconds = ('00' + date.getMilliseconds()).slice(-3);
      const length = format.match(/S/g).length;
      for (let i = 0; i < length; i++) {
        format = format.replace(/S/, milliSeconds.substring(i, i + 1));
      }
    }
    return format;
  }
}

jQuery で カレンダーUIの input 要素を選択し、datepickerを呼び出すことで画面上で利用できるようになります。その処理を、 Angular の ngOnInit で行っています。これでカレンダーが利用できるようになるはずです。

datepicker-before.png

たしかにカレンダーUIを呼び出せていることがわかりますが、「バインドされているモデルの値」が更新されません。Angular では、HTML上で [(ngModel)]=変数名のように書くと双方向バインディングが有効になり、カレンダーで選択された値が変数(今回の場合は date)にセットされることを期待しますが、うまく変数に入らないようです。どうやら、bootstrap-datepicker を使う場合、値が変更されたという通知(onChange)がうまく Angular 側に伝搬されないようです。そこで回避策を仕込みます。

カレンダーで選択した日付が Angular で利用できることを確認する

フォーカスが外れたタイミングで明示的に値をセットするようにします。

<div class="container-fluid">
  <div class="card">
    <div class="card-header">
      <span>カレンダー入力</span>
    </div>
    <div class="card-body">
      <div class="form-group row">
        <label for="datepicker" class="col-sm-2 col-form-label">bootstrap-datepicker</label>
        <div id="datepicker" class="col-sm-4">
          <input type="text" class="form-control date" [(ngModel)]="date" #datePicker (blur)="date = datePicker.value">
        </div>
        <div class="col-sm-6 align">
          <span> バインドされているモデルの値:{{date}} </span>
        </div>
      </div>
    </div>
  </div>
</div>

#datePicker (blur)="date = datePicker.value" を追加しました。 #datePicker というAngularが理解できる名前をつけ、フォーカスが外れた時、その value を date にセットしています。この処理により、カレンダーUIによってユーザーが日付を入力すると、その値が意図どおりAngularの変数にもセットされます。

datepicker-after1.png

このあと、フォーカスを外すと…

datepicker-after2.png

「バインドされているモデルの値」が更新されていることがわかります。これで、Angularのプログラムで日付情報が利用できます。

おわりに

サーバーレス開発でシングルページアプリケーションを開発するときは、なるべくライブラリの組み合わせや相性の問題に遭遇したくないものです。とはいえよりリッチにしていくために様々なツールを導入した結果、思いもよらぬ動作をしてしまうこともあると思います。根本的な解決を待つというのが手段のひとつですが、自力で解決してプルリクエストを送る、回避策を模索する、利用するツールを変えてみるという手もあります。今回は Angular と bootstrap-datepicker の組み合わせで発生した課題に対して回避策を取りました。同じような組み合わせの問題で困っている方の参考になれば幸いです。