20190607のvue.jsに関する記事は17件です。

vue-cliのビルドファイルをGitHub Pagesにデプロイ

1.vue.config.js に正しい publicPath を設定

https://<USERNAME>.github.io/ にデプロイしている場合は、publicPathを省略することができる(デフォルトは "/"のため)。

https://<USERNAME>.github.io/<REPO>/にデプロイしている場合(例えば、リポジトリがhttps://github.com/<USERNAME>/<REPO>にある場合)、publicPath"/<REPO>/" に設定。

例えば、リポジトリ名が "my-project"の場合、vue.config.js(config/dev.env.js) は次のようになる。

module.exports = {
  publicPath: process.env.NODE_ENV === 'production'
    ? '/my-project/'
    : '/'
}

2.プロジェクト内で、次の内容でdeploy.shを作成し、それを実行してデプロイ

deploy.sh
#!/usr/bin/env sh

# エラー
set -e

# build
npm run build

# 出力されたディレクトリに移動
cd dist

# カスタムドメインにデプロイする場合
# echo 'www.example.com' > CNAME

git init
git add -A
git commit -m 'deploy'

# https://<USERNAME>.github.io にデプロイする場合
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master

# https://<USERNAME>.github.io/<REPO> にデプロイする場合
# git push -f git@github.com:<USERNAME>/<REPO>.git master:gh-pages

cd -

シェルスクリプトファイル(deploy.sh)を実行
chmod 755 deploy.sh
./deploy.sh

これでデプロイが完了です

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

ブラウザ側でサイズ圧縮(リサイズ)して画像表示やアップロードを行う(Vue.js)

概要

Webアプリでクライアントサイド(ブラウザ)で画像を圧縮(リサイズ)する方法の紹介です。
サンプルとして下図のようなアップロードした画像をプレビュー表示した上で、確定時にサーバーに画像をアップロードするまでのサンプルコードを紹介します。

※今回はvue.jsとvuetifyによるサンプルになりますが、UIのレイアウト部分以外は基本的に一般的なjavascirptですので
他のフレームワークを使用している場合等でも参考にはなるかと思います

sample.png

動くデモとGitHubのサンプルコードは下記です。
Demo
GitHub

背景

Webアプリで、画像ファイルをアップロードするといったシチュエーションはよくあると思います。
スマホ等で撮影した写真等だと、解像度やファイルサイズも大きいため、そのまま使用せず一度リサイズしたりする事が多いかと思います。
アップロード後にサーバ側で処理するといった手法もありますが、今回はWEBブラウザ側でリサイズする方法とします。

実現方法

当初はCanvasにリサイズした画像を描画して、再度データ化する方法を使用していました。
参考

しかしながら、もっとお手軽にかつファイルサイズも小さくしてアップできる、Browser Image Compression といったものがあるのでそちらを使用します。

画面レイアウト

以下のような、3つの領域を作成します。

  • 画像のプレビュー領域
     アップロードした画像のプレビュー表示します。サンプルではvuetifyのv-imgタグを使用していますが、imgタグに相当するもの配置します。合わせてファイル名も表示します。

  • ファイルのアップロード
     ファイルのアップロードするinputタグです。アップロードするとプレビュー領域に表示されます。

  • Submitボタン
     プレビュー表示した画像をサーバに送信するためのボタン。画像ファイルのアップロードが完了して、プレビュー表示されるまでは押せなくします。

これらをvue.jsのコンポーネントで書くと下記のようになります。
処理等は無く、まだレイアウトだけです。

ImageUpload.vue
<template>
  <v-container>
    <v-layout text-xs-center wrap>
      <v-flex xs12>
        <!-- 画像のプレビュー表示領域 -->
        <v-img :src="upimage.fileUrl" aspect-ratio="2" :contain="true"></v-img>
        <p>{{ upimage.fileName }}</p>
        <p>圧縮前サイズ(MB):{{ fileInfo.before.size }}</p>
        <p>圧縮後サイズ(MB):{{ fileInfo.after.size }}</p>
      </v-flex>
      <v-flex xs12>
        <!-- ファイルの選択 -->
        <input @change="selectedFile" type="file" accept="image/jpeg, image/jpg, image/png">
      </v-flex>
      <v-flex xs12>
        <!-- Submitボタン -->
        <v-btn color="primary" :disabled="isUploading">submit</v-btn>
      </v-flex>
    </v-layout>
  </v-container>
</template>
<script>
export default {
  data() {
    return {
      isUploading: false, // 画像ファイルアップロード中の判断フラグ
      upimage: { fileUrl: "", fileName: "", blob: null } // 画像ファイル
    };
  },
  methods: {
    async selectedFile(e) {
      // ファイルアップロード時の処理 
      // e.target.filesにファイルの情報が格納
    },
    async submit() {
      // 画像をサーバに送信する処理
    }
  }
};
</script>

画面のリサイズ

アップロードされた画像をプレビュー表示前にリサイズする処理を作成します。
まずは、npmかyarnでbrowser-image-compressionをインストールします。

npm install browser-image-compression --save
or
yarn add browser-image-compression

次に以下の2つの処理を作成します。

  1. 入力された画像ファイルの圧縮を行う処理
  2. プレビュー表示用にDataUrlを取得する処理

DataUrlにするのはimgタグにおいてプレビュー表示を行うためですので、直接アップロードしたい場合は不要です。
画像の圧縮時には最大のファイルサイズおよび、解像度を指定します。
今回は最大サイズは1MB, 解像度を800に指定しています。
その他にもオプションがありますので、詳細はbrowser-image-compressionのGitHubを参照してください。

ImageUtil.js
import imageCompression from "browser-image-compression";

export default {
  // アップロードされた画像ファイルを取得
  async getCompressImageFileAsync(file) {
    const options = {
      maxSizeMB: 1, // 最大ファイルサイズ
      maxWidthOrHeight: 800 // 最大画像幅もしくは高さ
    };
    try {
      // 圧縮画像の生成
      return await imageCompression(file, options);
    } catch (error) {
      console.error("getCompressImageFileAsync is error", error);
      throw error;
    }
  },
  // プレビュー表示用のdataurlを取得
  async getDataUrlFromFile(file) {
    try {
      return await imageCompression.getDataUrlFromFile(file);
    } catch (error) {
      console.error("getDataUrlFromFile is error", error);
      throw error;
    }
  }
};

画面への処理の取り込み

この画像処理を先ほどのレイアウト内で呼び出せば終了です。

ImageUpload.vue
// 略
<script>
import ImageUtil from "../lib/imageUtil";
export default {
  // 略
  methods: {
    async selectedFile(e) {
      this.isUploading = true;

      const file = e.target.files[0];
      if (!file) {
        return;
      }

      try {
        // 圧縮した画像を取得
        const compFile = await ImageUtil.getCompressImageFileAsync(file);

        //ファイルサイズの表示
        this.fileInfo.before.size = (file.size / 1024 / 1024).toFixed(4);
        this.fileInfo.after.size = (compFile.size / 1024 / 1024).toFixed(4);
        // 画像情報の設定
        this.upimage.blob = compFile;
        this.upimage.fileUrl = await ImageUtil.getDataUrlFromFile(compFile);
        this.upimage.fileName = file.name;
      } catch (err) {
        // エラーメッセージ等を表示
      } finally {
        this.isUploading = false;
      }
    },
    submit() {
      const fd = new FormData();
      try {
        fd.append(this.upimage.fileName, this.upimage.blob, this.upimage.fileName);
        // ここにサーバーへのアップロード処理を実装する
      } catch (err) {
        // エラーメッセージ等を表示
      }
    }
  }
};
</script>

まとめ

ということで、browser-image-compressionを使うことで、お手軽に画像の圧縮及び表示が可能となります。
是非試してみてください。

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

【Vue.js - 4】データの監視と加工

まとめ

・算出プロパティは結果をキャッシュしてくれる
・データの状態に対して処理をしたいならウォッチャ
・カスタムディレクティブは監視をしながらDOMの操作ができる
・更新後のDOMにアクセスしたいならnextTick

1. 算出プロパティで処理を含むデータを作成

算出プロパティ(Computed 処理を含むデータ)

html
<p>{{ width }} の半分は {{ halfWidth }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    //算出プロパティhalfWidthを定義
    halfWidth: function() {
      return this.width / 2
    }
  }
})

算出プロパティの組み合わせ

html
<p>X: {{ halfPoint.x }}</p>
<p>Y: {{ halfPoint.y }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800,
    height: 600
  },
  computed: {
    halfWidth: function() {
      return this.width / 2
    },
    halfHeight: function() {
      return this.height / 2
    },
    // 「width × height」の中心座標をオブジェクトで返す
    halfPoint: function() {
      return {
        x: this.halfWidth,
        y: this.halfHeight
      }
    }
  }
})

ゲッターとセッター

html
<input v-model.number="width"> {{ width }}
<input v-model.number="halfWidth"> {{ halfWidth }}
Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    halfWidth: {
      get: function() {
        return this.width / 2
      },
      //halfWidth の2倍の数値を width に代入する
      set: function(val) {
        this.width = val * 2
      }
    }
  }
})

算出プロパティのキャッシュ機能
算出プロパティ…リアクティブな依存データに基づき、結果をキャッシュする。キャッシュを再構築するトリガになるのはリアクティブなデータのみ
メソッド…キャッシュされない

html
<!-- 算出プロパティ:キャッシュされるため、何度使用しても同じ結果 -->
<ol>
  <li>{{ computedData }}</li><!-- 0.8205563271901375 -->
  <li>{{ computedData }}</li><!-- 0.8205563271901375 -->
</ol>

<!-- メソッド:キャッシュされない。呼び出すたび変わる -->
<ol>
  <li>{{ methodsData() }}</li>0.13761479619266637
  <li>{{ methodsData() }}</li>0.45429414765683895
</ol>
Vue.js
new Vue({
  el: '#app',
  computed: {
    computedData: function() { return Math.random() }
  },
  methods: {
    methodsData: function() { return Math.random() }
  }
})

サンプルコード:リストの絞り込み・ソート機能

html
<div id="app">
  <input v-model.number="budget"> 円以下に絞り込む
  <input v-model.number="limit"> 件を表示
  <button v-on:click="order=!order">切り替え</button>
  <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p>
  <ul>
    <!-- v-forでは最終結果、算出プロパティのlimitedを使用する -->
    <li v-for="item in limited" v-bind:key="item.id">
      {{ item.name }} {{ item.price }}円
    </li>
  </ul>
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    //フォームの入力と紐付けるデータ
    budget: 300,
    //表示件数
    limit: 2,
    //もとになるリスト
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
      { id: 3, name: 'いちご', price: 400 },
      { id: 4, name: 'おれんじ', price: 300 },
      { id: 5, name: 'めろん', price: 500 }
    ]
  },
  computed: {
    //budget以下のリストを返す算出プロパティ
    matched: function () {
      return this.list.filter(function (el) {
        return el.price <= this.budget
      }, this)
    },
    //matchedで返ったデータをlimit件返す算出プロパティ
    limited: function () {
      return this.matched.slice(0, this.limit)
    }
  }
})

