20210213のvue.jsに関する記事は23件です。

【JavaScript】Vue-chartjsを使い始める

現場では、Charjsを使用して、グラフを描いています。
勉強ノードをメモします。

Chart.jsの公式サイト:https://www.chartjs.org/

Chartjsを導入する方法:

  1. 外部スクリプト
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
  1. インストール
     開発ディレクトリ内、npmで下記のコマンドでインストールする。
     (スタンドアロン版、バンドル版両方インストールされる)
$ npm install chart.js --save

下記のnode_modulesモジュールとpacage-lock.jsonパッケージが生成される
スクリーンショット 2021-02-13 21.10.27.png

スタンドアロン版とバンドル版

スタンドアロン版
ファイル:
 dist/Chart.js
 dist/Chart.min.js
 (時間軸を使用したい場合は、Moment.jsをインストールする必要)

バンドル版(時間軸を使用可。Moment.jsインストール不要。)
ファイル:
 dist/Chart.bundle.js
 dist/Chart.bundle.min.js

必要に応じて、pathを確認、index.htmlファイルに導入する。例えば:Chart.js

<script src="node_modules/chart.js/dist/Chart.js"></script>

以上の設定は準備でしたら、例のコードを練習します。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 外部スクリプト方法 -->
    <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script> -->
    <!-- インストール方法 -->
    <script src="node_modules/chart.js/dist/Chart.js"></script>  
</head>
<body>
    <p>chart_</p>
    <!-- canvas要素を設置 -->
    <canvas id="myChart" width="400" height="400"></canvas>
    <script>
        // 要素またはコンテキストを取得
        var ctx = document.getElementById("myChart");
        // チャートタイプをインスタンスかする
        var myChart = new Chart(ctx, {
            // 棒グラフ
            type: 'bar',  
            // データを設置
            data: {
                // データのラベル
                labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
                datasets: [{
                    // 凡例
                    label: '# of Votes',
                    // データの量
                    data: [12, 19, 3, 5, 2, 3],
                    // 各データの色
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(255, 206, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(255, 159, 64, 0.2)'
                    ],
                    // 棒枠色
                    borderColor: [
                        'rgba(255,99,132,1)',
                        'rgba(54, 162, 235, 1)',
                        'rgba(255, 206, 86, 1)',
                        'rgba(75, 192, 192, 1)',
                        'rgba(153, 102, 255, 1)',
                        'rgba(255, 159, 64, 1)'
                    ],
                    // 棒枠線の厚さ
                    borderWidth: 1
                }]
            },
            // オプション
            options: {
                // スケール設定
                scales: {
                    // 縦スケールを設定
                    yAxes: [{
                        ticks: {
                            // ゼロから表示
                            beginAtZero:true
                        }
                    }]
                }
            }
        });
        </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】Vue-chartjsを使い始める ーー実装編

現場では、Charjsを使用して、グラフを描いています。
勉強ノードをメモします。

Chart.jsの公式サイト:https://www.chartjs.org/

Chartjsを導入する方法:

  1. CDNで
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script>
  1. インストール
     開発ディレクトリ内、npmで下記のコマンドでインストールする。
     (スタンドアロン版、バンドル版両方インストールされる)
$ npm install chart.js --save

下記のnode_modulesモジュールとpacage-lock.jsonパッケージが生成される
スクリーンショット 2021-02-13 21.10.27.png

スタンドアロン版とバンドル版

スタンドアロン版
ファイル:
 dist/Chart.js
 dist/Chart.min.js
 (時間軸を使用したい場合は、Moment.jsをインストールする必要)

バンドル版(時間軸を使用可。Moment.jsインストール不要。)
ファイル:
 dist/Chart.bundle.js
 dist/Chart.bundle.min.js

必要に応じて、pathを確認、index.htmlファイルに導入する。例えば:Chart.js

<script src="node_modules/chart.js/dist/Chart.js"></script>

以上の設定は準備でしたら、例のコードを練習します。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 外部スクリプト方法 -->
    <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.min.js"></script> -->
    <!-- インストール方法 -->
    <script src="node_modules/chart.js/dist/Chart.js"></script>  
</head>
<body>
    <p>chart_</p>
    <!-- canvas要素を設置 -->
    <canvas id="myChart" width="400" height="400"></canvas>
    <script>
        // 要素またはコンテキストを取得
        var ctx = document.getElementById("myChart");
        // チャートタイプをインスタンスかする
        var myChart = new Chart(ctx, {
            // 棒グラフ
            type: 'bar',  
            // データを設置
            data: {
                // データのラベル
                labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
                datasets: [{
                    // 凡例
                    label: '# of Votes',
                    // データの量
                    data: [12, 19, 3, 5, 2, 3],
                    // 各データの色
                    backgroundColor: [
                        'rgba(255, 99, 132, 0.2)',
                        'rgba(54, 162, 235, 0.2)',
                        'rgba(255, 206, 86, 0.2)',
                        'rgba(75, 192, 192, 0.2)',
                        'rgba(153, 102, 255, 0.2)',
                        'rgba(255, 159, 64, 0.2)'
                    ],
                    // 棒枠色
                    borderColor: [
                        'rgba(255,99,132,1)',
                        'rgba(54, 162, 235, 1)',
                        'rgba(255, 206, 86, 1)',
                        'rgba(75, 192, 192, 1)',
                        'rgba(153, 102, 255, 1)',
                        'rgba(255, 159, 64, 1)'
                    ],
                    // 棒枠線の厚さ
                    borderWidth: 1
                }]
            },
            // オプション
            options: {
                // スケール設定
                scales: {
                    // 縦スケールを設定
                    yAxes: [{
                        ticks: {
                            // ゼロから表示
                            beginAtZero:true
                        }
                    }]
                }
            }
        });
        </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

共変ベクトル、反変ベクトル生成器を作ってみた(MathJaxがFirebaseで動かなくて中途半端なでき)

共変ベクトルと反変ベクトルの計算を楽にしたい

一般相対性理論を勉強する場合に最大の難関が、「共変ベクトルや反変ベクトルのイメージがつかない」ことだと思います。
共変ベクトルと反変ベクトルを掛け合わせると綺麗にスカラーになる部分が一番面白いと個人的には思うのですが、その手前でイメージがつかないと挫折しそうでした。
そんなとき、「自動でクリストッフェルや共変や反変を計算してくれるサイトがあればなあ」と思っていました。
思っていたので、作りたくなり、作りました。

反変ベクトルと共変ベクトルの計算機

自分のコンピューターでみた場合 vs Firebaseにホスティングした場合

自分のコンピューターでみると、うん!ばっちり!なできです 1次〜100次元まで計算できます!
スクリーンショット 2021-02-13 午後9.17.54.png
 
しかし、Firebaseにホスティングすると、LaTex表示になってしまいます。
スクリーンショット 2021-02-13 午後9.18.26.png

なぜこうなったのか

webに数式を表示するには、「MathJax」というjavascriptの手法を使うのが楽で、
MathJaxを使えば、Latex形式で貼り付ければ数式が表示されます。
しかし、FirebaseはMathJaxと相性が悪いらしく、なんらかのおまじないを打たなければ厳しいみたいです。

中の仕組み

今回の作品は、全然ダメではありますが、
「LaTex形式では掃き出せている」ので、個人的には満足です(wordとかに貼り付けて変換するなりしていただければ使えるでしょうし)

その場で計算式を解いているわけではなく、pythonでn=1~100まで計算させてからその結果を予めhtmlに忍ばせており、
vue.jsでイベントハンドリング(数値が入力されたら、showされるものを変える)しています。

結果と考察

・webに数式を表示する技術に技術的限界が現状あり、これは誰かが解決してもいい問題のように感じた
 ⇨そのため、今回はwebブラウザ上での計算を諦め、予めpythonで計算したものを表示することにした。

・クリストッフェルをそのまま計算したりする手法は、pythonやC++では存在するが、web上でだれでも計算できるようには現状なっていない。ここは誰かが解決してもいいような問題に感じた
 ⇨大学生のレポートが楽になるし、相対性理論で苦しめられる学生が減るのでは?

・LaTexにおいて、「\」が多様されるが、pythonはこの記号がエスケープに使われるため、かなりプログラム自体に苦戦した。
 ⇨pythonにおいては、「\」によって、「\」一個分を表示する(エスケープでエスケープする)という文法であることを学んだ。これに気づくまでがまた長かった。

今後

Firebaseがダメなら、HerokuとAWSは試したいところ。
できれば、共変ベクトル、反変ベクトルの経験をいかして、リッチテンソル、クリストッフェルも計算したいところ!(これもブラウザ上計算ではなく、結果を予め出力したものが妥当に思います。)

HTMLでLaTex形式で出力するpythonのソースコード

for n in range(1,101):
    First_1 = "          <jigen"
    First_2 = " v-show="
    First_3 = "\"jigen == "
    First_4 = "\">"
    First = First_1 + str(n) + First_2+ First_3 + str(n) + First_4
    print(First)

    Second_1 = "            ~~~~~~~~~~~~~~"
    print(Second_1)

    Third_1 = "            <br>共変ベクトル \[q_i^\prime=\]"
    print(Third_1)
    Forth_1 = "            <p class="+ "\"sample1\"" + ">"
    print(Forth_1)
    Fifth_1 = "              <!-- 共変ベクトル -->"
    print(Fifth_1)

    #共変
    res_start = "\["
    res1 = "\\"+ " " + "\\"+ " " +"\\"+ "frac{\partial x_"
    res2 = "}{\partial x_i\ }\ q_"
    res_end = "\]"

    res_ans1 = "" 

    for s in reversed(range(1,n+1)):
      res = res1 + str(s) + res2 + str(s)
      #print(res)
      res_ans1 = res + "+" + " " + res_ans1
      #print(res_ans1)

    res_ans1 = res_ans1.rstrip(" +")
    res_ans2 = "              " +res_start + res_ans1 + res_end
    print(res_ans2)

    Seventh_1 = "            </p>"
    print(Seventh_1)

    Eighth_1 = "            ~~~~~~~~~~~~~~"
    print(Eighth_1)

    Ninth_1 = "            <br>反変ベクトル \[q^{\prime i}=\]"
    print(Ninth_1)
    Tenth_1 = "            <p class="+ "\"sample1\"" + ">"
    print(Tenth_1)
    Eleventh_1 = "              <!-- 反変ベクトル -->"
    print(Eleventh_1)




    #反変
    res_start = "\["
    res1 = "\\"+ " " + "\\" + "frac{\partial x^{\prime i}}{\partial x^"
    res2 = "\ }\ q^"
    res_end = "\]"

    res_ans1 = "" 

    for s in reversed(range(1,n+1)):
      res = res1 + str(s) + res2 + str(s)
      #print(res)
      res_ans1 = res + "+" + " " + res_ans1
      #print(res_ans1)

    res_ans1 = res_ans1.rstrip(" +")
    res_ans2 = "              " + res_start + res_ans1 + res_end
    print(res_ans2)

    Thirteenth_1 = "            </p>"
    print(Thirteenth_1)

    Forteenth_1 = "          </jigen"+str(n)+">"
    print(Forteenth_1)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでAdobe Fonts(TypeKit)を使う

Nuxt.jsでもAdobe Fontsを使いたい

静的なHTMLページをNuxt.jsで作り直そうと思ったのですが、Adobe Fontsの埋め込みコードを発行しても、以下のようにピュアなJS用のコードしか発行されない。

AdobeFontsが生成するJS
(function (d) {
  var config = {
      kitId: '各自異なるID',
      scriptTimeout: 3000,
      async: true,
    },
    h = d.documentElement,
    t = setTimeout(function () {
      h.className = h.className.replace(/\bwf-loading\b/g, '') + ' wf-inactive'
    }, config.scriptTimeout),
    tk = d.createElement('script'),
    f = false,
    s = d.getElementsByTagName('script')[0],
    a
  h.className += ' wf-loading'
  tk.src = 'https://use.typekit.net/' + config.kitId + '.js'
  tk.async = true
  tk.onload = tk.onreadystatechange = function () {
    a = this.readyState
    if (f || (a && a != 'complete' && a != 'loaded')) return
    f = true
    clearTimeout(t)
    try {
      Typekit.load(config)
    } catch (e) {}
  }
  s.parentNode.insertBefore(tk, s)
})(document)

これだとプロジェクトにそのまま読み込めない。

Googleで調べてみる

Cannot find name 'Typekit'

Nuxt.jsでいい感じにAdobe fontsのwebフォントを読み込む

こちらの記事の方法でもできるはできるのですが、Cannot find name 'Typekit'.エラーが出てしまい、buildができない。

Vue.jsでのやり方があった

Vue.jsとBootstrapVue上でAdobeの日本語ウェブフォントを使う

pluginとして、Adobe Fontsで生成されたJSを単体ファイルとして作成し、それを読み込むという方法。これならいけそう!

解決

まずはAdobe Fontsで生成されたJSをpluginsで作成

plugins/typekit.js
/* eslint-disable */
// めちゃめちゃ怒られるのでESLintは切っておく
;(function (d) {
  var config = {
      kitId: '各自異なるID',
      scriptTimeout: 3000,
      async: true,
    },
    h = d.documentElement,
    t = setTimeout(function () {
      h.className = h.className.replace(/\bwf-loading\b/g, '') + ' wf-inactive'
    }, config.scriptTimeout),
    tk = d.createElement('script'),
    f = false,
    s = d.getElementsByTagName('script')[0],
    a
  h.className += ' wf-loading'
  tk.src = 'https://use.typekit.net/' + config.kitId + '.js'
  tk.async = true
  tk.onload = tk.onreadystatechange = function () {
    a = this.readyState
    if (f || (a && a != 'complete' && a != 'loaded')) return
    f = true
    clearTimeout(t)
    try {
      Typekit.load(config)
    } catch (e) {}
  }
  s.parentNode.insertBefore(tk, s)
})(document)

作成したtypekit.jsをグローバルに読み込む

nuxt.config.js
export default {
  ~ 省略 ~
  plugins: [
    { src: '~/plugins/typekit.js', mode: 'client' }
  ],
  ~ 省略 ~
}

ここでmode: 'client'オプションをつけておかないと、SSR時に「documentが見つからないよ!」となるので注意。

おしまい

有料だし、あんまりAdobe Fontsを使うことはないかもしれないけどAdobe CC契約している人はぜひ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueとLaravelでStripeElementsを使ってクレジットカード情報を更新する方法

毎日投稿59日目

毎日記事を更新しているのですが、意外とネタは尽きないものです。

さて今回は、タイトルの通りStripeでクレジットカードの変更を行っていきます。

StripeのAPIについてはそこまで詳しく説明は致しません!

恐らくクレジットカードの情報を変更したいなと思った方は、Stripeの基礎的な部分は理解していると思うので。

イメージとしてはこんな感じです。

無題.png

また、HTMLやCSSの記述が多いのでここらへんはコピペしてください。

話はこの辺にして早速説明を見ていきましょう!

