作りたいものがありすぎる

40歳を過ぎてプログラミングを始めた人の顛末とこれからなど

基礎から学ぶVue.js CHAPTER2 データの登録と更新 読書メモ

今回は輪読担当ではありませんが、ひとまずメモをまとめたのでアップします。

以下の記事は2019/2/7 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch2 データの登録と更新(初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER2 Vue.jsとデータの登録と更新 のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js


CAPTER2

SECTION 07 基本データのバインディング

Mustache(マスタッシュ記法) hogeプロパティをhtmlにバインドする

<p>{{ hoge }}</p>

バインドは属性に使えない
下はエラー

<input type="text" value="{{ message }}">

これが正しい
属性へのバインドはv-bindディレクティブを使う

<input type="text" v-bind:value="message">
<!-- 省略して書くとこう -->
<input type="text" :value="message">

cssスタイルはキャメルケースで指定

<button v-on:click="isActive=!isActive">isActiveを切り替える</button>
<p v-bind:class="{ child: isChild, 'is-active': isActive }" class="item">
  動的なクラス
</p>
<p v-bind:style="{ color: textColor, backgroundColor: bgColor }" class="item">
  動的なスタイル
</p>
new Vue({
    el: '#app',
    data: {
        isChild: true,
        isActive: true,
        textColor: 'red',
        bgColor: 'lightgray'
    }
})
.item {
  padding: 4px 8px;
  transition: background-color 0.4s;
}
.is-active {
  background: #ffeaea;
}

オブジェクトデータで渡す方法
Vue.js側でひとまとめで定義

<p v-bind:class="classObject">Text</p>
<p v-bind:class="classObject_2">Text</p>
new Vue({
    el: '#app',
    data: {
        classObject: {
            isChild: true,
            isActive: true,
            textColor: 'red',
            bgColor: 'lightgray'
        },
        classObject_2: {
            isChild: true,
            isActive: true,
            textColor: 'red',
            bgColor: 'lightgray'
        }
    }
})

複数の属性のデータバインディング

沢山のプロパティがあっても

new Vue({
    el: '#app',
    data: {
        item: {
            id: 1,
            src: 'item1.jpg,
            alt: 'サムネ画像です',
            width: 200,
            height: 100,
        }
    }
})

まとめて定義できる

<img v-bind="item">

特定の要素のみに変更を加えることも可能

<img v-bind="item" v-bind:id="'thunb-' + item.id">
<!-- thunb-1 の id が付与される -->

SVGのデータバインディングベクター画像)

<div id="app">
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1">
    <circle cx="100" cy="75" v-bind:r="radius" fill="lightpink" />
  </svg>
  <input type="range" min="0" max="100" v-model="radius">
</div>
new Vue({
    el: '#app',
    data: {
        radius: 50
    }
})

SECTION 09 テンプレートにおける条件分岐

v-if``v-showディレクディブは付与した要素の描画・表示に条件を適用する。

okプロパティがtrueの時のみdiv要素を表示する

<div v-if="ok">hoge</div>
<div v-show="ok">hoge</div>
new Vue({
    el: '#app',
    data: {
        ok: false
    }
})

条件を満たさない場合に生成されるhtml display:none となる

<div style="display: none;">hoge</div>

v-if と v-show の違いと使い分け

v-if の場合

DOMレベルでない事になる

v-show の場合

display:none が付与される
切り替え頻度が高いならこっちが処理早い

タグによるv-if グループ化

複数の要素を if で切り替えたい場合グループ化できる

<tamplate>
    <header>title</header>
    <div><contents/div>
</tamplate>

v-else-if 及び v-else によるグループ化

<div v-if="type === 'A'">AAA</div>
<div v-else-if="type === 'B'">BBB</div>
<div v-else>どちらでも無い場合</div>

v-else-if v-else key

keyを設定して属性の重複による発動しない状態を回避する

<!-- 2つのdivが違う要素である事を明示的にする -->
<div v-if="loaded" key="content-visible">
  content
</div>
<div v-else key="content-loading">
  loading now...