2. ウォッチャでデータを監視して処理を自動化する

ウォッチャ

Vue.js
new Vue({
   ...
  watch: {
    監視するデータ: function (新しい値, 古い値) {
       valueが変化したときに行いたい処理
    },
    'item.value': function (newVal, oldVal) {
       //オブジェクトのプロパティも監視できる
    }
  }
})

ウォッチャ(オプションあり)

Vue.js
new Vue({
   ...
  watch: {
    list: {
      handler: function (newVal, oldVal) {
         //listが変化したときに行いたい処理
      },
      deep: true,//ネストされたオブジェクトも監視するか
      immediate: true //初期読み込み時にも呼び出すか
    }
  }
})

インスタンスメソッドでの登録(オプションなし)

Vue.js
this.$watch(監視するデータ, ハンドラ, オプション(任意))
this.$watch('value', function(newVal, oldVal) {
   //...
})

インスタンスメソッドでの登録(オプションあり)

Vue.js
this.$watch('value', function (newVal, oldVal) {
   ...
}, {
  immediate: true,
  deep: true
})

一度だけ動作するウォッチャ(unwatchで解除する)

Vue.js
new Vue({
  el: '#app',
  data: {
    edited: false,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
    ]
  },
  created: function() {
    var unwatch = this.$watch('list', function () {
      //listが編集されたことを記録する
      this.edited = true
      //監視を解除
      unwatch()
    }, {
      deep: true
    })
  }
})

実行頻度の制御p130
フォームの入力などによって監視する値が高頻度で変化する→ウオッチャの実行頻度を制御する

html
※lodash.min.jsを読み込む
<!-- <input type="text" v-model="value"> -->
Vue.js
new Vue({
  el: '#app',
  data: {
    value: '編集してみてね'
  },
  watch: {
    value: _.debounce(function (newVal) {実行から指定ミリ秒がすぎた場合にコールバックを呼び出す
         ここへコストの高い処理を書く
        console.log(newVal)
      },
       valueの変化が終わるのを待つ時間をミリ秒で指定
      500)
  }
})

複数の値を監視する

Vue.js
//インスタンスメソッドを使い監視対象を関数にして登録
 this.$watch( function() {
   return [this.width, this.height]
 }, function() {
   widthまたはheightが変化した時の処理
 })
//オプションに登録する場合は、算出プロパティを監視する
 cpmputed: {
   watchTarget: function() {
     return [this.width, this.height]
   }
 },
 watch: {
   watchTarget: function() { ... }
 }

オブジェクト型の古い値との比較方法 p132

フォームを監視してAPIからデータを取得(検索フォームに応用できる)
※axios.min.jsを読み込む

html
 <div id="app">
   <select v-model="current">
     <option v-for="topic in topics" v-bind:value="topic.value">
       {{ topic.name }}
    </option>
  </select>
  <div v-for="item in list">{{ item.full_name }}</div>
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    list: [],
    current: '',
    topics: [
      { value: 'vue', name: 'Vue.js' },
      { value: 'jQuery', name: 'jQuery' }
    ]
  },
  watch: {
    current: function (val) {
       GitHubAPIからトピックのリポジトリを検索
      axios.get('https:api.github.com/search/repositories', {
        params: {
          q: 'topic:' + val
        }
      }).then(function (response) {
        this.list = response.data.items
      }.bind(this))
    }
  },
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js - 4】データの監視と加工

まとめ

・算出プロパティは結果をキャッシュしてくれる
・データの状態に対して処理をしたいならウォッチャ
・カスタムディレクティブは監視をしながらDOMの操作ができる
・更新後のDOMにアクセスしたいならnextTick

1. 算出プロパティで処理を含むデータを作成

算出プロパティ(Computed 処理を含むデータ)

html
<p>{{ width }} の半分は {{ halfWidth }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    //算出プロパティhalfWidthを定義
    halfWidth: function() {
      return this.width / 2
    }
  }
})

算出プロパティの組み合わせ

html
<p>X: {{ halfPoint.x }}</p>
<p>Y: {{ halfPoint.y }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800,
    height: 600
  },
  computed: {
    halfWidth: function() {
      return this.width / 2
    },
    halfHeight: function() {
      return this.height / 2
    },
    // 「width × height」の中心座標をオブジェクトで返す
    halfPoint: function() {
      return {
        x: this.halfWidth,
        y: this.halfHeight
      }
    }
  }
})

ゲッターとセッター

html
<input v-model.number="width"> {{ width }}
<input v-model.number="halfWidth"> {{ halfWidth }}
Vue.js
new Vue({
  el: '#app',
  data: {
    width: 800
  },
  computed: {
    halfWidth: {
      get: function() {
        return this.width / 2
      },
      //halfWidth の2倍の数値を width に代入する
      set: function(val) {
        this.width = val * 2
      }
    }
  }
})

算出プロパティのキャッシュ機能
算出プロパティ…リアクティブな依存データに基づき、結果をキャッシュする。キャッシュを再構築するトリガになるのはリアクティブなデータのみ
メソッド…キャッシュされない

html
<!-- 算出プロパティ:キャッシュされるため、何度使用しても同じ結果 -->
<ol>
  <li>{{ computedData }}</li><!-- 0.8205563271901375 -->
  <li>{{ computedData }}</li><!-- 0.8205563271901375 -->
</ol>

<!-- メソッド:キャッシュされない。呼び出すたび変わる -->
<ol>
  <li>{{ methodsData() }}</li>0.13761479619266637
  <li>{{ methodsData() }}</li>0.45429414765683895
</ol>
Vue.js
new Vue({
  el: '#app',
  computed: {
    computedData: function() { return Math.random() }
  },
  methods: {
    methodsData: function() { return Math.random() }
  }
})

サンプルコード:リストの絞り込み・ソート機能

html
<div id="app">
  <input v-model.number="budget"> 円以下に絞り込む
  <input v-model.number="limit"> 件を表示
  <button v-on:click="order=!order">切り替え</button>
  <p>{{ matched.length }} 件中 {{ limited.length }} 件を表示中</p>
  <ul>
    <!-- v-forでは最終結果、算出プロパティのlimitedを使用する -->
    <li v-for="item in limited" v-bind:key="item.id">
      {{ item.name }} {{ item.price }}円
    </li>
  </ul>
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    //フォームの入力と紐付けるデータ
    budget: 300,
    //表示件数
    limit: 2,
    //もとになるリスト
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
      { id: 3, name: 'いちご', price: 400 },
      { id: 4, name: 'おれんじ', price: 300 },
      { id: 5, name: 'めろん', price: 500 }
    ]
  },
  computed: {
    //budget以下のリストを返す算出プロパティ
    matched: function () {
      return this.list.filter(function (el) {
        return el.price <= this.budget
      }, this)
    },
    //matchedで返ったデータをlimit件返す算出プロパティ
    limited: function () {
      return this.matched.slice(0, this.limit)
    }
  }
})

2. ウォッチャでデータを監視して処理を自動化する

ウォッチャ

Vue.js
new Vue({
   ...
  watch: {
    監視するデータ: function (新しい値, 古い値) {
       valueが変化したときに行いたい処理
    },
    'item.value': function (newVal, oldVal) {
       //オブジェクトのプロパティも監視できる
    }
  }
})

ウォッチャ(オプションあり)

Vue.js
new Vue({
   ...
  watch: {
    list: {
      handler: function (newVal, oldVal) {
         //listが変化したときに行いたい処理
      },
      deep: true,//ネストされたオブジェクトも監視するか
      immediate: true //初期読み込み時にも呼び出すか
    }
  }
})

インスタンスメソッドでの登録(オプションなし)

Vue.js
this.$watch(監視するデータ, ハンドラ, オプション(任意))
this.$watch('value', function(newVal, oldVal) {
   //...
})

インスタンスメソッドでの登録(オプションあり)

Vue.js
this.$watch('value', function (newVal, oldVal) {
   ...
}, {
  immediate: true,
  deep: true
})

一度だけ動作するウォッチャ(unwatchで解除する)

Vue.js
new Vue({
  el: '#app',
  data: {
    edited: false,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
    ]
  },
  created: function() {
    var unwatch = this.$watch('list', function () {
      //listが編集されたことを記録する
      this.edited = true
      //監視を解除
      unwatch()
    }, {
      deep: true
    })
  }
})

実行頻度の制御p130
フォームの入力などによって監視する値が高頻度で変化する→ウオッチャの実行頻度を制御する

html
<!-- ※lodash.min.jsを読み込む -->
<input type="text" v-model="value">
Vue.js
new Vue({
  el: '#app',
  data: {
    value: '編集してみてね'
  },
  watch: {
    //実行から指定ミリ秒がすぎた場合にコールバックを呼び出す
    value: _.debounce(function (newVal) {
       //ここへコストの高い処理を書く
      },
      //valueの変化が終わるのを待つ時間をミリ秒で指定
      500)
  }
})

複数の値を監視する

Vue.js
//インスタンスメソッドを使い監視対象を関数にして登録
 this.$watch( function() {
   return [this.width, this.height]
 }, function() {
    //widthまたはheightが変化した時の処理
 })
//オプションに登録する場合は、算出プロパティを監視する
 cpmputed: {
   watchTarget: function() {
     return [this.width, this.height]
   }
 },
 watch: {
   watchTarget: function() { ... }
 }

オブジェクト型の古い値との比較方法 p132

フォームを監視してAPIからデータを取得(検索フォームに応用できる)

html
<!-- ※axios.min.jsを読み込む -->
<div id="app">
  <select v-model="current">
    <option v-for="topic in topics" v-bind:value="topic.value">
       {{ topic.name }}
    </option>
  </select>
  <div v-for="item in list">{{ item.full_name }}</div>
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    list: [],
    current: '',
    topics: [
      { value: 'vue', name: 'Vue.js' },
      { value: 'jQuery', name: 'jQuery' }
    ]
  },
  watch: {
    current: function (val) {
      //GitHubのAPIからトピックのリポジトリを検索
      axios.get('https:api.github.com/search/repositories', {
        params: {
          q: 'topic:' + val
        }
      }).then(function (response) {
        this.list = response.data.items
      }.bind(this))
    }
  },
})

3. フィルタの使い方

html
<!-- Mustacheで使用する場合 -->
<div>{{ 対象のデータ | フィルタの名前 }}</div>
<!-- v-bindで使用する場合 -->
<div v-bind:id=" 対象のデータ | フィルタの名前 "></div>

ローカルでフィルタを使う

html
<p>{{ price | localNum }}円</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    price: 19800
  },
  filters: {
    localeNum: function (val) {
      return val.toLocaleString()
    }
  }
})
//グローバルでフィルタを使う(全てのコンポーネントから使用できる)
Vue.filter('localNum', function(val) {
  return val.toLocalString()
})

フィルタに引数を持たせる

