グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



Vue3でjQueryのdatepickerを使いたい!実装手順と注意点を解説します。

皆様、ご無沙汰しております。

今回は、先日Vue3とjQueryのDatepickerを組み合わせて実装する機会があったので、その方法と、つまずいたポイントおよび解決策についてご紹介したいと思います。

要件

  • 日付の入力欄が複数個欲しい
  • 入力欄の個数は任意で増減させることができる
  • それぞれの入力欄をカレンダーピックを使って選択したい

前提条件

  • Vue 3.5.9
  • jQuery 3.6.1

完成形

以下画像のような挙動を実現できるようにしたいです。

  1. 初期状態は入力欄の枠なしで、枠を追加できるように「+」ボタンを置いておく。

  2. 「+」ボタンで入力欄の枠を増やすことができ、「-」ボタンで入力欄の枠を減らすことができる。

  3. 各入力欄にdatepickerを使って、カレンダーを表示して、日付を入力できる。

準備

以下のhtmlファイルとjsファイルと画像達を用意して、必要なライブラリたちをCDN経由で読み込みます。

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>use datepicker in vue and jquery</title>
</head>

<body>
  <h1>Use datepicker in vue and jquery</h1>

  <div id="app">

    <div>
      <h2>日程</h2>
    </div>

    <div>
      <!-- ここに追加していく -->
    </div>

  <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/themes/smoothness/jquery-ui.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@3.5.9/dist/vue.global.min.js"></script>
  <script src="./index.js"></script>
</body>

</html>

index.js

const { createApp } = Vue

createApp({
  delimiters: ["${", "}"],
  data() {
    return {
      // ここに追加していく
    };
  },
  methods: {
  // ここに追加していく
    isEmpty: function (obj) {
      return Object.keys(obj).length === 0;
    },
  },
}).mount("#app");

実装

追加した全体は以下の通りです。

追加した全体

index.html

    <div>
      <template v-if="isEmpty(dates)">
        <a href="javascript:void(0);" @click="addDateBox(0)">
          <img src="./addbutton.png" alt="枠追加ボタン"></a>&nbsp;
      </template>
      <template v-else>
        <div v-for="date in dates" :key="date.id" class="sp-pt020">
          <input class="sys-input-txt sys-txt20" type="text" v-model="date.value"
            :ref="`date${date.id}`">&nbsp;

          <a href="javascript:void(0);" @click="addDateBox(date.id+1)">
            <img src="./addbutton.png" alt="枠追加ボタン"></a>&nbsp;
          <a href="javascript:void(0);" @click="removeDateBox">
            <img src="./removebutton.png" alt="枠削除ボタン"></a>
        </div>
      </template>
    </div>

index.js

const { createApp } = Vue

createApp({
  delimiters: ["${", "}"],
  data() {
    return {
      dates: [],
    };
  },
  watch: {
    "dates.length"(newVal, oldVal) {
      if (newVal !== oldVal) {
        let id = newVal - 1;
        this.$nextTick(() => {
          jQuery(this.$refs['date'+id]).datepicker({
            dateFormat: "yy-mm-dd",
            showOn: "button",
            buttonImage: "./calendar.png",
            buttonImageOnly: true,
            onSelect: (dateText) => {
              this.dates[id].value = dateText;
            },
          });
        });
      }
    },
  },
  methods: {
    addDateBox(id) {
      this.dates.push({ id: id, value: "" });
    },
    removeDateBox() {
      this.dates.pop();
    },
    isEmpty: function (obj) {
      return Object.keys(obj).length === 0;
    },
  },
}).mount("#app");

これ以降、部分的に見ていきます。

プロパティの作成

data() {
    return {
        dates: [],
    };
},

{ id: id, value: ''}というようなオブジェクトの配列となるdatesプロパティを作ります。

枠追加ボタンの表示

<template v-if="isEmpty(dates)">
    <a href="javascript:void(0);" @click="addDateBox(0)"><img src="./addbutton.png" alt="枠追加ボタン"></a>&nbsp;
</template>

クリックするとdatesプロパティにオブジェクトを追加するaddDateBox()関数を@clickで発火するよう設定してます。
また、datesプロパティが空の場合は入力欄は表示せず、枠追加ボタンだけ表示するようにisEmpty()関数を使って制御しています。

addDateBox()の作成

addDateBox(id) {
    this.dates.push({ id: id, value: "" });
},