</div>

SECTIOM 10 リストデータの表示と更新

要素を繰り返して描画する

みたまんま、こんな感じで繰り返し描画できる。
v-for="item in list"php や JS のfor 文や foreach と同じ様に使える

  <ul>
    <li v-for="item in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    }
})

出力結果

<ul>
    <li>ID.1 スライム HP.100</li>
    <li>ID.2 ゴブリン HP.200</li>
    <li>ID.3 ドラゴン HP.500</li>
</ul>

インデックスとオブジェクトキーの使用

変数部分をカッコで囲んで配列インデックスを任意に受け取れる

<li v-for="(item, index ) in list"> ...</li>

オブジェクトなら「値」「キー」「インデックス」の順で任意に受け取れる

<li v-for="(item, key, index ) in list"> ...</li>

キーの役割

これ大事! v-bind:key="item.id"

<li v-for="item in list" v-bind:key="item.id">

要素にユニークなキー属性を追加するのが望ましい。ほぼ必須と考えて良い。
キーが無いと要素全部の更新が入る。なのでSQLidを入れる位に考えると良い。

繰り返し描画しながら様々な条件を適用する

v-if を絡めて、比較演算子で条件付けて、特定条件での表示などもできる。

  <ul>
    <li v-for="item in list" v-bind:key="item.id" v-bind:class="{ tuyoi: item.hp > 300 }">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
      <span v-if="item.hp > 300">つよい!</span>
    </li>
  </ul>

出力結果

<ul>
    <li>ID.1 スライム HP.100</li>
    <li>ID.2 ゴブリン HP.200</li>
    <li>ID.3 ドラゴン HP.500<span>つよい!</span></li>
</ul>

リストの更新

注意点として以下のケースで更新を検知できない

  1. インデックス数値を使った配列要素の更新
  2. 後から追加されたプロパティの更新

リストに要素を追加

push,unshiftを使う

this.list.push(要素)

以下、サンプル、ボタン押下でフォーム内の名前のモンスターがリストに追加される。IDは自動生成。HPは500固定

  <!-- このフォームの入力値を新しいモンスターの名前に使う -->
  名前
  <input v-model="name">
  <button v-on:click="doAdd">モンスターを追加</button>
  <ul>
    <li v-for="item in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        name: 'キマイラ',
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    methods: {
        // 追加ボタンをクリックしたときのハンドラ
        doAdd: function () {
            // リスト内で1番大きいIDを取得
            var max = this.list.reduce(function (a, b) {
                return a > b.id ? a : b.id
            }, 0)
            // 新しいモンスターをリストに追加
            this.list.push({
                id: max + 1, // 現在の最大のIDに+1してユニークなIDを作成
                name: this.name, // 現在のフォームの入力値
                hp: 500
            })
        }
    }
})

リストから削除する

リストからの削除は配列メソッドのspliceを使う

li毎に削除ボタンが表示され、クリックで対象を消せる

  <ul>
    <li v-for="(item, index) in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
      <!-- 削除ボタンをv-for内に作成 -->
      <button v-on:click="doRemove(index)">モンスターを削除</button>
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    methods: {
        // 要素を削除ボタンをクリックしたときのハンドラ
        doRemove: function (index) {
            // 受け取ったインデックスの位置から1個要素を削除
            this.list.splice(index, 1)
        }
    }
})

リスト要素( <li>hoge</li> )に関しては以下の様な配列メソッドを使用して操作が可能

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

これは駄目

this.list[0] = { id: 1, name: 'hoge', hp: 500 }

これなら大丈夫

Vue.setメソッドを使用して明示的に更新できる。エイリアスthis.$setとなる

this.$set(更新するデータ , インデックスorキー , { 新しい値 })

上記からの具体例だとこんな感じ

this.$set(this.list, 0, { id: 1, name: 'hoge', hp: 500 })

プロパティを追加する

this.$setメソッドは持ってないプロパティをリアクティブデータとして追加するために使用できる。