html
<p>{{ message | filter(foo, 100) }}</p>
Vue.js
new Vue({
  filters: function(message, foo, num) {
    console.log(message, foo, num)
  }
})

複数のフィルタを繋げて使用

html
<p>180 度は {{ 180 | radian | round }} ラジアンだよ</p>
Vue.js
new Vue({
  el: '#app',
  filters: {
    //小数点以下を第2位に丸めるフィルタ
    round: function (val) {
      return Math.round(val * 100) / 100
    },
    //度からラジアンに変換するフィルタ
    radian: function (val) {
      return val * Math.PI / 180
    }
  }
})

4. カスタムディレクティブ

コンポーネントのdirectivesオプションに登録することで、特定のコンポーネント内で使用できる

html
<input type="text" v-focus>
Vue.js
new Vue({
  el: '#app',
  directives: {
    focus: {
      //紐付いている要素がDOMに挿入されるとき
      inserted: function (el) {
        el.focus()  //要素にフォーカスを当てる
      }
    }
  }
})
// グローバルへの登録
Vue.directive('focus', {
  inserted: function(el){
    el.focus()
  }
})

使用可能なフック

Vue.js
Vue.directive('example', {
  //ディレクティブが初めて要素と紐づいた時
  bind: function (el, binding) {
    console.log('v-example bind')
  },
  //紐づいた要素が親Nodeに挿入された時
  inserted: function (el, binding) {
    console.log('v-example inserted')
  },
  //紐付いた要素を包含しているコンポーネントのVNodeが更新された時
  update: function (el, binding) {
    console.log('v-example update')
  },
  //包含しているコンポーネントと子コンポーネントのVNodeが更新された時
  componentUpdated: function (el, binding) {
    console.log('v-example componentUpdated')
  },
  //紐付いていた要素からディレクティブから削除される時
  unbind: function (el, binding) {
    console.log('v-example unbind')
  }
})

//updateおよびcomponentUpdate:
//コンポーネントの仮想DOMが更新された時に呼び出される

フックの引数

引数 作用
el ディレクティブが付与されている要素
binding バインドされた値、引数、修飾子のオブジェクト
vnode 要素に対応するVNode
oldVnode 更新前のVNode(updateおよびcomponentUpdatedのみ使用可)

フックの関数による省略記法
第2引数として関数を渡す:bindとupdateにフックされる

Vue.js
Vue.directive('example', function(el, binding, vnode, oldVnode) {
  //bindとupdateで呼び出される処理
})

例)動画の再生を操作する

html
 <div id="app">
   <button v-on:click="video1=true">再生</button>
   <button v-on:click="video1=false">再生</button>
   <video src="movie1.mp4 v-video="video1">再生</video>
   <button v-on:click="video2=true">再生</button>
   <button v-on:click="video2=false">再生</button>
   <video src="movie1.mp4 v-video="video2">再生</video>
 </div>
Vue.js
new Vue({
  el: '#app',
  data: {
    video1: false,
    video2: false
  },
  directives: {
    video(el, binding) {
      if (binding.value !== binding.oldValue) {前の処理と比較して処理を行う
        binding.value ? el.play() : el:pause()
      }
    }
  }
})

第2引数bindingは次のプロパティを含むオブジェクトになる

1 2
arg 引数
modifiers 修飾子のオブジェクト
value 新しい値
oldValue 古い値(updateおよびcomponentUpdatedのみ使用可)

5. nextTickで更新後のDOMにアクセスする

html
<button v-on:click="list.push(list.length+1)">追加</button>
<ul ref="list">
  <li v-for="item in list">{{ item }}</li>
</ul>
Vue.js
new Vue({
  el: '#app',
  data: {
    list: []
  },
  watch: {
    list: function () {
      //更新後のul要素の高さを取得できない…
      console.log('通常:', this.$refs.list.offsetHeight)
      //nextTickを使えばできる!
      this.$nextTick(function () {
        console.log('nextTick:', this.$refs.list.offsetHeight)
      })
    }
  }
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

blurイベントで特定のボタンだけでイベントをキャンセルするのに、event.relatedTargetが使えなかったとき。

概要

  • 次はWeb屋さんに転職するので、勉強も兼ねて Vue.js + Vuetify でUIを作っていた矢先。
  • せっかくだからレスポンシブにして、実機で確認していたら、その時は訪れました。
  • クリック(タップ)すると編集モード(テキストフィールド)になるようなUIで、
  • 編集モードだけ表示されるボタンを置いたら、
  • PC版Chromeのエミュレーターではタイトルのイベントが飛ぶのに、実機では飛びませんでした。
  • 自分へのメモも兼ねています。

環境

プラットフォームの関係からNode.jsを使わずにJavascriptだけで書いています。jqueryも使える環境なので結構使ってます。

最初の実装

今回作っていたのは、名前と予定が一覧表示され、クリックするだけで自由に編集できる、会社によくあるホワイトボードのようなものです。以下、usersにユーザオブジェクトがリストされていてのループでuserを表示するくだりです。

code1.js
  :
<template v-for="(user) in users">
<v-layout wrap class="user">
<template v-if="user.editTask">
<v-flex>
<v-text-field v-model="user.task" @keyup.enter="updateTask(user)" @blur="updateTask(user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
</v-flex>
</template>
<template v-else>
<v-flex class="user-name" @click="editTask(user)">{{user.name}}</v-flex>
<v-flex class="user-task" @click="editTask(user)">{{user.task}}</v-flex>
</template>
  :
,
methods: {
  editTask: function(user) {
    user.editTask = true;
    this.$nextTick(function() {
      var ref = this.$refs['edit' + user.id][0];
      ref.focus();
    });
  },
  updateTask: function(user) {
    user.editTask = false;
    // ここでDBを更新する処理など...
  },
  :

ユーザの名前と予定が一覧表示されていて、クリックすると editTaskメソッドが呼ばれ、予定を入力するためのテキストフィールドが現れます。尚、$nextTickのくだりは、出現したテキストフィールドにフォーカスを当てるためのものです。Enterを押すか、フォーカスが外れたらupdateTaskメソッドが呼ばれ、名前と予定の表示に戻ります。ここまでは大きな問題もなく、PCでもモバイルでも想定通り動作しました。例にはちゃんと書いてませんが、レイアウトはVuetifyのGrid systemを活用しました。元々Bootstrapを使っていたので、特に迷うことなく移行できました。

使い勝手を改善する

その後、予定を文章で入力するのがめんどくさい、という人のために、テキストフィールドの横に一発で文章を入力するためのボタンを置こうと思いつき、次のようなテンプレートに変更しました。

code2.js
  :
<template v-if="user.editTask">
<v-flex>
<v-text-field v-model="user.task" @keyup.enter="updateTask(user)" @blur="updateTask(user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
</v-flex>
<v-flex>
<v-btn small round class="quick-task" color="primary" text-color="white" @click="addTask(user,'打合せ')">打合せ</v-btn>
</v-flex>
</template>
  :
,
methods: {
  :
  addTask: function(user,text) {
    user.task = user.task + text;
  },

UI自体は簡単にできたのですが、ボタンのクリックよりもフォーカスが外れるイベントが先に飛ぶのでしょうね、ボタンをクリックしようとしてもボタンは消え、実際にはクリックされずにaddTaskメソッドが呼ばれません。なんとなく押したような感じにはなるのが憎いです。で、何か手立てはないか探してみたところ、event.relatedTargetに関連する要素が入るようで、ボタンをクリックしたときはここにボタンの要素が入ってました。そこで、以下のように修正しました。

code3.js
  :
<v-text-field v-model="user.task" @keyup.enter="updateTask($event,user)" @blur="updateTask($event,user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
  :
,
methods: {
  :
  updateTask: function(event, user) {
    if (event && event.relatedTarget && $(event.relatedTarget).hasClass('quick-task')) {
      event.stopPropagation();
      return;
    }
    user.editTask = false;
    // ここでDBを更新する処理など...
  },
  addTask: function(user,text) {
    user.task = user.task + text;
    this.updateTask(null, user);
  },
  :

先にupdateTaskメソッドが呼ばれ、event.relatedTargetがあれば、クラスを使って該当のボタンであればイベントをキャンセルするようにします。そうすると消えずにボタンがクリックできるので、addTaskメソッドが呼ばれます。指定した文章を追加した後に、updateTaskメソッドをevent引数をnullで呼び、イベントチェックの部分をパスさせます。そうして、名前と予定の表示に戻す処理を行うことで、最初のUIと同じ動きになりました。Chromeのdevice toolbarでiPhoneのエミュレートなどに切り替えても問題無く動作しました。しかし、そう甘くは無かったのです!

実機だと動かない

実現しようとしているプラットフォームは、Viewやスクリプトでカスタマイズできるようになっていて、モバイル端末用のアプリやMobile Safariなんかでも動作します。既述のようにエミュレータでも動作したので、実機で確認してみました。が、ダメでしたorz... event.relatedTargetが null のままのようです。stack overflow には以下のような記事が。

Javascript: on blur getting the event's relatedTarget on mobile safari

記事にあったリンク先に、mousedownで予めフラグを立てといてblur時にごにょごにょする、というようなアイディアがあったので、touchstartでごにょごにょすることにしました。まず、以下のようにイベントハンドラを追加します。

code4.js
<v-btn small round class="quick-task" color="primary" text-color="white" @touchstart="touch" @click="addTask(user,'打合せ')">打合せ</v-btn>

touchメソッドではevent.targetをチェックしてフラグをtrueにします。が、ハンドラを記述した要素からしか呼ばれないでしょうから、要素のチェック処理は要らないかもしれません。updateTaskメソッドの方は、this.touch = true の場合もblurイベントを止めるようにします。

code5.js
  touch: function(event) {
    if (event && event.target && $(event.target).parent().hasClass('quick-task')) {
      this.touch = true;
    }
  },
  updateTask: function(event, user) {
    if (this.touch || event && event.relatedTarget && $(event.relatedTarget).hasClass('quick-task')) {
      this.touch = false;
      event.stopPropagation();
      return;
    }
    this.touch = false;
    user.editTask = false;
    // ここでDBを更新する処理など...
  },

最後に

この実装で、実機はもちろんですが、Chromeのエミュレータ環境でも動作するようになりました。ちょっとworkaroundな気もしますが、アイディアの一つとしてシェアしておきます。Vue.jsやVuetifyは本質的には関係無いと思いますが、あくまで題材ということでご容赦ください。また、潜在的な課題として、PC環境でTABでフォーカスを移すとちょうど該当のボタンに当たってしまい、フォーカス処理が働かない(クリックも飛ばない)というのがありますが、そちらはまたなんとかしようと思います。なにぶんフロント初心者ですので、それはやっちゃダメ、こういうやり方もあるよ、というご意見が助けになりますので、ぜひお待ちしております。

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

Vue.jsとThree.jsでwebカメラアプリ作った

経緯

  • バイト先ではjqueryでバリバリ書いているので、もっとモダンな開発をしたくなった。
  • どうせならおしゃれにwebGLしたい → Three.js

学習に使ったもの

結果

  • こんなwebアプリが出来ました。
  • 以下、アプリの説明です。

ぼやっとした内部構造

プレゼンテーション1.jpg

役に立つかわからない技術tips

Vueの中でThree.jsを書くには...

  1. Three.jsのコアパッケージをnpmとかyarnとかでwork directryにインストール。
  2. Three.jsのサブパッケージはインストールできるものもあるが、現状githubから必要なものだけ持ってきてファイル内最上部でコアパッケージをimportするのがいいかも。(フルインストール出来たら楽なのに...)
  3. 使う変数はdata()内でconst宣言しておくのがよい。
  4. 映像を扱う関係上、バグるとPCがフリーズ&アツアツになり死ぬので、出来るだけ最小のモジュール単位でデバッグする(当たり前)。
  5. シェーダ書き換える必要がある場合もあるので、vedaでも使って動くコードに直すといいんじゃないかな。

まとめ

  • Vueはいいぞ(あまり恩恵を受けていない気はするが)
  • Three.jsもいいぞ(むずいけど)
  • 雑魚スマホで見ると固まったりするので、Nuxt.js使ってSSRで書き直すのもいいかも。
  • UI/UXもっと勉強します。(誰のためのデザイン?読みました。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】3. イベントとフォーム入力の受け取り

1. イベントハンドラ

html
<!-- メソッドイベントハンドラ -->
<button v-on:click="handleClick">クリック</button>
<!-- 省略記述 -->
<button @click="handleClick">クリック</button>

<!-- インラインメソッドハンドラ -->
<button v-on:click="count++"></button>

<!-- 引数(元のイベントオブジェクトとスコープ内のitem) -->
<button v-on:click="handleClick($event, item)">クリック</button>

フォーム入力の取得
(v-modelを使用するとシンプルになるが、v-onを使用することで入力内容を確認してからデータに代入するといったフック処理ができる)

html
<input v-bind:value="message" v-on:change="handleInput">
Vue.js
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js',
  },
  methods: {
    handleInput: function (event) {
      //代入前に何か処理を行う…
      this.message = event.target.value
    }
  }
})

イベント修飾子

修飾子 作用
.stop event.stopPropagation()を呼ぶ
.prevebt event.preventDefault()を呼ぶ
.capture キャプチャモードでDOMイベントをハンドルする
.self イベントがこの要素から発火した時のみハンドラを呼び出す
.native コンポーネントのルート要素上のネイティブイベントをハンドルする
.once 最大1回ハンドラを呼び出す
.passive {passive:ture}でDOMイベントをハンドルする

クリックイベントの修飾子

修飾子 作用
.left マウスの左ボタンが押された時のみハンドラを呼び出す
.right マウスの右ボタン…
.middle マウスの中央ボタン…

キー修飾子

html
<input v-on:keydown.13="handler">
<!-- よく使用されるキーコードはエイリアスとして登録されている -->
<input v-on:keydown.enter="handler">

使用可能なキーコードのエイリアス

修飾子 作用
.enter エンターキーが押された時
.tab Tabキーが押された時
.delete Deleteキーが押された時
.esc ESCキーが押された時
.space スペースキーが押された時
.up 矢印の上キーが押された時
.down 矢印の下キーが押された時
.left 矢印の左キーが押された時
.right 矢印の右キーが押された時

複数つけた場合、ORを意味する

html
<input v-on:keydown.up.down.left.right="handler">

システム修飾子

修飾子 作用
.ctrl Ctrlキーが押されている場合
.alt Altキーが押されている場合
.shift Shiftキーが押されている場合
.meta Metaキーが押されている場合
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js - 3】イベントとフォーム入力の受け取り

1. イベントハンドラ

html
<!-- メソッドイベントハンドラ -->
<button v-on:click="handleClick">クリック</button>
<!-- 省略記述 -->
<button @click="handleClick">クリック</button>

<!-- インラインメソッドハンドラ -->
<button v-on:click="count++"></button>

<!-- 引数(元のイベントオブジェクトとスコープ内のitem) -->
<button v-on:click="handleClick($event, item)">クリック</button>

フォーム入力の取得
(v-modelを使用するとシンプルになるが、v-onを使用することで入力内容を確認してからデータに代入するといったフック処理ができる)

html
<input v-bind:value="message" v-on:change="handleInput">
Vue.js
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js',
  },
  methods: {
    handleInput: function (event) {
      //代入前に何か処理を行う…
      this.message = event.target.value
    }
  }
})

