- 投稿日:2020-04-09T21:45:36+09:00
【感想】【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-sharing、Chart.js、vue-chartjs、Vue Router(グローバルビフォーガイド)、Axios、VeeValidate、vue-loading-overlay、vue-notification、
PHP
Carbon(日付計算)、Factory(自動ダミーデータ生成機能)、helpers(文字列ライブラリ)、Laravel-admin
その他
Font Awesome、SQLite(DB)、JSON Viewer、ngrok(windowsはこちらで対応、1周目は割愛した。)、Basic認証
おわりに
作りながら学べる系の教材としては、かなり難しい部類に入ると思います。僕もまだ1周終えた段階で全然理解できていません。
ですが、これを理解したら何ができるだろうかと妄想を膨らませると非常にワクワクします。笑
※Laravel-Adminは作るアプリによってはToo muchな気もしました。GUIでDB弄れれば問題ない気もします。ここら辺も踏まえて復習していきます。
引き続き頑張ります◎
- 投稿日:2020-04-09T20:56:58+09:00
【初心者向け】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)
の設計思想です。- なんか難しそうなので超簡単にイメージだけ説明します。
(公式ガイドより抜粋)
View
はDOM(Document Object Model)のことで、JavaScriptで扱うHTMLの要素です。Model
はJavaScriptオブジェクトのこと。ViewModel
が重要で、Model
とView
を同期するオブジェクト、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.html
、style.css
、main.js
の3ファイルで作成します。まず完成形から共有
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.cssul { 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.jsnew Vue({ el: '#to-do' });3. 初期値を設定しよう
HTMLにタスクが3つありますが、それらを初期値としてVueで設定します。
main.jsnew 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.cssul > li > .complete { text-decoration: line-through; color: #ddd; }v-model
まず
v-model="todo.isCompleted"
ですが、
v-model
は「双方向データバインディング」を実現するディレクティブです。値には同期させたいデータを指定するので、今回の場合
checkbox
のvalue
となります。
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.jsnew Vue({ el: '#to-do', data: { newTask: '',次にボタンを押したらタスクが追加できるように、クリックイベントを作成します。
index.html<button v-on:click="addTodo()">Add</button>main.jsmethods: { 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.jsmethods: { 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.jsnew 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.cssul { 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; }※参考サイト再掲
下記サイトも参考にしたので、試してみてください。
- 投稿日:2020-04-09T20:22:58+09:00
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もちゃんと入ってるから関数側で色んな事ができますね!!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へのアップデート
- 投稿日:2020-04-09T19:22:40+09:00
[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-token
やclient
などが格納されていないというトラブルが発生しました。
解決すると「こんなことで…」と思いますが、ほぼ1日ハマってしまったので共有です。ぶつかった壁
まずnuxt側ですが、以下のようにloginアクションを実装しています。
login.vueasync 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.rbRails.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 endexposeに
'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.rbRails.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"おわり
- 投稿日:2020-04-09T19:11:52+09:00
propsと$emitでデータを引き渡す
コンポーネント間の基本的なデータの受け渡し方法をまとめます。
$ vue create learn-props-emit $ cd learn-props-emit $ npm run serve $ touch src/components/Child.vueprops
親→子に値を渡す時には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
や引数で送信されたデータを受け取る
- 投稿日:2020-04-09T19:04:32+09:00
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.sync
はv-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-model
はv-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名の制約や、オブジェクト要素の場合の値のマージなどが必要ないので、使いやすいかなと思ってます。参考
以下記事参考にさせて頂きました!!良記事ありがとうございます。
- 投稿日:2020-04-09T17:15:58+09:00
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と同じ扱いになります。
- 投稿日:2020-04-09T10:01:45+09:00
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-samplesTL;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
にも反映される、というリアクティブな性質をもつオブジェクトです。関数内で使用する場合はcomputed
やwatch
で監視する必要があります。子コンポーネント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
など)を指定した場合はちゃんと型がつきますが、Array
やObject
を指定した場合は中身の情報がないので補完されません。子コンポーネント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
が引数を取れるようになるそうです。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 以外にも新機能がたくさんあるので待ち遠しいですね。
それでは。
GitHub をさかのぼってみたら、2 年前の大きな改訂 で消えていました。つい最近見た気がするけど、そんなに前だったとは……。 ↩