StripeElementsの設置

Stripeでクレジットカードの情報を変更するには、カード番号、有効期限、セキュリティコードを入力してもらいトークンを作成します。

そのトークンを使ってクレジットカードの情報を変更します。

とりあえずStripeElementsの設置

Stripe.vue
<template>
   <div class="group">
      <label for="card-element"> クレジットカード情報 </label>
      <div id="card-element"></div>
   </div>
</template>

<script>
export default {
  data() {
    return {
      stripe: null,
      card: null,
      token: ''
    }
  },
  async mounted() {
    this.stripe = window.Stripe(process.env.VUE_APP_STRIPE_PUBLIC_KEY)
    const elements = this.stripe.elements()
    this.card = await elements.create('card', {
      hidePostalCode: true,
      style: {
        base: {
          iconColor: '#666EE8',
          color: '#31325F',
          lineHeight: '40px',
          fontWeight: 300,
          fontFamily: 'Helvetica Neue',
          fontSize: '15px',

          '::placeholder': {
            color: '#CFD7E0'
          }
        }
      }
    })
    this.card.mount('#card-element')
  },
}
</script>

これでクレジットカードを入力する要素が表示されると思います。

必要項目の入力

次に、住所や名前を入力してもらうフォームを作成します。

ここに関しては先ほどの記述を一旦消してコピペしてください。

CSSの記述も多いので。

Stripe.vue
<template>
  <div class="creditcard-change">
    <div class="cell">
          <form>
            <div class="group">
              <label for="card-element"> クレジットカード情報 </label>
              <div id="card-element"></div>
            </div>
            <div class="group">
              <label>
                <span>氏名</span>
                <input
                  id="name"
                  name="name"
                  type="text"
                  class="field"
                  placeholder="YOSHIHIRO FUJIWARA"
                  autocomplete="name"
                />
              </label>
              <label>
                <span>メールアドレス</span>
                <input
                  id="email"
                  v-model="email"
                  type="email"
                  name="email"
                  class="field"
                  placeholder="zaemonia@example.com"
                  autocomplete="email"
                />
              </label>
            </div>
            <div class="group">
              <label>
                <span>郵便番号</span>
                <input
                  id="postal-code"
                  name="address_line1"
                  type="text"
                  class="field"
                  placeholder="1234567"
                  maxlength="7"
                  autocomplete="postal-code"
                />
              </label>
              <label>
                <span>都道府県</span>
                <input
                  id="address-state"
                  name="address_state"
                  type="text"
                  class="field"
                  placeholder="神奈川県"
                  autocomplete="address-level1"
                />
              </label>
              <label>
                <span>市区町村</span>
                <input
                  id="address-city"
                  name="address_city"
                  type="text"
                  class="field"
                  placeholder="相模原市緑区二本松"
                  autocomplete="address-level2"
                />
              </label>
              <label>
                <span>番地</span>
                <input
                  id="address-line1"
                  name="address-line1"
                  type="text"
                  class="field"
                  placeholder="1-34-10"
                  autocomplete="address-line1"
                />
              </label>
              <label>
                <span>マンション・アパート名</span>
                <input
                  id="address-line2"
                  name="address-line2"
                  type="text"
                  class="field"
                  placeholder="マンションズタワー502"
                  autocomplete="address-line2"
                />
              </label>
            </div>
            <b-button
              class="recaptcha-button-v3"
              type="is-success"
              disabled="true"
              style="width: 100%; padding: 25px 0"
              @click="submit"
            >
              クレジットカードを変更する
            </b-button>
          </form>
        </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      stripe: null,
      card: null,
      token: ''
    }
  },
  async mounted() {
    this.stripe = window.Stripe(process.env.VUE_APP_STRIPE_PUBLIC_KEY)
    const elements = this.stripe.elements()
    this.card = await elements.create('card', {
      hidePostalCode: true,
      style: {
        base: {
          iconColor: '#666EE8',
          color: '#31325F',
          lineHeight: '40px',
          fontWeight: 300,
          fontFamily: 'Helvetica Neue',
          fontSize: '15px',

          '::placeholder': {
            color: '#CFD7E0'
          }
        }
      }
    })
    this.card.mount('#card-element')
  },
  methods: {
    submit() {
      const options = {
        name: document.getElementById('name').value,
        email: document.getElementById('email').value,
        address_line1: document.getElementById('address-line1').value,
        address_line2: document.getElementById('address-line2').value,
        address_city: document.getElementById('address-city').value,
        address_state: document.getElementById('address-state').value,
        address_zip: document.getElementById('postal-code').value,
        address_country: 'JP'
      }
      Object.keys(options).forEach((value) => {
        if (value === '') {
          alert('未入力の項目があります')
        }
      })
      this.stripe.createToken(this.card, options).then((result) => {
        // エラーの場合
        if (result.error) {
          alert(result.error.message)
          // 成功の場合
        } else {
          this.token = result.token.id
        }
      })
    },
  }
}
</script>

<style scoped>
.creditcard-change {
  padding: 120px 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  min-height: 100%;
  width: 550px;
  margin: 0 auto;
}
.creditcard-change > div {
  flex: 0 0 100%;
}
/* クレジットカード入力欄 */
.cell {
  margin-top: 30px;
  background: #e6ebf1;
  padding: 30px;
}
.group {
  background: white;
  box-shadow: 0 7px 14px 0 rgba(49, 49, 93, 0.1), 0 3px 6px 0 rgba(0, 0, 0, 0.08);
  border-radius: 4px;
  margin-bottom: 20px;
}

.cell label {
  position: relative;
  color: #8898aa;
  font-weight: 300;
  height: 100%;
  line-height: 40px;
  margin-left: 20px;
  display: flex;
  flex-direction: row;
}

.group label:not(:last-child) {
  border-bottom: 1px solid #f0f5fa;
}

label > span {
  width: 120px;
  text-align: right;
  margin-right: 30px;
}

.field {
  background: transparent;
  font-weight: 300;
  border: 0;
  color: #31325f;
  outline: none;
  flex: 1;
  padding-right: 10px;
  padding-left: 10px;
  cursor: text;
}

.field::-webkit-input-placeholder {
  color: #cfd7e0;
}

.field::-moz-placeholder {
  color: #cfd7e0;
}

.outcome {
  float: left;
  width: 100%;
  padding-top: 8px;
  min-height: 24px;
  text-align: center;
}
</style>

b-button初めて気になった方はこちらの記事をご覧ください。

初心者必見!サイト制作は楽してなんぼ。CSSフレームワークBuefyの紹介!!

Buefyはほんとにおススメなのでぜひ使ってください!

クレジットの更新

submitで取得したresult.token.idをバックエンドに送ってください。

適当にaxiosとかで。

そしたら、Laravelのコントローラーでクレジットカードを更新します。

StripeController.php
        public function creditUpdate(Request $request) {
        // ユーザーのクレジットカード情報の更新
        require_once(__DIR__.'/../../../vendor/autoload.php');

        $secret_key = config('app.STRIPE_SECRET_KEY');

        $stripe = new \Stripe\StripeClient($secret_key);

        // カスタマーID、トークンの取得
        $customer_id = $request->id;
        $token = $request->token;

        $customer = $stripe->customers->retrieve(
            $customer_id,
            []
        );

        // クレジットを登録していない場合、処理を終了
        if(!$customer) {
            return response()->json([
                'message' => 'The customer is undefiend',
            ], 400);
        }

        // ソースの作成
        $new_card = $stripe->customers->createSource(
            $customer_id,
            ['source' => $token]
        );
        $customer->default_source = $new_card->id;
        $customer->save();

        return response()->json([
            'data' => $new_card,
            'message' => 'Updating customer`s paymentmethods is success',
        ], 200);
    }

$stripe->customers->createSourceに先ほど取得したトークンをソースとして登録することでクレジットカードの情報を更新することができます。

save()忘れないように!!

実際に変更をされているかはStripeの画面から確認して見て下さい!

いかがだったでしょうか??

記述が多いので嫌かもしれないですが、本当にコピペしてもらって結構です。

むしろコピペしてください!

また、クレジットカードの情報を取得したい場合はStripeの公式ドキュメントを参照してください!

https://stripe.com/docs/api

以上、「VueとLaravelでStripeElementsを使ってクレジットカード情報を更新する方法」でした!

良ければ、LGTM、コメントお願いします。

また、何か間違っていることがあればご指摘頂けると幸いです。

他にも初心者さん向けに記事を投稿しているので、時間があれば他の記事も見て下さい!!

Thank you for reading

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue3】オブジェクトのプロパティを追加してもリアクティブに動作するようになりました

【Vue.js】オブジェクトのプロパティを追加してもリアクティブに動作しなかった時の備忘録にて、Vue2.xにおいて、オブジェクトのプロパティを追加してもリアクティブに動作しないこと、またその解決策について書きました。

Vue3で同じことをしようとすると、どのような挙動になるのか確認してみました。結論を先に言ってしまうと、Vue3からオブジェクトにプロパティを追加した場合もリアクティブに動作するようになりました。

Vue3環境の準備

vue-cliコマンドを利用してVue3をインストールしていきます。vue createコマンドでVue 3 Previewを選択します。(vue-cliがv4.5以上でないとVue3環境は作成できません。)

vue create study-vue3
? Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)

(ディレクトリ名はstudy-vue3としました。)

npm run serveを実行して以下の画面が確認できれば、インストールは無事に完了しています。

image.png

HelloWorldを書き換える

オブジェクトのプロパティがv-ifに指定されるようHelloWorld.vueを以下に書き換えます。

<template>
  <div>
    <button @click="show">表示する</button>
    <p v-if="hoge.isShow">表示</p>
  </div>
</template>

<script>
import { reactive } from 'vue'
export default {
  name: 'HelloWorld',
  setup() {
    const hoge = reactive({})
    const show = () => {
      hoge.isShow = true
    }
    return { hoge, show }
  }
}
</script>

Vue2.xの場合、ボタンを押しても表示の文字が出ないことは、【Vue.js】オブジェクトのプロパティを追加してもリアクティブに動作しなかった時の備忘録の記事で説明しましたが...

Vue3から導入されたreactiveメソッドによってこの課題が解決されました。

reactiveメソッドから返されるオブジェクトは、プロパティが追加された場合でもリアクティブに動作させる事が可能になります。??

Reactivity API
The reactive conversion is "deep"—it affects all nested properties.

【動作イメージ】
Kapture 2021-02-13 at 01.30.39.gif

Vue3環境でVue2.xコードを書いて動作確認してみる

Vue2.xでは、リアクティブに動作しなかったコードを、Vue3環境で動かすとどうなるのか確認してみました。

<template>
  <div>
    <button @click="show">表示する</button>
    <p v-if="hoge.isShow">表示</p>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      hoge: {
        // isShow: false, コメントアウトで未定義にしています
      }
    }
  },
  methods: {
    show () {
      this.hoge.isShow = true
    }
  }
}
</script>

Vue3ではリアクティブなプロパティとして認識されるようになっていました。
Kapture 2021-02-13 at 01.30.39.gif

この挙動になる理由について記載されたリファレンス等は見つかり次第追記していきます。?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue】Vuetify.jsにコントリビュートするための準備

スクリーンショット 2021-02-14 1.54.29.png

Vuetify.jsにコントリビュートしてみたい!という方に開発環境の構築を本記事でまとめました。

コードをcloneする

git clone https://github.com/vuetifyjs/vuetify.git

以降のコマンドはすべてcloneで取得したvuetifyディレクト配下で実行しています。

yarnコマンドを使用できるよう準備

  • npmからyarnをインストールする
npm install -g yarn

パッケージをインストールする

yarn

ビルドの実行

yarn build

開発サーバの起動

  • 起動する前に、packages/vuetify/dev/Playgroud.vue を以下に書き換える
packages/vuetify/dev/Playgroud.vue
<template>
  <v-container>
    <p>ただいま開発環境の動作確認中</p>
  </v-container>
</template>

<script>
  export default {
    data: () => ({
    //
    }),
  }
</script>

  • 開発サーバを起動する
yarn dev

image.png

Playgroud.vueの内容がブラウザから確認できれば、vuetify.jsの開発環境構築は完了です。

issueで上がっているバグを再現する際は、Playground.vueにコピペすると大抵は再現確認できるので、改修作業を行う際はPlaygroud.vueを利用しましょう。?

PRを出す際は、既にMergeされているPRを参考に作成すると、レビューを見てもらいやすくなります。報告内容をFixes:xxxだけにしないで、修正前後の挙動の違いについても記載すると良さそうです。

参考記事:https://vuetifyjs.com/en/getting-started/contributing/#working-with-github

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue3でlodash入れた時のエラー

lodashインストール時のエラー

lodashの公式ページ通りやって、yarn serveしてみたところ

npm install --save vue-lodash lodash

エラーが出た。

Error: Cannot find module 'vue-loader-v16/package.json'
Require stack:
- /Users/kiratanaka/Vue/MyCreate/portfolio/node_modules/@vue/cli-service/lib/config/base.js
- /Users/kiratanaka/Vue/MyCreate/portfolio/node_modules/@vue/cli-service/lib/Service.js
- /Users/kiratanaka/Vue/MyCreate/portfolio/node_modules/@vue/cli-service/bin/vue-cli-service.js
Error: Cannot find module 'vue-loader-v16/package.json'

これしたらいいんじゃない?と言われるけど、無駄

npm i --save-dev vue-loader-v16

解決

このページに助けられた。

yarn add lodash

これだけ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】vue3でlodash入れた時のエラー

lodashインストール時のエラー

lodashの公式ページ通りやって、yarn serveしてみたところ

npm install --save vue-lodash lodash

エラーが出た。

Error: Cannot find module 'vue-loader-v16/package.json'
Require stack:
- /Users/kiratanaka/Vue/MyCreate/portfolio/node_modules/@vue/cli-service/lib/config/base.js
- /Users/kiratanaka/Vue/MyCreate/portfolio/node_modules/@vue/cli-service/lib/Service.js
- /Users/kiratanaka/Vue/MyCreate/portfolio/node_modules/@vue/cli-service/bin/vue-cli-service.js
Error: Cannot find module 'vue-loader-v16/package.json'

これしたらいいんじゃない?と言われるけど、無駄

npm i --save-dev vue-loader-v16

解決

このページに助けられた。

yarn add lodash

これだけ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

音楽理論の学習・音楽制作支援ウェブアプリを作りました。

こんにちは!クフルダモノーツYoshito Kimura(k1mu)です!

音楽理論の学習・音楽制作支援ウェブアプリ「O-TO」を作りました。

音楽を20年以上やっている僕が「あったら良いな」と思う機能を具現化しました。

プログラミングは得意ではないので、技術的に優れているかは分かりません。
ただ、機能的にはめちゃくちゃ便利だと思います。(๑˃̵ᴗ˂̵)و

O-TO」について

基本的には音楽に関する「音楽の本質と関係無い思考を肩代わりしてくれるアプリ」です。