イベント修飾子

修飾子 作用
.stop event.stopPropagation()を呼ぶ
.prevebt event.preventDefault()を呼ぶ
.capture キャプチャモードでDOMイベントをハンドルする
.self イベントがこの要素から発火した時のみハンドラを呼び出す
.native コンポーネントのルート要素上のネイティブイベントをハンドルする
.once 最大1回ハンドラを呼び出す
.passive {passive:ture}でDOMイベントをハンドルする

クリックイベントの修飾子

修飾子 作用
.left マウスの左ボタンが押された時のみハンドラを呼び出す
.right マウスの右ボタン…
.middle マウスの中央ボタン…

キー修飾子

html
<input v-on:keydown.13="handler">
<!-- よく使用されるキーコードはエイリアスとして登録されている -->
<input v-on:keydown.enter="handler">

使用可能なキーコードのエイリアス

修飾子 作用
.enter エンターキーが押された時
.tab Tabキーが押された時
.delete Deleteキーが押された時
.esc ESCキーが押された時
.space スペースキーが押された時
.up 矢印の上キーが押された時
.down 矢印の下キーが押された時
.left 矢印の左キーが押された時
.right 矢印の右キーが押された時

複数つけた場合、ORを意味する

html
<input v-on:keydown.up.down.left.right="handler">

システム修飾子

修飾子 作用
.ctrl Ctrlキーが押されている場合
.alt Altキーが押されている場合
.shift Shiftキーが押されている場合
.meta Metaキーが押されている場合

2. フォーム入力バインディング(v-model)

双方向データバインディング(フォームの入力値および選択値をデータと同期させる)

インプット

html
<input v-model="message">
<p>{{ message }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    message: 'Hello!'
  }
})

テキストエリア

html
<textarea v-model="message"></textarea>
<pre>{{ message }}</pre>
Vue.js
new Vue({
  el: '#app',
  data: {
    message: 'Hello!'
  }
})

チェックボックス(単一要素)

html
<label>
  <input type="checkbox" v-model="val"> {{ val }}
</label>
Vue.js
new Vue({
  el: '#app',
  data: {
    val: true//値の型はBoolean
  }
})

チェックボックス(複数要素)

html
<label><input type="checkbox" v-model="val" value="A"> A</label>
<label><input type="checkbox" v-model="val" value="B"> B</label>
<label><input type="checkbox" v-model="val" value="C"> C</label>
<p>{{ val }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    val: []//値の型は配列 チェックされている値が格納される
  }
})

ラジオボタン

html
<label><input type="radio" value="a" v-model="val"> A</label>
<label><input type="radio" value="b" v-model="val"> B</label>
<label><input type="radio" value="c" v-model="val"> C</label>
<p>{{ val }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    val: ''//値の型は文字列
  }
})

セレクトボックス(単一要素)

html
<select v-model="val">
  <option disabled="disabled">選択してください</option>
  <option value="a">A</option>
  <option value="b">B</option>
  <option value="c">C</option>
</select>
<p>{{ val }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    val: ''//値の型は文字列
  }
})

セレクトボックス(複数要素)

html
<select v-model="val" multiple>
  <option value="a">A</option>
  <option value="b">B</option>
  <option value="c">C</option>
</select>
<p>{{ val }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    val: []//値の型は配列
  }
})

画像ファイル

html
<input type="file" v-on:change="handleChange">
<div v-if="preview"><img v-bind:src="preview"></div>
Vue.js
new Vue({
  el: '#app',
  data: {
    preview: ''
  },
  methods: {
    handleChange: function (event) {
      var file = event.target.files[0]
      if (file && file.type.match(/^image\/(png|jpeg)$/)) {
        this.preview = window.URL.createObjectURL(file)
      }
    }
  }
})

range

html
<input type="range" v-model.number="val"> {{ val }}
Vue.js
new Vue({
  el: '#app',
  data: {
    val: 50
  }
})

v-modelの修飾子

修飾子 作用
.lazy inputの代わりにchangeイベントをハンドルする
.number 値を数値に変換する
.trim 値の余分なスペースを削除する

3. マウント要素外(window,body)のイベントと操作

スクロールイベントの取得

Vue.js
new Vue({
  el: '#app',
  data: {
    scrollY: 0,
    timer: null
  },
  created: function () {
    //ハンドラを登録
    window.addEventListener('scroll', this.handleScroll)
  },
  beforeDestroy: function () {
    //ハンドラを解除(コンポーネントやSPAの場合忘れずに!)
    window.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    //違和感のない程度に200ms間隔でscrollデータを更新する例
    handleScroll: function () {
      if (this.timer === null) {
        this.timer = setTimeout(function () {
          this.scrollY = window.scrollY
          clearTimeout(this.timer)
          this.timer = null
        }.bind(this), 200)
      }
    }
  }
})

ライブラリで実装

html
<script src="https:cdn.jsdelivr.net/npm/smooth-scroll@12.1.5"></script>
<div id="app">
  <div class="content">...</div>
  <div v-on:click="scrollTop">
    ページ上部へ移動
  </div>
</div>
Vue.js
var scroll = new SmoothScroll()
new Vue({
  el: '#app',
  methods: {
    scrollTop: function () {
      scroll.animateScroll(0)
    }
  }
})

Vue.js以外からのイベントの受け取り(dispatchEventを使用してイベントを検知)

html
<div id="app">
  <input id="message" v-on:input="handleInput">
  <button data-update="jQuery!">jQueryからの更新</button>