new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    created: function() {
        // すべての要素にactiveプロパティを追加したい
        this.list.forEach(function(item) {
            this.$set(item, 'active', false)
            // 「item.active = false」ではリアクティブにならない
        }, this)
    }
})

リスト要素プロパティを更新する

プロパティ hp を更新する 作例

  <ul>
      <li v-for="(item, index) in list" v-bind:key="item.id" v-if="item.hp">
          ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
      <span v-if="item.hp < 50">瀕死!</span>
      <!-- ボタンはv-for内に作成 -->
      <button v-on:click="doAttack(index)">攻撃する</button>
    </li>
  </ul>

liの最後にあるv-if="item.hp"は hpが0になると消える処理になる。
<span v-if="item.hp < 50">瀕死!</span>は hp 50未満で表示される。

new Vue({
    el: '#app',
    data: {
        list: [
            { id: 1, name: 'スライム', hp: 100 },
            { id: 2, name: 'ゴブリン', hp: 200 },
            { id: 3, name: 'ドラゴン', hp: 500 }
        ]
    },
    methods: {
        // 攻撃ボタンをクリックしたときのハンドラ
        doAttack: function (index) {
            this.list[index].hp -= 10 // HPを減らす
        }
    }
})

ユニークキーを持たない配列

出来ない訳ではない、簡易にする際はこれでもOK

<option v-for="item in list">{{ item }}</option>
data: {
    list: ['aaa', 'bbb', 'ccc']
}

オプションにデータを持たないv-for

v-forに数値をセットすると以下の例の様にspanで囲まれた1~15の値を出力できる

<span v-for="item in 15">{{ item }}</span>

同様に 1,5,10,15 の4つを出力する

<span v-for="item in [1, 5, 10, 15]">{{ item }}</span>

文字列に対するv-for

文字列にv-for を使うと1文字ずつ別々の要素で描画される

<span v-for="item in text">{{ item }}</span>
new Vue({
    el: '#app',
    data: {
        text: 'hoge'
    }
})

出力結果

<span>h</span>
<span>o</span>
<span>g</span>
<span>e</span>

これを利用するとテキストアニメーションが作れるらしい

外部からデータを取得する

外部データはJSONやWebAPIで取得する必要がある。
JSONを外部データから取り込んでみる。
htmlの下の方にある<script>内に javascriptライブラリのaxiosCDNを読み込む1行を追加する。

  <script src="https://cdn.jsdelivr.net/npm/axios@0.17.1/dist/axios.min.js"></script>

これでAJAXが使える様になる

  <ul>
    <li v-for="(item, index) in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
new Vue({
    el: '#app',
    data: {
        // あらかじめ空リストを用意しておく
        list: []
    },
    created: function () {
        axios.get('list.json').then(function (response) {
            // 取得完了したらlistリストに代入
            this.list = response.data
        }.bind(this)).catch(function (e) {
            console.error(e)
        })
    }
})

ライフサイクルフックの created を使って new 直後にjsonを非同期で取り込む、取り込む前は data.list の空配列[]が一瞬だが、適用されている。ここが表示されるまでに、ローディングアニメーション処理とか入るとカッコよくなる。

JSONファイル list.json

[
  { "id": 1, "name": "スライム", "hp": 100 },
  { "id": 2, "name": "ゴブリン", "hp": 200 },
  { "id": 3, "name": "ドラゴン", "hp": 500 }
]

SECTION 11 DOMを直接参照する $el と $refs

DOMにアクセスするには、インスタンスプロパティ$el,$refsを使用する。
但し、ライフサイクルフックのmounted以降でないと使えない

$el の使い方

テンプレートを囲んでいるルート要素は$el を使って参照できる。
例えば<canvas>要素などにアクセスしたい時などに使用する。

var app = new Vue({
  el: '#app'
      mounted: function() {
      console.log(this.$el)  // <div id="app"></div>
  }
})

$refsの使い方

<div id="app">
    <p ref="hello">hello</p>
    <!--  p要素にhelloと名を付けた   -->
</div>

以下の様にアクセス