たとえば、音符の長さの計算を頑張っても音楽が良くなるわけではありません。むしろ余計に疲れます。
音楽理論の学習も、難しい文章から読み解くより、視覚的に分かりやすく勉強できた方が楽です。

そういった“創造の本質”とは関係の無い部分に、余計な頭脳と時間を使わないために活用してもらえればな と思います。

作った経緯

個人的に音楽の作業には「コレは無駄じゃ無いか?」と感じるものがたくさんありました。

たとえば、ディレイタイムを計算したり、五度圏で音の流れを考える作業などです。

これらの作業は、音楽の良し悪しには関係ありません。
むしろ、瞬時に結果が分かった方が試行錯誤の時間が多く確保できます。

また、僕は音楽理論の学習に苦戦した経験から
「もっと音楽の専門知識を活用する敷居を下げたい」気持ちがずっとありました。
(音楽理論関係のブログを書いているのもそのため)
https://khufrudamonotes.com/category/music-theory-and-dtm

そういうわけで、「音楽制作や音楽理論の学習を補助するウェブアプリ」の目指して出来上がったのが「O-TO(Ongaku Tools)」です。

現在提供している機能と、その説明

コードネーム

主な和音の構成音を視覚的に確認できます。
コードを学ぶ方は、解説図を見ながら表示を切り替えてみると、コードネームの仕組みが分かってくると思います。

最終的には(特にジャズをやる方は)、頭の中でコードの構成音がパッと把握出来た方が良いかもしれません。
ただ、僕自身は出来るまで10年近くかかった気がするので、補助輪としても使えると思います。

スケール

主要な音階の構成音を視覚的に確認できます。
こちらも、初学者の方がスケールを学ぶのに役立つはずです。

少し変わったスケールも登録しておいたので
色々なスケールを試してみたいときに、いちいちキーの変更を頭の中で考えなくて良いので楽な筈です。

モーダル・インターチェンジ

モーダルインターチェンジの候補を絞り込む機能です。

コード知識がある人は、特定のコード上で使用できるスケールを探すのにも使えます。
たとえば、ドミナント7thコード上でアドリブできるスケールと
テンションノートの関係などは視覚的に理解しやすいと思います。

コード進行

良く使用されるコード進行を50種類以上まとめました。

オリジナル曲を作る方なんかは、ネタに困った時に便利だと思います。

しかも、表示を全てのキーに切り替えられます。
(ピアノの黒鍵に当たる異名同音をちゃんと処理しているところが、個人的な頑張りポイントです。)

ダイアトニック・コード

主なダイアトニックコードの一覧表です。
こちらも全てのキーに表示を切り替えられます。

ちなみに、僕は大体この表の環境を概ね「1つの環境」として頭の中で捉えていて、
この環境から離れる場合に「転調している」と考える場合が多いです。

転調の間隔

転調の間隔を簡単に調べられます。

転調と言っても、キーが半音上がるくらいは簡単に分かると思います。
しかし、モードチェンジを絡めた複雑な転調とそのキー(調号)の判定を頭の中でやるのはなかなか大変だと思います。

この機能を使えば、どんな複雑な関係にあるキーの転調も一瞬で分かります。

音価の計算

シンプルに音価の計算ができるだけです。

ここまで紹介した機能は、音楽を長くやっていればある程度脳内で対応できる人も多いと思います。
しかし、各BPMごとの音価を暗記している人は少ないんじゃないでしょうか。
ある意味一番便利な機能かもしれません。

メトリック・モジュレーション

メトリックモジュレーションに関係する情報を調べられます。
メトリックモジュレーションは、整合性を持ったテンポチェンジなどに使われる手法です。

おまけで音符を「分音符のみで表記した場合」や「符点音符のみで表記した場合」も計算される機能をつけました。
「現在の記譜法」の上手くできている部分と、そうでない部分を感じれる気がします。笑

プログラミングの技術的な内容など

こちらの記事にまとめました!(๑˃̵ᴗ˂̵)و

・プログラミング何も分からない初心者が、独学でウェブアプリを作るまでの軌跡 まとめ
https://qiita.com/k0419/items/292d10f729992e7e22e3

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vueでローディング画面の実装

概要

Vueでローディング画面を作成します。

こういうの↓
ダウンロード (2).gif

実装

ローディングのモジュールはいくつかありますが、今回はvue-loadersというモジュールを利用します。
アニメーションの種類が多かったのでこれにしました。

導入方法はこちら
github

今回はmounted()のタイミングでローディングを開始し、mounted()が終わったタイミングでローディングを終了したいと思います。

store

store/loading.js
const state ={
    loading: true, //true:ローディングを表示, false:ローディング非表示
};

const getters ={
    loading: state => state.loading ? state.loading: '',
};

const mutations ={
    setLoading(state,loading){
        state.loading = loading;
    },
};
const actions = {
    async startLoad(context){
        context.commit('setLoading', true);
    },
    async endLoad(context){
        context.commit('setLoading', false);
    } 
};

export default{
    namespaced: true,
    state,
    getters,
    mutations,
    actions
}

ローディングが開始したらstartLoadを発火、終わったらendLoadを発火して非表示にします。

template

<template>
    <div class="new-article">
            <div class="loader-space" v-show="loading">
                <vue-loaders-ball-beat color="#FFF" class="loader"></vue-loaders-ball-beat>
            </div>
            <div class="contents" v-show="!loading">
            <!--ローディング後に表示したいコンテンツ-->
            </div>
        </div>

</template>
<script>
export default {
    computed: {
        loading(){
            return this.$store.getters["loading/loading"];
        },
    },
    methods: {
        async getPost() {
            const post = this.$store.getters["newtimeline/post"];
            if(!post){
                await this.$store.dispatch('newtimeline/getPost');
            }
        },
    },
    mounted() {
        console.log('mounted start');
        this.$store.dispatch('loading/startLoad')               //ローディング開始をする loading = true;
            .then(()=>this.getPost())                           //getPost()メソッドを開始
            .then(()=>this.$store.dispatch('loading/endLoad')); //ローディング終了 loading = false;
    },
}
</script>

非同期処理で動かさないとendLoad()の方が先に走ってしまうので、非同期で実装しましょう。

最後に

割と簡単に実装することができました。
面白いアニメーションもあり、使ってて楽しかったです。

参考文献

Vue.jsで28種類のローディングアニメーションを実装する「vue-loaders」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】はじめてのVue.js〜概要+インスタンス+ディレクティブ〜

今回はJavaScriptのフレームワークである「Vue.js」についての記事です。学習途中で至らない点等あると思うのですが、備忘録兼自分と同じ初学者の方向けにまとめてみました。

現在はVue.jsを用いてポートフォリオを書き換えつつ、学習を進めています。記述したコードも用いているので、是非参考にしてみてください。ポートフォリオはこちら、GitHubはこちらのページをご覧ください。ポートフォリオ内の学習スキルを記述した以下の部分と
代替テキスト
ヘッダーのアイコン部分を
代替テキスト
の2つを例にインスタンス、ディレクティブの書き方をまとめています。

目次

  1. 概要

    1. Vue.jsとは
    2. メリット
    3. 環境準備
  2. インスタンス

    1. インスタンスとは
    2. 書き方
    3. elオプションオブジェクト
    4. dataオプションオブジェクト
    5. methodsオプションオブジェクト
  3. ディレクティブ

    1. ディレクティブとは
    2. v-bind
    3. v-on
    4. v-html
    5. v-for
  4. 最後に

1. 概要

1.1 Vue.jsとは

JavaScriptのフレームワーク1の一種。DOM操作を自動化して効率良く、そしてコードを複雑にしすぎることなくシンプルにフロントエンドの開発が行うことができる。
Vue.jsという土台の上で、ライブラリを用いて必要な機能を追加していくことで、効率良く開発を進めることができる。

1.2 メリット

扱うデータやイベントが多くなっても、コードが複雑になりにくいことが挙げられる。理由は大きく分けて2つある。

①DOM操作を自動で行ってくれる
今回の記事で紹介するインスタンスやディレクティブを用いることで実現することができる。詳細は以下の「2.インスタンス」「3.ディレクティブ」を参照。

②DOM要素を部品化して使い回すことができる
こちらは次の記事で紹介するコンポーネントを用いることで実現することができる。

1.3 環境準備

HTMLファイルにCDN2を読み込む。至ってシンプル。HTMLのファイル名をindex.htmlとする。

index.html
<!-- 開発バージョン、便利なコンソールの警告が含まれています -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

または

index.html
<!-- 本番バージョン、サイズと速度のために最適化されています -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>

2. インスタンス

2.1 インスタンスとは

一言で表すと、複製したオブジェクト3のこと。

2.2 書き方

new Vue({...}) によって、Vueオブジェクトのインスタンスを作る。今回はskillsというインスタンスを作成している。

main.js
const skills = new Vue({
    el:'#skills',
    data:{
        skills:[
            {
                id:'frontend_skills',
            },
            //他のバックエンド、DataBase等も同様なので省略   
        ],
    },
})

const header = new Vue({
    el:'header',
    methods:{
        scroll:function(event){
            switch(event.currentTarget){
                case $('.about-icon')[0]:
                    const about = $('#about').offset().top;
                    $("html, body").animate({ scrollTop: about - 20 }, 200);
                    break;
                //他のcaseも同様なので省略
            }
        }
    }
})

newは演算子の一つ、Vue({...})は、オブジェクトを複製するための特別な関数(コンストラクタ)である。コンストラクタの引数には、オブジェクトを渡す。Vue.jsではこのオブジェクトを、オプションオブジェクトという。オプションオブジェクトに決められたプロパティを設定することで、Vue.jsの機能を利用することができる。

skillsインスタンスではelオプションオブジェクトdataオプションオブジェクトを、headerインスタンスではelオプションオブジェクトmethodsオプションオブジェクトを使用している。

2.3 elオプションオブジェクト

element(要素)の略称。DOM要素を設定する。
#skillsでは、id属性がskillsの要素を設定している。この要素内でのみインスタンスの中身を使用することができる。

2.4 dataオプションオブジェクト

データを設定する。このデータにはVueインスタンスのプロパティとしてアクセスできる。

index.html
<div id="skills">
  <p>{{ skills[0].id }}</p>
</div>

このように記述することで、インスタンスのdataオプションオブジェクト内のskills配列のインデックス番号0番目要素のidプロパティの値を表示することができる。
{{...}}をMustache構文といい、インスタンスのdataオプションオブジェクト内のデータとHTMLを結びつけることができる。これをデータバインディングという。
ただし、Mustache構文では値をテキストデータとして扱うため、プロパティの値に<br><span>要素を記述しても、文字列として表示されてしまう。HTMLへの変換はv-htmlディレクティブを用いる必要がある。

2.5 methodsオプションオブジェクト

elで設定したDOM要素内で関数を使用したい場合に用いる。v-onディレクティブで指定することで関数を使用することができる。

3. ディレクティブ

3.1 ディレクティブとは

「2.4 dataオプションオブジェクト」で紹介したように、Mustache構文を用いることで、DOM要素とインスタンスのデータをバインディングすることができた。
しかし、DOM要素の属性とインスタンスのデータをバインディングする際にMustache構文は使うことができない。
そこで登場するのがディレクティブ。一言で表すと、Vue.jsで使用する特別な属性のこと。

3.2 v-bind

上記の例で、インスタンスのdataオプションオブジェクト内のskills配列のインデックス番号0番目要素のidプロパティの値を表示するのではなく、<p>のid属性に設定したい場合は、以下のように記述する。

index.html
<div id="skills">
  <p v-bind:id="skills[0].id">v-bindディレクティブ</p>
</div>

v-bind:の後に属性を指定し、属性値にインスタンスのdataオプションオブジェクトのプロパティを記述する。

動的なclass属性のバインディングは少し特殊なのでメモしておく。
v-bind:classにオブジェクトを渡すことでクラスを動的に切り替えることができる。その際、クラスの付与をデータプロパティの真偽性によって決めている。参考までに、ドキュメントの例を載せておく。
以下のような要素があったとする。isActivetrueの場合はactiveクラスが付与され、hasErrortrueの場合はtext-dengerクラスが付与されることを表している。

<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>

インスタンス内のdataオプションオブジェクトは以下のものだったとする。

data: {
  isActive: true,
  hasError: false
}

すると、以下のように特定のクラスのみを付与することができる。

<div class="static active"></div>

3.3 v-on

「2.5 methodsオプションオブジェクト」で紹介したmethodsオプションオブジェクトをDOM要素内で使用する際に用いる。

index.html
<header>
  <span v-on:click="scroll"><i class="fas fa-user-alt"></i></span>
  <!-- 他のアイコンも同様なので省略 -->
</header>
main.js
const header = new Vue({
    el:'header',
    methods:{
        scroll:function(event){
            switch(event.currentTarget){
                case $('.about-icon')[0]:
                    const about = $('#about').offset().top;
                    $("html, body").animate({ scrollTop: about - 20 }, 200);
                    break;
                //他のcaseも同様なので省略
            }
        }
    }
})

methodsオプションオブジェクトを使用したいDOM要素にv-onディレクティブを記述する。
v-on:click="scroll"のように、v-on:イベントの発生条件:"イベントハンドラ4"の形で記述する。

3.4 v-html

「2.4 dataオプションオブジェクト」で紹介したように、Mustache構文を用いることでDOM要素とインスタンスのデータをバインディングすることができるが、値をテキストデータとして扱ってしまう。値に<br><span>を使用してDOM要素内で反映させたい場合、v-htmlディレクティブを用いる。

main.js
const skills = new Vue({
    el:'#skills',
    data:{
        skills:[
            {
                show:'<strong>フロントエンド</strong>',
            },
            //他のバックエンド、DataBase等も同様なので省略   
        ],
    },
})
index.html
<div id="skills">
  <p v-html="skills[0].show">v-htmlディレクティブ</p>
</div>

このように記述することで、インスタンスのdataオプションオブジェクト内のskills配列のインデックス番号0番目要素のshowプロパティの値を生のHTMLとして出力することができるため、<strong>が反映される。

3.5 v-for

繰り返し処理を行うために用いる。
例えば、以下のskills配列のshowプロパティの値を全てDOM要素にバインディングして表示したいとする。

main.js
const skills = new Vue({
    el:'#skills',
    data:{
        skills:[
            {
                id:'frontend_skills',
                show:'フロントエンド',
            },
            {
                id:'backend_skills',
                show:'バックエンド',
            },
            {
                id:'database_skills',
                show:'DataBase',
            },
            {
                id:'infrastructure_skills',
                show:'インフラ',
            },
            {
                id:'others',
                show:'その他',
            },
            {
                id:'stydying',
                show:'学習中',
            },

        ],
    },
})

「2.4 dataオプションオブジェクト」で紹介したように、Mustache構文を用いて以下のように記述することができる。

