20200409のvue.jsに関する記事は8件です。

【感想】【Laravel x Vue.js】SPAクイズアプリケーションを作ってみよう!を終えて

はじめに

今回、私が学習した教材は【Laravel x Vue.js】SPAクイズアプリケーションを作ってみよう!です。

かなりボリューム大・難易度も高めだったので、個人的な備忘録も兼ねて感想を書きます。

動機

購入したのは2月。当時は全然理解できなくて挫折。

ですが、LaravelとVue.js各々でポートフォリオ制作を終えた今、次なる挑戦はやはりLaravel+Vue一択。

スキル的にも2か月前に比べてだいぶ変わったはず。

そう思い、再びこの教材に挑戦してみました。

感想


この教材ではタイトル通りLaravel+VueでSPA開発を体験できます。(環境構築はMac)

私はWindows&XAMPP(SQLite⇒MySQL)に置き換えて挑戦しました。

良かった点


①Laravel-Adminを扱っている点。

以前から気になっていたのですが、これを扱う教材は初めてだったのでとても参考になりました。

②SPAかつ複数ページのサイト構築を学べる点。

以下は難しすぎて、頭を整理する際に作ったものです。笑

他でここまで教材にしてくれているのはこれくらいな気がします。⇒Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう

印象的だった点


・LaravelとVueをディレクトリを切り離して作成し、後程1つにまとめるという作り方。

・Login/Register含めすべてのビューをVue側で作成。機能面をLaravel側で実装している。

難しかった点


・webpackやら環境構築。設定面は見よう見まねでしか現状はできないです。

・複数ページに渡るSPA開発を1つのプロジェクトで扱う。コンポーネントファイルもたくさん作ります。
※有名なこちらの本(PHPフレームワーク Laravel実践開発)でもLaravelでのVue.js実装方法は説明されていますが、本当に導入部分の説明のみです。(コンポーネントファイル1つでの実装。Vue Routerも扱わない。)

・api.phpの組み込み(というより6章以降総じて難しい。)

・Laravel-Adminの実装

活用ライブラリ・機能など


CSS


Bootstrap3、Sass、※BEM規約

JavaScript


browserSync、vue-social-sharingChart.jsvue-chartjsVue Router(グローバルビフォーガイド)AxiosVeeValidatevue-loading-overlayvue-notification

PHP


Carbon(日付計算)、Factory(自動ダミーデータ生成機能)、helpers(文字列ライブラリ)、Laravel-admin

その他


Font Awesome、SQLite(DB)、JSON Viewer、ngrok(windowsはこちらで対応、1周目は割愛した。)、Basic認証

おわりに


作りながら学べる系の教材としては、かなり難しい部類に入ると思います。

僕もまだ1周終えた段階で全然理解できていません。

ですが、これを理解したら何ができるだろうかと妄想を膨らませると非常にワクワクします。笑

※Laravel-Adminは作るアプリによってはToo muchな気もしました。GUIでDB弄れれば問題ない気もします。ここら辺も踏まえて復習していきます。

引き続き頑張ります◎

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

【初心者向け】Vue.jsをToDoアプリを作りながら学ぼう

この記事では、Todoアプリを作りながら学んだVue.jsの基本をまとめています。
超初心者向け、超入門です。難しいことは書けないのでご容赦ください。。

筆者の学習時の技術レベル

  • プログラミング学習開始して5ヶ月、エンジニア実務経験 (Rails) 3ヶ月
  • バックエンド Rubyを中心に勉強しており、今後もバックエンドがメインのつもり
  • フロントエンドはJavaScript、jQueryの基本的な読み書きはできる程度

必要な前提知識

  • HTML/CSS/JavaScriptの基本的な知識

どうやってVue.jsを勉強したの?

超Vue JS 2 完全パック - もう他の教材は買わなくてOK! (Vue Router, Vuex含む)

僕は動画のほうが入りやすいので、まずUdemyですね。

タイトルにもあるように、他の教材を買わなくてもいいくらい充実した内容です。
Todoアプリを作るくらいなら全部やらなくても大丈夫ですが、
「Vue Routerを使ったSPA(single page application)を作りたい」
「Vue.jsで大規模開発を経験したい」といった人の導入にもおすすめです。

基礎から学ぶ Vue.js

こちらは定番のVue.js入門書です。

公式サポートサイトも充実しており、
今回のTodoアプリ作成もこのサイトをかなり参考しました。

公式ドキュメント(日本語)

Vue.jsは日本語の公式ドキュメントも充実しています。
上記のような教材で学びつつ公式情報を確認するのが、個人的にはいいかなと思いました。

Vue.jsってなにがいいの?

Vue.jsは、JavaScriptフレームワークの1つです。
要は「JavaScriptを簡単に使いやすくしたもの」で、他にはjQueryやReactがあります。

※フレームワークとライブラリの違いは割愛→こちらを参考にしてください

Vue.jsの特徴は主に下記3つです。(他にも色々とありますが、、)

1. 学習コストが低い

  • 日本語ドキュメント情報が充実している
  • 書籍や動画などの教材もわりと多い
  • 構造がシンプルで記述量も少ないため、比較的早く習得することができる

2. MVVMモデルが採用されている

  • MVVMモデルはModel(M)-View(V)-ViewModel(VM)の設計思想です。
  • なんか難しそうなので超簡単にイメージだけ説明します。