</div>
Vue.js
$(document).on('click', '[data-update]', function () {
  $('#message').val($(this).attr('data-update'))
  //入力値を更新したらイベントを発生させる
  $('#message')[0].dispatchEvent(new Event('input'))
})
new Vue({
  el: '#app',
  methods: {
    handleInput: function (event) {
      console.log(event.target.value)
    }
  }
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue-gtmを使う

Vue-CLI 3でGTMを入れる

プラグインがあった。
https://www.npmjs.com/package/vue-gtm

どこに書くのか

自分の場合はvue-routerを使うのでそこに書いておきました。

router.js
import VueGtm from 'vue-gtm'

開発用サーバーのアクセスが混ざらないように、分けておきます。
GTMのタグは開発用と、本番用の2つ用意した方が良いです。

router.js
if(location.hostname != 'www.example.com'){
  Vue.use(VueGtm, {
    id: 'GTM-▲▲▲▲',//開発用
    enabled: true, 
    debug: true,
    // vueRouter: router,
    ignoredViews: ['']
  });
}else{
  Vue.use(VueGtm, {
    id: 'GTM-●●●●',//本番環境
    enabled: true,
    debug: true,
    // vueRouter: router,
    ignoredViews: ['']
  });
}

「vueRouter: router」が(私の場合)うまくうごかないように思ったので
コメントアウトしてます。GTM側のトリガーで対応しました。

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

【Vue.js - 2】データの登録と更新

1. リアクティブデータの定義

リアクティブデータ…反応のできるデータ

Vue.js
//オプション外でデータを定義
var state = { count: 0 }
//Vueインスタンス
var app = new Vue (
  el : '#app',
  data : {
    message: 'Vue.js!',
    state: state,
    //オプション外で定義されたデータでもリアクティブデータに変換
    //内容が決まってない場合のでも、初期値や空データにしておく
    //(なるべく、後から代入されるデータと同じ型で定義する)
    visitCount: 0,
    hideCompletedTodos: false,
    todos: [],
    error: null
  }
);
state.count++

2. テキストのデータバインディング

Mustache(マスタッシュ)で定義

html
<!-- テキストを表示 -->
<p>{{ text }}</p>

<!-- オブジェクトのプロパティを表示 -->
<p>{{ message.value }}</p>

<!-- 文字列の長さを表示 -->
<p>{{ message.value.length }}</p>

<!-- 配列を表示 -->
<p>{{ list[2] }}</p>

<!-- プロパティを組み合わせて使用 -->
<p>{{ list[num] }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    //テキストデータ
    text: 'Vue.js!',
    //オブジェクトデータ
    message: {
      value: 'Hello Vue.js!'
    },
    //配列データ
    list: ['りんご', 'ばなな', 'いちご'],
    //別のデータを使用してlistから取り出す要素を動的に 4 で使用
    num: 1
  }
})

MustacheにはJSの式は使えるが、文はエラーに(イコールの右側に記述できるのが式)

html
<!-- ok -->
<p>{{ 1 + 1 }}</p>
<!-- error -->
<p>{{ var foo = message }}</p>

3. 属性のデータバインディング

v-bindディレクティブ

html
<!-- error -->
<input type="text" value="{{ message }}">
<!-- b-vindで記述 -->
<input type="text" v-bind:value="message">
<!-- 省略記法 -->
<input type="text" :value="message">

v-bindの修飾子

修飾子 説明
.prop     属性の代わりにDOMプロパティとしてバインド
.camel  ケバブケースの属性名をキャメルケースに変換する
.sync 双方向パインディングをお香なう
html
<div v-bind:text-content.prop="message"></div>
<div v-bind:scroll-top.prop="scroll"></div>
js
new Vue({
  el: '#app',
  data: {
    message: 'Vue.js!',
    scroll: 0
  },
  mounted: function() {
    this.scroll = 100//要素のスクロール量を操作
  }
});

4. データの更新

html
<div id="app">
   <p>{{ count }}回クリックしたよ! </p>
   <button v-on:click="increment">カウントを増やす</button>
 </div>
Vue.js
new Vue({
  el: '#app',
  data: {
    count: 0
  },
  methods: {
    increment: function () {
      this.count += 1 //処理は再代入するだけでOK!
    }
  }
})

5. クラスとスタイルのデータバインディング

html
<button v-on:click="isActive=!isActive">
    isActiveを切り替える
</button>
<p v-bind:class="{ child: isChild, 'is-active': isActive }" class="item">
    childとis-activeというclassが付与される動的なクラス
</p>
<p v-bind:style="{ color: textColor, backgroundColor: bgColor }" class="item">
    動的なスタイル
</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    isChild: true,
    isActive: true,
    textColor: 'red',
    bgColor: 'lightgray'
  }
})

クラスの条件の三項演算子を使う

html
<p v-bind:class="[isActive ? 'active' : 'normal', otherClass]">Text</p>
<!-- 複数のクラスやスタイルは、dataオプションにオブジェクトを定義してから渡すと見通しが良くなる -->
<p v-bind:class="classObject">Text</p>
<p v-bind:style="classObject">Text</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    classObject: {
      child: true,
      'is-active': false
    },
    styleObject: {
      color: 'red',
      backgroundColor: 'lightgray'
    }
  }
})

6. 複数の属性の一括データバインディング

Vue.js
new Vue({
  el: '#app',
  data: {
    item: {//複数の属性をオブジェクトにまとめる
      id: 1,
      src: 'item.jpg',
      alt: '商品サムネイル',
      width: 200,
      height: 200,
    }
  }
})
html
<!-- これだと冗長 -->
<img v-bind:src="item.src"
     v-bind:alt="item.alt"
     v-bind:width="item.width"
     v-bind:height="item.height">
<!-- こうする(引数部分を省略してオブジェクトを渡す) -->
<img v-bind="item">
<!-- 引数を持ったv-bindを併用すれば、特的の要素のみ変更を加えられる -->
<img v-bind="item" v-bind:id="'thumb-'+ item.id">

7. SVGのデータバインディング

html
<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>
Vue.js
new Vue({
  el: '#app',
  data: {
    radius: 50
}

8. 条件分岐

html
<div v-if="ok">v-if条件による描写</div>
<div v-show="ok">v-show条件による表示</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    ok: false
  }
})

条件を満たさなかった場合:
v-if
要素がDOMレベルで削除・全ての監視は解除
コンポーネントならインスタンスは破棄
次に描写される時には状態は初期化
v-show
単純にdisplay:none;を付与

使い分け:
v-if
内側に directiveやコンポーネントを多様している
特定のデータを持ってないとエラーが起きる
v-show
内側にディレクティブやコンポーネントが少ない、切り替えの頻度が高い

グループ化(templateタグ)

html
<template v-if="ok">
 <header>タイトル</header>
 <div>コンテンツ</div>
</template>

複数条件(v-else-if,v-else)はそれぞれにユニークなkeyを設定して、予期せぬ動作を回避

html
<div v-if="type === 'A'" key="ユニークなkey名"></div>
<div v-else-if="type === 'B'"  key="ユニークなkey名"></div>
<div v-else key="ユニークなkey名"></div>

9. リストデータの表示と更新

リストの表示

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

キーの役割(ユニークなIDであるidプロパティをキーとしてデータバインディングする)

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

繰り返し+条件分岐

html
<div id="app">
  <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>
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'スライム', hp: 100 },
      { id: 2, name: 'ゴブリン', hp: 200 },
      { id: 3, name: 'ドラゴン', hp: 500 }
    ]
  }
})

リストの更新

html
<div id="app">
 <!-- このフォームの入力値を新しいモンスターの名前に使う -->
 名前
 <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>
</div>
Vue.js
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
      })
    }
  }
})

リストを削除

html
<div id="app">
 <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>
</div>
Vue.js
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)
    }
  }
})

リストの置き換え

Vue.js
//this.$set(更新するデータ, インデックスorキー, 新しい値)
this.$set(this.list, 0, { id: 1, name: 'キングスライム', hp: 500})

プロパティの値を追加

html
<div id="app">
  <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>
</div>
Vue.js
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を減らす
    }
  }
})

リスト自体を置き換える(filter)

Vue.js
this.list = this.list.filter( function(el) {
  return el.hp >= 100
})

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

html
<span v-for="item in 15">{{ item }}</span>
<span v-for="item in [1, 5, 10, 15]">{{ item }}</span>
<!-- 文字列に対するv-forは、1文字ずつ配列になる -->
<span v-for="item in text">{{ item }}</span>

外部からデータを取得

list.json
[
  { "id": 1, "name": "スライム", "hp": 100 },
  { "id": 2, "name": "ゴブリン", "hp": 200 },
  { "id": 3, "name": "ドラゴン", "hp": 500 }
]
html
<div id="app">
  <ul>
    <li v-for="(item, index) in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
</div>
Vue.js
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)
    })
  }
})

10. $elや$refsは一時的な変更!

ルートやコンポーネント要素を囲ってるルート要素は、
インスタンスプロパティの\$elを使って参照

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

ルート以外の要素は$refsを使って参照

html
<div id="app">
  <button v-on:click="handleClick">カウントアップ</button>
  <button v-on:click="show=!show">表示/非表示</button>
  <span ref="count" v-if="show">0</span><!-- 対象となる要素にref属性で名前をつけておく -->
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    show: true
  },
  methods: {
    handleClick() {
      var count = this.$refs.count
      if (count) {
        count.innerText = parseInt(count.innerText, 10) + 1
      }
    }
  }
})

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

v-pre(テンプレートのコンパイルをスキップする)
サーバーサイドレンダリング時のXSS対策に用いる、操作をする予定のない長いHTML文章を記述している部分に使用する

html
 <!-- Hello {{message}}がそのまま表示する -->
 <a v-bind:href="url" v-pre>Hello {{ message }}</a> 

v-once(一度だけバインディングを行う)

html
 <!-- 1回だけmessageの内容を表示して以降、リアクティブを解除 -->
 <a v-bind:href="url" v-once>Hello {{ message }}</a>

v-text(Mustacheの代わりにテキストコンテンツを描写)

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

v-html(HTMLタグをそのまま描画する)
APIから取得したHTML文章を描写する時に使用(信頼できるコンテンツのみ使用)

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

v-cloak(インスタンスの準備が終わると取り除かれる)
インスタンス作成までの間、Mustacheなどのコンパイル前のテンプレートが画面に表示されてしまうことを防ぐ

html
<div id="app" v-cloak>
  {{ message }}
</div>
css
[v-cloak] { display: none; }

v-cloak2
画面の読み込み時には#app要素を隠し、
インスタンスが作成されるとv-cloak属性が外れ、フェードインしながら表示させる

css
 @keyframes cloak-in {
   0% {
     opacity: 0;
   }
 }
 #app {
   animation: cloak-in 1s;
 }
 #app[v-cloak] {
   opacity: 0;
 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】2. データの登録と更新

1. リアクティブデータの定義

リアクティブデータ…反応のできるデータ

Vue.js
//オプション外でデータを定義
var state = { count: 0 }
//Vueインスタンス
var app = new Vue (
  el : '#app',
  data : {
    message: 'Vue.js!',
    state: state,
    //オプション外で定義されたデータでもリアクティブデータに変換
    //内容が決まってない場合のでも、初期値や空データにしておく
    //(なるべく、後から代入されるデータと同じ型で定義する)
    visitCount: 0,
    hideCompletedTodos: false,
    todos: [],
    error: null
  }
);
state.count++

2. テキストのデータバインディング

Mustache(マスタッシュ)で定義

html
<!-- テキストを表示 -->
<p>{{ text }}</p>

<!-- オブジェクトのプロパティを表示 -->
<p>{{ message.value }}</p>

<!-- 文字列の長さを表示 -->
<p>{{ message.value.length }}</p>

<!-- 配列を表示 -->
<p>{{ list[2] }}</p>

<!-- プロパティを組み合わせて使用 -->
<p>{{ list[num] }}</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    //テキストデータ
    text: 'Vue.js!',
    //オブジェクトデータ
    message: {
      value: 'Hello Vue.js!'
    },
    //配列データ
    list: ['りんご', 'ばなな', 'いちご'],
    //別のデータを使用してlistから取り出す要素を動的に 4 で使用
    num: 1
  }
})

MustacheにはJSの式は使えるが、文はエラーに(イコールの右側に記述できるのが式)

html
<!-- ok -->
<p>{{ 1 + 1 }}</p>
<!-- error -->
<p>{{ var foo = message }}</p>

3. 属性のデータバインディング

v-bindディレクティブ

html
<!-- error -->
<input type="text" value="{{ message }}">
<!-- b-vindで記述 -->
<input type="text" v-bind:value="message">
<!-- 省略記法 -->
<input type="text" :value="message">

v-bindの修飾子

修飾子 説明
.prop     属性の代わりにDOMプロパティとしてバインド
.camel  ケバブケースの属性名をキャメルケースに変換する
.sync 双方向パインディングをお香なう
html
<div v-bind:text-content.prop="message"></div>
<div v-bind:scroll-top.prop="scroll"></div>
js
new Vue({
  el: '#app',
  data: {
    message: 'Vue.js!',
    scroll: 0
  },
  mounted: function() {
    this.scroll = 100//要素のスクロール量を操作
  }
});

4. データの更新

html
<div id="app">
   <p>{{ count }}回クリックしたよ! </p>
   <button v-on:click="increment">カウントを増やす</button>
 </div>
Vue.js
new Vue({
  el: '#app',
  data: {
    count: 0
  },
  methods: {
    increment: function () {
      this.count += 1 //処理は再代入するだけでOK!
    }
  }
})

5. クラスとスタイルのデータバインディング

html
<button v-on:click="isActive=!isActive">
    isActiveを切り替える
</button>
<p v-bind:class="{ child: isChild, 'is-active': isActive }" class="item">
    childとis-activeというclassが付与される動的なクラス
</p>
<p v-bind:style="{ color: textColor, backgroundColor: bgColor }" class="item">
    動的なスタイル
</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    isChild: true,
    isActive: true,
    textColor: 'red',
    bgColor: 'lightgray'
  }
})

クラスの条件の三項演算子を使う

html
<p v-bind:class="[isActive ? 'active' : 'normal', otherClass]">Text</p>
<!-- 複数のクラスやスタイルは、dataオプションにオブジェクトを定義してから渡すと見通しが良くなる -->
<p v-bind:class="classObject">Text</p>
<p v-bind:style="classObject">Text</p>
Vue.js
new Vue({
  el: '#app',
  data: {
    classObject: {
      child: true,
      'is-active': false
    },
    styleObject: {
      color: 'red',
      backgroundColor: 'lightgray'
    }
  }
})

6. 複数の属性の一括データバインディング

Vue.js
new Vue({
  el: '#app',
  data: {
    item: {//複数の属性をオブジェクトにまとめる
      id: 1,
      src: 'item.jpg',
      alt: '商品サムネイル',
      width: 200,
      height: 200,
    }
  }
})
html
<!-- これだと冗長 -->
<img v-bind:src="item.src"
     v-bind:alt="item.alt"
     v-bind:width="item.width"
     v-bind:height="item.height">
<!-- こうする(引数部分を省略してオブジェクトを渡す) -->
<img v-bind="item">
<!-- 引数を持ったv-bindを併用すれば、特的の要素のみ変更を加えられる -->
<img v-bind="item" v-bind:id="'thumb-'+ item.id">

7. SVGのデータバインディング

html
<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>
Vue.js
new Vue({
  el: '#app',
  data: {
    radius: 50
}

8. 条件分岐

html
<div v-if="ok">v-if条件による描写</div>
<div v-show="ok">v-show条件による表示</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    ok: false
  }
})

条件を満たさなかった場合:
v-if
要素がDOMレベルで削除・全ての監視は解除
コンポーネントならインスタンスは破棄
次に描写される時には状態は初期化
v-show
単純にdisplay:none;を付与

使い分け:
v-if
内側に directiveやコンポーネントを多様している
特定のデータを持ってないとエラーが起きる
v-show
内側にディレクティブやコンポーネントが少ない、切り替えの頻度が高い

グループ化(templateタグ)

html
<template v-if="ok">
 <header>タイトル</header>
 <div>コンテンツ</div>
</template>

複数条件(v-else-if,v-else)はそれぞれにユニークなkeyを設定して、予期せぬ動作を回避

html
<div v-if="type === 'A'" key="ユニークなkey名"></div>
<div v-else-if="type === 'B'"  key="ユニークなkey名"></div>
<div v-else key="ユニークなkey名"></div>

9. リストデータの表示と更新

リストの表示

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

キーの役割(ユニークなIDであるidプロパティをキーとしてデータバインディングする)

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

繰り返し+条件分岐

html
<div id="app">
  <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>
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'スライム', hp: 100 },
      { id: 2, name: 'ゴブリン', hp: 200 },
      { id: 3, name: 'ドラゴン', hp: 500 }
    ]
  }
})

リストの更新

html
<div id="app">
 <!-- このフォームの入力値を新しいモンスターの名前に使う -->
 名前
 <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>
</div>
Vue.js
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
      })
    }
  }
})

リストを削除

html
<div id="app">
 <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>
</div>
Vue.js
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)
    }
  }
})

リストの置き換え

Vue.js
//this.$set(更新するデータ, インデックスorキー, 新しい値)
this.$set(this.list, 0, { id: 1, name: 'キングスライム', hp: 500})

プロパティの値を追加

html
<div id="app">
  <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>
</div>
Vue.js
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を減らす
    }
  }
})

リスト自体を置き換える(filter)

Vue.js
this.list = this.list.filter( function(el) {
  return el.hp >= 100
})

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

html
<span v-for="item in 15">{{ item }}</span>
<span v-for="item in [1, 5, 10, 15]">{{ item }}</span>
<!-- 文字列に対するv-forは、1文字ずつ配列になる -->
<span v-for="item in text">{{ item }}</span>

外部からデータを取得

list.json
[
  { "id": 1, "name": "スライム", "hp": 100 },
  { "id": 2, "name": "ゴブリン", "hp": 200 },
  { "id": 3, "name": "ドラゴン", "hp": 500 }
]
html
<div id="app">
  <ul>
    <li v-for="(item, index) in list" v-bind:key="item.id">
      ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }}
    </li>
  </ul>
</div>
Vue.js
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)
    })
  }
})

10. $elや$refsは一時的な変更!

ルートやコンポーネント要素を囲ってるルート要素は、
インスタンスプロパティの\$elを使って参照

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

ルート以外の要素は$refsを使って参照

html
<div id="app">
  <button v-on:click="handleClick">カウントアップ</button>
  <button v-on:click="show=!show">表示/非表示</button>
  <span ref="count" v-if="show">0</span><!-- 対象となる要素にref属性で名前をつけておく -->
</div>
Vue.js
new Vue({
  el: '#app',
  data: {
    show: true
  },
  methods: {
    handleClick() {
      var count = this.$refs.count
      if (count) {
        count.innerText = parseInt(count.innerText, 10) + 1
      }
    }
  }
})

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

v-pre(テンプレートのコンパイルをスキップする)
サーバーサイドレンダリング時のXSS対策に用いる、操作をする予定のない長いHTML文章を記述している部分に使用する

html
 <!-- Hello {{message}}がそのまま表示する -->
 <a v-bind:href="url" v-pre>Hello {{ message }}</a> 

v-once(一度だけバインディングを行う)

html
 <!-- 1回だけmessageの内容を表示して以降、リアクティブを解除 -->
 <a v-bind:href="url" v-once>Hello {{ message }}</a>

v-text(Mustacheの代わりにテキストコンテンツを描写)

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

v-html(HTMLタグをそのまま描画する)
APIから取得したHTML文章を描写する時に使用(信頼できるコンテンツのみ使用)

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

v-cloak(インスタンスの準備が終わると取り除かれる)
インスタンス作成までの間、Mustacheなどのコンパイル前のテンプレートが画面に表示されてしまうことを防ぐ

html
<div id="app" v-cloak>
  {{ message }}
</div>
css
[v-cloak] { display: none; }

v-cloak2
画面の読み込み時には#app要素を隠し、
インスタンスが作成されるとv-cloak属性が外れ、フェードインしながら表示させる

css
 @keyframes cloak-in {
   0% {
     opacity: 0;
   }
 }
 #app {
   animation: cloak-in 1s;
 }
 #app[v-cloak] {
   opacity: 0;
 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuexによる状態管理

Vue.jsでアプリケーションを作る際に、小規模だと特に問題にならないのですが、中規模以上のアプリケーションをvueで作る場合に以下の様な問題に直面する事があります。

1. 状態データの受け渡しが大変

コンポーネント間でのデータのやり取りは、emitとpropsを利用して行うのですが、階層が深くなるとデータの受け渡しが大変になります。

image.png

理想としては、以下の様に参照する状態データを別で分けて一箇所から参照できる様になれば煩雑さから解放されます。

image.png

2. 状態データの更新と流れが管理しにくい

以下の様な状態を表すデータを分けたカウンターアプリを作成します。

const store = {
    state : {
        count : 0
    }
}

new Vue({
    template: `
    <div>
        {{count}}
        <button v-on:click="increment">+</button>
    </div>
    `,
    data : store.state,
    methods : {
        increment () {
            store.state.count += 1 
        }
    },
})

上記の例では、カウントアップする処理をコンポーネント内に書いています。
これでも問題なく動作するのですが、他のコンポーネントで count を変更したい時に同じ処理を書くことになり煩雑です。
以下の様に状態を変更する処理を一箇所に集約しておけば、他のコンポーネントでも同じ処理を呼び出すだけで状態を変更する箇所を流れを管理する事ができます。

const store = {
    state : {
        count : 0
    },
    increment () {
        this.state.count += 1
    }
}


new Vue({
    template: `
    <div>
        {{count}}
        <button v-on:click="increment">+</button>
    </div>
    `,
    data : store.state,
    methods : {
        increment () {
            store.increment()
        }
    },
})

そこでVuex

Vuexとは

Vuex は Vue.js アプリケーションのための 状態管理パターン + ライブラリです。 これは予測可能な方法によってのみ状態の変異を行うというルールを保証し、アプリケーション内の全てのコンポーネントのための集中型のストアとして機能します。

https://vuex.vuejs.org/ja/

Vuexは、大きく4の機能に分けられます。

ステート

Stateはストアで管理されている状態そのものでコンポーネントで言う所のdataに近い存在。
アプリケーション全体で利用されるデータや通信中であるかどうかのフラグなどを保持しておく。

ゲッター

ステートから別の値を算出する時に利用してコンポーネントで言う所のcomputedに近い存在。
姓名を別で保持している時に、フルネームを生成する時などに利用できる。

ミューテーション

ステートの更新が可能な唯一のメソッドでコンポーネントで言う所のmethodsに近い存在。
必ず同期的な処理を記述する必要がある。

アクション

非同期処理を利用できるメソッド。データの加工やAPIとの通信を行なってデータの取得などを行う事ができるが、ステートの変更を行う際は必ずミューテーションを呼ぶ必要がある。

図に表すとこういうイメージ

image.png

先ほどのカウンターアプリをVuexで作成すると以下の様になります。

// storeの生成
const store = new Vuex.Store({
    state : {
        count : 0
    },
    mutations : {
        increment: state => state.count + 1
    }
})

// カウンター用のコンポーネント
const Counter = {
  template: `
    <div>
        {{count}}
        <button v-on:click="increment">+</button>
    </div>
    `,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

// ルートコンポーネント
new Vue({
    el: '#app',
    store,
    components : { Counter }
    template: `
    <div class="app">
        <counter></counter>
    </div>
    `,
    data : {
        count : store.state.count
    },
    methods : {
        increment () {
            store.commit('increment')
        }
    },
})

まとめ

Vuexを利用することで状態データの管理と更新処理の流れが統一する事ができる事がわかりました。シンプルなアプリケーションに置いては、むしろ煩雑な処理が増えるのですが、規模が大きくなる予定のあるアプリケーションであれば、Vuexの設計フローに基づいて作る事を早いうちから検討してもよいと思いました。

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

【Vue.js - 1】基礎知識

1. 公式サイト

Vue.js日本公式サイト
https://jp.vuejs.org/

2. リソースサイト

リソース
https://github.com/vuejs/awesome-vue
https://curated.vuejs.org/

UIコンポーネント集
Element
https://element.eleme.io/#/en-US
OnsenUI
https://onsen.io/

3. Vue.jsのインストール

CDNを読み込む

html
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

lodash.min.js(ユーティリティ用ライブラリ)

html
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>

axios.min.js(HTTP 通信用ライブラリ)

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

4. Vue.jsの基本機能

Vueインスタンスを作成
(変数名は、慣例的にappやvm(viewmodelの略)が使われる)

javascript
var app = new Vue({
  el: '#app'
})

テキストのバインディング {{}}

html
<p>{{ message }}</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  }
})

オプションに定義したデータはコンソールからアクセスできる

javascript
console.log(app.message)

繰り返し v-for

html
<ol>
  <li v-for="item in list">{{ item }}</li>
</ol>
javascript
var app = new Vue({
  el: '#app',
  data: {
    list: ['りんご', 'ばなな', 'いちご']
  }
})

イベント v-on:click

html
<button v-on:click="handleClick">Click</button>
javascript
var app = new Vue({
  el: '#app',
  methods: {
    handleClick: function (event) {
      alert(event.target)  [object HTMLButtonElement]
    }
  }
})

フォームの入力と同期 v-model

html
<input v-model="message">
<p>{{ message }}</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    message: '初期メッセージ'
  }
})

number修飾子を付与→入力値を数値として受け取る

html
<input v-model.number="message">

条件分岐 v-if

html
<button v-on:click="show=!show">切り替え</button>
<p v-if="show">Hello Vue.js!</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})

トランジション&アニメーション trantisionタグ

html
<button v-on:click="show=!show">切り替え</button>
<transition>
<p v-if="show">Hello Vue.js!</p>
</transition>
javascript
var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})
 .v-enter-active, .v-leave-active {
   transition: opacity 1s;
 }
 /* opacity:0から1へのフェードイン&フェードアウト */
 .v-enter, .v-leave-to {
   opacity: 0;
 }

オプションの構成

javascript
var app = new Vue ( {
  //mountする要素
  el: '#app',
  //アプリケーションで使用するデータ
  data: {
    message: 'Vue.js';
  },
  //算出プロパティ(関数によって算出されたデータ)
  computed: {
    computedMessage: function(){
      return this.message + '!'
    }
  },
  //ライフサイクルフック
  created: function(){
    ///処理
  },
  //アプリケーションで使用するメソッド
  methods: {
    myMethod: function() {
      //処理
    }
  }
});

ライフサイクルフック

Left align Left align
beforeCreate インスタンスが作成され、リアクティブの初期化がされる前
created
beforeMount インスタントがマウントされる前
mounted データが変更され、DOMに適用される前
updated
beforeDestroy Vueインスタンスが破棄される前
destroyed 任意の子孫コンポーネントからエラーが捕捉された時
errorCaptured その後
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsまとめ

1. Vue.jsとフレームワークの基礎知識

1-1. 公式サイト

Vue.js日本公式サイト
https://jp.vuejs.org/

1-2. リソースサイト

リソース
https://github.com/vuejs/awesome-vue
https://curated.vuejs.org/

UIコンポーネント集
Element
https://element.eleme.io/#/en-US
OnsenUI
https://onsen.io/

1-3. Vue.jsのインストール

CDNを読み込む

html
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

lodash.min.js(ユーティリティ用ライブラリ)

html
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>

axios.min.js(HTTP 通信用ライブラリ)

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

1-4. Vue.jsの基本機能

Vueインスタンスを作成
(変数名は、慣例的にappやvm(viewmodelの略)が使われる)

javascript
var app = new Vue({
  el: '#app'
})

テキストのバインディング {{}}

html
<p>{{ message }}</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  }
})

オプションに定義したデータはコンソールからアクセスできる

javascript
console.log(app.message)

繰り返し v-for

html
<ol>
  <li v-for="item in list">{{ item }}</li>
</ol>
javascript
var app = new Vue({
  el: '#app',
  data: {
    list: ['りんご', 'ばなな', 'いちご']
  }
})

イベント v-on:click

html
<button v-on:click="handleClick">Click</button>
javascript
var app = new Vue({
  el: '#app',
  methods: {
    handleClick: function (event) {
      alert(event.target)  [object HTMLButtonElement]
    }
  }
})

フォームの入力と同期 v-model

html
<input v-model="message">
<p>{{ message }}</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    message: '初期メッセージ'
  }
})

number修飾子を付与→入力値を数値として受け取る

html
<input v-model.number="message">

条件分岐 v-if

html
<button v-on:click="show=!show">切り替え</button>
<p v-if="show">Hello Vue.js!</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})

トランジション&アニメーション trantisionタグ

html
<button v-on:click="show=!show">切り替え</button>
<transition>
<p v-if="show">Hello Vue.js!</p>
</transition>
javascript
var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})
 .v-enter-active, .v-leave-active {
   transition: opacity 1s;
 }
 /* opacity:0から1へのフェードイン&フェードアウト */
 .v-enter, .v-leave-to {
   opacity: 0;
 }

オプションの構成

javascript
var app = new Vue ( {
  mountする要素
  el: '#app',
  アプリケーションで使用するデータ
  data: {
    message: 'Vue.js';
  },
  算出プロパティ(関数によって算出されたデータ)
  computed: {
    computedMessage: function(){
      return this.message + '!'
    }
  },
  ライフサイクルフック
  created: function(){
    処理
  },
  アプリケーションで使用するメソッド
  methods: {
    myMethod: function() {
      処理
    }
  }
});

ライフサイクルフック

Left align Right align Center align
beforeCreate インスタンスが作成され、リアクティブの初期化がされる前 This
created column column
beforeMount will will
mounted be be
updated right center
beforeDestroy aligned aligned

その後
インスタントがマウントされる前

データが変更され、DOMに適用される前

Vueインスタンスが破棄される前
destroyed
errorCaptured 任意の子孫コンポーネントからエラーが捕捉された時

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

【Vue.js】1. 基礎知識

1. 公式サイト

Vue.js日本公式サイト
https://jp.vuejs.org/

2. リソースサイト

リソース
https://github.com/vuejs/awesome-vue
https://curated.vuejs.org/

UIコンポーネント集
Element
https://element.eleme.io/#/en-US
OnsenUI
https://onsen.io/

3. Vue.jsのインストール

CDNを読み込む

html
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

lodash.min.js(ユーティリティ用ライブラリ)

html
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>

axios.min.js(HTTP 通信用ライブラリ)

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

4. Vue.jsの基本機能

Vueインスタンスを作成
(変数名は、慣例的にappやvm(viewmodelの略)が使われる)

javascript
var app = new Vue({
  el: '#app'
})

テキストのバインディング {{}}

html
<p>{{ message }}</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  }
})

オプションに定義したデータはコンソールからアクセスできる

javascript
console.log(app.message)

繰り返し v-for

html
<ol>
  <li v-for="item in list">{{ item }}</li>
</ol>
javascript
var app = new Vue({
  el: '#app',
  data: {
    list: ['りんご', 'ばなな', 'いちご']
  }
})

イベント v-on:click

html
<button v-on:click="handleClick">Click</button>
javascript
var app = new Vue({
  el: '#app',
  methods: {
    handleClick: function (event) {
      alert(event.target)  [object HTMLButtonElement]
    }
  }
})

フォームの入力と同期 v-model

html
<input v-model="message">
<p>{{ message }}</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    message: '初期メッセージ'
  }
})

number修飾子を付与→入力値を数値として受け取る

html
<input v-model.number="message">

条件分岐 v-if

html
<button v-on:click="show=!show">切り替え</button>
<p v-if="show">Hello Vue.js!</p>
javascript
var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})

トランジション&アニメーション trantisionタグ

html
<button v-on:click="show=!show">切り替え</button>
<transition>
<p v-if="show">Hello Vue.js!</p>
</transition>
javascript
var app = new Vue({
  el: '#app',
  data: {
    show: true
  }
})
 .v-enter-active, .v-leave-active {
   transition: opacity 1s;
 }
 /* opacity:0から1へのフェードイン&フェードアウト */
 .v-enter, .v-leave-to {
   opacity: 0;
 }

オプションの構成

javascript
var app = new Vue ( {
  //mountする要素
  el: '#app',
  //アプリケーションで使用するデータ
  data: {
    message: 'Vue.js';
  },
  //算出プロパティ(関数によって算出されたデータ)
  computed: {
    computedMessage: function(){
      return this.message + '!'
    }
  },
  //ライフサイクルフック
  created: function(){
    ///処理
  },
  //アプリケーションで使用するメソッド
  methods: {
    myMethod: function() {
      //処理
    }
  }
});

ライフサイクルフック

Left align Left align
beforeCreate インスタンスが作成され、リアクティブの初期化がされる前
created
beforeMount インスタントがマウントされる前
mounted データが変更され、DOMに適用される前
updated
beforeDestroy Vueインスタンスが破棄される前
destroyed 任意の子孫コンポーネントからエラーが捕捉された時
errorCaptured その後
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptでVuexを使わないという選択肢

はじめに

この記事はVueとTypeScriptの開発で状態管理について悩んでいる方へ,Vuexを使わないという選択をした場合どのように状態管理をしていくかを綴った記事です。
Vuexを否定することが目的ではありません,Vuexを使いたい場合は@tsrnkさんが書かれているこちらの記事のようにvuex-module-decoratorsを使う方法をおすすめします。

何に悩んでいたのか

TypeScriptとVuexは相性が悪く型安全性について度々問題にぶつかります。
厳密にはTypeScriptで書くVueとVuexの相性が悪いです。
そもVue 2.xはJavaScript製であり,もともとTypeScriptのような型安全を考慮した作りにはなっていません。
ですのでTypeScriptの型定義ファイルによって型をあと付けして可能な限り型安全が保たれるようにしています。

Vuexで作ったストアをコンポーネントから呼び出すためにはコンポーネント内でthis.$storeを使います。
例えばストアのcountというステートをコンポーネントから呼び出したい場合には以下の様に書きます。

store.ts
export default new Vuex.Store({
  state: {
    count: 0
  },
});
App.vue
@Component
export default class extends Vue {
  created() {
    console.log(this.$store.state.count);
  }
}

このときthis.$storeの持つオブジェクトはStore型であり,ストアの内容であるStoreOptions型のオブジェクトとは異なるものです。
これもVueがJavaScript製でありメタプログラミングによってVueとVuexのやり取りをしていることに起因しています。
そのため,コンポーネントからストアを呼び出す際にはvuex-classのようにVuexとコンポーネントを対応させるというアプローチでこの問題に対処してきました。

例えばvuex-classではコンポーネントを以下のように書きます(公式より抜粋)。

<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import { State, Getter, Action, Mutation, namespace } from 'vuex-class';

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo;
  @State(state => state.bar) stateBar;
  @Getter('foo') getterFoo;
  @Action('foo') actionFoo;
  @Mutation('foo') mutationFoo;

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo;
  @Getter bar;
  @Action baz;
  @Mutation qux;

  created() {
    this.stateFoo; // -> store.state.foo
    this.stateBar; // -> store.state.bar
    this.getterFoo; // -> store.getters.foo
    this.actionFoo({ value: true }); // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }); // -> store.commit('foo', { value: true })
    this.moduleGetterFoo; // -> store.getters['path/to/module/foo']
  }
}
</script>

以下はデコレーター部分だけ取り出したものです。
ストア内での名前を文字列で指定し,コンポーネント内で使いたい名前と対応させています。

  @State('foo') stateFoo;
  @State(state => state.bar) stateBar;
  @Getter('foo') getterFoo;
  @Action('foo') actionFoo;
  @Mutation('foo') mutationFoo;

このままでは対応させたステートやミューテーションはanyのままなので,更にインタフェースを用いて型をあと付けすることができます。

しかしどうでしょうか,そも文字列内の名前を書き間違えてしまうと対応するものがなくなってしまうので動かなくなります。
型安全を手に入れるために文字列を使って安全ではない書き方をしなければならないという新たな危険をはらんでいるのです。
そして,Vuexが肥大化すればするほどVueとTypeScriptの型安全のために大量のインタフェースとデコレーターを書かなければなりません。

Vuexを使わないことのメリット

TypeScriptの知識のみでストアを書ける

Vue 2.6.0から登場したVue.observable()により簡単にリアクティブなオブジェクトを作ることができるようになりました。
オブジェクトの内容はリアクティブ化する際に付与されるいくつかのプロパティやメソッドを除けば,それ以外は元のオブジェクトと同じです。
つまりは,ストアとして扱いたいオブジェクトとimportexportを書ければ良いのでTypeScriptの知識のみで実装することができ追加のコストがかかりません。

大量の型をあと付けしなくて良い

先に挙げた大量のデコレーターを書く必要がなくなり,importexportを使ってオブジェクトを引き回しても型安全が保たれるのでコードの記述量が減ります。

ファイルサイズが減る

ごくわずか(10KB程度)とはいえVuexを含めなくて良い分ファイルサイズが減ります。

Vuexを使わないことのデメリット

公式の開発ツール拡張と連携できない

Vue公式による開発ツール拡張との連携ができなくなるので,タイムトラベルデバッグやスナップショットを利用できなくなります。
これはVuexを使わないことの大きなデメリットになります。
Vuexの力が必要な規模の開発ではvuex-module-decoratorsを検討しましょう。

ストアの実装

ストアを作る前にVue.observable()について知っておきます。
公式より以下のように書かれています。

オブジェクトをリアクティブにします。内部的には、Vue は data 関数から返されたオブジェクトに対してこれを使っています。

戻り値のオブジェクトは、描画関数 や 算出プロパティ の中で直接使え、値が変更されたときには適切な更新をトリガーします。単純なシナリオでは、コンポーネントをまたぐ最小の state ストアとして使用することもできます:

つまり,Vue.observable()にオブジェクトを渡すとリアクティブなオブジェクトにしてくれるというもの。
例えば以下のようなオブジェクトを作ります。

const store = {
  count: 0
};

なんの変哲もないただのオブジェクトですが,storeconsole.log()等で出力して確認してみます。

78117a9a-9cd7-30b6-47da-b19e482deffe.png

これをVue.observable()に渡してみます。

import Vue from 'vue';

const store = Vue.observable({
  count: 0
});

すると以下のようになります。

01c54faa-a682-5585-47c9-2587fb798f3d.png

Observerオブジェクトの入った__ob__プロパティとcountのゲッターメソッドとセッターメソッドが生えました。
countプロパティへのアクセスはこのゲッターメソッドとセッターメソッドを経由することでリアクティブを実現しているわけですね。

注意ですが公式で次のように書かれています。

Vue 2.x では、Vue.observable は渡されたオブジェクトを直接操作するため、ここでデモされる ように戻り値のオブジェクトと等しくなります。Vue 3.x では、代わりにリアクティブプロキシを返し、元のオブジェクトを直接変更してもリアクティブにならないようにします。そのため、将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨します。

つまり,参照によって引数に与えられたオブジェクトをリアクティブにするので次のようにも書けるわけですね。

const store = {
  count: 0
};

Vue.observable(store);

しかし,Vue 3.xからは元のオブジェクトが変わらないようになるとのことなので,戻り値から受け取ったほうが良さそうです。

さて,このままストアとして利用しても良いですがクラスで書き直します。
クラスで書くことで継承・カプセル化・ポリモーフィズムをより簡単に書けるようになります。

一旦Vue.observable()は使わず以下のようにクラスに書き直しインスタンス化します。

store.ts
class Store {
  private count = 0;
}

const store = new Store();

f16c0830-8cca-c425-9cf8-5be5e3393503.png

Storeクラスからインスタンス化したのでStoreオブジェクトができました。
このStoreオブジェクトもオブジェクトですのでVue.observable()でリアクティブにできるはずです。
以下のようにリアクティブ化します。

store.ts
import Vue from 'vue';

class Store {
  private count = 0;
}

const store = Vue.observable(new Store());

fd7013b7-ac4e-b561-0322-ab09167383e0.png

先程と同様にリアクティブに必要な情報が付与されました。

続けてVuexのゲッターやミューテーションと同様のものをクラスに実装します。
プロパティが_countになっている点に注意してください。
また,他所でimportするためにexport defaultしています。

store.ts
import Vue from 'vue';

class Store {
  private _count = 0;

  public get count() {
    return this._count;
  }

  public increment() {
    this._count++;
  }

  public decrement() {
    this._count--;
  }
}

export default Vue.observable(new Store());

private修飾子によって_countは隠蔽され,プロパティの変更はincrement()decrement()ミューテーションからのみ操作できるようになりました。

次にコンポーネントからストアを呼び出してみます。

App.vue
<script lang="ts">
import store from '@/store';
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class extends Vue {
  private store = store;

  private onPlusClicked() {
    store.increment();
  }

  private onMinusClicked() {
    store.decrement();
  }
}
</script>

<template>
  <div>
    <p>{{ store.count }}</p>
    <button @click="onPlusClicked" type="button">+</button>
    <button @click="onMinusClicked" type="button">-</button>
  </div>
</template>

テンプレートから利用するためにprivate store = storeと書いていますが,気持ち悪いと思う場合はストアから値を受け取るためのゲッターをコンポーネントに実装してください。

さいごに

公式にあるようにシンプルなストアパターンで良い場合は,この方法であれば安全に低コストで実装することができます。
また,ある程度の規模であっても集中型のストアかつ型安全であるが故に,状態やコンテキストの把握が容易になるので開発ツール拡張の補助を必要としない場合にも良い方法だと思います。

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

Vue.js の slot を使用して並べ替えと絞り込みを行う

slot を使用しての並べ替えと絞り込み

実装例
上記において { pref } の値によるまとまりの並べ替えと
内容となる list 自体の反転を行っています。

props の指定により name を変更する slot

list の並べ替えに使用するコンポーネントとなります。

  • 子コンポーネント自体には表示対象となる list を持たせません。
  • slotname の指定に props からの関数を使用させます。
  • slot を並べ替える基となる sortListprops として渡します。
DynamicSlot
<template>
  <div>dynamic
    <slot v-for="(row, key) in sortList" :name="slotName(row, key)">
      <!-- slot タグには :key 指定ができない模様 -->
      <span :key="slotName(row, key)"></span>
    </slot>
  </div>
</template>

<script>
import Vue from "vue";
export default Vue.extend({
  props: {
    sortList: {
      type: Array,
      default: function() {
        return [];
      }
    },
    slotName: {
      type: Function,
      default: (row, key) => `dynamic-${key}`
    }
  }
});
</script>

slot による並べ替えコンポーネントの使用

以下の実装をしています。

  • 表示データとなる listpref を並べ替える根拠となる sortListdata に指定
  • 前述の slotname を指定するための slotNamemethods
  • 配列操作として list sortList の反転操作、各要素ごとの pref へのインクリメント
    • prefsortList に含まれない値になった時点で表示対象より除外されます。
App.vue
<template>
  <div id="app">
    <button @click="sortList = sortList.reverse()">pref</button>
    <button @click="list = list.reverse()">list reverse</button>
    <DynamicSlot :sort-list="sortList" :slot-name="slotName">
      <p v-for="(row, key) in list" :key="key" :slot="slotName(row, key)">
        {{ row }}
        <button @click="list[key].pref = list[key].pref + 1">pref increment</button>
      </p>
    </DynamicSlot>
  </div>
</template>

<script>
import DynamicSlot from "./components/DynamicSlot";

export default {
  components: {
    DynamicSlot
  },
  data() {
    return {
      sortList: [{ pref: 2 }, { pref: 3 }, { pref: 1 }],
      list: [
        { id: 1, name: "aaa", pref: 1 },
        { id: 2, name: "bbb", pref: 2 },
        { id: 3, name: "ccc", pref: 1 },
        { id: 4, name: "ddd", pref: 3 },
        { id: 5, name: "eee", pref: 2 },
        { id: 6, name: "fff", pref: 1 },
        { id: 7, name: "ggg", pref: 1 },
        { id: 8, name: "hhh", pref: 3 },
        { id: 9, name: "iii", pref: 3 },
        { id: 10, name: "jjj", pref: 1 }
      ]
    };
  },
  methods: {
    slotName(row, key) {
      return `dynamic-${row.pref}`;
    }
  }
};
</script>

pref の値を基に slotName の結果が変更されますが、
sortList の反転により並び変わるのは こちらの挙動 を利用しており
バグっぽいような感じもしなくもないです・・・。

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