index.html
<div id="skills">
  <p v-bind:id="skills[0].id">{{ skills[0].show }}</p>
  <p v-bind:id="skills[1].id">{{ skills[1].show }}</p>
  <p v-bind:id="skills[2].id">{{ skills[2].show }}</p>
  <p v-bind:id="skills[3].id">{{ skills[3].show }}</p>
  <p v-bind:id="skills[4].id">{{ skills[4].show }}</p>
  <p v-bind:id="skills[5].id">{{ skills[5].show }}</p>
</div>

しかし、変更が生じた場合、上記の書き方だと全ての要素を変えないといけないため非効率である。そこでv-forディレクティブを用いる。以下のように書き換えることができる。

index.html
<div id="skills">
  <p v-for="skill in skills" v-bind:id="skill.id">{{ skill.show }}</p>
</div>

v-for="skill in skills"で、skills配列の中身を取り出し、skillという変数に格納し、配列の要素の数だけ繰り返すことを表している。以降skill.プロパティ名でデータにアクセスしている。

4. 最後に

ポートフォリオを修正する中で使用したディレクティブのみ紹介しています。今後他のディレクティブを使用した際は、再度記事を更新します。

調べて概要を理解してコードを書く。これを繰り返すことが理解の1番の近道だと思うので、今後も継続していきます。

来週はDOM要素を部品化をするためのコンポーネントを紹介します。


  1. フレームワークとは、Webアプリケーションやシステムを開発するのに必要な機能が予め用意された骨組みのこと。便利ではあるが、フレームワーク毎にルールが存在する。 

  2. CDNとは、「コンテンツデリバリーネットワーク」(Content Delivery Network) の略で、世界中に張り巡らされた配信ネットワークを利用して、Webサイトにアクセスしようとするユーザーに効率的かつ高速にWebコンテンツを配信する仕組み。
    配信元のサーバー(オリジンサーバー)にエンドユーザーのリクエストが集中するとサーバーへの負担が大きくなってしまう。そこで、オリジンサーバーのウェブコンテンツのコピーを取得した複数のキャッシュサーバーでウェブコンテンツを配信することで、オリジンサーバーへの負担を軽減している。 

  3. データや処理のまとまり。配列との違いは、データや処理に名前(キー)を指定することができる点。特徴は「継承:変数を持たせてオブジェクトを複製することができる」、「多態性:インスタンス内でオブジェクトの内容を上書きすることができる」、「カプセル化:変数のアクセス権の指定」がある。 

  4. 指定したイベント(click,hover等)が発生した際にプログラムが実行する処理。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue.jsでaxiosを使用してログイン時にレスポンスからユーザ情報を取得する

初めに

RailsとVue.jsを使用してアプリを作成していて、ログイン時にvue側でユーザ情報を取得したいと思ったのでやってみた。

謎にハマってかなり時間がかかったので、備忘録として残しておく。

やり方

取得するときのvue.jsの処理。簡潔にするために色々と省略している。