mvvm.png
(公式ガイドより抜粋)

  • ViewはDOM(Document Object Model)のことで、JavaScriptで扱うHTMLの要素です。
  • ModelはJavaScriptオブジェクトのこと。
  • ViewModelが重要で、ModelViewを同期するオブジェクト、Vueインスタンスです。ここで双方向データバインディングを実現しています。

双方向データバインディングとは、データと描写(View)を同期させる仕組みのことで、上記の場合View側・Model側どちらからでもデータを変更すれば同期されるようになっています。
Vue.jsは少ないコード量でこの仕組を実現しています。

設計パターンの難しい説明は割愛しますが、画面上に表示されるViewとJavaScriptを簡単につなげてくれるのがVueです。
主にここをゴニョゴニョ書いていきます。

3. SPA(シングルページアプリケーション)の作成に向いている

SPAは、単一のWebページからなるWebアプリケーションです。
画面遷移がほぼなく、動作が速い使い勝手の良いWEBアプリで、例えばWeb版のSlackやFacebookメッセンジャー、GoogleMapなどですね。

Vue.jsプロジェクトで、SPAは比較的簡単に作ることができます。

実際にVue.jsでToDoアプリを作ろう

下記サイトを参考にしたので、試してみてください。
- Vue.jsではじめるMVVM入門 | DeNA DESIGN BLOG
- ToDoリストを作りながら学習しよう! | 基礎から学ぶ Vue.js

今回はVue CLIのような開発ツールやVue Routerは使わず、シンプルにindex.htmlstyle.cssmain.jsの3ファイルで作成します。

まず完成形から共有

Image from Gyazo

1. HTML、CSSをざっくり組み立てます

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Vue.js TODO APP</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="to-do"> <!-- Vue.jsで扱うDOM要素を指定するためidを付与 -->
    <p>
      NewTask:
      <input type="text">
      <button>Add</button>
    </p>
    <hr>
    <ul>
      <li>
        <input type="checkbox">
        <span>Rubyの勉強をする</span>
        <button>Delete</button>
      </li>
      <li>
        <input type="checkbox">
        <span>Vue.jsのアプリを作る</span>
        <button>Delete</button>
      </li>
      <li>
        <input type="checkbox">
        <span>Youtubeをみる</span>
        <button>Delete</button>
      </li>
    </ul>
  </div>
</body>
</html>
style.css
ul {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

ul > li {
  margin: 5px;
  text-indent: 0;
}

#to-do {
  width: 800px;
  margin: 0 auto;
}

2. Vue.jsを導入していきます

今回は簡単にscriptタグでHTMLに直接埋め込むCDNを使用します。

index.html
  <!-- Vue.js -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
  <script src="./main.js"></script>

そしてjsファイルにまずVueインスタンスを作成し、Vue.jsで扱う要素を指定します。
今回はid="to-do"以下の要素を扱います。
elはviewと紐付ける要素を指定します。classの場合、.to-doのようになります。

main.js
new Vue({
  el: '#to-do'
});

3. 初期値を設定しよう

HTMLにタスクが3つありますが、それらを初期値としてVueで設定します。

main.js
new Vue({
  el: '#to-do',
  data: {
    todos: [
      { task: 'Rubyの勉強をする', isCompleted: false },
      { task: 'Vue.jsのアプリを作る', isCompleted: false },
      { task: 'Youtubeをみる', isCompleted: false }
    ]
  }
});

isCompleted: falseは後ほど説明します。
dataは使用するデータの初期値を定義するオプションです。オブジェクトは配列も登録できます。

今回はtodosにタスクを3つ配列で、中身は連想配列で定義しています。

4. 登録したTodoを表示する

ここで登場するのが「ディレクティブ」です。
ディレクティブはviewに動きをつけるための特別な属性で、v-〇〇といった形をしています。

今回は配列で指定したTodoを繰り返し処理で表示したいので、v-forを使用します。

index.html
<li v-for="todo in todos">
  <input type="checkbox">
  {{ todo.task }}
  <button>Delete</button>
</li>

v-for

v-forの値はtodo in todosですが、
形式は(個々の要素) in (繰り返したいオブジェクト)といった具合です。

vueの値は二重中括弧で表示できるので、
タスク名の取得は{{ todo.task }}となります。
(連想配列のキーから取得しています)

5. チェックするとタスク完了にする(取り消し線を入れる)

次にチェックするとタスク名に取り消し線が入るようにします。

index.html
<li v-for="todo in todos">
  <input type="checkbox" v-model="todo.isCompleted">
  <span :class="{ 'complete': todo.isCompleted }">{{ todo.task }}</span>
  <button>Delete</button>
</li>
style.css
ul > li > .complete {
  text-decoration: line-through;
  color: #ddd;
}

v-model

まずv-model="todo.isCompleted"ですが、
v-modelは「双方向データバインディング」を実現するディレクティブです。

値には同期させたいデータを指定するので、今回の場合checkboxvalueとなります。
checkboxの場合、v-modelの値はboolean値(true/false)をとります。
初期値(data)設定の際、各タスクにisCompleted: falseと定義していましたが、
これはチェックボックスにチェックを入れていない状態を指します。

チェックON・OFFでtrue/falseと変化するので、v-modelの値は
v-model="todo.isCompleted"とします。