new Vue({
    el: '#app',
      mounted: function() {
      console.log(this.$refs.hello)  // p要素のDOMとなる
    }
})

$el や$refsは一時的な変更です!

これらは仮想DOMではないので描画処理の最適化をしない
操作の都度描画するので注意

<div id="app">
  <button v-on:click="handleClick">カウントアップ</button>
  <button v-on:click="show=!show">表示/非表示</button>
  <span ref="count" v-if="show">0</span>
</div>
new Vue({
  el: '#app',
  data: {
    show: true
  },
  methods: {
    handleClick() {
      var count = this.$refs.count
      if (count) {
        count.innerText = parseInt(count.innerText, 10) + 1
      }
    }
  }
})

カウントアップに対して、表示・非表示ボタンがあるが、count up した状態で、非表示・再表示すると、カウントが0にもどってしまう。これは、DOMに対して加算をしたのみなので、DOMが消えると、値も消えてしまうため。 vue.js での指定なら仮想DOMなのでこうはならない。

SECTION12 テンプレート制御ディレクティブ

ディレクティブ 作用
v-pre テンプレートのコンパイルをスキップする XSS対策に有効
v-once 一度だけバインディングを行う
v-text Mustash {{ }} の代わりにテキストコンテンツを描画
v-html HTMLタグをそのまま描画する
v-cloak インスタンスの準備が終わると取り除かれる

v-pre

XSS対策などで使う

<a v-bind:href="#" v-pre>
    hello {{ message }}
</a>

<!-- これは以下のよう生だし描画される -->
<a v-bind:href="#" v-pre> hello {{ message }}</a>

v-once

描画されたあとに 指定したプロパティの値が変わってもDOMは更新されない

v-text

Mustash {{ kore }} を使わないで書くパターンがある場合に使える

var app = new Vue({
  el: '#app'
      data: {
          message: 'Hello!'
  }
})

こんな風にmessageでバインドできる。

<span v-text="message"></span>

v-html

v-pre (XSS対策)とは逆に、htmlのタグ等、をそのまま出してしまう奴
自分がコントロールできない外部やユーザー要因のデータ部分に使うと脆弱性ありありなので使い所には注意が必要。

var app = new Vue({
  el: '#app'
      data: {
          message: 'hello<strong>Vue.js!</strong>'
  }
})

こんな風にhtmlがそのまま出力する事ができる。

<span v-html="message"></span>
<!-- これは以下のよう描画される -->
<span>hello<strong>Vue.js!</strong></span>

v-cloak

インスタンスの準備ができると自動的に取り除かれる。コンパイル前のテンプレートが表示されるのを防げる

CSSに以下の様なスタイル定義をする

[v-cloak] { display: none}

以下のやつで画面読み込み時に#app要素を隠せる。
インスタンス生成でv-cloak属性が外れてフェードイン表示される。

@keyframes cloak-in {
  0% {
    opacity: 0;
  }
}
#app {
  animation: cloak-in 1s;
}
#app[v-cloak] {
  opacity: 0;
}

仮想DOMとは?

超要約するとDOMツリーの上にもう一枚仮想DOMのツリーを作ってVue.jsでは基本的に仮想側の操作をする。 DOM自体が変わったり、変えたりした際は非同期で本来のDOMを変更しているので、タイムラグや、DOMの入れ替え時に反映されない事がある。
例えば v-if,v-elseで分岐表示させた際等にこれが起こりうる。そのため、分岐の各要素に異なる key を付けて別物ということを認識させる事でちゃんと描画されるようになる。

jQuery などのDOMライブラリとの併用

も、一応可能だが、Vue.jsがDOMを直接いじる $el,$refsが同様の事ができるので、併用はできるけど、まあ、無意味化しつつあるよね。ということらしい。

まとめ

  • 使用したいデータはdataオプションに登録しよう
  • 操作するリストには不変でユニークなkey属性を設定しよう
  • 配列インデックスを使った更新はVue.setを使う
  • 関数の呼び出し方ではthisは変化することがある
  • $elと$refsはmounted以降で使う