methods: {
  loginUser: function () {
   axios.post('api/auth/sign_in', this.user).then((response) => {
  }, (error) => {
   console.log(error)
  })
}

この処理を行うと以下の結果が返ってくる。今回はテストで「Advanced REST client」を使ってる。

スクリーンショット 2021-02-13 15.32.35.png
スクリーンショット 2021-02-13 15.32.49.png

そうするとユーザ情報を持っているdataが返ってくる。まずdataは以下のコードで取得することができる。

response.data['data']

これをさっきの処理に入れてコンソールに出力する。

methods: {
  loginUser: function () {
   axios.post('api/auth/sign_in', this.user).then((response) => {
    // 追加
    console.log(response.data['data'])
  }, (error) => {
   console.log(error)
  })
}

スクリーンショット 2021-02-13 15.46.06.png

画像のようにデータが取得できるので、この後は自分が欲しい値を選択するだけ。例えばidが欲しいならこんな感じ。末尾のキーを変更すればOK。

response.data['data'].id

後はこれをlocalStrageとか実装で使ってるやつにうまく当てはめて使用すればいい。

response.dataの中にさらにdataあるとか思わなくてめちゃくちゃ沼って時間かかった。最初にresponse.data.idを試してデータが取得できなかった時点で丁寧にvalueとkeyを確認してやってくべきだった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GASでWebアプリ「映画鑑賞記録」を作る⑦

 今年も早くも2月になりましたネ。
 昨年は、コロナ禍の影響で映画館での鑑賞本数が減りましたが、今年も1本でも多く観ていきたいと思います。

今回やること

 と言う事で、本アプリが単年の管理となっている為、複数年の鑑賞記録の管理が行える様に改修したいと思います。
 具体的には、「鑑賞年」のプルダウンリストを追加して、過去の年を選択する事によって一覧表示されている鑑賞記録を切り替えられる様にします。
 変更は次の項目に沿って行いますが、今回は「「鑑賞年」…」までに対応します。

  • 「映画鑑賞記録」のファイルIDの管理方法の変更
  • 「鑑賞年」プルダウンリストの追加
  • データの一覧表示処理の変更
  • データ更新処理の変更

「映画鑑賞記録」のファイルIDの管理方法の変更

 データが記録されている「映画鑑賞記録」(スプレッドシート)を1年毎に分けて管理する事とします。
 新しい年になった場合、次の手順で準備を行う事とします。

  1. 既存のファイル(「映画鑑賞記録」)をコピーする。
  2. コピーしたファイルの[鑑賞履歴]シートのデータをクリアする。

 複数のファイルを管理できる様に、Config.gs での設定を変更します。

Config.gs

 年毎の値をオブジェクトにして配列で管理します。
   id  :ID番号。
   year :表示年。プルダウンリストの表示等に使います。
   fileId:「映画鑑賞記録」のファイルID。

const YearSettings = [
  {id: '2021', year: '2021年', fileId: 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'},
  {id: '2020', year: '2020年', fileId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'}
]
以前の記述
var ViewingRecordID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';       //「映画鑑賞記録」のファイルID

「鑑賞年」プルダウンリストの追加

Index.html

 〔記録追加〕ボタンの左側にプルダウンリストを追加します。

    <div style="text-align: center;">
      <select v-model="selectedYear">
        <option disabled value="">鑑賞年</option>
        <option v-for="year in optionYear" 
          v-bind:value="year.year" 
          v-bind:key="year.id">
        {{ year.year }}
        </option>
      </select>&nbsp;&nbsp;&nbsp;&nbsp;
      <button type="button" class="btn btn-primary" id="show-modal" @click="doShowModal">記録追加</button>
    </div>

css.html

 プルダウンリスト用のスタイルを追加します。

  select {
      height: 35px;
      border: 1;
      border-radius: 5px;
  }

Vuejs.html

 data:に、selectedYearoptionYearを追加します。
  selectedYearは、<select>v-model="selectedYear"で連携され、プルダウンリストで
   選択した項目の値がセットされます。
   初期値として今年の表示年(2021年)をセットしておきます。
  optionYearは、プルダウンリストのリストデータとして使用します。(配列)
   <option>v-for="year in optionYear"で連携されます。
 mounted:に、getYearSettings()の呼出しを追加します。

var app = new Vue({
  el: '#app',
  data: {
    processingType: '記録追加',
       
    selectedYear: '2021年',
    optionYear: []
  },
  mounted: function() {
    getYearSettings();
    searchResults();
  },

javascript.html

 getYearSettings()を追加します。本処理でサーバ側のgetYearSettings()呼出します。
 取得したYearSettingsを、Vuejs.htmloptionYearにセットする事により、プルダウンリストの各項目が表示されます。

<script>
  function getYearSettings() {
    google.script.run.withSuccessHandler(
      function(v, element) {
        app.optionYear = v;
      })
      .withFailureHandler(
        function(msg, element) {
          showError(msg);
        })
      .withUserObject(this)
      .getYearSettings();
  }

ViewingRecord.gs

 getYearSettings()を追加します。本処理でYearSettingsを取得します。

function getYearSettings() {
  return YearSettings;
}

 ◆参考サイト Vue.js で プルダウンメニューの作り方 (基礎編)

結果

 プルダウンリストが表示される様になりました。
プルダウンリスト.png

◆前の記事 GASでWebアプリ「映画鑑賞記録」を作る⑥

◆索引 GASでWebアプリ「映画鑑賞記録」を作る《索引》

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue3 の Composition API の reactive() は Map や Set も扱える

前置き

Vue3 の Composition API で使う reactive() は、Map(), Set(), WeakMap(), WeakSet() も扱えることに気づいたので、メモ程度に残しておきます。

シンプルな reactive() の使い方

シンプルにオブジェクトを定義して、a というキーの値が更新されると、リアクティブに b の値を a の10倍で更新する、という処理です。

import { reactive, effect, isReactive } from 'vue';

// Object
const reactiveObj = reactive({ a: 1, b: 2 })

console.log(isReactive(reactiveObj))
// true

effect(() => {
  reactiveObj.b = reactiveObj.a * 10
})

console.log(reactiveObj)
// { a: 1, b: 10 }

reactiveObj.a = 10

console.log(reactiveObj)
// { a: 10, b: 100 }

new Map() で同じことをする

a に 10 を代入すると、b は 100 に更新されています。

// Map
const mp = new Map()
mp.set('a', 1)
mp.set('b', 2)
const reactiveMap = reactive(mp)

console.log(isReactive(reactiveMap))
// true

effect(() => {
  reactiveMap.set('b', reactiveMap.get('a') * 10)
})

console.log(reactiveMap.get('b'))
// 10

reactiveMap.set(('a'), 10);

console.log(reactiveMap.get('b'))
// 100

new Set() で同じことをする

a に 10 を代入すると、b は 100 に更新されています。

// Set
const st = new Set()
st.add({ a: 1 })
const reactiveSet = reactive(st)

console.log(isReactive(reactiveSet))
// true

effect(() => {
  reactiveSet.forEach(value => {
    if (value.hasOwnProperty('a')) {
      reactiveSet.add({ b: Number(Object.values(value)[0]) * 10 })
    }
  })
})
reactiveSet.forEach(value => console.log(value))
// value { a: 1 }
// value { b: 10 }

reactiveSet.add({ a: 10 })
reactiveSet.forEach(value => console.log(value))
// value { a: 10 }
// value { b: 100 }

new WeakMap() で同じことをする

o1 に 10 を代入すると、o2 は 100 に更新されています。

// WeakMap
const wm = new WeakMap()
const o1 = { a: 1 }
const o2 = { b: 2 }
wm.set(o1, 1);
wm.set(o2, 2);
const reactiveWm = reactive(wm)

console.log(isReactive(reactiveWm))
// true

effect(() => {
  reactiveWm.set(o2, reactiveWm.get(o1) * 10)
})

console.log(reactiveWm.get(o2))
// 10

reactiveWm.set(o1, 10);

console.log(reactiveWm.get(o2))
// 100

new Array() はうまくいかない

a に 10 を代入すると、b は 100 に更新される、という面においては、リアクティブになっています。しかし、元の[{ a: 1 }, { b: 2 }] に加えて、'0': { a: 1 }, '1': { b: 10 } というオブジェクトが新しく追加されています。これだと、配列系の関数である.map().filter() は、意図している動作にならない気がします。
ただ、ソースコードを見る限り、きっと意図的な仕様なのでもうちょっと調べてみる必要がありますが、reactive() を Array 型に対して使うのは一旦なしかなと。

// Array
const reactiveArr = reactive([{ a: 1 }, { b: 2 }])

console.log(isReactive(reactiveArr))
// true

effect(() => {
  reactiveArr[1].b = reactiveArr[0].a * 10
})

console.log(reactiveArr)
// [ { a: 1 }, { b: 10 }, '0': { a: 1 }, '1': { b: 10 } ]

reactiveArr[0].a = 10

console.log(reactiveArr)
// [ { a: 10 }, { b: 100 }, '0': { a: 10 }, '1': { b: 100 } ]

reactive() が対応している型は結局何?

公式には、reactive() の型チェックは以下のように定義してあると書かれています。

function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

https://v3.vuejs.org/api/basic-reactivity.html#reactive

object であれば、何でも OK ということですね。
そういうことであれば、Map(), Set(), WeakMap(), WeakSet() もすべて object なので OK ということだと思います。

typeof new Array()
"object"

typeof new Map()
"object"

typeof new WeakMap()
"object"

typeof new Set()
"object"

typeof new WeakSet()
"object"

ソースコードベースで reactive() が対応してる型を確認する

reactive() のターゲットオブジェクトのタイプを判定する処理があります。
Object, Array, Map, Set, WeakMap, WeakSet 以外の場合は、INVALID を返しています。

reactive() に対応しているのは、Object, Array, Map, Set, WeakMap, WeakSet ということですね。

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/reactive.ts

WeakSet だけサンプルコードを作成していないですが、Set とだいたい同じなので、問題はなさそうです。

余談

Object, Array, Map, Set, WeakMap, WeakSet は、内部的には扱いが異なります。

ObjectArrayTargetType.COMMON を返し、それ以外が TargetType.COLLECTION を返しているのは、Proxy オブジェクトのハンドラーが異なるため、判定処理を入れているようです。ここで判定した処理結果は、以下の箇所で使用されています。

const proxy = new Proxy(
  target,
  targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)

まとめ

Array 型は、もうちょっと調べる必要はありますが、他の型はリアクティブに扱えそうなので、アイデアがあれば実装で使ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsとTypescriptとSCSSでかんたんなページ作成入門

公式ドキュメントに沿ってNuxt.jsでページを作成していきます。
日本語版があるのはありがたいですね。

テキストエディタはおすすめされているVS Codeを利用します。OSはMacOS Big Surです。

プロジェクトの作成

対話式で各種設定が行われます。
UI frameworkは少し悩みましたが無しにしてみました。

# yarn create nuxt-app nuxt-practice
yarn create v1.22.10
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...

success Installed "create-nuxt-app@3.5.2" with binaries:
      - create-nuxt-app
[#######################################################################################################################################] 342/342
create-nuxt-app v3.5.2
✨  Generating Nuxt.js project in nuxt-practice
? Project name: nuxt-practice
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None

開発サーバーの起動

# cd nuxt-practice
# yarn dev
? Are you interested in participating? No

これで、 http://localhost:3000/ にサーバーが立ち上がって、Nuxt.jsの空ページが表示されます。

image.png

ページの作成

codeコマンドで、カレントディレクトリをVS Codeで開けるようにしておくと便利です。

# code .

NuxtLinkでルーティングも出来ます。

pages/about.vue
<template>
  <NuxtLink to="/">Home page</NuxtLink>
</template>
pages/index.vue
<template>
  <main>
    <h1>Home page</h1>
    <NuxtLink to="/about">
      About Page
    </NuxtLink>
  </main>
</template>

scssの追加

sassパッケージのインストール

yarn add -D sass sass-loader@10 fibers

グローバルに読み込ませたいcssを設定しておきます。

  // Global CSS: https://go.nuxtjs.dev/config-css
  css: [
    { src: '~/assets/style.scss', lang: 'scss' },
  ],

SCSSぽい入れ子のCSSを作成します。

assets/style.scss
main {
    margin: 20px;
    h1 {
        color: red;
    }
}

無事CSSが反映されました。

image.png

機能追加

少しVueぽい書き方で動きをつけてみます。

pages/index.vue
<template>
  <main>
    <h1>Home page</h1>
    <NuxtLink to="/about">
      About Page
    </NuxtLink>
    <br><br>
    <button v-on:click="counter += 1">Click!</button>
    <p>The button above has been clicked {{ counter }} times.</p>
  </main>
</template>

<script>
export default {
  data() {
    return{
      counter: 0
    }
  },
  mounted() {
    alert('mounted!');
  }
}
</script>

ボタンをクリックするとカウントアップするようになりました。

image.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【導入編】Vue.js、Vuetifyをvue-cliでAmazon Linux2にインストール

はじめまして、昨年まで沖縄で金融システムのAWS構築をして
現在は東京のHRTechのSaaSを作っているベンチャーでWEBエンジニアとして働いています、おーしろです。

はじめに

VuejsとVuetifyをEC2(Amazon Linux2)で利用する機会があったのでインストールまでの手順を備忘録として残しました。
同様のケースがある方にも参考になればと思います。

前提条件

EC2の構築までは完了していることが前提です。
まだの方は下記URLを参考にして構築してみてください:smiley:
チュートリアル: Amazon EC2 Linux インスタンスの開始方法

インストール手順

4ステップです。

  1. Node.jsのインストール(vue-cliの利用に必要)
  2. vue-cliのインストール
  3. Vue.jsのプロジェクト作成
  4. Vuetifyのインストール

1. Node.jsのインストール(vue-cliの利用に必要)

ダウンロードして、

curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -

インストール。

sudo yum install --enablerepo=nodesource nodejs

2. vue-cliのインストール

Vuejsの環境を構築する際にはvue-cliを利用するととても便利なのでvue-cliをインストールします。

npm install -g @vue/cli

3. Vue.jsのプロジェクト作成

my-app にはプロジェクト名を記入。

vue create my-app

2022年2月時点ではVuetifyはVuejs3をサポートしていなため、[Vue 2]を選択し、[Enter(return)]。
スクリーンショット 2021-02-13 13.48.24.png

「Successfully」と表示されればプロジェク作成は完了です。
スクリーンショット 2021-02-13 14.24.38.png

念の為Vuejsを起動し、ブラウザから確認してみましょう。

①先ほど作成したプロジェクトのディレクトリに移動。

cd my-app

②Vuejsの起動

npm run serve

③ブラウザにアクセス
・EC2で構築した場合は、「パブリック IPv4 DNS」または「パブリック IPv4 アドレス」を利用。
※アクセスできない場合はセキュリティグループの設定がされていない可能性があるので、セキュリティグループでhttpの8080を許可されているか確認してみてください。
http://「パブリック IPv4 DNS」または「パブリック IPv4 アドレス」:8080/

・ローカル環境で構築した方は下記にURL。
http://localhost:8080/

このようなページが表示されたら成功です!
次はVuetifyをインストールしますので、ターミナルから「control + c」で一度サーバーを停止しましょう。
スクリーンショット 2021-02-13 14.34.47.png

4. Vuetifyのインストール

vue add vuetify

今回はそのまま「Default」を選択。
スクリーンショット 2021-02-13 14.40.28.png

「Successfully」と表示されればVuetifyのインストールはが完了です。
スクリーンショット 2021-02-13 14.43.28.png

ブラウザからもVuetifyがインストールされていることを確認しましょう。
先程と同様 npm run serve で起動し、ブラウザからアクセスするとページのデザインがVuetifyになっていればVuetifyが正常にインストールされ利用できる状態になっています。お疲れさまでした:thumbsup:
スクリーンショット 2021-02-13 14.47.37.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js概要? #他のVirtualDOM (VDOM) ベースFW との違い

はじめに

本文はこちら

他の VirtualDOM (VDOM) ベースFW との違い

Virtual DOM (VDOM) とは?

直訳すると仮想 DOM となりますが、DOM とは実は直接、関係がありません。
なので、勘違いしてしまいそうな名前ですが、DOM を置き換えるものでは
ありません。

端的に表現すると、HTML のサーバーサイド・レンダリングをクライアント
サイドにもってきて、HTML の再構築前後の差分のみ抽出し DOM を用いて
適用するようにした仕組み、となります。

仮想 DOM では、DOM と同じように HTML の構造をツリー構造として表現
しています。

DOM の更新タイミング

VDOM ベース・フレームワークでは任意のタイミングで用意された差分
抽出・適用の関数を呼び出して DOM の差分適用を行うことができますが
、Vue.jsでは DOM の更新はフレームワークにより管理され自動でバッチ
更新されるようになっています、手動で行うことはできません。

VDOM ベース・フレームワークでは差分のみの DOM への適用により
ブラウザでのUIレンダリングが走る頻度を少なくしているのに対し、
Vue.js1.xまでは、 DOM 処理のまとめてバッチ適用によりブラウザ
での UI レンダリングの走る頻度を少なくしていました。

VDOM ベース・フレームワークが有効なケースとは?

Vue.js(2.x以降)以外の、VDOM ベース・フレームワークは今のところ、
一般的なケース全てで有用なフレームワークではありません。

HTMLがリアルタイムで変化し、UIレンダリングの発生頻度が問題になるアプリ、
主にリアルタイムで画面が変化するゲームアプリetcで、威力を発揮する気が
します。

VDOMベース・フレームワークの課題?

VDOMではHTMLの構築をDOMとはわけて行うので、差分を取るアルゴリズム
が優秀でないとダメな気がします、差分管理の多くを手動で行うor意識しておく
必要がある?現状では万人向けとはいえないと思います。

React.js も万人向けといえるほどには熟れていない
と感じています。

VDOM ベース・フレームワークのこれから

Vue.js については以前「VDOM に対する対応を行う予定はない」と
作者であるEvan You 氏が、以前、Issue にて明言し、
「VDOM対応すると、別物になってしまう」とのことでしたので、
DOM と同じレベルのレイヤーで DOM との互換性の高い VDOM
ライブラリが出てくれば対応が行われるかもしれません、と書いていました。
軽量な低レベルVDOM実装 snabbdom がリリースされたことで状況が変化し、
snabbdom の採用により、内部的には VDOM を採用しながらも、利用者は
VDOM を意識しなくても使用できる形で実装された Vue.js2.0 の開発が
進められ、リリースされました。
(Vue.js は 2.0.0 以降、2.xでは、snabbdom をフォークしてカスタマイズ
した VDOM 実装を内蔵しています。)

(VDOMについては現状の高レベル・レイヤでの実装ではなく、
より低レベル・レイヤでの実装が出てきて、フレームワーク内部で
共通に利用されるようにでもならない限り一般化してはいかない
ような気がします。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでnpm runしたときのメモ

Vueテンプレートを使って、Laravelの認証画面を作ろうとして詰まった。

  • Laravelに laravel/uiを導入
  • vueテンプレートを指定
  • npmインストール

この流れでやると失敗した。

こちらの記事の内容も試してみたが、エラーは解決しなかった。
laravelでnpm run devを実行すると「cross-env: not found」というエラーが出る件対応したった

npm installまではうまくいくが、npm run devでエラーになる

npm ERR! code 1
npm ERR! path /var/www/html/larabel
npm ERR! command failed
npm ERR! command sh -c cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2021-02-12T22_41_21_472Z-debug.log
npm ERR! code 1
npm ERR! path /var/www/html/laravel
npm ERR! command failed
npm ERR! command sh -c npm run development

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2021-02-12T22_41_21_507Z-debug.log

1. webpack.mix.jsの修正

mix.js('resources/js/app.js', 'public/js')
    .vue() // → 削除
    .sass('resources/sass/app.scss', 'public/css');

npm 再インストール

rm -rf node_modules/
rm -rf package-lock.json

npm install
npm run dev

これで解決した。

98% after emitting SizeLimitsPlugin

 DONE  Compiled successfully in 24793ms                               10:47:18 PM

       Asset      Size   Chunks             Chunk Names
/css/app.css   179 KiB  /js/app  [emitted]  /js/app
  /js/app.js  1.41 MiB  /js/app  [emitted]  /js/app
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js概要? #KnockoutJSとの違い

はじめに

 本文はこちら

KnockoutJSとの違い

Vue.jsと同じく、MVVMを設計基盤とした先発のフレームワークに
KnockoutJSがあります。

Vue.jsに対するKnockoutJSの唯一のアドバンテージはレガシー・ブラウザ
のサポートです。

レガシー・ブラウザにあわせて設計されていることもあり、KnockoutJSの
記法や構文は若干シンプルではありません。

後発のVue.jsのほうが記法や構文を含め、洗練されています。

KnockoutJSはMS勤務の開発者により開発・メンテンスされ、
Visual Studioでも採用されているため、数多くの企業内業務
システムで採用されていると考えられます、企業内業務システム
では稼働しているシステムの動作環境の変更が許可されることは
ないため、レガシー・ブラウザが淘汰されるまでレガシー・
ブラウザのサポートが継続されることはほぼ確実であり、企業内
業務システムの開発エンジニアが保守的傾向が強く変化を好まない
こともあり、構文や記法が今以上にシンプルになることはないと
思われます。

設計の違い

ブラウザー・サポート

KnockoutJSはレガシー・ブラウザをサポートしています。

Class指向

KnockoutJSはClass指向にふれていますが、オーソドックスなスタイルの
名残が所々にあります。例えば、クラスの設計において何をプロパティとして
扱い何をメソッド(メンバー関数)の引数として扱うかが整理されていないよう
にみえます。

Vue.jsはClass指向で設計されているため、クラスの設計はKnockoutJSより
洗練されています。

記法の違い

Vue.jsはモダン・ブラウザのみをターゲットにすることにより、
KnockoutJSよりも記法をシンプルにすることに成功しています。

VMとバインディング

KnockoutJSではプロパティ(変数)は追加時or後、バインディングできる
プロパティであることを用意された関数を呼び出す形で明示する(より正確
にはES5で導入された機能を利用できないためにプロパティにセットする
オブジェクトに対して(値の変更のトラッキング機能と)セッター・ゲッター
を付加する専用の関数を呼び出す)必要があります、Vue.jsではプロパティ
はVMに追加するだけでバインディング可能になります。

KnockoutJSではバインディング可能なプロパティに対するアクセサが
専用の関数を呼び出すことにより自動生成されます、モダン・ブラウザの
サポートするES5以降の機能を利用できないためにアクセサは関数と
なっています。

Vue.jsでもプロパティに対する(トラッキング機能と)アクセサは
自動生成されます、ES5の機能を利用して自動生成されたアクセサ
はプロパティへのダイレクト・アクセスのように扱えます。

KnockoutJSでもKnockout-ES5プラグインを導入すれば、プロパティを
追加するだけでバインディング可能、かつ、アクセサをプロパティへの
ダイレクト・アクセスにできますが、Knockout-ES5プラグインはモダン
・ブラウザ用であり、レガシー・ブラウザを切り捨てられない場合は
利用できません。
(Knockout-ES5プラグインはプロパティまわりをES5の機能を用いて
置き換えるだけで、内部構造にはタッチしませんので、導入しても
モダン・ブラウザに導入されたJS(ES5)の他の新機能の恩恵を受けられる
わけではありません。)

Knockout-ES5: a plugin to simplify your syntax
http://blog.stevensanderson.com/2013/05/20/knockout-es5-a-plugin-to-simplify-your-syntax/

バインディング

KnockoutJSのバインディングに相当する機能をVue.jsでは
ディレクティブとよびます。

KnockoutJSではViewに記述するバインディングの記法も
Vue.jsのディレクティブほどシンプルではなく、種類別の
整理も行なわれていません。

Vue.jsのディレクティブでは種類毎に独自属性が用意され、
KnockoutJSよりも見通しがよくなっています。

KnockoutJSのカスタム・バインディングに相当する機能は
Vue.jsではカスタム・ディレクティブです。

Vue.jsのカスタム・ディレクティブはKnockoutJSの
カスタム・バインディングと機能的には同等ですが、
作成が楽になっています。

テンプレート

KnockoutJSではtemplateバインディングを用いて、
指定した要素の中身をテンプレートで差し替えることが
できますが、AngularJSのディレクティブに相当する
機能は用意されていません。

Vue.jsには、AngularJSのディレクティブに相当する、
独自タグを定義できる機能、コンポーネント(component)
があります。

学習コスト

KnockoutJSは学習コストがAngularJS1.xよりかなり少ないですが、
Vue.js はより少ないです。

学習コストはAngular >> Knockout > Vue.js です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js概要? #AngularJS(1.x)との違い

はじめに

本文はこちら

AngularJS(1.x)との違い

設計の違い

AngularJSはMVC(厳密にはMVW?)、対してVue.jsはMVVM、Viewと
Controllerの間にView Model(VM)を挟み、AngularJSのように
Controllerと画面(View)を対応させるのではなく、
VMと画面(厳密には画面のUIコンテナ(コンポーネント))を
対応させています。

Vue.js(というかMVVM)ではControllerは隠蔽されており、
隠蔽されたControllerとは別にControllerを作成して
使用するかは利用者の判断に委ねられています。

AngularJSはController駆動(ドリブン)、Vue.jsはModel駆動
といえると思います。

AngularJSは大規模アプリを想定しているため、中小規模アプリでは
高コストすぎる機能も本体に含んでいますが、Vue.jsは必要な機能
のみを組合せて利用することを前提としているのか、いらない機能は
本体に含まず、本体はViewまわりのみの最少構成を維持する方向と
なっています。

(上記はMVC・MVVMの解説ではありません、Vue.jsとAngularJSの
しくみの違いについて書いているのみです。MVC・MVVMについて
詳しく知りたければ、Wikipedia等をあたるのが正解です。)

MVVM

http://ja.wikipedia.org/wiki/Model_View_ViewModel

DI(Dependency Injection)とバインディング

AngularJSではControllerとViewのバインディングを行っています
が、Vue.jsはView Model(VM)とViewでバインディングを行っています。

AngularJSではデータと振る舞いがControllerとModelに分離されて
いるため、画面のUIコンテナ(コンポーネント)のルート要素にController
を紐付けた上でController経由でModelを画面と紐付ける形になり
ますが、Vue.jsではVMにデータと振る舞いを一元化しているので、
画面のUIコンテナ(コンポーネント)にView Modelを紐付るだけで済みます。

ViewとVMのバインディングはVue.jsにより管理されるため、
View ModelをControllerとは切り離して考えることが
できます。

Vue.jsではフォームの入力系要素の値はVMとViewの
間で適切に紐付けを行っていると、VM⇔View間で自動的に
同期されます。

AngularJSではデータと振る舞いが別々に存在し、それらとView
を繋ぐ糊が必要であるため、DI(Dependency Injection)機能を
標準搭載していますが、Vue.jsではViewとVMの間でデータと振る舞い
の紐付けを完結できるので、DI機能は標準搭載されていません。

Modelの扱い

AngularJSではModelは基本、振る舞いを持たないデータのいれものに
すぎませんが、Vue.jsのView Modelはデータと振る舞いを
持っています。

Web Components

PolymerとWebComponents
http://steps.dodgson.org/b/2013/05/19/polymer-and-web-components/

AngularJS(1.x)はWeb Components仕様及びそのPolyfill実装
であるPolymerを考慮していないため、組合せての利用では
問題があることが指摘されていて、2.0でWeb Components仕様
及びPolymer(のPlatform.js?)への対応が予定されていました。

Vue.jsでは0.11.xでWeb Components最新?仕様との同調?が予定
されていました。

コンポーネント指向

Vue.jsはAngularJS(1.x)とは違い、既存のor新規作成した
UIコンポーネントを組合せてページを構成することを前提に
しています。

Class指向

AngularJS(1.x)はClassベースではありませんが、
Vue.jsはClassベースとなっています。

AngularJS(1.x)についてはAngular Classy を被せることにより
Classベースでの開発(Controllerまわりのみ)が
行えるようになります。

Angular Classy
http://davej.github.io/angular-classy/

Angular(JS)は2.0以降でClass(ES6ではなくTypeScript)ベースと
なっています。

ブラウザー・サポート

AngularJS(1.x)はレガシー・ブラウザをサポートしています。

Angular(JS)は2.0以降でレガシー・ブラウザを切り捨てていますが、
高速な動作が実現されているかは要検証です。

AngularJS(1.x)はレガシー・ブラウザーに特化した設計となっていて、
モダン・ブラウザ向けのチューニングを行う(モダン・ブラウザを用いて
高速化できる処理を切出し、レガシーブラウザとモダン・ブラウザで
処理を切換えられるようにする)ことが難しい模様。

構文の違い

ディレクティブ (directive)

AngularJSとはディレクティブの定義が異なり、
Vue.jsのディレクティブは要素に付加できる独自属性
であり、この機能により、VMとViewの結びつけを行う
とともに、Vue.js組込みor独自定義したディレクティブ
を要素に付加することにより、要素を操作することが
できます。

AngularJS(1.x)ではHTML仕様による要素の属性毎に対応した独自属性を
用意していますが、Vue.jsは要素の属性をVMのデータと対応させるための
ディレクティブv-bind(旧v-attr)のみが用意されています、v-bindで
要素の属性(複数可)とVMのデータとの対応を記述するようになっています。

イベント操作も同様に、AngularJS(1.x)ではHTML仕様のイベント毎に
対応した独自属性を用意していますが、Vue.jsは要素のイベント(操作)を
VMのふるまい(メソッド)と対応させるためのディレクティブv-onのみが
用意されています、v-onで要素のイベント(複数可)とVMのメソッドとの
対応を記述するようになっています。

独自のディレクティブ(カスタム・ディレクティブ)を定義し
利用することも可能です。

コンポーネント(component)

Vue.jsのComponentはAngularJSのディレクティブに相当する
機能です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Optional Chaining のエラーをもう一度整理

前回の反省点として、Optional Chainingに問題をすり替えてしまって、結果、混沌としてしまったです。
よくよく考えれば、ts-loaderもbabelも Optional Chaining はサポートしているのであって、こいつらはローダー君は指定されたバージョンのjavascriptへ変換するのがお仕事です。
つまり、ここが原因でエラーがどうとか話しすと話が混乱するのですな。

ということで、もう一度出力されているエラーを見てみる。

storybook をビルドして発生するエラー

ERROR in ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& (./node_modules/vue-docgen-loader/lib??ref--12!./node_modules/ts-loader??ref--4-0!./node_modules/vue-loader/lib??vue-loader-options!./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&) 8:36
Module parse failed: Unexpected token (8:36)
File was processed with these loaders:
 * ./node_modules/vue-docgen-loader/lib/index.js
 * ./node_modules/vue-docgen-loader/lib/index.js
 * ./node_modules/ts-loader/index.js
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|         const canvas = ref();
|         watch(canvas, () => {
>             const gl = canvas.value?.getContext("webgl");
|             if (gl) {
|                 gl.clearColor(0.0, 0.0, 0.0, 1.0);
 @ ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& 1:0-236 1:252-255 1:257-490 1:257-490
 @ ./src/components/_sandbox/WebGL.vue
 @ ./src/components/_sandbox/WebGL.stories.ts
 @ ./src sync ^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|ts|tsx))$
 @ ./.storybook/generated-stories-entry.js
 @ multi ./node_modules/@storybook/core/dist/server/common/polyfills.js ./node_modules/@storybook/core/dist/server/preview/globals.js ./.storybook/storybook-init-framework-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/common/config.js-generated-other-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/vue/config.js-generated-other-entry.js ./node_modules/@storybook/addon-links/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addArgs.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addParameter.js-generated-other-entry.js ./node_modules/@storybook/addon-knobs/dist/preset/addDecorator.js-generated-other-entry.js ./.storybook/preview.js-generated-config-entry.js ./.storybook/generated-stories-entry.js (webpack)-hot-middleware/client.js?reload=true&quiet=false&noInfo=undefined

Module parse failed: Unexpected token (8:36) ということは、どうやら期待されていないトークンに遭遇したらしい。
次に、const gl = canvas.value?.getContext("webgl");という、ソースコードの場所を明示してくれている。
で、webpackというヤツはルールに従ってloaderがバケツリレーをしながら javascript に置き換えるので、「誰が音を上げた」というのが問題になる。
スタックトレース的なものが表示されているが、コイツが昇順なのか降順なのか、想像はできるが確定できないので、少し手を変えてみる。

エラーを表示している場所を探す

スクリプト系の言語なら、エラーを表示しているコードを探せるかも、って事で You may need an additional loader to handle the result of these loaders. を検索してみると webpack/lib/ModuleParseError.js で検索されていることがわかる。
こいう時のコツとしては、runtimeのエラー文字列っぽくないものを探すのがコツ。

で、このエラー文字列は ModuleParseError の constructor() で使っている(生成している)ことが分かる。
じゃぁ、どこから呼び出されているのかを調べたくなるので、console.trace()を追加してみて、再度ビルドしてみる。

Trace
    at new ModuleParseError (/Users/teru/git/uilib/node_modules/webpack/lib/ModuleParseError.js:27:12)
    at handleParseError (/Users/teru/git/uilib/node_modules/webpack/lib/NormalModule.js:469:19)
    at /Users/teru/git/uilib/node_modules/webpack/lib/NormalModule.js:503:5
    at /Users/teru/git/uilib/node_modules/webpack/lib/NormalModule.js:358:12
    at /Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:373:3
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:214:10)
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
    at /Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:236:3
    at context.callback (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:111:13)
    at Object.module.exports (/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js:28:5)
    at LOADER_EXECUTION (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:119:14)
    at runSyncOrAsync (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:120:4)
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:232:2)
    at iterateNormalLoaders (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:221:10)
    at /Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:236:3
    at context.callback (/Users/teru/git/uilib/node_modules/loader-runner/lib/LoaderRunner.js:111:13)

ふむ。。。 あ〜 vue-docgen-loader が居るな。。。
こいつ何なんだろうか。。。
まぁ、それは後で調べるにして、もうちょい情報が欲しいから、この constructor() に渡されるパラメータをダンプしてみよう。
定義はこんな感じModuleParseError(module, source, err, loaders)だったんで。。。

module

/* NormalModule */ {
  dependencies: [],
  blocks: [],
  variables: [],
  type: 'javascript/auto',
  context: '/Users/teru/git/uilib/src/components/_sandbox',
  debugId: 1422,
  hash: undefined,
  renderedHash: undefined,
  resolveOptions: {},
  factoryMeta: {},
  warnings: [],
  errors: [],
  buildMeta: { tsLoaderFileVersion: 1 },
  buildInfo: {
    cacheable: true,
    fileDependencies: Set { '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue' },
    contextDependencies: Set {},
    assets: undefined,
    assetsInfo: undefined
  },
  reasons: [
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyImportSideEffectDependency],
      explanation: undefined,
      _chunks: null
    },
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyImportSpecifierDependency],
      explanation: undefined,
      _chunks: null
    },
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyImportSideEffectDependency],
      explanation: undefined,
      _chunks: null
    },
    ModuleReason {
      module: [NormalModule],
      dependency: [HarmonyExportImportedSpecifierDependency],
      explanation: undefined,
      _chunks: null
    }
  ],
  _chunks: SortableSet [Set] {
    _sortFn: [Function: sortById],
    _lastActiveSortFn: null,
    _cache: undefined,
    _cacheOrderIndependent: undefined
  },
  id: null,
  index: null,
  index2: null,
  depth: null,
  issuer: NormalModule {
    dependencies: [
      [HarmonyCompatibilityDependency],
      [HarmonyInitDependency],
      [ConstDependency],
      [HarmonyImportSideEffectDependency],
      [HarmonyExportHeaderDependency],
      [HarmonyExportExpressionDependency],
      [HarmonyImportSpecifierDependency],
      [ConstDependency],
      [HarmonyImportSideEffectDependency],
      [HarmonyExportImportedSpecifierDependency]
    ],
    blocks: [],
    variables: [],
    type: 'javascript/auto',
    context: '/Users/teru/git/uilib/src/components/_sandbox',
    debugId: 1421,
    hash: undefined,
    renderedHash: undefined,
    resolveOptions: {},
    factoryMeta: {},
    warnings: [],
    errors: [],
    buildMeta: { exportsType: 'namespace' },
    buildInfo: {
      cacheable: true,
      fileDependencies: Set {},
      contextDependencies: Set {},
      assets: undefined,
      assetsInfo: undefined,
      strict: true,
      exportsArgument: '__webpack_exports__'
    },
    reasons: [ [ModuleReason], [ModuleReason], [ModuleReason], [ModuleReason] ],
    _chunks: SortableSet [Set] {
      _sortFn: [Function: sortById],
      _lastActiveSortFn: null,
      _cache: undefined,
      _cacheOrderIndependent: undefined
    },
    id: null,
    index: null,
    index2: null,
    depth: null,
    issuer: NormalModule {
      dependencies: [Array],
      blocks: [],
      variables: [],
      type: 'javascript/auto',
      context: '/Users/teru/git/uilib/src/components/_sandbox',
      debugId: 1217,
      hash: undefined,
      renderedHash: undefined,
      resolveOptions: {},
      factoryMeta: {},
      warnings: [],
      errors: [],
      buildMeta: [Object],
      buildInfo: [Object],
      reasons: [Array],
      _chunks: [SortableSet [Set]],
      id: null,
      index: null,
      index2: null,
      depth: null,
      issuer: [NormalModule],
      profile: undefined,
      prefetched: false,
      built: true,
      used: null,
      usedExports: null,
      optimizationBailout: [],
      _rewriteChunkInReasons: undefined,
      useSourceMap: true,
      _source: [OriginalSource],
      request: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue',
      userRequest: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue',
      rawRequest: './WebGL.vue',
      binary: false,
      parser: [Parser],
      generator: JavascriptGenerator {},
      resource: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue',
      matchResource: undefined,
      loaders: [Array],
      error: null,
      _sourceSize: null,
      _buildHash: 'd57de076f7ac96f23ab1c0ce9011f9e8',
      buildTimestamp: 1613107043574,
      _cachedSources: Map {},
      lineToLine: false,
      _lastSuccessfulBuildMeta: [Object],
      _ast: null
    },
    profile: undefined,
    prefetched: false,
    built: true,
    used: null,
    usedExports: null,
    optimizationBailout: [],
    _rewriteChunkInReasons: undefined,
    useSourceMap: true,
    _source: OriginalSource {
      _value: 'import mod from "-!../../../node_modules/vue-docgen-loader/lib/index.js??ref--12!../../../node_modules/ts-loader/index.js??ref--4-0!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./WebGL.vue?vue&type=script&lang=ts&"; export default mod; export * from "-!../../../node_modules/vue-docgen-loader/lib/index.js??ref--12!../../../node_modules/ts-loader/index.js??ref--4-0!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./WebGL.vue?vue&type=script&lang=ts&"',
      _name: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-loader/lib/loaders/pitcher.js??ref--4!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&'
    },
    request: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-loader/lib/loaders/pitcher.js??ref--4!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    userRequest: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    rawRequest: './WebGL.vue?vue&type=script&lang=ts&',
    binary: false,
    parser: Parser {
      _pluginCompat: [SyncBailHook],
      hooks: [Object],
      options: {},
      sourceType: 'auto',
      scope: undefined,
      state: undefined,
      comments: undefined
    },
    generator: JavascriptGenerator {},
    resource: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    matchResource: undefined,
    loaders: [ [Object], [Object], [Object], [Object] ],
    error: null,
    _sourceSize: null,
    _buildHash: '76f6da385d20205e0eefa56e89ec04d1',
    buildTimestamp: 1613107043951,
    _cachedSources: Map {},
    lineToLine: false,
    _lastSuccessfulBuildMeta: { exportsType: 'namespace' },
    _ast: null
  },
  profile: undefined,
  prefetched: false,
  built: true,
  used: null,
  usedExports: null,
  optimizationBailout: [],
  _rewriteChunkInReasons: undefined,
  useSourceMap: true,
  _source: SourceMapSource {
    _value: 'import { defineComponent, ref, watch } from "@vue/composition-api";\n' +
      'export default defineComponent({\n' +
      '    name: "WebGL",\n' +
      '    props: {},\n' +
      '    setup: (props, ctx) => {\n' +
      '        const canvas = ref();\n' +
      '        watch(canvas, () => {\n' +
      '            const gl = canvas.value?.getContext("webgl");\n' +
      '            if (gl) {\n' +
      '                gl.clearColor(0.0, 0.0, 0.0, 1.0);\n' +
      '                gl.clear(gl.COLOR_BUFFER_BIT);\n' +
      '            }\n' +
      '        });\n' +
      '        return { canvas };\n' +
      '    }\n' +
      '});\n',
    _name: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
    _sourceMap: {
      version: 3,
      file: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue.ts',
      sourceRoot: '',
      sources: [Array],
      names: [],
      mappings: 'AAKA,OAAO,EAAE,eAAe,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAEnE,eAAe,eAAe,CAAC;IAC7B,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,EAAE;IACT,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACpB,MAAM,MAAM,GAAG,GAAG,EAAqB,CAAC;QACxC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE;YACjB,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;YAC7C,IAAI,EAAE,EAAE;gBACN,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;gBAClC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;aAC/B;QACH,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,CAAC;IACpB,CAAC;CACF,CAAC,CAAC',
      sourcesContent: [Array]
    },
    _originalSource: undefined,
    _innerSourceMap: undefined,
    _removeOriginalSource: undefined
  },
  request: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
  userRequest: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js??ref--12!/Users/teru/git/uilib/node_modules/ts-loader/index.js??ref--4-0!/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js??vue-loader-options!/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
  rawRequest: '-!../../../node_modules/vue-docgen-loader/lib/index.js??ref--12!../../../node_modules/ts-loader/index.js??ref--4-0!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./WebGL.vue?vue&type=script&lang=ts&',
  binary: false,
  parser: Parser {
    _pluginCompat: SyncBailHook {
      _args: [Array],
      taps: [Array],
      interceptors: [],
      call: [Function: lazyCompileHook],
      promise: [Function: lazyCompileHook],
      callAsync: [Function: lazyCompileHook],
      _x: undefined
    },
    hooks: {
      evaluateTypeof: [HookMap],
      evaluate: [HookMap],
      evaluateIdentifier: [HookMap],
      evaluateDefinedIdentifier: [HookMap],
      evaluateCallExpressionMember: [HookMap],
      statement: [SyncBailHook],
      statementIf: [SyncBailHook],
      label: [HookMap],
      import: [SyncBailHook],
      importSpecifier: [SyncBailHook],
      export: [SyncBailHook],
      exportImport: [SyncBailHook],
      exportDeclaration: [SyncBailHook],
      exportExpression: [SyncBailHook],
      exportSpecifier: [SyncBailHook],
      exportImportSpecifier: [SyncBailHook],
      varDeclaration: [HookMap],
      varDeclarationLet: [HookMap],
      varDeclarationConst: [HookMap],
      varDeclarationVar: [HookMap],
      canRename: [HookMap],
      rename: [HookMap],
      assigned: [HookMap],
      assign: [HookMap],
      typeof: [HookMap],
      importCall: [SyncBailHook],
      call: [HookMap],
      callAnyMember: [HookMap],
      new: [HookMap],
      expression: [HookMap],
      expressionAnyMember: [HookMap],
      expressionConditionalOperator: [SyncBailHook],
      expressionLogicalOperator: [SyncBailHook],
      program: [SyncBailHook],
      hotAcceptCallback: [SyncBailHook],
      hotAcceptWithoutCallback: [SyncBailHook]
    },
    options: {},
    sourceType: 'auto',
    scope: undefined,
    state: undefined,
    comments: undefined
  },
  generator: JavascriptGenerator {},
  resource: '/Users/teru/git/uilib/src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&',
  matchResource: undefined,
  loaders: [
    {
      options: [Object],
      ident: 'ref--12',
      loader: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js'
    },
    {
      loader: '/Users/teru/git/uilib/node_modules/vue-docgen-loader/lib/index.js',
      options: [Object],
      ident: 'ref--12'
    },
    {
      loader: '/Users/teru/git/uilib/node_modules/ts-loader/index.js',
      options: [Object],
      ident: 'ref--4-0'
    },
    {
      loader: '/Users/teru/git/uilib/node_modules/vue-loader/lib/index.js',
      options: {},
      ident: 'vue-loader-options'
    }
  ],
  error: null,
  _sourceSize: null,
  _buildHash: '',
  buildTimestamp: 1613107043952,
  _cachedSources: Map {},
  lineToLine: false,
  _lastSuccessfulBuildMeta: {},
  _ast: null
}

source

import { defineComponent, ref, watch } from "@vue/composition-api";
export default defineComponent({
    name: "WebGL",
    props: {},
    setup: (props, ctx) => {
        const canvas = ref();
        watch(canvas, () => {
            const gl = canvas.value?.getContext("webgl");
            if (gl) {
                gl.clearColor(0.0, 0.0, 0.0, 1.0);
                gl.clear(gl.COLOR_BUFFER_BIT);
            }
        });
        return { canvas };
    }
});

err

SyntaxError: Unexpected token (8:36)
    at Parser.pp$4.raise (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2825:15)
    at Parser.pp.unexpected (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:689:10)
    at Parser.pp$3.parseExprAtom (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2270:12)
    at Parser.pp$3.parseExprSubscripts (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2089:21)
    at Parser.pp$3.parseMaybeUnary (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2066:19)
    at Parser.pp$3.parseExprOps (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2010:21)
    at Parser.pp$3.parseMaybeConditional (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1993:21)
    at Parser.pp$3.parseMaybeAssign (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1968:21)
    at Parser.pp$3.parseMaybeConditional (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1998:30)
    at Parser.pp$3.parseMaybeAssign (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1968:21)
    at Parser.pp$1.parseVar (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1228:26)
    at Parser.pp$1.parseVarStatement (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1092:10)
    at Parser.pp$1.parseStatement (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:842:19)
    at Parser.pp$1.parseBlock (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:1161:23)
    at Parser.pp$3.parseFunctionBody (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2671:24)
    at Parser.pp$3.parseArrowExpression (/Users/teru/git/uilib/node_modules/acorn/dist/acorn.js:2634:10) {
  pos: 260,
  loc: Position { line: 8, column: 36 },
  raisedAt: 261
}

loaders

[
  './node_modules/vue-docgen-loader/lib/index.js',
  './node_modules/vue-docgen-loader/lib/index.js',
  './node_modules/ts-loader/index.js',
  './node_modules/vue-loader/lib/index.js'
]

ここにきて、ようやくエラーの根源を発見!(実は、当初のビルドで表示されているんだけどね)
acorn が例外投げてるっぽい。
で、acornって、何者だ?

あと、errのスタックトレースが acorn 内で終わっているので、そもそも、これ呼び出しているのは誰?って事で調べてみる。

エラーの根源

もう一度、整理すると。

  • ModuleParseError でエラーが構成されているが、これは発生したエラーを受け、表示用にエラーを構成しているもので、エラーそのものではない。
  • ModuleParseError を誰かが呼び出している。
  • エラーの発生は acorn で確定。

ということで、ModuleParseError って誰が呼び出しているのかを調べてみる。

ModuleParseError の生成箇所を追う

とりあえず、検索してみると NormalModule.js で生成していることが分かる。
そして、NormalModule.build() 内で、 doBuild() を呼び出し、ここのコールバックで生成している。
で、条件としては、 this.parser.parse() でエラーが発生した時、または、this.parser.parse()が例外を発生させた時のみ。
念の為、この2つのケースに console.log() を入れてみて確認してみると、catch節で ModuleParseError が生成されることを確認した。

acorn って何者?

で、javascriptのパーサーでした。
npm で週4千万ダウンロードされてる、神様的なライブラリですね。
いつもお世話になっているのに、知らなくてゴメンね。
で、この子は、文字列とパラメータを与えると、構文解析して、ESTree Specにくれるものみたいです。

https://github.com/acornjs/acorn/tree/master/acorn

つまり、この子が理解できないと例外を発生させたのですが、最新の acorn 8.0.5 は ECMA12までサポートしてます。
storybook が使っている acorn はというと。。。 storybook -> webpack -> acorn で 7.1.0 か。。。
どうなんだろう? これかな?

そういえば NormalModule って?

そうそう、エラー出しているのは NormalModule の this.parser.parse() だったっけ。
ところで、こいつの this.parser って何者なんだ?
acron にも parse() って関数あるんだけど、引数違うから、まだ途中に誰か居るな。

ということで、 NormalModule.parser は誰なのか追ってみる。

どうやら、this.parserに代入している箇所は、constructor() と updateCacheModule() だが、取り敢えず、何者かを知りたいので、constructor に絞って探してみる。
(あ〜、動的型付け言語は追いづれ〜〜〜)
試しに、 "new NormalModule" で検索してみると、NormalModuleFactory.js がヒットするので、何となくコレっぽい。

  let createdModule = this.hooks.createModule.call(result);
  if (!createdModule) {
    if (!result.request) {
      return callback(new Error("Empty dependency (no request)"));
    }

    createdModule = new NormalModule(result);
  }

が、こいつ、条件付きで new されるみたいで、hook とかいうヤツが生成する場合がある(どっちが多いんだか分からん)。

ちと、 NormalModule って、資料ないのかと調べてみる。

https://webpack.js.org/api/loaders/

あれ? webpack 本家サイトで "NormalObject" が登場するのはここしかない。
NormalObject を生成するファクトリについては解説しているが。。。
https://webpack.js.org/api/normalmodulefactory-hooks/

何だ? 知ってて当然で、知らない人はお断り的なヤツなんか?
まぁ、実際はwebpackの内部クラスで、webpack作っている方々以外は触るもんじゃないんでしょう。
で、NormalModuleを置き換えるプラグイン的な切り口については解説してますな。
https://webpack.js.org/plugins/normal-module-replacement-plugin/

ここまでくると、デバッガで動かさないと分からん。。。
ひとます、置いといて。。。

え〜っと、何やってるんだっけ?(苦笑)

残りは vue-docgen-loader

こいつは、vue を解析して、propertyやらeventやらを抽出する便利なヤツです。
こいつが動くってことは、誰かが呼び出しているはずなので探してみると。。。。
@storybook/addon-docs の中で、webpackFilnal() 関数の中におりました。
まともに動かないのを覚悟で、 vue-docgen-loader を module.rules に追加している箇所をコメントアウトしてみると。

ERROR in ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& (./node_modules/ts-loader??ref--4-0!./node_modules/vue-loader/lib??vue-loader-options!./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts&) 8:36
Module parse failed: Unexpected token (8:36)
File was processed with these loaders:
 * ./node_modules/ts-loader/index.js
 * ./node_modules/vue-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
|         const canvas = ref();
|         watch(canvas, () => {
>             const gl = canvas.value?.getContext("webgl");
|             if (gl) {
|                 gl.clearColor(0.0, 0.0, 0.0, 1.0);
 @ ./src/components/_sandbox/WebGL.vue?vue&type=script&lang=ts& 1:0-174 1:190-193 1:195-366 1:195-366
 @ ./src/components/_sandbox/WebGL.vue
 @ ./src/components/_sandbox/WebGL.stories.ts
 @ ./src sync ^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(js|jsx|ts|tsx))$
 @ ./.storybook/generated-stories-entry.js
 @ multi ./node_modules/@storybook/core/dist/server/common/polyfills.js ./node_modules/@storybook/core/dist/server/preview/globals.js ./.storybook/storybook-init-framework-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/common/config.js-generated-other-entry.js ./node_modules/@storybook/addon-docs/dist/frameworks/vue/config.js-generated-other-entry.js ./node_modules/@storybook/addon-links/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-actions/dist/preset/addArgs.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addDecorator.js-generated-other-entry.js ./node_modules/@storybook/addon-backgrounds/dist/preset/addParameter.js-generated-other-entry.js ./node_modules/@storybook/addon-knobs/dist/preset/addDecorator.js-generated-other-entry.js ./.storybook/preview.js-generated-config-entry.js ./.storybook/generated-stories-entry.js (webpack)-hot-middleware/client.js?reload=true&quiet=false&noInfo=undefined

まだエラー出てるけど、vue-docgen-loader がエラー出てるんじゃないのね。
で、更に、webpackの設定周りを追っていると、 iframe-webpack.config.js にぶちあたり、"generated-stories-entry.js" という、エラーで見かけた文字列を発見。

こりゃ、まだ続くな。。。

取り敢えず、これまでの解析結果は。。。

  • storybook で Optional Chaining を使うとエラーになる
  • tsconfig.json で target を ES6 にするとエラーはなくなる
  • エラーの発生に関連するものは。。。
    • acron(例外発生場所)
    • webpack の NormalModule.build() (エラーハンドリング場所)
    • vue-docgen-loader
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ついにVue.jsで作った美容室用の自作カレンダー

プログラミングスクール時代、友人が美容室を経営していたため、ポートフォリオ作成ついでに美容室の予約システムに挑戦した。
しかしそれはLaravelのみで作っており、当時知識も何もなくとにかく作ってみたという事もあり、完成したものはすごい無駄も多かっただろう。
様々な要因があり、そのカレンダーのページは開くのに12秒かかるという今の時代のページにはなかなかない仕様となった。
これではいかんとJSを勉強し、jQueryで挑戦することに。
かなりスピードは早くなったものの、for文地獄のコードにより様々な条件を考慮し、〇をまず描画してから×に変えるという強引な戦法をとった。
記念にそいつものせておく。
なお前回のカレンダーは2週間分しか見ることができないのに対し、今回は無限に過去や未来へいけるようになっている。

Index.vue
<template>
    <div>
        <router-view></router-view>
    </div>
</template>
Home.vue
<template>
    <div style="max-width: 1100px;width: 100%;margin:auto;">
        <div>
            <calendar :duration="duration"></calendar>
            <div style="max-width: 725px;margin: auto;">
                <!--実際にこのボタンは使うことはないが、changeCalendar()メソッドに引数をいれるとちゃんと変わるか確認用-->
                <v-btn @click="changeCalendar(0)">所要時間が0.5時間</v-btn>
                <v-btn @click="changeCalendar(1)">所要時間が1.0時間</v-btn>
                <v-btn @click="changeCalendar(2)">所要時間が1.5時間</v-btn>
                <v-btn @click="changeCalendar(3)">所要時間が2.0時間</v-btn>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            duration: 0
        }
    },
    methods: {
        changeCalendar(index){
            this.duration = index
        }
    }
}
</script>
Calendar.vue
<template>
    <div>
        <div class="d-flex text-center align-items-center" style="max-width:900px;margin: auto">
            <div>
            <!--15日前のカレンダー-->
                <v-btn @click="previousWeek" :disabled="ready">Back</v-btn>
            </div>
                <div class="px-3" style="max-width: 740px; width: 100%;">
                    <transition name="fade">
                        <div class="d-flex text-center justify-content-start">
                            <div>
                                <div style="display: inline-block">&nbsp;&nbsp;</div>
                  <!--営業時間は10:00~19:30なので予約できる時間は19:00まで表示。
                                     なお所要時間1時間の人は19:00に予約はできない-->
                                <div class="p-1" v-for="(period , i) in periods" :key="i">{{ period }}</div>
                            </div>
                            <div class="d-flex" style="margin: auto" v-if="calendar.length!=0">
                                <div v-for="(c, i) in calendar" :key="i">
                                    <!--本日から15日分表示-->
                                    <div style="width: 45px;" v-if="days">{{ december(days[i].getMonth()+1) }}/{{ days[i].getDate() }}</div>
                                    <div class="p-1" v-for="(time, j) in c" :key="j">
                      <!--クリックすると確認画面に遷移。予約できない×はdisabled-->
                                        <router-link to="/confirmation" :class="{ 'is-disabled': time.isBooking }">
                                            {{ (time.isBooking==false) ? '' : '×' }}
                                        </router-link>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </transition>
                </div>
            <div>
                <!--15日後のカレンダー-->
                <v-btn @click="nextWeek" :disabled="ready">Next</v-btn>
            </div>
        </div>
    </div>
</template>
<script>
export default {
    props: ['duration'],
    data() {
        return {
            calendar: [],
            bookings: [//ここの値は実際はデータベースから引っ張ってくる
                {id: 1, from: new Date('2021/02/13 11:30:00'), to: new Date('2021/02/13 12:00:00')},
                {id: 2, from: new Date('2021/02/13 13:00:00'), to: new Date('2021/02/13 14:00:00')},
                {id: 3, from: new Date('2021/02/13 17:00:00'), to: new Date('2021/02/13 19:30:00')},
                {id: 4, from: new Date('2021/02/15 10:00:00'), to: new Date('2021/02/13 11:00:00')},
                {id: 5, from: new Date('2021/02/14 14:00:00'), to: new Date('2021/02/14 16:30:00')},
                {id: 6, from: new Date('2021/02/16 13:00:00'), to: new Date('2021/02/16 18:00:00')},
                {id: 7, from: new Date('2021/02/14 17:00:00'), to: new Date('2021/02/14 18:00:00')},
                {id: 8, from: new Date('2021/02/17 13:30:00'), to: new Date('2021/02/14 17:00:00')},
                {id: 9, from: new Date('2021/02/20 11:00:00'), to: new Date('2021/02/20 15:30:00')},
                {id: 10, from: new Date('2021/02/28 12:00:00'), to: new Date('2021/02/28 16:00:00')},
            ],
            periods: ['10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30','19:00'],
            days: this.makeTwoWeeks(0),//本日から15日間の日付を表示。引数に1が入れば15日後が表示。
            dateIndex: 0,//makeTwoWeeks()関数とmakeCalendar()関数に入る引数。次のカレンダーに移動すると+1増える。
            ready: false,//カレンダー変更時にボタンをdisabledにする
        }
    },
    watch: {
        duration: {
            handler(){
                this.makeCalendar(this.dateIndex)//;所要時間が変わるたびにカレンダーを作り直す。
            },
            immediate: true,//これを使わない場合、createdにmakeCalendar(0)関数を書く必要がある
        },
    },
    methods: {
        makeCalendar(index){
            this.calendar = [];
            axios.get('api/time').then(response => {//サーバーから時間を引っ張ってくる(laravelのCarbon)
                //初期設定は引数に0が入るのでi=0で本日から15日間10:00~19:00までのカレンダーを作成。
                for (let i = 15*index; i < 15+15*index; i++) {
                    let dates = []
                    for (let j = 0; j < 19; j++) {
                        const today = new Date(response.data)
                        today.setMonth(today.getMonth())
                        today.setDate(today.getDate()+i)
                        today.setHours(10)
                        today.setMinutes(30*j)
                        today.setSeconds(0)
                        dates.push({date: today, isBooking: false});
                    }
                    this.calendar.push(dates)
                }
            })
            .then(()=>{
                this.getBookings()//順番に関数を呼び出す
            })
            .then(()=>{
                this.bookableDate(this.duration)
            })
            .catch((error)=>{
                console.log(error)
                this.ready = false;
            })
        },
        getBookings(){
            this.calendar.forEach((date, i) => {
                date.forEach((time, j) => {
                    this.bookings.forEach(booking=>{
                        if (this.floor(booking.from)<=this.floor(time.date)&&this.floor(time.date)<this.floor(booking.to)) {
                            //予約のある場所のオブジェクトの値isBookingをtrueに変更
                            this.$set(this.calendar[i][j], 'isBooking', true)
                        }
                    })
                })
            })
        },
        bookableDate(index){
            this.calendar.forEach((date, i) => {
                date.forEach((time, j) => {
                    this.bookings.forEach(booking=>{
                        //floor()関数は下記に記載
                        if (this.floor(booking.from)==this.floor(time.date)) {
                            for (let k = 1; k < index+1; k++) {
                                if((j-k)!==-1){
                                    //所要時間を考慮し、既存の予約の前の時間帯をtrueに変更
                                    this.$set(this.calendar[i][j-k], 'isBooking', true);
                                } else {
                                    return;
                                }
                            }
                        }
                    })
                })
            })
            this.calendar.forEach((date, i) => {
                date.forEach((time, j)=>{
                    if(index!==0 && (date.length-index)<=j){
           //所要時間を考慮し、閉店前の時間帯をtrueに変更
                    this.$set(this.calendar[i][j], 'isBooking', true)
                }
                })
            })
            let that = this;
            setTimeout(function(){
         //setTimeoutでボタンのdisabledの時間を調整
                that.ready = false;
            },200)
        },
        floor(time){
            //getTimeで1970年1月1日 00:00:00 UTC からの経過ミリ秒(1614420000450ミリ秒)を取得しミリ秒を秒に変換(1614420000.45)。
            //更にMath.floorで小数点を切り捨て(1614420000)。こうすることによって時間同士を比較する
            return Math.floor(time.getTime()/1000)
        },
        makeTwoWeeks(index){
            let daysArray = [];
            axios.get('api/time').then(response => {//サーバーから時間を取得
         //15日間の日付を作成
                for (let i = 15*index; i < 15+15*index; i++) {
                    const days = new Date(response.data);
                    days.setMonth(days.getMonth())
                    days.setDate(days.getDate()+i)
                    daysArray.push(days);
                }
            })
            return daysArray;
        },
        december(num){
            return num === 0 ? 12 : num
        },
        nextWeek(){
            //次の15日間のカレンダーを作成
            this.ready = true;
            this.calendar = [];
            this.dateIndex+=1;
            this.days = this.makeTwoWeeks(this.dateIndex);
            this.makeCalendar(this.dateIndex)
        },
        previousWeek(){
            //前の15日間のカレンダーを作成
            this.ready = true;
            this.calendar = [];
            this.dateIndex-=1;
            this.days = this.makeTwoWeeks(this.dateIndex);
            this.makeCalendar(this.dateIndex)
        },
    }
}
</script>

<style>
    .is-disabled {
        pointer-events: none;
        opacity: .5;
    }
    .fade-enter-active, .fade-leave-active {
        will-change: opacity;
        transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
    }
    .fade-enter, .fade-leave-to {
        opacity: 0
    }
</style>
routes.js
import VueRouter from 'vue-router';
import Home from './pages/Home.vue';



const routes = [
    {
        path: '/home',
        component: Home
        name: 'home'
    },
];

const router = new VueRouter({
    routes: routes,
    mode: 'history',
});

export default router;

以上ここまで。
これより先は決しておすすめできるものではないが記念に掲載。
初期のよりもましではあるがそれでも6秒くらいかかる仕様だろう。

calendar.blade.php
<table class="table-responsive mt-2 calender" border="5">
    <thead>
        <tr class="text-center">
            <th width="15%">{{ Carbon\Carbon::now()->format('n月') }}</th>
            @for($i = 1; $i <= 14; $i++)
            <th width="3%">{{ \Carbon\Carbon::today()->addDays($i-1)->format("d日") }}<br>{{ \Carbon\Carbon::today()->addDays($i-1)->isoformat("(ddd)") }}</th>
            @endfor
        </tr>
    </thead>
    <tbody>
        @for($j = 0; $j <= 18; $j++)
        <tr>
            <th class="align-middle" scope="row">{{ \Carbon\Carbon::today()->addHours(10)->addMinutes($j*30)->format("H:i") }}</th>
            @for($i = 1; $i <= 14; $i++)
                @if (\App\BookingController::find(1)->where('day', 'like', '%'.\Carbon\Carbon::today()->addDays($i-1)->format("N"). '%')->first())
                    <td class="align-middle text-center">
                        <input class="calender-cell text-center" type="text" placeholder="{{\Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("ndHi")}}" value="×" size="1" disabled>
                    </td>
                @else
                <td class="align-middle text-center">
                    @if (null !== $user_booking && $user_booking->booking_date_number > \Carbon\Carbon::now()->format("ndHi"))
                    <input class="calender-cell" type="button"
                    value="〇" placeholder="{{\Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("ndHi")}}" size="1">
                    @else
                    <input class="calender-cell" type="button"
                    onclick="location.href='{{ action('HomeController@reservation', ['booking_date' => \Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("n月d日 H:i"),
                    'cut' => $cut, 'perm' => $perm, 'color' => $color, 'treatment' => $treatment, 'spa' => $spa,
                    'price' => $price, 'length_of_time' => $length_of_time,
                    'booking_date_month' => \Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("n"),
                    'booking_date_day' => \Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("d"),
                    'booking_date_hour' => \Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("H"),
                    'booking_date_minute' => \Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("i")]) }}'"
                    value="〇" placeholder="{{\Carbon\Carbon::today()->addDays($i-1)->addHours(10)->addMinutes($j*30)->format("ndHi")}}" size="1" >
                    @endif
                </td>
                @endif
            @endfor
        </tr>
        @endfor
    </tbody>
</table>
<input id="length_of_time" type="hidden" value="{{ $length_of_time }}">

calendar.js
$(function() {
    var input = $('form[name="Form"] input');
    for (var i = 0; i < input.length - 1; i++) {
        var placeholder = input.eq(i).attr("placeholder");
        if (placeholder < twoHoursFromNow) {
                    input.eq(i).val("×");
                    input.eq(i).attr('disabled', true);
        }
      }
    // {{--  ブッキングコントローラー特定の日付用  --}}1
    bookingController.forEach(function(item, index) {
      var dayOfTheWeek = item.day_of_the_week;
      for (var i = 0; i < document.Form.length - 1; i++) {
        var placeholder = input.eq(i).attr("placeholder");
        if (Math.floor(placeholder / 10000) == dayOfTheWeek) {
          input.eq(i).val("×");
          input.eq(i).attr('disabled', true);
        }
      }
    })
    // {{--  ブッキングコントローラー特定の時間帯用  --}}
    bookingController2.forEach(function(item, index) {
      var dayTime = item.day_time;
      for (var i = 0; i < document.Form.length - 1; i++) {
        var placeholder = input.eq(i).attr("placeholder");
        if (placeholder == dayTime) {
            for (var l = 0; l < 16; l += 1) {
                if ($('form[name="Form"] #length_of_time').val() === 1 + 0.5 * l + '時間') {
                  for (var j = 0; j < 14 * l + 15; j += 14) {
                    input.eq(i - j).val("×");
                    input.eq(i - j).attr('disabled', true);
                  }
                } else {
                  input.eq(i).val("×");
                  input.eq(i).attr('disabled', true);
                }
            }
        }
      }
    })
    // {{--  既存の予約用  --}}
    booking.forEach(function(item, index) {
      var bookingDate = item.booking_date_number;
      var time = item.length_of_time;
      for (var i = 0; i < document.Form.length - 1; i++) {
        if (input.eq(i).attr("placeholder") == bookingDate) {
          input.eq(i).prop("name", time)
          for (var l = 0; l < 16; l += 1) {
            if (input.eq(i).attr('name') === 1 + 0.5 * l + '時間') {
              for (var j = 0; j < 14 * l + 15; j += 14) {
                input.eq(i + j).val("×");
                input.eq(i + j).attr('disabled', true);
              }
            } else {
              input.eq(i).val("×");
              input.eq(i).attr('disabled', true);
            }
          }
          for (var m = 0; m < 16; m += 1) {
            if ($('form[name="Form"] #length_of_time').val() === 1 + 0.5 * m + '時間') {
                if(input.eq(i) == '×') {
                    for (var j = 0; j < 14 * m + 15; j += 14) {
                input.eq(i - j).val("×");
                input.eq(i - j).attr('disabled', true);
              }
                }
            } else {
              input.eq(i).val("×");
              input.eq(i).attr('disabled', true);
            }
        }
        }
      }
      for (var m = 0; m < 16; m += 1) {
        if ($('form[name="Form"] #length_of_time').val() === 1 + 0.5 * m + '時間' && input.eq(266).val() !== "" && input.eq(266).val() !== "×") {
          for (var k = 252-14 * m; k < 266; k++) {
              input.eq(k).val("×");
              input.eq(k).attr('disabled', true);
            }
        }
      }
    })
  })
  $(function() {
    var input2 = $('form[name="Form2"] input');
    for (var i = 0; i < document.Form2.length - 1; i++) {
        var placeholder = input2.eq(i).attr("placeholder");
        if (placeholder < twoHoursFromNow) {
                    input2.eq(i).val("×");
                    input2.eq(i).attr('disabled', true);
        }
      }
    // {{--  ブッキングコントローラー特定の日付用  --}}
    bookingController.forEach(function(item, index) {
      var dayOfTheWeek = item.day_of_the_week;
      for (var i = 0; i < document.Form2.length - 1; i++) {
        var placeholder = input2.eq(i).attr("placeholder");
        if (Math.floor(placeholder / 10000) == dayOfTheWeek) {
          input2.eq(i).val("×");
          input2.eq(i).attr('disabled', true);
        }
      }
    })
    // {{--  ブッキングコントローラー特定の時間帯用  --}}
    bookingController2.forEach(function(item, index) {
      var dayTime = item.day_time;
      for (var i = 0; i < document.Form2.length - 1; i++) {
        var placeholder = input2.eq(i).attr("placeholder");
        if (placeholder == dayTime) {
            for (var l = 0; l < 16; l += 1) {
                if ($('form[name="Form2"] #length_of_time').val() === 1 + 0.5 * l + '時間') {
                  for (var j = 0; j < 14 * l + 15; j += 14) {
                    input2.eq(i - j).val("×");
                    input2.eq(i - j).attr('disabled', true);
                  }
                } else {
                  input2.eq(i).val("×");
                  input2.eq(i).attr('disabled', true);
                }
            }
        }
      }
    })
    // {{--  既存の予約用  --}}
    booking.forEach(function(item, index) {
      var bookingDate = item.booking_date_number;
      var time = item.length_of_time;
      for (var i = 0; i < document.Form2.length - 1; i++) {
        if (input2.eq(i).attr("placeholder") == bookingDate) {
          input2.eq(i).prop("name", time)
          for (var l = 0; l < 16; l += 1) {
            if (input2.eq(i).attr('name') === 1 + 0.5 * l + '時間') {
              for (var j = 0; j < 14 * l + 15; j += 14) {
                input2.eq(i + j).val("×");
                input2.eq(i + j).attr('disabled', true);
              }
            } else {
              input2.eq(i).val("×");
              input2.eq(i).attr('disabled', true);
            }
          }
          for (var m = 0; m < 16; m += 1) {
            if ($('form[name="Form2"] #length_of_time').val() === 1 + 0.5 * m + '時間') {
              for (var j = 0; j < 14 * m + 15; j += 14) {
                input2.eq(i - j).val("×");
                input2.eq(i - j).attr('disabled', true);
              }
            } else {
              input2.eq(i).val("×");
              input2.eq(i).attr('disabled', true);
            }
        }
        }
      }
      for (var m = 0; m < 16; m += 1) {
        if ($('form[name="Form2"] #length_of_time').val() === 1 + 0.5 * m + '時間' && input2.eq(266).val() !== "" && input2.eq(266).val() !== "×") {
          for (var k = 252-14 * m; k < 266; k++) {
              input2.eq(k).val("×");
              input2.eq(k).attr('disabled', true);
            }
        }
      }
    })
  })

とてもシンプルに作れたと思う。
使う機会あればぜひお使いください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む