v-bind

次に:class="{ 'complete': todo.isCompleted }"ですが、
これはv-bind:class="{ 'complete': todo.isCompleted }"の省略記法です。

v-bindはhtmlの属性値をバインドする(結びつける)ためのディレクティブです。
ここではtodo.isCompletedの値(true/false)を受け取り、
classにcompleteをつけるかつけないを、チェックボックスのON・OFFで変更できるようにしています。

つまり、
1. 初期値:チェックボックスOFF、isCompleted: false
2. チェックボックスにチェックを入れる
3. isCompleted: trueとなる
4. v-bindで値を受け取り、completeクラスが付与されcssに定義したデザインが適用される

このような流れになります。

6. タスクを追加できるようにする

まず、入力フォームからタスクを追加するために、受け皿としてdataを追加します。

index.html
<p>
  NewTask:
  <input type="text" v-model="newTask">
  <button>Add</button>
</p>
main.js
new Vue({
  el: '#to-do',
  data: {
    newTask: '',

次にボタンを押したらタスクが追加できるように、クリックイベントを作成します。

index.html
<button v-on:click="addTodo()">Add</button>
main.js
methods: {
  addTodo: function() {
    if (this.newTask == '') return;
    this.todos.push(
      { task: this.newTask, isCompleted: false }
    );
    this.newTask = '';
  }
}

v-on

v-onはDOMイベントの際に使用するディレクティブで、クリックイベントの場合v-on:clickとなります。

そしてvue側でクリックイベントに対応するメソッドを定義します。

まず最初のif (this.newTask == '') return;ですが、入力したタスクはthis.newTaskで取得できます。
未入力の場合にこれ以降の処理を行わない(returnする)ということです。

そしてTodoリストであるthis.todosにタスクを追加し、newTaskは初期化しておきます。

7. タスクを削除できるようにする

index.html
<button v-on:click="deleteTodo(todo)">Delete</button>
main.js
methods: {
  addTodo: function() {
    if (this.newTask == '') return;
    this.todos.push(
      { task: this.newTask, isCompleted: false }
    );
    this.newTask = '';
  },
  deleteTodo: function(todo) {
    var index = this.todos.indexOf(todo)
    this.todos.splice(index, 1)
  }
}

最後にdeleteボタンでタスクを削除します。

deleteTodo(todo)では、引数で該当する要素を取得し、
methodにてtodosの配列がらindexを取得、タスクを削除します。

v-on:click@clickと省略できます。

完成!

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Vue.js TODO APP</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="to-do">
    <p>
      NewTask:
      <input type="text" v-model="newTask">
      <button @click="addTodo()">Add</button>
    </p>
    <hr>
    <ul>
      <li v-for="todo in todos">
        <input type="checkbox" v-model="todo.isCompleted">
        <span :class="{ 'complete': todo.isCompleted }">{{ todo.task }}</span>
        <button @click="deleteTodo(todo)">Delete</button>
      </li>
    </ul>
  </div>

  <!-- Vue.js -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
  <script src="./main.js"></script>
</body>
</html>
main.js
new Vue({
  el: '#to-do',
  data: {
    newTask: '',
    todos: [
      { task: 'Rubyの勉強をする', isCompleted: false },
      { task: 'Vue.jsのアプリを作る', isCompleted: false },
      { task: 'Youtubeをみる', isCompleted: false }
    ]
  },
  methods: {
    addTodo: function() {
      if (this.newTask == '') return;
      this.todos.push(
        { task: this.newTask, isCompleted: false }
      );
      this.newTask = '';
    },
    deleteTodo: function (todo) {
      var index = this.todos.indexOf(todo)
      this.todos.splice(index, 1)
    }
  }
});
style.css
ul {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

ul > li {
  margin: 5px;
  text-indent: 0;
}

#to-do {
  width: 800px;
  margin: 0 auto;
}

ul > li > .complete {
  text-decoration: line-through;
  color: #ddd;
}

※参考サイト再掲
下記サイトも参考にしたので、試してみてください。

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

VeeValidate3のfile|image系バリデーションの@change="validate"を関数で呼び出す�

↓下記の記事にたどり着けたなら正直このQiitaは不要かも知れません。
https://logaretm.github.io/vee-validate/advanced/file-validation.html#html-file-validation

この記事はVeeValidate3の記事です。
VeeValidate2では書き方が異なります。

refを使ってVeeValidateのvalidateを外の関数から呼び出そう!

ref属性にはDOM要素のオブジェクトとコンポーネントインスタンスが登録されています!
中を追って見るとfileもちゃんと入ってるから関数側で色んな事ができますね!!

vm.$refs

validate関数はただ呼び出して上げるだけでruleに則したエラーを吐いてくれます!

<template>
  <ValidationProvider rules="required|image" ref="provider" v-slot="{ validate, errors }">
    <input type="file" @change="handleFileChange" />
    <p>{{ errors[0] }}</p>
  </ValidationProvider>
</template>

<script>
export default {
  methods: {
    handleFileChange(e) {
      // NOTE: refはValidationProviderタグで定義したproviderの中にslotのvalidateが入ってる!
      this.$refs.provider.validate(e);

      // ここ以降に自由にfile changeした時にやりたい処理を書こう!!
    }
  }
};
</script>

まとめ

最初に紹介したこのVeeValidate公式を読めばオールオッケイ!!
https://logaretm.github.io/vee-validate/advanced/file-validation.html#html-file-validation

この記事マジでいらねえぞ!?
せっかく書いたし消すのもあれだな…。

検索苦手な方の手助けになれば…なるかな??

参考にこの一休.comさんの記事も読んで見てください!
VeeValidate 2から3へのアップデート

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

[Rails6.0 + Nuxt.js] response.headersにcorsのexpose(access-tokenなど)が入らない

この記事を書くにいたるまで

現在Rails6.0とNuxt.jsを使って適当にアプリを作っています。(Nuxtの練習です
RailsはAPIモード、認証にはdevise_token_authを使用しており、
Nuxtからaxios経由でpostしてtoken認証を行うアプリになっています。

今回loginをaxiosで行ったのですが、そのresponse(response.headers)の中に
access-tokenclientなどが格納されていないというトラブルが発生しました。
解決すると「こんなことで…」と思いますが、ほぼ1日ハマってしまったので共有です。

ぶつかった壁

まずnuxt側ですが、以下のようにloginアクションを実装しています。

login.vue
  async login(vuexContext, data) {
    await this.$axios.post("/api/v1/auth/sign_in", data)
      .then(response => {
        console.log(response.headers)
      })
      .catch(e => {
        console.log(e)
      })
  }

次にrails側ですが、CORS対策としてはrack-corsを導入しています。
以下corsの設定です。

config/application.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3001'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      expose: ['access-token', 'expiry', 'client']
  end
end

exposeに'access-token', 'expiry', 'client'を記述しています。
通常だとこれでresponse.headers内に上記3つの結果が表示されるはずです。

しかし以下のような結果しか表示されませんでした。

console.log(response.headers)
headers:
  cache-control: "max-age=0, private, must-revalidate"
  content-type: "application/json; charset=utf-8"

chrome-devtools => [Network] => Response Headersを見たところ、
こちらにはaccess-tokenの情報が来ているのですが、
responseというオブジェクトから取り出すことができません。

解決策

調べたところ、rack-corsのissuesにたどり着きました。
するとどうやらRack::Corsという設定が重複しているようです。

念のためgit grep Rackを実行したところ、なんとconfig/initializers/cors.rbが見つかり
そこに同じ設定がありました…(それまでconfig/application.rbに書いていました)

コメントアウトされていたので重複はしていないはずですが、
そもそもの設定を書く場所が間違っていたことになります。

設定をconfig/initializers/cors.rbに移行し再ログインしたところ、
うまくheadersに反映されました。

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3001'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      expose: ['access-token', 'expiry', 'client']
  end
end

結果

console.log(response.headers)
headers:
  access-token: "VPPrzn2DnJuqJi_uDEI4hg"
  cache-control: "max-age=0, private, must-revalidate"
  client: "TVmLwY1GM23koi9r9HY17g"
  content-type: "application/json; charset=utf-8"
  expiry: "1587634926"

おわり

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

propsと$emitでデータを引き渡す

コンポーネント間の基本的なデータの受け渡し方法をまとめます。

$ vue create learn-props-emit
$ cd learn-props-emit
$ npm run serve
$ touch src/components/Child.vue

props

親→子に値を渡す時にはpropを使います。

src/components/Child.vue
<template>
  <div>
    <p>{{ greet }}</p>
  </div>
</template>

<script>
export default {
  props: {
    greet: {
     type: String,
     default: 'hogehoge'
    }
  }
}
</script>
src/App.vue
<template>
  <div>
    <h1>Hello from App.vue</h1>
    <Child greet='Hello with props'/>
  </div>
</template>

<script>
import Child from './components/Child.vue'
export default {
  components: {
    Child
  }
}
</script>

ポイント

  • 子コンポーネント内にpropsという属性を定義し、propの名前と型を定義する
  • 親コンポーネント内で子コンポーネントを使う時に、prop名と値を受け渡す

$emit

親→子に値を渡す時にはpropを使います。結構めんどいです。

src/components/Child.vue
<template>
  <div>
    <p>child_num: {{ child_num }}</p>
    <button @click='send'>親に値を渡す</button>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      child_num: 0
    };
  },
  methods: {
    send() {
      this.$emit("my-click", this.child_num);
    }
  }
};
</script>
src/App.vue
# パターン1:受け取った値をそのまま使う場合は$eventで受け取る
<template>
  <div>
    <h1>Hello from App.vue</h1>
    <p>parent_num: {{ parent_num }}</p>
    <Child @my-click='parent_num = $event'/>
  </div>
</template>

<script>
import Child from './components/Child.vue'
export default {
  data: function() {
    return {
      parent_num: 100
    }
  },
  components: {
    Child
  }
}
</script>
src/App.vue
# パターン2. 受け取った値を関数で使う場合は、適当な変数(value)を定義するとそこに値が入る
<template>
  <div>
    <h1>Hello from App.vue</h1>
    <p>parent_num: {{ parent_num }}</p>
    <Child @my-click='reflectNum'/>
  </div>
</template>

<script>
import Child from './components/Child.vue'
export default {
  data: function() {
    return {
      parent_num: 100
    }
  },
  components: {
    Child
  },
  methods: {
    reflectNum(value) {
      this.parent_num = value
    }
  }
}
</script>

ポイント

  • 子コンポーネント内で、$emitでカスタムイベント(clickとかchange的な使われ方をするやつ)を作る
    • 第2引数に送信するデータを渡す
  • 親コンポーネント内で子コンポーネントを呼び出す時に、作成したカスタムイベントを付与する
  • $eventや引数で送信されたデータを受け取る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでForm要素のラッパーコンポーネントを作る際の2つの方法(v-bind.sync, v-model)

input欄を含むコンポーネントを作る際に毎回忘れるのでメモ

v-bind.syncを使う方法

https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier

v-bind.syncv-bind:hoge, v-on:update:hoge="hoge = $event"のシンタックスシュガーです。
これを意識して子コンポーネントでイベントを発行することで、Form要素を内包するコンポーネントを簡潔に実装できます。

<!-- <MyInput v-bind:hoge.sync="hoge">は以下と同義 -->
<MyInput
  :hoge="hoge" 
  v-on:update:hoge="hoge = $event"
>

プリミティブな値の場合

input要素単体のコンポーネントなどの場合は、単純にコンポーネント内での変更感知のemit名をupdate:props名として値を発行すればOKです。

子コンポーネント(input要素)

MyInput.vue
<template>
  <label
    >{{ label }}
    <input type="text" :value="value" @input="updateValue($event.target.value)" />
  </label>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  props: {
    label: {
      require: true,
      type: String
    },
    value: {
      require: true,
      type: String
    }
  },
  setup(_, { emit }) {
    const updateValue = (value: string) => emit("update:value", value);
    return {
      updateValue
    };
  }
});
</script>

親コンポーネント

Parent.vue
<MyInput 
  label="ラベル" 
  :value.sync="myInputValue" 
/>

オブジェクトの場合

Form要素を一括でコンポーネントにする場合などは、子コンポーネントでpropsを個々に定義しつつ
v-bind.syncに直接オブジェクトを渡すことで簡潔に実装できます。

子コンポーネント

MyForm.vue
<template>
  <form>
    <label>
      title
      <input
        type="text"
        :value="title"
        @input="updateValue('title', $event.target.value)"
      />
    </label>
    <label>
      content
      <input
        type="textarea"
        :value="content"
        @input="updateValue('content', $event.target.value)"
      />
    </label>
    <button @click="onSubmit">submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  props: {
    title: {
      require: true,
      type: String
    },
    content: {
      require: true,
      type: String
    }
  },
  setup(props, { emit }) {
    const updateValue = (key: string, value: string) => {
      emit(`update:${key}`, value);
    };
    const onSubmit = () => emit("submit");
    return {
      onSubmit,
      updateValue
    };
  }
});
</script>

親コンポーネント

Parent.vue
<MyFormSync 
  v-bind.sync="myFormData" 
  @submit="onSubmit"
/>

v-modelを使う方法

https://vuejs.org/v2/guide/components-custom-events.html#Customizing-Component-v-model

v-modelv-bind:value="hoge", @input="hoge = $event.target.value"のシンタックスシュガーです。

<!-- <input v-model=hoge>は以下と同義 -->
<MyInput
  :value="name" 
  @input="name = $event.target.value"
>

プリミティブな値の場合

v-modelをカスタムコンポーネントで使う場合は、子コンポーネントが受け取るpropsを必ずvalueにする必要があります。また、$emitの際のイベント名は必ずinputにする必要があります。

子コンポーネント

MyInput.vue
<template>
  <label
    >{{ label }}
    <input type="text" :value="value" @input="updateValue($event.target.value)" />
  </label>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

export default defineComponent({
  props: {
    label: {
      require: true,
      type: String
    },
    value: {
      require: true,
      type: String
    }
  },
  setup(_, { emit }) {
    const updateValue = (value: string) => emit("input", value);
    return {
      updateValue
    };
  }
});
</script>

親コンポーネント

Parent.vue
<MyInput 
  label="ラベル" 
  v-model="myInputValue"
/>

オブジェクトの場合

Form要素をまとめて一つのコンポーネントにする場合はオブジェクトを渡せると良いですよね。
その際は、プリミティブの値と同等の制約を持ちつつ、emitの際に、emit("input", { ...props.value, [key]: value });の形式で変更プロパティと、他のプロパティのマージを行い、コピーのオブジェクトをemitするようにします。

子コンポーネント(form要素)

MyForm.vue
<template>
  <form>
    <label>
      title
      <input
        type="text"
        :value="value.title"
        @input="updateValue('title', $event.target.value)"
      />
    </label>
    <label>
      content
      <input
        type="textarea"
        :value="value.content"
        @input="updateValue('content', $event.target.value)"
      />
    </label>
    <button @click="onSubmit">submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent } from "@vue/composition-api";

type FormData = {
  title: string;
  content: string;
};

export default defineComponent({
  props: {
    value: {
      require: true,
      type: Object as () => FormData
    }
  },
  setup(props, { emit }) {
    const updateValue = (key: string, value: string) => {
      emit("input", { ...props.value, [key]: value });
    };
    const onSubmit = () => emit("submit");
    return {
      onSubmit,
      updateValue
    };
  }
});
</script>

親コンポーネント

Parent.vue
<MyForm 
  v-model="myFormValue" 
  @submit="onSubmit"
/>

どっちを使うべき?

完全に好みだと思いますが、同様の目的であればv-bind.syncの方がprops名の制約や、オブジェクト要素の場合の値のマージなどが必要ないので、使いやすいかなと思ってます。

参考

以下記事参考にさせて頂きました!!良記事ありがとうございます。

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

mapStateヘルパー

mapStateヘルパー

this.$store.state.messageと書くのは少し長くて面倒なので、
ヘルパー関数が用意されています。

App.vueのスクリプトの部分を以下のように書き換えます。

App.vue
<script>
import { mapState } from 'vuex';

export default {
  computed: {
    message () {
      return this.$store.state.message;
    }
    ...mapState({
      message: 'message'
    })
  }
};
</script>

'message'というように文字列を渡すと内部的にはstate.messageと同じ扱いになります。

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

Vue.js の Composition API における親子コンポーネント間のデータ受け渡し

皆様いかがお過ごしでしょうか。
いろいろと大変な時期ですが頑張っていきましょう。

Vue 3.0 のリリースを目前に控え、今後メインの記法となっていくであろう Composition API における親子コンポーネント間のデータのやりとりにフォーカスについてまとめてみました。
※ 2020 年 4 月現在、 vue@2.6.11 + @vue/composition-api@0.5.0 での挙動を元に書いています。

デモサイトを用意してあります。記事の該当する部分と交互に見ていただくと理解しやすいかもしれません。
https://vue-props-samples.netlify.app/
※ 記事内のコードは、装飾用のクラスなどを省略しています。
※ ブラウザ拡張機能 Vue.js devtools を使うと、コンポーネント内の様子などを確認できるようにビルドしてあります。
※ ソースはこちら。 https://github.com/jay-es/vue-props-samples

TL;DR

従来の記法 Options API に精通している方向けのまとめ。
親のテンプレートと子の props オプションは今までと同じ。子の setup 関数のひとつめの引数にも props が入っている(ただし分割代入は NG)

では、順を追って詳しく解説していきます。

1. props: 親から子にデータを渡す

まずは一番基本の、子コンポーネントへのデータの渡し方を説明していきます。

親コンポーネントの書き方

親コンポーネントのテンプレート内で、子コンポーネントのカスタム属性に変数を渡します。
:title="foo" は子コンポーネントの title というプロパティに foo 変数の中身(下の例の場合は abc)を渡す、という意味です。
v-bind:title="foo" とも書けますが、本記事では省略記法を使用していきます。
※ テンプレートの書き方は従来(Options API)と変わりません。
※ 親の変数名と子のプロパティ名が同じだと分かりづらいため、あえて別々にしています。

親コンポーネント
<template>
  <div>
    <Child :title="foo" :count="bar" />
  </div>
</template>

<script>
import { ref } from '@vue/composition-api'
import Child from './child.vue'

export default {
  components: { Child },
  setup () {
    const foo = ref('abc')
    const bar = ref(123)

    return { foo, bar }
  }
}
</script>

子コンポーネントで親からのデータを受け取る方法

一番シンプルなのは、コンポーネントオプションの props にプロパティ名の配列を指定する方法です。

子コンポーネント
<template>
  <div>
    title: {{ title }}<br />
    count: {{ count }}<br />
  </div>
</template>

<script>
export default {
  props: ['title', 'count'] // プロパティ名の配列
}
</script>

この方法は簡単ではあるものの、他の開発者(や半年後の自分)が見たときにどのような値が渡ってくるのか分かりづらいため、避けたほうがよいです。

かわりに、オブジェクト形式にして「キーにプロパティ名、値に変数の型(コンストラクタ)」のように指定したり、

export default {
  props: {
    title: String,  // プロパティ名: 型
    count: Number
  }
}

もう 1 階層ネストしたオブジェクトにして type で型を指定し、required(必須かどうかの真偽値)や、default(省略された場合の値)などを指定するとよいでしょう。

export default {
  props: {
    title: {         // プロパティ名
      type: String,  // 型
      required: true // 必須かどうか
    },
    count: {
      type: Number,
      default: 0     // 親から値が渡されなければ 0 になる
    }
  }
}

※ デフォルト値を関数で生成したり、値のバリデーション関数を定義することもできます。詳細は Vue 公式サイト: プロパティのバリデーション を参照してください。

setup 関数で props を使う

Composition API の setup 関数では、ひとつめの引数で props を取得できます。
親コンポーネントで値が変わったら props にも反映される、というリアクティブな性質をもつオブジェクトです。関数内で使用する場合は computedwatch で監視する必要があります。

子コンポーネント
import { computed } from '@vue/composition-api'

export default {
  props: {
    title: {
      type: String,
      required: true
    },
    count: {
      type: Number,
      default: 0
    }
  },
  setup (props) {
    const doubleCount = computed(() => props.count * 2)

    return { doubleCount }
  }
}

ただし、引数を分割代入で取得してしまうと、リアクティブではなくなってしまうので注意が必要です。
(自分はたまに忘れてやってしまいます。最初はちゃんと表示されるので気づきにくいんですよね……)

propsを分割代入
  setup ({ count }) {
    // 親コンポーネントで値が変わっても、子コンポーネントは初期値のまま変わらない
    const doubleCount = computed(() => count * 2)

TypeScript で型の情報をつける

TypeScript の場合、props にプリミティブな型(String など)を指定した場合はちゃんと型がつきますが、ArrayObject を指定した場合は中身の情報がないので補完されません。

子コンポーネント
import { defineComponent } from '@vue/composition-api'

export default defineComponent({
  props: {
    names: {
      type: Array, // 本当は文字列の配列
      required: true
    },
    staff: {
      type: Object, // 本当はユーザー定義型
      required: true
    }
  },
  setup (props) {
    type Names = typeof props['names'] // -> unknown[] になってしまう
    type Staff = typeof props['staff'] // -> { [key: string]: any } になってしまう
  }
})

PropType という型関数が用意されていますので、それを使ってキャストすることで型の補完が効くようになります。

子コンポーネント
import { defineComponent, computed, PropType } from '@vue/composition-api'
import { Person } from './Person'

export default defineComponent({
  props: {
    names: {
      type: Array as PropType<string[]>, // PropType で型の情報を付与
      required: true
    },
    staff: {
      type: Object as PropType<Person>, // PropType で型の情報を付与
      required: true
    }
  },
  setup (props) {
    type Names = typeof props['names'] // -> string[] と認識される
    type Staff = typeof props['staff'] // -> Person 型と認識される
  }
})

vue パッケージも PropType を export しているので、 Options API でも使えます。
※ 今回の例では PropType を使わず、 Array as () => string[] のように書いても同様の効果を得られます。
※ ちなみに、コンポーネントを作る関数名は以前 createComponent でしたが、@vue/composition-api@0.4 から defineComponent に変更されています。古いバージョンからアップデートした場合、 createComponent のままでも動きますが、コンソールに下記のエラーが表示されます。

`createComponent` has been renamed to `defineComponent`.

2. emit: 子から親にイベントを発生させる

次は子から親へデータを渡す方法です。
props のように直接データを渡す方法は用意されていないので、イベントを通じてデータを送ります。

子コンポーネントでイベントを発生させる方法

Composition API の setup 関数のふたつめの引数に context というオブジェクトが渡されてきます。これは従来(Options API)の this に入っていたプロパティやメソッドの一部が格納されています。
context.emit(eventName) を実行することで、カスタムイベントを発生させることができます。

子コンポーネント
<template>
  <div>
    <button @click="handleClick">Click me!</button>
  </div>
</template>

<script>
export default {
  setup (props, context) {
    const handleClick = () => {
      context.emit('my-event')
    }

    return { handleClick }
  }
}
</script>

props と違い、context は分割代入しても悪影響はありません。

export default {
  setup (props, { emit }) {
    const handleClick = () => {
      emit('my-event')
    }

    return { handleClick }
  }
}

また、 emit には任意の数の引数を渡すことができます。

子コンポーネント
export default {
  setup (props, { emit }) {
    const handleClick = () => {
      emit('my-event', 123, 'abc', false)
    }

    return { handleClick }
  }
}

親コンポーネントで子のイベントを受け取る方法

通常のクリックイベントなどと同じように、テンプレート内で @ もしくは v-on ディレクティブを使います。

親コンポーネント
<template>
  <div>
    <Child @my-event="handleEvent" />
  </div>
</template>

<script lang="ts">
import Child from './child.vue'

export default {
  components: { Child },
  setup () {
    const handleEvent = () => {
      alert('イベント発生!')
    }

    return { handleEvent }
  }
}
</script>

子コンポーネントの emit で 2 つ以上の引数を指定した場合は、イベントハンドラの引数として受け取ることができます。

子コンポーネント(前後略)
emit('my-event', 123, 'abc', false)
親コンポーネント(テンプレートは前と同じ)
export default {
  components: { Child },
  setup () {
    const handleEvent = (...args) => {
      alert(args) // -> 123, 'abc', false
    }

    return { handleEvent }
  }
}

※ ちなみに親のテンプレートで @my-event="handleEvent($event)" とすると、イベントハンドラには emit の第 2 引数のみ(上記の場合は 123)が入ってきます。

3. 双方向バインディング

さて、子に渡した親の変数を書き換えたい場合はどうしたらよいでしょう。
子コンポーネントの中で props の中身を直接変更しようとすると、以下のようにエラーになってしまいます。

子コンポーネント(テンプレート略)
export default {
  props: {
    count: Number
  },
  setup (props) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      props.count += 1
      /*
       * 以下のエラーが発生(改行は筆者が追加)
       * Avoid mutating a prop directly since the value will be overwritten
       * whenever the parent component re-renders.
       */
    }

    return { handleClick }
  }

そこでイベントを使います。
子コンポーネントでイベントを発生させ、その引数を(親コンポーネントの)イベントハンドラ内で代入する、という手順を踏むことで親コンポーネントの変数の値を変更できます。

子コンポーネント
export default {
  props: {
    count: Number
  },
  setup (props, { emit }) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      emit('my-event', props.count + 1)
    }

    return { handleClick }
  }
}
親コンポーネント
<template>
  <div>
    <!-- 上と下 どちらの書き方でもよい -->
    <Child :count="num" @my-event="num = $event" />
    <Child :count="num" @my-event="newVal => num = newVal" />

    <!-- setup 内で作った関数を渡して、その中で更新するのもあり -->
    <Child :count="num" @my-event="handleEvent" />
  </div>
</template>

<script>
import { ref } from '@vue/composition-api'
import Child from './child.vue'

export default {
  components: { Child },
  setup () {
    const num = ref(0)

    // イベントハンドラ内で num の値を更新
    const handleEvent = (newVal) => {
      num.value = newVal
    }

    return { num, handleEvent }
  }
}
</script>

ただ、これだといちいち代入の処理を書かないといけないので少し大変です。
短くかけるシンタックスシュガーが 2 種類用意されています。

3-1. v-model

まずは古参の v-model から。
親コンポーネント側は v-model というディレクティブに変数を入れるだけで準備完了です(input タグなどと同じ)。
先程に比べると、相当シンプルですね。

親コンポーネント
<Child v-model="num" />

子コンポーネントでは value という名前でプロパティが渡されてきます。更新の際は input イベントを発生させます。

子コンポーネント
export default {
  props: {
    value: Number
  },
  setup (props, { emit }) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      emit('input', props.value + 1)
    }

    return { handleClick }
  }
}

つまり、v-model は以下と同等です。

親コンポーネント
<Child :value="num" @input="num = $event" />

※ 当記事と直接関係ないですが、 v-model について深く知りたい場合は、先日公開された Vue.jsの双方向バインディング再入門 という記事がとても参考になります。

3-2. .sync

さて、もうひとつは Vue 2.3 で加わった .sync 修飾子です。
親コンポーネント側はプロパティを渡す際、後ろに .sync を付け足します。
これも属性がひとつだけなので、すっきりしてますね。

親コンポーネント
<Child :count.sync="num" />

子コンポーネントでのプロパティの受け取り方は通常どおりです。更新の際は update:プロパティ名 のイベントを発生させます。

子コンポーネント
export default {
  props: {
    count: Number
  },
  setup (props, { emit }) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      emit('update:count', props.count + 1)
    }

    return { handleClick }
  }
}

したがって、 .sync 修飾子は、以下の省略形ということになります。

親コンポーネント
<Child :count="num" @update:count="num = $event" />

3-3. Vue 3.0 での変更点

来たる Vue 3.0 では、上記の .sync 修飾子は廃止され、 v-model が引数を取れるようになるそうです。

引用元: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md

Instead of:
<MyComponent v-bind:title.sync="title" />

the syntax would be:
<MyComponent v-model:title="title" />

<MyComponent v-model="xxx" />

<!-- would be shorthand for: -->

<MyComponent
  :model-value="xxx"
  @update:model-value="newValue => { xxx = newValue }"
/>
<MyComponent v-model:aaa="xxx"/>

<!-- would be shorthand for: -->

<MyComponent
  :aaa="xxx"
  @update:aaa="newValue => { xxx = newValue }"
/>

これを見ると、子コンポーネント側の受け取り方と更新方法は今までの .sync 修飾子のやり方に統一されていますね。
v-model に引数がない場合は model-value というプロパティが渡されるので、update:model-value イベントで更新します。

3-4. アンチパターン

プリミティブでない値(配列やオブジェクトなど)を渡した場合は、子コンポーネントから直接中身を変更できてしまいます。

親コンポーネント
<template>
  <div>
    <Child :obj="foo" :arr="bar" :dt="baz" />
  </div>
</template>

<script>
import { ref } from '@vue/composition-api'
import Child from './child.vue'

export default {
  components: { Child },
  setup () {
    const foo = ref({
      num: 0
    })
    const bar = ref([])
    const baz = ref(new Date())

    return { foo, bar, baz }
  }
}
</script>
子コンポーネント
export default {
  props: {
    obj: Object,
    arr: Array,
    dt: Date
  },
  setup (props) {
    // ボタンのイベントハンドラ
    const handleClick = () => {
      props.obj.num += 1 // 親コンポーネントの foo.num に反映される
      props.arr.push(0)  // 親コンポーネントの bar の要素が増加する
      props.arr[0] += 1  // 配列内の値を変更することもできる
      props.dt.setDate(Math.random()) // 日付が変わる
    }

    return { handleClick }
  }
}

イベントの処理を書く手間が省けるので便利に感じるかもしれませんが、どこで値を変えているのかが追いづらく、メンテナンス性が著しく下がってしまうので避けたほうがよいです(経験談)。
子はイベントを発生させるだけにして、親の変数の更新は親の中のみで行ないましょう。

まとめ

Vue.js の新しい記法、 Composition API での親子コンポーネント間で直接データを受け渡しする方法について紹介しました。
冒頭にもまとめましたが、従来の書き方を知っている場合はそれほど大きな変更点はありませんね。setup 関数内での使い方も一度知ってしまえば難しくはないです。ただ分割代入には気をつけましょう。

以前 Vue.js 公式サイトで、親子のコンポーネントの関係は props down, events up という言葉で説明されていました。1 このページで説明してきた内容を端的に表している言葉ですね。

今回の主旨から外れますが、親子間以外でもデータを共有したい場合は store パターン を試してみたり、公式の状態管理ライブラリ Vuex の導入を検討するとよいでしょう。

Vue 3.0 は Composition API 以外にも新機能がたくさんあるので待ち遠しいですね。
それでは。


  1. GitHub をさかのぼってみたら、2 年前の大きな改訂 で消えていました。つい最近見た気がするけど、そんなに前だったとは……。 

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