入力欄の作成

<div v-for="date in dates" :key="date.id" class="sp-pt020">
    <input class="sys-input-txt sys-txt20" type="text" v-model="date.value" :ref="`date${date.id}`">&nbsp;
</div>

v-modeldateプロパティのvalueを設定し、:ref属性にはdateという文字列に各dateプロパティのidの値をくっつけて一意の文字列にしています。これにより入力欄(inputタグ)が複数個になっても、各入力欄(inputタグ)の任意のものを参照できるようになります。

躓いた箇所①

<div v-for="date in dates" :key="date.id" class="sp-pt020">
    <input class="sys-input-txt sys-txt20" type="text" v-model="date.value" :ref="date">&nbsp;
</div>

:ref属性の値をオブジェクトで設定したところ、jqueryのdatepickerで:ref属性の値を参照できなくなった。一意の値になるようにすると上手くいく。

ただ、そもそものvueプロジェクトの作り方によっては、これで上手くいく場合もあった。難しい、、、

removeDateBox()の作成

removeDateBox() {
    this.dates.pop();
},

ループ内での枠追加ボタンと枠削除ボタンの作成

<a href="javascript:void(0);" @click="addDateBox(date.id+1)">
    <img src="./addbutton.png" alt="枠追加ボタン"></a>&nbsp;
<a href="javascript:void(0);" @click="removeDateBox">
    <img src="./removebutton.png" alt="枠削除ボタン"></a>

datepickerの設定

watch: {
    "dates.length"(newVal, oldVal) {
        if (newVal !== oldVal) {
            let id = newVal - 1;
            this.$nextTick(() => {
                jQuery(this.$refs['date'+id]).datepicker({
                    dateFormat: "yy-mm-dd",
                    showOn: "button",
                    buttonImage: "./calendar.png",
                    buttonImageOnly: true,
                    onSelect: (dateText) => {
                        this.dates[id].value = dateText;
                    },
                });
            });
        }
    },
},

何をしたいかと言うと、各入力欄毎にクリック用のカレンダー画像とdatepickerのカレンダーを表示させて、選択できるようにしたい。これを達成するために、dates配列の長さが変更されたときに、新しいdatepickerを初期化し、ユーザーが選択した日付を対応するdates配列の要素に設定できるようにします。

watchプロパティを使用して、dates.lengthの変更を監視し、dates.lengthが変更されると、指定された関数が呼び出されます。

newValdates.lengthの新しい値、oldValは以前の値で、newValoldValが異なる場合に処理が走るようにします。

this.$nextTick$nextTickメソッドは、DOMの更新が完了した後に指定されたコールバックを実行するので、これを利用してDOMが更新された後にjQueryのdatepickerを初期化しています。

jQuery(this.$refs['date'+id]).datepickerthis.$refs['date'+id]で、Vue.jsのref属性を使用して各入力欄(inputタグ)のDOM要素を参照しています。idはnewVal - 1で計算し、これはdates配列の最後の要素のidとなるようにします。

onSelectコールバック:ユーザーが日付を選択したときに呼び出されるコールバック関数で、選択された日付(dateText)をthis.dates[id].valueに設定します。

躓いた箇所②

addDateBox(id) {
    this.dates.push({ id: id, value: "" });
    jQuery(this.$refs["date" + id]).datepicker({
        dateFormat: "yy-mm-dd",
        showOn: "button",
        buttonImage: "./calendar.png",
        buttonImageOnly: true,
        onSelect: (dateText) => {
            this.dates[id].value = dateText;
        },
    });
},

datepickerの初期化のタイミングを、dates配列の値が更新された後にしないといけない。例えば、addDateBox()関数で実装すると、カレンダーピックの読み込みがvueの再レンダリングの前に行われて、以下の画像の通り上手くいかない。

おわりに

以上、いかがでしたでしょうか。

あまりないユースケースだと思いますが、と同時にあまり参考になる記事もなかったので、いつかどこかで誰かの一助となれば幸いです。

この記事を書いた人

KJG
KJGソリューション事業部 システムエンジニア
大学4年時春に文系就職を辞め、エンジニアになることを決意し、独学でRuby、Ruby on Railsを学習。
約1年間の独学期間を経てアーティスへWebエンジニアとして入社。現在はWebエンジニアとして、主にシステムの開発・運用に従事している。
抽象的なもの、複雑なものを言語化して文章にするのが好きで得意。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします