【輪読会資料】基礎から学ぶVue.js CHAPTER5 コンポーネントでUI部品を作る 読書メモ
今回は輪読担当ではなかったので、完全な自分のメモですが、アップしておきます。
var で定義できるローカルのコンポーネントもある、
// コンポーネントを定義 var myComponent = { template: '<p>MyComponets</p>' } new Vue({ el: '#app', componets: { // 処理 'myComponet' : myComponet } })
コンポーネント間の通信
P155
親から子
// コンポーネントの定義 comp-child Vue.component('comp-child', { // テンプレートで受け取ったvalを使用 template: '<p>{{ val }}</p>', // 受け取る属性名を指定 props: ['val'] }) // 親コンポーネント new Vue({ el: '#app', data: { valueA: 'これは子A', valueB: 'これは子B' } })
<div id="app"> <comp-child val="これは子A"></comp-child> <comp-child val="これは子B"></comp-child> </div>
出力結果 子コンポーネントの
で囲む状態が出力されている
<p>これは子A</p> <p>これは子B</p>
props
で受け取ってないものは子供側では上書きが基本、class等の属性は親と子でマージされ両方反映
コンポーネントのルートタグ
Vue.component('comp-child', { template: '<p id="child" class="child">ChildComponent</p>', })
コンポーネントのカスタムタグ
<comp-child id="parent" class="parent"></comp-child>
出力される実態
<comp-child id="parent" class="child parent">ChildComponent</comp-child>
// 子 Vue.component('comp-child', { template: '<li>{{ name }} HP.{{ hp }}</li>', props: ['name', 'hp'] }) // 親 new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] } })
list は親を使っている name, hp は 子のprops定義で使える様になっている
<!-- これも親となる comp-child で子を呼び出し--> <ul> <comp-child v-for="item in list" v-bind:key="item.id" v-bind:name="item.name" v-bind:hp="item.hp"></comp-child> </ul>
子コンポーネントで値を書き換えてもconsoleに[Vue warn]が出力される。
当然親の値は書き換えられない
Vue.component('comp-child', { template: '<li>{{ name }} HP.{{ hp }}\ <button v-on:click="doAttack">攻撃する</button></li>', props: ['name', 'hp'], methods: { doAttack: function () { // 勝手に攻撃! this.hp -= 10 // -> [Vue warn] error! } } })
もし、値の書き換えを行いたいなら算出プロパティを使って新しいデータを作成する。
もし親のデータ自体を変更したい場合は、$emit
を使って親にアクションを起こさせる。等する
props
で値を受け取る際は、データ型を指定しておくのが推奨されている。
Vue.component('comp-child', { props: { val: String // 文字列型のみ許可 } ])
型の指定方法一覧
データ型 | 説明 | 例 |
---|---|---|
String | 文字列 | '1' |
Number | 数値 | 1 |
Boolean | 真偽値 | true, false |
Function | 関数 | function() {} |
Object | オブジェクト | { name: 'foo' } |
Array | 配列 | [1, 2, 3], [{ id: 1 }, { id: 2 }] |
カスタム | インスタンス | new Cat() |
null | すべての型 | 1, '1', [1] |
型チェック無しの場合の書き方
Vue.component('example', { props: ['value'] // どんな型も受け入れる })
型チェックする場合の書き方
Vue.component('example', { props: { value: // ここにデータ型を指定 } })
型チェック以外にオプションで様々なバリデーションに対応できる
参照元
ほとんどControllerみたいになってきた。
Vue.component('my-component', { props: { // 基本的な型の検査 (`null` と `undefined` は全てのバリデーションにパスします) propA: Number, // 複数の型の許容 propB: [String, Number], // 文字列型を必須で要求する propC: { type: String, required: true }, // デフォルト値つきの数値型 propD: { type: Number, default: 100 }, // デフォルト値つきのオブジェクト型 propE: { type: Object, // オブジェクトもしくは配列のデフォルト値は // 必ずそれを生み出すための関数を返す必要があります。 default: function () { return { message: 'hello' } } }, // カスタマイズしたバリデーション関数 propF: { validator: function (value) { // プロパティの値は、必ずいずれかの文字列でなければならない return ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } })
子から親
P161
字のデータを親に渡すには$emit
を使う
親のコード
on で受け取る
<child-comp v-on:childs-event="parentMethod">
子のコード
$emitで渡す
this.$emit('childs-event')
parentsMethod
がchilds-event
を通じて親に渡る
<comp-child v-on:childs-event="parentsMethod"></comp-child>
Vue.component('comp-child', { template: '<button v-on:click="handleClick">イベント発火</button>', methods: { // ボタンのクリックイベントのハンドラでchilds-eventを発火する handleClick: function () { // `parentsMethod`が`childs-event`を通じて親に渡る this.$emit('childs-event') } } }) new Vue({ el: '#app', methods: { // childs-eventが発生した! parentsMethod: function () { alert('イベントをキャッチ! ') } } })
親が持つデータを操作する
前述のモンスターのHPを子のメソッドで処理していた例を動くものにしたのが以下の例
良く読み込んで動きはなんとか理解はしたけど、今は書ける気がしない...。
<ul> <comp-child v-for="item in list" v-bind:key="item.id" v-bind:name="item" v-on:attack="handleAttack"></comp-child> </ul>
Vue.component('comp-child', { template: '<li>{{ name }} HP.{{ hp }}\ <button v-on:click="doAttack">攻撃する</button></li>', props: { id: Number, name: String, hp: Number }, methods: { // ボタンのクリックイベントのハンドラから$emitでattackを発火する doAttack: function () { // 引数として自分のIDを渡す v-on:attackの中身`handleAttack`を親に渡す this.$emit('attack', this.id) } } }) new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] }, methods: { // attackが発生した! handleAttack: function (id) { // 引数のIDから要素を検索 var item = this.list.find(function (el) { return el.id === id }) // HPが0より多ければ10減らす if (item !== undefined && item.hp > 0) item.hp -= 10 } } })
カスタムタグのイベントハンドリンク
この書き方だと、コンポーネント側からclick
を$emit
で呼び出さないと発火しないらしい。
<my-icon v-on:click="handleClick"></my-icon>
元々のイベントは直接発火したい場合は.native
修飾子を付ける。
<my-icon v-on:click.native="handleClick"></my-icon>
非親子コンポーネント
親子関係以外だと、this
,props
の通信はできない。
Vueインスタンスを「イベントバス」として使用するが、多用しない方が良い。コードがカオス化するので、詳細はP166参照
子コンポーネントを参照する「$refs」
親コンポ―ネント
<comp-child ref="child">
親のメソッド内
new Vue({ el: '#app', methods: { handleClick: function () { // 子コンポーネントのイベントを発火 this.$refs.child.$emit('open') } } })
子コンポーネント内
Vue.component('comp-child', { template: '<div>...</div>', created: function () { // 自分自身のイベント this.$on('open', function () { console.log('なにか処理') }) } })
親側からはv-on
を使うようにする
コンポーネントの属性のスコープ
コンポーネントの属性の値部分は親のスコープになる
<comp-child v-on:child-event="親のメソッド">
<comp-child v-on:child-event="parentMethod(親のデータ)">
子コンポーネントが引数をもって$emit
を実行している場合、子コンポーネントの引数は$event
変数で使用できる
<comp-child v-on:child-event="parentMethod($event, parantsData)">
new Vue({ el: '#app', data: { parentsData: '親のデータ' }, methods: { methods: { parentsMethod: function(childArg, parentsArg) { // } } } })
$event
変数は$emit
の第一引数しか持たないので、複数の引数を渡す際は1つのオブジェクトにまとめる。
this.$emit('childs-event', {id: 1, name: '新しい名前'})
【輪読会資料】基礎から学ぶVue.js CHAPTER4 データの監視と加工 読書メモ
以下の記事は2019/2/21 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch4 データの監視と加工 (初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER4 データの監視と加工 のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
さすがに輪読担当4回中3回ともなると疲れて来た…。
書籍用のサイトのCHAPTER4記述ページ(サンプルコード有り)
これまでの輪読会資料
CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる
CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita
CHAPTER 3
【輪読会資料】基礎から学ぶVue.js CHAPTER3 イベントとフォーム入力の受け取り 読書メモ - 作りたいものがありすぎる
さきにまとめを書いて全体を把握
- 算出プロパティは結果のキャッシュをしてくれる
- データの状態に対して処理をしたいならウォッチャ
- カスタムディレクティブは監視をしながらDOMの操作ができる
- 更新後のDOMにアクセスしたいならnextTick
リアルタイムにDOMの値を調べて、処理(関数)をして、その結果を画面にリアルタイムに反映できる!という事っぽい。
CHAPTER4 データの監視と加工
SECTION 16 算出プロパティで処理を含むデータを作成する
要は普通に自分で作った関数での処理ができてデータもバインドできるので、リアルタイムな処理が可能という事らしい。
算出プロパティの使い方
function
とあるのでメソッド(関数)っぽいけど、プロパティ(変数・値)
従来散々使ったVueのdata:
と並行した値でcomputed:
を持たせ、そこに各プロパティを定義した関数を入れる構成
computed
はのプロパティ以降は関数で定義する。
マスタッシュ({{ hoge }}
)等でプロパティとして使える
<p>{{ width }} の半分は {{ halfWidth }}</p>
new Vue({ el: '#app', data: { width: 800 }, computed: { // 算出プロパティhalfWidthを定義 halfWidth: function() { return this.width / 2 } } })
算出プロパティを組み合わせて使用しよう
算出プロパティを組み合わせたり、配列を使う事もできる
this.プロパティ名
で他の算出プロパティで定義したり計算した結果を引っ張って利用するとこができる。かなり普通のプログラムっぽい感じになる。
たぶん、画面の高さや幅を算出して、必要な位置に表示物を出す際なんかに強力に使える感じ?
<p>X: {{ halfPoint.x }}</p><!-- 400 --> <p>Y: {{ halfPoint.y }}</p><!-- 300 -->
new Vue({ el: '#app', data: { width: 800, height: 600 }, computed: { halfWidth: function() { return this.width / 2 // 400 }, halfHeight: function() { return this.height / 2 // 300 }, // 「width × height」の中心座標をオブジェクトで返す halfPoint: function() { return { x: this.halfWidth, // 400 y: this.halfHeight // 300 } } } })
ゲッターとセッター
上の例では算出プロパティの値を代入(ユーザーからの入力等)しても実はエラーになってしまう
しかし、セッターもあるので、入力への対応も可能方法はこんな感じ
先週やった相互に値を影響し合える v-model
を使う!
セッターの例 halfWidth
をユーザーフォームにバインディングする
width:<input v-model.number="width"> {{ width }} <br> halfWidth:<input v-model.number="halfWidth"> {{ halfWidth }}
セッターを利用するには以下の様にget
,set
の2つの名前決め打ちのプロパティを定義すること
new Vue({ el: '#app', data: { width: 800 }, computed: { halfWidth: { // width の半分の値を取得する get: function() { return this.width / 2 }, // halfWidth の2倍の数値を width に代入する set: function(val) { this.width = val * 2 } } } })
これをデモすると、2つの入力フォームに数値が入っており、片方の値を入れると、もう片方の値に影響を与える事ができる!
halfWidth:
プロパティ(内の各function)がしていること
get
でwidth
の値を取得して
set
でhalfWidth
の値に代入する
つまり値を順繰りに回しているループしないか不安になるが、これで値をインタラクティブで双方向に連携ができる
算出プロパティのキャッシュ機能
算出プロパティとメソッド(関数)の違い
算出プロパティ リアクティブなデータをキャッシュ化してしまう。
<p>算出プロパティ キャッシュ化されるので一度しか処理が走らず同じ値が出力される</p> <ol> <li>{{ computedData }}</li> <li>{{ computedData }}</li> </ol> <p>メソッド 毎回処理が走る為、値が変わる</p> <ol> <li>{{ methodsData() }}</li> <li>{{ methodsData() }}</li> </ol>
new Vue({ el: '#app', computed: { computedData: function () { return Math.random() } }, methods: { methodsData: function () { return Math.random() } } })
ここがわかった上での使い分けが大事
リストの絞り込みに利用しよう
算出プロパティは内のfunction
は一度だけ処理されキャッシュ化されるので、複雑な処理に向いている
例えば絞り込みリスト等を作る際に便利
<div id="app"> <input v-model.number="budget"> 円以下に絞り込む <input v-model.number="limit"> 件を表示 <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>
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) } } })
一回読み込んだ、mached
処理のキャッシュの上にlimited
の処理が乗ってlimited
のみで算出の修正が行われている事になるらしい。
ソート機能を追加しよう
さらに書籍には 上記にソート機能を追加したコードがのっているが、最初動かなかった。
_.orderBy
ってのが怪しんで、最初はタイポで本来どう書けばいいのか悩む。JSにはない関数でVue.jsの1.X系にはある?でも _.
って何をしている事なのか不明だったが、よく見るとLodash
入れろとあった…。だめじゃん、俺
ということでLodash
を入れる。この1行追加
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.5/lodash.min.js"></script>
<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>
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) }, // sortedを新しく追加 この _.orderBy ってのが Lodash のメソッドらしい // https://lodash.com/docs/#orderBy sorted: function() { return _.orderBy(this.matched, 'price', this.order ? 'desc' : 'asc') }, // limitedで使用するリストをsortedに変更 limited: function() { return this.sorted.slice(0, this.limit) } } })
Lodashの_.orderBy
関数。
_.orderBy(collection, [iteratees=[_.identity]], [orders])
今回の例の第三引数はthis.order ? 'desc' : 'asc'
三項演算子としている。
SECTION 17 ウォッチャでデータを監視して処理を自動化する
ウォッチャとは特定のデータ、算出プロパティの状態を監視して、データに変化があった際に処理を実行してくれるフック機能のこと
ウォッチャ自体は値を返さない
ウォッチャの使い方
コンポーネントにwatch
を用意して、監視対象の名前と、変化した際の処理を書く
第二引数は無くてもOK
オプションを使用しない場合
new Vue({ // ... watch: { 監視するデータ: function (新しい値, 古い値) { // valueが変化したときに行いたい処理 }, 'item.value': function (newVal, oldVal) { // オブジェクトのプロパティも監視できる } } })
オプションを使用する場合
プロパティ | 値 | 説明 |
---|---|---|
deep | bool | ネストされたオブジェクトも監視 |
immdiate | bool | 読み込み時に即呼び出し |
new Vue({ // ... watch: { list: { handler: function (newVal, oldVal) { // listが変化したときに行いたい処理 }, // ここからがオプション deep: true, immediate: true } } })
インスタンスメソッドでの登録
this.$watch
と書くと各メソッド内でウォッチャが使える
this.$watch('value', function(newVal, oldVal) { // ... })
こんな風に書く,オプションの位置が独特
created: function () { this.$watch('value', function () { // ...処理をかく }, { immediate: true, deep: true }) }
ウォッチャの解除
インスタンスメソッドで登録した場合、返り値を使って監視を解除できる
var unwatch = this.$watch('value', handler) unwatch() // value の監視を解除
一度だけ動作するウォッチャ
unwatch
を利用する
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 }) } })
実行頻度の制御
フォーム入力などで監視を使うと、高頻度で値が変わるので、非同期通信が度々発生してパフォーマンスに影響する。その為setTimeout
やLodash
などを利用してウォッチャの実行頻度を制御すると良い。
watch: { // "_." なのでLodashですね 指定ミリ秒後にコールバック呼び出しをする value: _.debounce(function (newVal) { // ここへコストの高い処理を書く console.log(newVal) }, // valueの変化が終わるのを待つ時間をミリ秒で指定 500) }
デモ、 最後のミリ秒経過後に、フォーム入力後の値がconsoleに出力される
<input type="text" v-model="value">
new Vue({ el: '#app', data: { value: '編集してみてね' }, watch: { value: _.debounce(function (newVal) { // ここへコストの高い処理を書く console.log(newVal) }, // valueの変化が終わるのを待つ時間をミリ秒で指定 1500) } })
複数の値を監視する
インスタンスメソッドを使い監視対象を関数にして登録する。
this.$watch(function () { return [this.width, this.height] }, function(){ // 上で言ってる関数ってここの funtionの事? // width , height が変化した際の処理 })
解らん場合はリファレンス
Vue.js インスタンスメソッド / データ
vm.$watch( expOrFn, callback, [options] )
今回は時間がないけど、余裕があるならリファレンスを書籍を行ったり来たりすると理解は進みそう。
オプションに登録する場合は、算出プロパティを監視することができる
computed: { watchTarget: function () { return [this.width, this.height] } }, // ここからオプション watch: { // これで算出プロパティを監視できる watchTarget: function() { ... } }
POINT
大事そうだけど省略
フォームを監視してAPIからデータを取得しよう
GitHub APIからリポジトリ一覧を取得するかんたんなアプリのデモ
axios
のAJAX(非同期通信)のgetでAPIからの値を取得して画面に表示している。
<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>
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)) } }, })
htmlのv-model="current"
になんでもかんでも入れてる感じ?
開発コンソールを開きNetwork
タブにあるgetパラメータをクリックすれば
取得した内容が見れる
要は以下の様なURLを生成している
https://api.github.com/search/repositories?q=topic:vue
https://api.github.com/search/repositories?q=topic:jQuery
Vue内のcurrent:
では以下のGETのパラメータを生成している
q=topic:vue
q=topic:jQuery
で、帰ったJSONをlist
に入れて、html側でv-for="item in list
のfor文で回して
items
内の配列の中に存在する項目full_name
をhtml側で表示している。
SECTION 18 フィルタでテキストの変換処理を行う
ここでいう「フィルタ」とは、文字数を丸める。数字にカンマを入れる。等のテキストベースの変換処理を指す。
注意: ローカルに登録した場合でもthis
へのアクセスはできない
フィルタの使い方
{{ Mustache }} か、v-bind
ディレクティブに |
で繋げて呼び出せる
<!-- Mustashのケース --> {{ 対象のデータ | フィルタの名前 }} <!-- v-bindのケース --> <div v-bind:id="対象のデータ| フィルタの名前"></div>
ローカルへの登録
コンポーネントのfilters
オプションに登録することで特定のコンポーネント内のみで仕様できる
new Vue({ el: '#app', data: { price: 19800 }, filters: { localeNum: function (val) { return val.toLocaleString() } } })
{{ price | localeNum }}円
こんな感じにつかえる
出力例 カンマ付き
19,800円
グローバルへの登録
グローバルメソッドVue.filter
に登録すると全てのコンポーネントから利用できる
Vue.filter('localeNum' ,function(val){ return val.toLocaleString() })
フィルタに引数を持たせる
引数を持たせることもできる
{{ message | filter(foo, 100) }}
この例は
第一引数message
プロパティの値
第二引数foo
プロパティの値
第三引数100
が受け取れる
複数のフィルタをつなげて使用する
{{ value | filter1 | filter2 }}
new Vue({ el: '#app', filters: { // 小数点以下を第2位に丸めるフィルタ round: function (val) { return Math.round(val * 100) / 100 }, // 度からラジアンに変換するフィルタ radian: function (val) { return val * Math.PI / 180 } } })
180 度は {{ 180 | radian | round }} ラジアンだよ
こんな風に180度のラジアンを出して、それを四捨五入表記にできる
SECTION 19 力スタムディレクティブでデータを監視しながらDOMを操作する
v-bind
の様なディレクティブを自作できる機能、DOMのAPIを使いたいケース等で、DOMの状態を検知しながらDOM操作ができる様になる。仮想DOMではないので描画の最適化はされない。
カスタムディレクティブの使い方
こんな風に任意の名前を付けられる?
<div v-hoge> </div>
トリガとなるデータを値としてバインドすることもできる
<!-- valueが変化すれば呼び出される --> <div v-hoge="value"> </div>
ローカルへの登録
カスタムディレクティブはdirectives:
オプションに登録すると特定のコンポーネントで使える
new Vue({ el: '#app', directives: { focus: { // 紐付いている要素がDOMに挿入されるとき inserted: function (el) { el.focus() // 要素にフォーカスを当てる(文字入力待ち状態) } } } })
<input type="text"><!-- フォーカスされない --> <input type="text" v-focus><!-- フォーカスされる -->
上のデモをすると入力フォームの二番目に |
の点滅が入り、文字入力受付状態が確認できる。
グローバルへの登録
グローバルメソッドに登録する方法はVue.directive
メソッドを使う
(コンポーネント以外でどこでも使う汎用的なものとかを登録すると良いかも?)
Vue.directive('forcus', { inserted: function (el) { el.focus() // 要素にフォーカスを当てる(文字入力待ち状態) } })
使用可能なフック
メソッド名 | タイミング |
---|---|
bind | ディレクティブが初めて要素と紐づいた時に |
inserted | 紐づいた要素が親Nodeに挿入された時に |
update | (仮想DOM更新時)紐づいた要素を包含しているコンポーネントのVNodeが更新された時に |
componentUpdated | (仮想DOM更新時)包含しているコンポーネントと子コンポーネントのVNodeが更新された時に |
unbind | 紐づいていた要素からディレクティブが削除される時に |
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') } })
フックの引数
vnode
から呼び出したコンポーネントを参照できるが難しいらしいので割愛。
『公式ガイド、仮想DOMを読め』とある
引数 | 内容 |
---|---|
el | 以下省略、書籍参照のこと |
binding | |
vnode | |
oldVnode |
フックの関数による省略方法
第二引数に関数を入れると bind
と update
にフックして同じ処理を呼び出せる。
Vue.directive('example', function(el, vnode, oldVnode) { // bind と updateで呼び出される }
動画の再生を操作する例
<!-- 動画1 --> <video src="demo1.mp4" v-video="video1" width="400"></video> <button v-on:click="video1=true">再生</button> <button v-on:click="video1=false">停止</button> <br> <!-- 動画2 --> <video src="demo2.mp4" v-video="video2" width="400"></video> <button v-on:click="video2=true">再生</button> <button v-on:click="video2=false">停止</button>
new Vue({ el: '#app', data: { video1: false, video2: false }, directives: { video(el, binding) { binding.value ? el.play() : el.pause() } } })
上記のコードをデモすると2つの動画をボタンで再生・停止できるようになる
基本的な読み方がいまだあいまいなので解説
html中のv-video
でバインドしたvideo1
の値はまず,スクリプト内でfalseに設定されている
video1: false,
再生ボタンを押すとvideo1
の値がhtml側でtrue
にされる
その値はスクリプト側のでdirectives:
のhtmlと紐づけたvideo( ... )
で監視されていて
el
で渡ったの引数に収納されたboolに対して、play()
,またはpause()
メソッドを実行している
という感じか?
ただし、この実装だと、video1のプロパティを変更すると、video2プロパティの要素のディレクティブも一緒に呼び出されるので、複雑な処理を書く際には不向きらしい。
次の方法はこれを改善できるという
前の状態と比較して処理を行う
力スタムディレクティブはメソッドの引数に古いVnodeと古いバインドデータを受け取れるので、バインドしたデータに変化があった際のみに処理を行わせることが可能。
arg | 引数 |
---|---|
modifiers | 修飾子のオブジェクト |
value | 新しい値 |
odlvalue | 古い値(updateとcomponetUpdateのみ) |
では先ほどの動画デモを修正してoldValue
プロパティの比較で無駄な処理を走らせないように改修したのが下の例
new Vue({ el: '#app', data: { video1: false, video2: false }, directives: { video(el, binding) { // ここに oldValue での判定を加えた if (binding.value !== binding.oldValue) { binding.value ? el.play() : el.pause() } } } })
SECTION 20 nextTickで更新後のDOMにアクセスする
非同期処理が走ると、JSのコードの中では処理が遅れて、更新状態を判定できない事がままある。 (なので、非同期じゃないPHPなんかでPOSTとかすると、値が返るまでそこで処理が止まる。)
特にJSで非同期でDOM絡みの操作を書くと、非同期でPOST、GETをして値を取ったので、JS内で監視してても、それは先にプログラムが走り終わってるので、取得した値を元に処理ができない!
なんてことが発生してしまう。それを何とかしてくれるのがnextTick
らしい
DOM更新を待ち受けて処理をしてくれるそうだ
nextTickの使い方
方法1, グローバルメソッドVue.nextTick
のコールバック関数として定義する。
方法2, インスタンスメソッドとしてthis.$nextTick
として使う。
this.$nextTick(function(){ // DOM更新後に行いたい処理を書く })
これで、関数内の処理がDOM更新後に行うように予約できる
更新後のDOMの高さを取得しよう
例えば、プルダウンのリストの高さを通常のVue.js的な書き方で取得しようとした場合。
DOMへのアクセスをするなら$refs
を使う
console.log("通常:", this.$refs.list.offsetHeight);
CH2のおさらい ここから------
DOMにアクセスするには、インスタンスプロパティ$el
,$refs
を使用する。
但し、ライフサイクルフックのmounted
以降でないと使えない
$refsの使い方
<div id="app"> <p ref="hello">hello</p> <!-- p要素にhelloと名を付けた --> </div>
以下の様にアクセス
new Vue({ el: '#app', mounted: function() { console.log(this.$refs.hello) // p要素のDOMとなる } })
$el や$refsは一時的な変更です!
これらは仮想DOMではないので描画処理の最適化をしない
操作の都度描画するので注意
CH2のおさらい ここまで------
でも、これだと処理の後にDOMが追加されてもその値を拾う事ができない。
こんなデモがある
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); }); } } });
<!-- list. の高さは最初は0なので数値1からを出力させる処理が,実はここに書いてある --> <button v-on:click="list.push(list.length+1)">追加</button> <ul ref="list"> <li v-for="item in list">{{ item }}</li> </ul>
開発タブでconsole見てボタンを押して<li>
を追加しても 通常:
の方の値は0となって取得できてない。が、$nextTick
を使った方は上書きしたDOMからの値が取得できている
nextTickの注意点:
ウェブフォント、画像が含まれていた場合、それらのロード(詳細情報?)は持たないので、画像の高さは決め打ちにして計算して使う。といったテクニックが必要になる。ということらしいです。
まとめ
- 算出プロパティは結果のキャッシュをしてくれる
- データの状態に対して処理をしたいならウォッチャ
- カスタムディレクティブは監視をしながらDOMの操作ができる
- 更新後のDOMにアクセスしたいならnextTick
Homestead環境で複数環境がある場合、外部から接続するアプリを選択する際の小技
先に結論
homestead(Laravel Homestead', '7.12.0)で複数のアプリを設定して外部端末から公開ipアドレスでアクセスする場合。
sites: の -map に指定するドメインのアルファベットの最昇順のサイトが表示される。
sites: - map: whois.test to: /home/vagrant/code/Laravel/public schedule: true - map: zero.test to: /home/vagrant/zero/Laravel/public # アルファベット順に最初のこれが外部アクセスからのデフォルト表示となる。 - map: aaa.test to: /home/vagrant/hoge/Laravel/public # 省略 networks: - type: "public_network" ip: "192.168.11.99" bridge: "en1: Wi-Fi (AirPort)"
こんな方法で設定するのは不本意なのですが、有志の日本語ドキュメントの以下の方法では上手くいきませんでした。
そもそもの話
windows10でvagrantのHomestead環境を使ってLaravelの開発をしていますが、複数のアプリケーションのプロジェクトを動かしているため、Homestead.yamlとhostに複数のアプリの環境を設定しています。
詳しくは以前の記事を参照の事 windows vagrant Homestead環境でLaravelアプリを追加する際の覚書 - 作りたいものがありすぎる
以下の例の様に複数のアプリの環境を設定できる。
Homested.yaml
folders: - map: C:/Vagrant/Whois to: /home/vagrant/code - map: C:/Vagrant/zero to: /home/vagrant/zero sites: - map: whois.test to: /home/vagrant/code/Laravel/public schedule: true - map: zero.test to: /home/vagrant/zero/Laravel/public
また、詳細は省きますがwindowsのhosts
ファイルにもローカルドメインとipを設定します。
この特定のアプリをローカルネットワーク(wi-fi環境)で他のマシンからも開発環境が見れるようにする必要がありましたので、これまではHomested.yaml
ファイルに以下の様な記述をすることで、他のPCから閲覧できるようにしていました。
Homested.yaml
networks: - type: "public_network" ip: "192.168.11.99" bridge: "en1: Wi-Fi (AirPort)"
上記の設定の192.168.11.99
に別のPCからアクセスすれば、これまで問題なく閲覧出来ていたのですが、先日の記事を書いて以降、意図しない別の開発環境が表示されてしまい、困った事になりました。
しかしこれまで、意図したアプリの画面が出ていたことがむしろ偶然の幸運で、複数アプリを設定してる場合、外部からアクセス可能なipでどのアプリが出るかを設定項目はyamlには書いて無い訳です。従来はたまたま偶然に望みのアプリが出ていた。という訳です。
そこで、どうすれば意図したアプリを外部から接続するか調べましたが、上手く行かず結論として上記の様なかなりその場しのぎの方法となりました。
環境の共有
共同作業者やクライアントと、現在作業中の内容を共有したい場合もあるでしょう。Vagrantには、vagrant shareにより、これをサポートする方法が組み込み済みです。しかし、この方法はHomestead.yamlファイルに複数サイトを設定している場合には動作しません。この問題を解決するため、Homesteadは独自のshareコマンドを持っています。使用を開始するには、vagrant sshによりHomesteadマシンとSSH接続し、share homestead.testを実行してください。これにより、Homestead.yaml設定ファイルのhomestead.testサイトが共有されます。もちろん、homestead.testの代わりに他の設定済みサイトを指定できます。
share homestead.test
とあるので、上記の通りshare whois.test
とか打った所以下の様な画面が表示されました。
vagrant@homestead:~$ share whois.test ngrok by @inconshreveable (Ctrl+C to quit) Session Status connecting Session Status online Sesion Expires 7 hours, 58 minutes Versionerface 2.2.8 10:4040 R gio United States (us) Web Interface http://192.168.10.10:4040t5 p50 p90 Forwarding http://a3e2a379.ngrok.io -> localhost:80 Forwarding https://a3e2a379.ngrok.io -> localhost:80 Connections ttl opn rt1 rt5 p50 p90 0 1 0.00 0.00 0.00 0.00 HTTP Requests ------------- GET /favicon.ico 200 OK
しかし、望みのサイトはip 192.168.11.99 や 192.168.10.10:4040t5 のいずれにアクセスしても他のアプリがでてきたり、アクセスできなかったりでした。
また、コンソールに表示されたhttps://a3e2a379.ngrok.io
にもアクセスしましたが、cssが適用されない画面であるうえ、レスポンスが大変遅く意図したもにはなりえません。
という事で困ったなー。でも外部から見た際に複数アプリがある場合、そもそもどういう法則で、今のアプリが表示されるんだろう?と悩ませている際にたまたま閃いて試したらドンピシャだったという訳です。
でもいずれ複数のアプリで外部PCからアクセスしたくなった場合は…。
これ以上良い方法がない場合はいちいちyamlフィアル書き換えるって事で対処します。
Laravelのブラウザtest duskとDBtestを混在させる場合に use RefreshDatabase;を使ってハマった話
Laravelでブラウザテストをしていますが、testの際のシナリオとして、以下の様な検証をするケースがありました。
- DBに値を入れない状態でtest開始
- いくつかのtestを行う。
- あるtableにレコードを入れた状態で同様のtestを行い表示の確認
tableが0件の状態とレコードがある状態で表示が変わるので、その検証。という事です。
ところが、テストの各項目の都度、 setUp() メソッドに書かれた refresh:migrate
, db:seed
とかいちいちやっていると時間がかかるので、以前の記事にある様に、初回だけ やって、後は現状のDBを使ったまま破綻しないDB操作の手順を考えつつtestを書く。という事をしてました。
<?php protected static $db_inited = false; use RefreshDatabase; protected static function initDB() { Artisan::call('migrate:refresh'); // 個別でシーディング Artisan::call('db:seed', ['--class' => 'CommunitiesTableSeeder']); Artisan::call('db:seed', ['--class' => 'CommunitiesUsersStatusesTableSeeder']); Artisan::call('db:seed', ['--class' => 'CommunityUserTableSeeder']); Artisan::call('db:seed', ['--class' => 'MacAddressesTableSeeder']); Artisan::call('db:seed', ['--class' => 'RolesTableSeeder']); Artisan::call('db:seed', ['--class' => 'RoutersTableSeeder']); Artisan::call('db:seed', ['--class' => 'UsersTableSeeder']); // Tumolink Tableは後で検証するので今は使わない // Artisan::call('db:seed', ['--class' => 'TumolinkTableSeeder']); } public function setUp() { parent::setUp(); // 以前の記事にもある通り、testの初回だけシーディングを実施 if (!static::$db_inited) { static::$db_inited = true; static::initDB(); } } // 以下省略 }
さて、上記の様な検証をしようと思っていざ以下の様なtestを書いた所、DBに値が入らないままブラウザtestが実施されて散々悩みました。
<?php /** * @test */ public function 未ログインで一覧画面表示のテスト() { // 検証用のデータを入れる factory(Tumolink::class)->create([ 'community_user_id' => 4, ]); factory(Tumolink::class)->create([ 'community_user_id' => 5, ]); factory(Tumolink::class)->create([ 'community_user_id' => 30, ]); // 入った検証データが表示される筈なので検証、しかしエラーとなる $this->browse(function (Browser $browser) { $browser->visit('/') ->assertSee('Tumolinkレコードが入った事で表示される文言'); }); // クエリも書かれずなぜかこのassertは通る $this->assertDatabaseHas('tumolink', ['community_user_id' => 30]); }
で、logを追うと、
2019-02-14T05:37:21.443929Z 90 Query START TRANSACTION 2019-02-14T05:37:21.574578Z 90 Prepare insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?) 2019-02-14T05:37:21.575225Z 90 Execute insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (4, '2019-02-14 15:37:21', '2019-02-14 15:37:21', 1, '2019-02-09 14:37:21', '2019-02-14 14:37:21') 2019-02-14T05:37:21.575591Z 90 Close stmt 2019-02-14T05:37:21.576674Z 90 Prepare insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?) 2019-02-14T05:37:21.577048Z 90 Execute insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (5, '2019-02-14 15:37:21', '2019-02-14 15:37:21', 1, '2019-02-09 14:37:21', '2019-02-14 14:37:21') 2019-02-14T05:37:21.579742Z 90 Close stmt 2019-02-14T05:37:21.581257Z 90 Prepare insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?) 2019-02-14T05:37:21.582885Z 90 Execute insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (30, '2019-02-14 15:37:21', '2019-02-14 15:37:21', 1, '2019-02-09 14:37:21', '2019-02-14 14:37:21') 2019-02-14T05:37:21.583499Z 90 Close stmt 2019-02-14T05:37:23.829384Z 91 Connect homestead@localhost on whois_test using TCP/IP 2019-02-14T05:37:23.832003Z 91 Query use `whois_test` 2019-02-14T05:37:23.833594Z 91 Prepare set names 'utf8mb4' collate 'utf8mb4_unicode_ci' 2019-02-14T05:37:23.833821Z 91 Execute set names 'utf8mb4' collate 'utf8mb4_unicode_ci' 2019-02-14T05:37:23.834181Z 91 Close stmt 2019-02-14T05:37:23.834538Z 91 Prepare set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2019-02-14T05:37:23.834770Z 91 Execute set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2019-02-14T05:37:23.834991Z 91 Close stmt 2019-02-14T05:37:23.835343Z 91 Prepare select `url_path` from `communities` where `url_path` = ? limit 1 2019-02-14T05:37:23.836422Z 91 Execute select `url_path` from `communities` where `url_path` = 'hoge' limit 1 2019-02-14T05:37:23.836692Z 91 Close stmt 2019-02-14T05:37:24.005278Z 91 Prepare select * from `communities` where `url_path` = ? limit 1 2019-02-14T05:37:24.005837Z 91 Execute select * from `communities` where `url_path` = 'hoge' limit 1 2019-02-14T05:37:24.006340Z 91 Close stmt 2019-02-14T05:37:24.046781Z 91 Prepare select `user_id` from `communities` where `id` = ? 2019-02-14T05:37:24.047358Z 91 Execute select `user_id` from `communities` where `id` = 1 2019-02-14T05:37:24.047703Z 91 Close stmt 2019-02-14T05:37:24.081444Z 91 Prepare select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?) 2019-02-14T05:37:24.081937Z 91 Execute select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 1) 2019-02-14T05:37:24.082748Z 91 Close stmt 2019-02-14T05:37:24.116417Z 91 Prepare select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?) 2019-02-14T05:37:24.116823Z 91 Execute select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 0) 2019-02-14T05:37:24.117543Z 91 Close stmt 2019-02-14T05:37:24.153092Z 91 Prepare select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> ? and `community_id` = ? and `hide` = ?) 2019-02-14T05:37:24.153243Z 91 Execute select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> 1 and `community_id` = 1 and `hide` = 0) 2019-02-14T05:37:24.153519Z 91 Close stmt 2019-02-14T05:37:24.178974Z 91 Prepare select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = ?) and `community_user`.`user_id` in (?, ?, ?, ?, ?) order by `last_access` desc 2019-02-14T05:37:24.179504Z 91 Execute select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = 1) and `community_user`.`user_id` in (4, 9, 10, 11, 12) order by `last_access` desc 2019-02-14T05:37:24.180164Z 91 Close stmt 2019-02-14T05:37:24.204641Z 91 Prepare select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = ? 2019-02-14T05:37:24.205081Z 91 Execute select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = 1 2019-02-14T05:37:24.205533Z 91 Close stmt 2019-02-14T05:37:24.925167Z 91 Quit 2019-02-14T05:37:25.928286Z 90 Query ROLLBACK 2019-02-14T05:37:25.933217Z 90 Quit
DBに検証用の値を入れるのは確認できます。その後、該当ページを表示して検証する際に呼ばれるクエリを読んだ直後にROLLBACKが走っています。
そしてそのあとに本来であれば、DBの値を検証する
$this->assertDatabaseHas('tumolink', ['community_user_id' => 30]);
に相当するクエリが走るべきなのですが、これをlogで確認できないまま、次のtestのクエリが走っていました。
で、色々なやんだ結果testクラスの上に書くこいつを消したところ上手くいきました。
// use RefreshDatabase;
SQL LOG
2019-02-14T05:45:25.585096Z 124 Connect homestead@localhost on whois_test using TCP/IP 2019-02-14T05:45:25.585600Z 124 Query use `whois_test` 2019-02-14T05:45:25.585867Z 124 Prepare set names 'utf8mb4' collate 'utf8mb4_unicode_ci' 2019-02-14T05:45:25.586105Z 124 Execute set names 'utf8mb4' collate 'utf8mb4_unicode_ci' 2019-02-14T05:45:25.586397Z 124 Close stmt 2019-02-14T05:45:25.586579Z 124 Prepare set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2019-02-14T05:45:25.586777Z 124 Execute set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2019-02-14T05:45:25.586958Z 124 Close stmt 2019-02-14T05:45:25.587218Z 124 Prepare insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?) 2019-02-14T05:45:25.587477Z 124 Execute insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (4, '2019-02-14 15:45:25', '2019-02-14 15:45:25', 1, '2019-02-09 14:45:25', '2019-02-14 14:45:25') 2019-02-14T05:45:25.591928Z 124 Close stmt 2019-02-14T05:45:25.592896Z 124 Prepare insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?) 2019-02-14T05:45:25.593254Z 124 Execute insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (5, '2019-02-14 15:45:25', '2019-02-14 15:45:25', 1, '2019-02-09 14:45:25', '2019-02-14 14:45:25') 2019-02-14T05:45:25.594983Z 124 Close stmt 2019-02-14T05:45:25.595811Z 124 Prepare insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (?, ?, ?, ?, ?, ?) 2019-02-14T05:45:25.596246Z 124 Execute insert into `tumolink` (`community_user_id`, `maybe_arraival`, `maybe_departure`, `google_home_push`, `created_at`, `updated_at`) values (30, '2019-02-14 15:45:25', '2019-02-14 15:45:25', 1, '2019-02-09 14:45:25', '2019-02-14 14:45:25') 2019-02-14T05:45:25.597019Z 124 Close stmt 2019-02-14T05:45:27.846052Z 125 Connect homestead@localhost on whois_test using TCP/IP 2019-02-14T05:45:27.848972Z 125 Query use `whois_test` 2019-02-14T05:45:27.850521Z 125 Prepare set names 'utf8mb4' collate 'utf8mb4_unicode_ci' 2019-02-14T05:45:27.850730Z 125 Execute set names 'utf8mb4' collate 'utf8mb4_unicode_ci' 2019-02-14T05:45:27.850912Z 125 Close stmt 2019-02-14T05:45:27.851136Z 125 Prepare set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2019-02-14T05:45:27.851316Z 125 Execute set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION' 2019-02-14T05:45:27.851492Z 125 Close stmt 2019-02-14T05:45:27.851773Z 125 Prepare select `url_path` from `communities` where `url_path` = ? limit 1 2019-02-14T05:45:27.852839Z 125 Execute select `url_path` from `communities` where `url_path` = 'hoge' limit 1 2019-02-14T05:45:27.853121Z 125 Close stmt 2019-02-14T05:45:28.046130Z 125 Prepare select * from `communities` where `url_path` = ? limit 1 2019-02-14T05:45:28.046618Z 125 Execute select * from `communities` where `url_path` = 'hoge' limit 1 2019-02-14T05:45:28.047275Z 125 Close stmt 2019-02-14T05:45:28.091173Z 125 Prepare select `user_id` from `communities` where `id` = ? 2019-02-14T05:45:28.091653Z 125 Execute select `user_id` from `communities` where `id` = 1 2019-02-14T05:45:28.092026Z 125 Close stmt 2019-02-14T05:45:28.127509Z 125 Prepare select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?) 2019-02-14T05:45:28.128013Z 125 Execute select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 1) 2019-02-14T05:45:28.128830Z 125 Close stmt 2019-02-14T05:45:28.161408Z 125 Prepare select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = ? and `current_stay` = ?) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> ? and `community_id` = ? and `provisional` = ?) 2019-02-14T05:45:28.162015Z 125 Execute select `user_id`, `unique_name`, `name`, `min_arraival_at`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` inner join (select community_user_id, min(arraival_at) as min_arraival_at from `mac_addresses` where (`hide` = 0 and `current_stay` = 1) group by `community_user_id` order by `min_arraival_at` desc) as `mac_addresses` on `community_user`.`id` = `mac_addresses`.`community_user_id` where (`user_id` <> 1 and `community_id` = 1 and `provisional` = 0) 2019-02-14T05:45:28.162965Z 125 Close stmt 2019-02-14T05:45:28.188154Z 125 Prepare select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> ? and `community_id` = ? and `hide` = ?) 2019-02-14T05:45:28.188561Z 125 Execute select `user_id` from `community_user` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` where (`user_id` <> 1 and `community_id` = 1 and `hide` = 0) 2019-02-14T05:45:28.189100Z 125 Close stmt 2019-02-14T05:45:28.231102Z 125 Prepare select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = ?) and `community_user`.`user_id` in (?, ?, ?, ?, ?) order by `last_access` desc 2019-02-14T05:45:28.231659Z 125 Execute select `user_id`, `name`, `last_access` from `community_user` left join `users` on `users`.`id` = `community_user`.`user_id` inner join `communities_users_statuses` on `communities_users_statuses`.`id` = `community_user`.`id` where (`community_id` = 1) and `community_user`.`user_id` in (4, 9, 10, 11, 12) order by `last_access` desc 2019-02-14T05:45:28.232347Z 125 Close stmt 2019-02-14T05:45:28.258731Z 125 Prepare select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = ? 2019-02-14T05:45:28.259224Z 125 Execute select `tumolink`.*, `users`.`name`, `users`.`name_reading`, `users`.`provisional`, `communities_users_statuses`.`hide` from `tumolink` inner join `community_user` on `community_user`.`id` = `tumolink`.`community_user_id` inner join `communities_users_statuses` on `community_user`.`id` = `communities_users_statuses`.`id` inner join `users` on `users`.`id` = `community_user`.`user_id` where `community_user`.`community_id` = 1 2019-02-14T05:45:28.260029Z 125 Close stmt 2019-02-14T05:45:29.048109Z 125 Quit 2019-02-14T05:45:29.981275Z 124 Prepare select count(*) as aggregate from `tumolink` where (`community_user_id` = ?) 2019-02-14T05:45:29.981864Z 124 Execute select count(*) as aggregate from `tumolink` where (`community_user_id` = 30) 2019-02-14T05:45:29.982288Z 124 Close stmt
‘use RefreshDatabase‘が無いため当然ROLLBACKはかかりません。また、DBの値を検討する以下のクエリもlogの最後の方に見られます。
2019-02-14T05:45:29.981864Z 124 Execute select count(*) as aggregate from `tumolink` where (`community_user_id` = 30)
RefreshDatabase の動きですが、有志作成の日本語リファレンスでは以下の様にあります。
https://readouble.com/laravel/5.5/ja/database-testing.html
各テスト後のデータベースリセット 前のテストがその後のテストデータに影響しないように、各テストの後にデータベースをリセットできると便利です。インメモリデータベースを使っていても、トラディショナルなデータベースを使用していても、RefreshDatabaseトレイトにより、マイグレーションに最適なアプローチが取れます。テストクラスてこのトレイトを使えば、全てが処理されます。
ところが実際に動かしてSQLのLOGを見ると、どうもtestのfunction単位でrollbackが発生するのでは無いのは明らかです。
このような動きの為、ブラウザの表示をして確認が取れる前に rollback が走ってせっかく挿入したレコードが消え、その後ブラウザの表示が完了して検証をする。といった動作の為、testが失敗する様です。
<?php public function 未ログインで恵比寿_滞在者一覧画面閲覧_ツモリスト有り() { factory(Tumolink::class)->create([ 'community_user_id' => 30, ]); $this->browse(function (Browser $browser) { $browser->visit('/') ->assertSee('Tumolinkレコードが入った事で表示される文言'); }); // RefreshDatabase を使うとここでrollbackが発生した上 // select count(*) as aggregate from `tumolink` where (`community_user_id` = 30) のクエリも走らず次のtestに行く $this->assertDatabaseHas('tumolink', ['community_user_id' => 30]); }
ではなぜ?という細かい所までは追ってませんが、ひとまずこんなハマり所があるので気を付けましょう。という話でした。
【輪読会資料】基礎から学ぶVue.js CHAPTER3 イベントとフォーム入力の受け取り 読書メモ
以下の記事は2019/2/14 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch3 イベントとフォーム入力 (初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER3 イベントとフォーム入力の受け取り のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
CHAPTER3 イベントとフォーム入力の受け取り
ちなみに Weeybleで去年同じ本の輪読会で使ったドキュメント、およびソースコードは以下にありました。
GitHub yasugahira0810/vuejs_chapter3
書籍用のサイトのCHAPTER3記述ページ(サンプルコード有り)
これまでの輪読会資料
CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる
CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita
SECTION 13 イベントハンドリング
イベントハンドラ
これまでのサンプルのボタンに出てた v-on
の事をここでは解説する
イベントに紐づける処理の内容をこの本では「イベントハンドラ」と呼び
イベントハンドラとイベントを紐づけることを「ハンドル」と呼ぶ
イベントはmousewheel
等IE9では動かないものもあるので注意
<button v-on:click="doRemove(index)">モンスターを削除</button>
@
で記述も可能
<button @click="doRemove(index)">モンスターを削除</button>
new Vue({ el: '#app', data: { }, methods: { doRemove: function (index) { // ボタンクリックでこの処理が走る this.list.splice(index, 1) } } })
click
同様ブラウザが対応していれば以下のイベントも使える
- scroll
- mousewheel
フォーム入力の取得
v-on
ディレクティブで入力内容を確認してからデータに代入することができる
<input v-bind:value="message" v-on:change="handleInput">
new Vue({ el: '#app', data: { message: 'Hello Vue.js', }, methods: { handleInput: function (event) { // 代入前に何か処理を行う… // バリデード処理とかできるのかな? this.message = event.target.value } } })
イベント修飾子
click などのDOMのふるまいを変更する
- .stop event.stopPropagation(); イベント伝播,バブリングを止める
- .prevment event.preventDefault(); 禁止操作の指定 リンク操作、
submit
の処理をキャンセル - .capture キャプチャーモードDOMイベントをハンドルする
- .self
- .native
- .once
- .passive { passive: true } でDOMイベントはハンドルする
クリックイベント マウスボタンを指定できる
- .left
- .right
- .middle
作例
<!-- いずれもhandlerメソッドでマウス右クリックでconsole.logにmouse event のlogを出力する --> <div v-on:click.right="handler">example</div> <!-- こっちは`.prevment`修飾子で右クリックメニューの表示を禁止している --> <div v-on:click.right.prevent="handler">example</div>
new Vue({ el: '#app', methods: { handler: function (comment) { console.log(comment) } } })
Extra DOMイベント伝播,バブリングについて
そもそもJavaScriptのバブリングの概念を知っておく必要あり
DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017
その上でイベント修飾子を付与することでバブリングを制御できる。
入れ子のDOMイベントの発生順序は
JSの addEventListener
には第三引数に省略可能でデフォルトはfalseの値がある。
これをuseCapture
という
<div id="outer"> <div id="inner" align="center"></div> </div>
function out(s) {return function() {console.log(s);}} document.getElementById('outer').addEventListener('click', out('outer'), false); // ←コレ document.getElementById('inner').addEventListener('click', out('inner'));
#outer
,.addEventListener
の第三引数を false
または省略した場合、innerのイベントが先に発火する。
逆にture
にすればouterが先に発火する
結果として、これを理解していないでアッチコッチにイベント仕込むと、親要素にイベントが伝播しまくって困った事になるらしい。
そこでVue.jsでは前述のイベント修飾子を使って伝播の制御を行う、という事らしい
では以下のJSを元に書くイベント修飾子の動きを確認してゆく
new Vue({ el: '#app', methods: { handler: function (comment) { console.log(comment) } } })
.stop
event.stopPropagation(); イベント伝播,バブリングを止める
div2クリックでdiv2
のみが出力
<div v-on:click="handler('div1')"> div1 <a href="#top" v-on:click.stop="handler('div2')">div2</a> </div>
.prevent
event.preventDefault(); 禁止操作の指定 リンク操作、submit
の処理をキャンセル
div2クリックでdiv2
,div1
と出力(操作の禁止をするのみなので伝播は通常通り起こるという事?)
<div v-on:click="handler('div1')"> div1 <a href="#top" v-on:click.prevent="handler('div2')">div2</a> </div>
.capture
キャプチャーモードでイベントを発生させる バブリングモードのイベントよりも先に発生する
div3クリックでdiv1
,div3
,div2
の順で出力
<div v-on:click.capture="handler('div1')"> div1 <div v-on:click="handler('div2')"> div2 <div v-on:click="handler('div3')">div3</div> </div> </div>
.self
evant.target
が自分自身の時だけハンドラが呼び出される
<div class="overlay" v-on:click.self="close">div</div>
.native
直接イベントを発火させたい場合に使う 詳細はCAPTER5に
<!-- コンポーネントをクリックするとハンドラが呼び出される --> <my-component v-on:click.native="handler"></my-component> <!-- コンポーネントをクリックしてもハンドラは呼び出されない --> <my-component v-on:click="handler"></my-component>
.passive
event.prevmentDefault()を呼び出さない事を明示的にする
.preventとの併用はNG
モバイル環境でのスクロールカク追記を防ぐ等に使用
キー修飾子
キーボード入力時に呼び出される様になる修飾子,キーコードか、キー指定でもOK
<!-- どちらもEnterキーを表す --> <input v-on:keydown.13="handler"> <input v-on:keydown.enter="handler">
システム修飾子
キーが押されている場合のみハンドラが呼び出される
以下はshiftキーの例
<button v-on:click.shift="doDelete"></button>
その他詳細はVue.js公式ガイド「イベントハンドリング」「システム修飾子キー」を参照のこと
SECTION14 フォーム入力バインディング
フォームの入力や選択値を、データを同期する 「双方向データバインディング」 にはv-model
ディテクティブを使う
v-model
の使い方
テキストフォームをmessage
プロパティとバインディングした例
<div id="app"> <input v-model="message"> <p>{{ message }}</p> </div>
new Vue({ el: '#app', data: { message: 'Hello!' } })
Vue.jsの双方向データディバイディング
入力した文字をデータに反映したい場合は、入力イベントをハンドルして取得したデータをリアクティブデータに代入する必要がある。
this.message = event.taget.value // ここでデータが書き換わる
一連の例に出て来るmessage
を使用した処理は良く行われる
v-mode
ディレクティブはDOMのデータバインディングと要素から取得したデータをリアクティブにするための鉄板構文らしい。
v-modelで受け取りデータの型
基本、入力フォームは文字列型、複数選択フォームは配列型となるが、値にデータバインディングを使用した場合、値の型はバインドされているデータによって変わる。
複数行テキスト
文字列となる。
<textarea v-model="message"></textarea> <pre>{{ message }}</pre>
new Vue({ el: '#app', data: { message: 'Hello!' } })
チェックボックス
単数の場合、は単純に bool
<label> <input type="checkbox" v-model="val"> {{ val }} </label>
new Vue({ el: '#app', data: { val: true } })
複数要素は配列、各要素にvalue
属性を設定
<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>
new Vue({ el: '#app', data: { val: [] } })
AとCの選択では ["A", "C"]
となる
ラジオボタン
デフォルトは文字列
<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>
new Vue({ el: '#app', data: { val: '' } })
セレクトボックス
単一選択プルダウン形式
デフォルト文字列
<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> new Vue({ el: '#app', data: { val: '' } })
複数選択リスト形式
<select v-model="val" multiple> <option value="a">A</option> <option value="b">B</option> <option value="c">C</option> </select> <p>{{ val }}</p>
new Vue({ el: '#app', data: { val: [] } })
AとCの選択では ["A", "C"]
となる
画像ファイル
v-model
は使用できない。リアクティブにするならchange
イベントをハンドルする
<input type="file" v-on:change="handleChange"> <div v-if="preview"><img v-bind:src="preview"></div>
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
,color
等HTML5の入力タイプも使える
横スライドレンジの数値が出る奴
<input type="range" v-model.number="val">{{ val }}
new Vue({ el: '#app', data: { val: 50 } })
修飾子
v-model
にくっつく奴
修飾子 | 作用 |
---|---|
.lazy | inputの代わりにchangeイベントはハンドルする |
.number | 値を数値に変換する |
.trim | 値の余分なスペースを削除する |
.number
の使用例
テキストフォームに入った値はtype="number"
としても文字列となる。
だが、これで数値として取得することが出来る。
<input type="text" v-model.number="price"> {{ price }}
new Vue({ el: '#app', data: { price: 50 } })
SECTION 15 マウント要素外のイベントと操作
v-on
はDOMのwindow
,body
では使用できない為、それらを扱いたい場合はJS純正のaddEventLisner
メソッドを使う事になる。注意点として、不要になっても自動的に解除されないので、不要になった際はフック(ライフサイクルフックCAPTER1の最後の奴)を使って解除する必要がある。
スクロールイベントの取得
発生頻度の高いイベント等は、タイマーを使用して処理の実行頻度を抑えると良い。
以下はwindow
のスクロールイベントを200ms間隔でwindow.scrollY
プロパティを更新する例
これを応用して、サイドバーを画面に常に固定したり、スクロールすると表示を変化させるメニュー等に使用可能。
<header v-bind:class="{ compact: scrollY > 200 }"> 200pxより下にスクロールしたら .compact を付与する </header>
new Vue({ el: '#app', data: { scrollY: 0, timer: null }, created: function () { // ハンドラを登録 window.addEventListener('scroll', this.handleScroll) }, // CAPTER1最後のライフサイクルダイアグラムの beforeDestroy 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) } } } })
bodyに適当な要素を縦長に書いてから開発コンソールのElements
を開いて確認してみる
スクロール前
<header class=""> 200pxより下にスクロールしたら .compact を付与する </header>
スクロール後
<header class="compact"> 200pxより下にスクロールしたら .compact を付与する </header>
当然、JS内の最後の値200
の数値を大きくすると反応はニブくなる。
スムーススクロールの実装
よくある『ページTOP』で滑らかに移動する奴はwindow
オブジェクトを操作している、ライブラリを使えば簡単に実装できる、ここでは「Smooth Scroll」を使った例を示す。
GitHub Smooth Scroll
<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>
// ここでSmoothScrollを変数に入れている var scroll = new SmoothScroll() new Vue({ el: '#app', methods: { scrollTop: function () { // scrollTopのバインドでSmoothScrollのメソッド animateScroll() を呼んでいる // 引数に画面最上部からの位置を指定できる scroll.animateScroll(0) } } })
COLUMN Vue.js以外からのイベントの読み取り
プラグインの実装等で、Vue.js以外のDOM操作ライブラリを使わざるを得ない場合、JSのdispatchEvent
を使ってイベント検知が出来る
以下はjQueryのval
メソッドと絡めた例
<div id="app"> <input id="message" v-on:input="handleInput"> <button data-update="jQuery!">jQueryからの更新</button> </div> <!-- html内でjQueryのCDNを別途読み込むこと --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
$(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) } } })
まとめ
Laravelのブラウザテストでtest用DBを使う際はコマンドに注意
短めですが、ブラウザテストの際の注意点。
以下のサイトにもあるような設定をしてから、テスト用のDBに切り替えて自動ブラウザテストが行われる様に諸々設定をしていたんですが...
Laravel5.6 テスト用データベースを作成してテストを実行するための設定方法
mysqlのlogを調べた所。なぜかArtisanコマンドはtest用のDBでシーディングをしているにも関わらず、いざブラウザテストとなると、ローカルの通常のDBを見てtestをしているようなのです。
原因はtest実施の際のコマンドでした、以下じゃだめです。
./vendor/bin/phpunit tests/Browser/IndexTest.php
ちゃんと duskのコマンドでやりましょう。
php artisan dusk tests/Browser/IndexTest.php
ユニットテストとブラウザテストは別物、と意識した方が良いですね。
基礎から学ぶVue.js CHAPTER2 データの登録と更新 読書メモ
今回は輪読担当ではありませんが、ひとまずメモをまとめたのでアップします。
以下の記事は2019/2/7 コワーキングスペース秋葉原Weeybleで行われる輪読会
[秋葉原] 基礎から学ぶVue.js輪読会 ch2 データの登録と更新(初心者歓迎!)のための読書メモとなります。
以下の書籍の CHAPTER2 Vue.jsとデータの登録と更新 のメモです。
- 作者: mio
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2018/05/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
CAPTER2
SECTION 07 基本データのバインディング
Mustache(マスタッシュ記法) hoge
プロパティをhtmlにバインドする
<p>{{ hoge }}</p>
バインドは属性に使えない
下はエラー
<input type="text" value="{{ message }}">
これが正しい
属性へのバインドはv-bind
ディレクティブを使う
<input type="text" v-bind:value="message"> <!-- 省略して書くとこう --> <input type="text" :value="message">
cssスタイルはキャメルケースで指定
<button v-on:click="isActive=!isActive">isActiveを切り替える</button> <p v-bind:class="{ child: isChild, 'is-active': isActive }" class="item"> 動的なクラス </p> <p v-bind:style="{ color: textColor, backgroundColor: bgColor }" class="item"> 動的なスタイル </p>
new Vue({ el: '#app', data: { isChild: true, isActive: true, textColor: 'red', bgColor: 'lightgray' } })
.item { padding: 4px 8px; transition: background-color 0.4s; } .is-active { background: #ffeaea; }
オブジェクトデータで渡す方法
Vue.js側でひとまとめで定義
<p v-bind:class="classObject">Text</p> <p v-bind:class="classObject_2">Text</p>
new Vue({ el: '#app', data: { classObject: { isChild: true, isActive: true, textColor: 'red', bgColor: 'lightgray' }, classObject_2: { isChild: true, isActive: true, textColor: 'red', bgColor: 'lightgray' } } })
複数の属性のデータバインディング
沢山のプロパティがあっても
new Vue({ el: '#app', data: { item: { id: 1, src: 'item1.jpg, alt: 'サムネ画像です', width: 200, height: 100, } } })
まとめて定義できる
<img v-bind="item">
特定の要素のみに変更を加えることも可能
<img v-bind="item" v-bind:id="'thunb-' + item.id"> <!-- thunb-1 の id が付与される -->
SVGのデータバインディング(ベクター画像)
<div id="app"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> <circle cx="100" cy="75" v-bind:r="radius" fill="lightpink" /> </svg> <input type="range" min="0" max="100" v-model="radius"> </div>
new Vue({ el: '#app', data: { radius: 50 } })
SECTION 09 テンプレートにおける条件分岐
v-if``v-show
ディレクディブは付与した要素の描画・表示に条件を適用する。
ok
プロパティがtrue
の時のみdiv
要素を表示する
<div v-if="ok">hoge</div> <div v-show="ok">hoge</div>
new Vue({ el: '#app', data: { ok: false } })
条件を満たさない場合に生成されるhtml display:none
となる
<div style="display: none;">hoge</div>
v-if と v-show の違いと使い分け
v-if の場合
DOMレベルでない事になる
v-show の場合
display:none が付与される
切り替え頻度が高いならこっちが処理早い
タグによるv-if グループ化
複数の要素を if で切り替えたい場合グループ化できる
<tamplate> <header>title</header> <div><contents/div> </tamplate>
v-else-if 及び v-else によるグループ化
<div v-if="type === 'A'">AAA</div> <div v-else-if="type === 'B'">BBB</div> <div v-else>どちらでも無い場合</div>
v-else-if v-else key
keyを設定して属性の重複による発動しない状態を回避する
<!-- 2つのdivが違う要素である事を明示的にする --> <div v-if="loaded" key="content-visible"> content </div> <div v-else key="content-loading"> loading now... </div>
SECTIOM 10 リストデータの表示と更新
要素を繰り返して描画する
みたまんま、こんな感じで繰り返し描画できる。
v-for="item in list"
は php や JS のfor 文や foreach と同じ様に使える
<ul> <li v-for="item in list" v-bind:key="item.id"> ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }} </li> </ul>
new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] } })
出力結果
<ul> <li>ID.1 スライム HP.100</li> <li>ID.2 ゴブリン HP.200</li> <li>ID.3 ドラゴン HP.500</li> </ul>
インデックスとオブジェクトキーの使用
変数部分をカッコで囲んで配列インデックスを任意に受け取れる
<li v-for="(item, index ) in list"> ...</li>
オブジェクトなら「値」「キー」「インデックス」の順で任意に受け取れる
<li v-for="(item, key, index ) in list"> ...</li>
キーの役割
これ大事! v-bind:key="item.id"
<li v-for="item in list" v-bind:key="item.id">
要素にユニークなキー属性を追加するのが望ましい。ほぼ必須と考えて良い。
キーが無いと要素全部の更新が入る。なのでSQLのid
を入れる位に考えると良い。
繰り返し描画しながら様々な条件を適用する
v-if
を絡めて、比較演算子で条件付けて、特定条件での表示などもできる。
<ul> <li v-for="item in list" v-bind:key="item.id" v-bind:class="{ tuyoi: item.hp > 300 }"> ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }} <span v-if="item.hp > 300">つよい!</span> </li> </ul>
出力結果
<ul> <li>ID.1 スライム HP.100</li> <li>ID.2 ゴブリン HP.200</li> <li>ID.3 ドラゴン HP.500<span>つよい!</span></li> </ul>
リストの更新
注意点として以下のケースで更新を検知できない
- インデックス数値を使った配列要素の更新
- 後から追加されたプロパティの更新
リストに要素を追加
push
,unshift
を使う
this.list.push(要素)
以下、サンプル、ボタン押下でフォーム内の名前のモンスターがリストに追加される。IDは自動生成。HPは500固定
<!-- このフォームの入力値を新しいモンスターの名前に使う --> 名前 <input v-model="name"> <button v-on:click="doAdd">モンスターを追加</button> <ul> <li v-for="item in list" v-bind:key="item.id"> ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }} </li> </ul>
new Vue({ el: '#app', data: { name: 'キマイラ', list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] }, methods: { // 追加ボタンをクリックしたときのハンドラ doAdd: function () { // リスト内で1番大きいIDを取得 var max = this.list.reduce(function (a, b) { return a > b.id ? a : b.id }, 0) // 新しいモンスターをリストに追加 this.list.push({ id: max + 1, // 現在の最大のIDに+1してユニークなIDを作成 name: this.name, // 現在のフォームの入力値 hp: 500 }) } } })
リストから削除する
リストからの削除は配列メソッドのsplice
を使う
li
毎に削除ボタンが表示され、クリックで対象を消せる
<ul> <li v-for="(item, index) in list" v-bind:key="item.id"> ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }} <!-- 削除ボタンをv-for内に作成 --> <button v-on:click="doRemove(index)">モンスターを削除</button> </li> </ul>
new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] }, methods: { // 要素を削除ボタンをクリックしたときのハンドラ doRemove: function (index) { // 受け取ったインデックスの位置から1個要素を削除 this.list.splice(index, 1) } } })
リスト要素( <li>hoge</li>
)に関しては以下の様な配列メソッドを使用して操作が可能
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
これは駄目
this.list[0] = { id: 1, name: 'hoge', hp: 500 }
これなら大丈夫
Vue.set
メソッドを使用して明示的に更新できる。エイリアスはthis.$set
となる
this.$set(更新するデータ , インデックスorキー , { 新しい値 })
上記からの具体例だとこんな感じ
this.$set(this.list, 0, { id: 1, name: 'hoge', hp: 500 })
プロパティを追加する
this.$set
メソッドは持ってないプロパティをリアクティブデータとして追加するために使用できる。
new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] }, created: function() { // すべての要素にactiveプロパティを追加したい this.list.forEach(function(item) { this.$set(item, 'active', false) // 「item.active = false」ではリアクティブにならない }, this) } })
リスト要素プロパティを更新する
プロパティ hp を更新する 作例
<ul> <li v-for="(item, index) in list" v-bind:key="item.id" v-if="item.hp"> ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }} <span v-if="item.hp < 50">瀕死!</span> <!-- ボタンはv-for内に作成 --> <button v-on:click="doAttack(index)">攻撃する</button> </li> </ul>
li
の最後にあるv-if="item.hp"
は hpが0になると消える処理になる。
<span v-if="item.hp < 50">瀕死!</span>
は hp 50未満で表示される。
new Vue({ el: '#app', data: { list: [ { id: 1, name: 'スライム', hp: 100 }, { id: 2, name: 'ゴブリン', hp: 200 }, { id: 3, name: 'ドラゴン', hp: 500 } ] }, methods: { // 攻撃ボタンをクリックしたときのハンドラ doAttack: function (index) { this.list[index].hp -= 10 // HPを減らす } } })
ユニークキーを持たない配列
出来ない訳ではない、簡易にする際はこれでもOK
<option v-for="item in list">{{ item }}</option>
data: { list: ['aaa', 'bbb', 'ccc'] }
オプションにデータを持たないv-for
v-forに数値をセットすると以下の例の様にspanで囲まれた1~15の値を出力できる
<span v-for="item in 15">{{ item }}</span>
同様に 1,5,10,15 の4つを出力する
<span v-for="item in [1, 5, 10, 15]">{{ item }}</span>
文字列に対するv-for
文字列にv-for を使うと1文字ずつ別々の要素で描画される
<span v-for="item in text">{{ item }}</span>
new Vue({ el: '#app', data: { text: 'hoge' } })
出力結果
<span>h</span> <span>o</span> <span>g</span> <span>e</span>
これを利用するとテキストアニメーションが作れるらしい
外部からデータを取得する
外部データはJSONやWebAPIで取得する必要がある。
JSONを外部データから取り込んでみる。
htmlの下の方にある<script>
内に javascriptライブラリのaxios
のCDNを読み込む1行を追加する。
<script src="https://cdn.jsdelivr.net/npm/axios@0.17.1/dist/axios.min.js"></script>
これでAJAXが使える様になる
<ul> <li v-for="(item, index) in list" v-bind:key="item.id"> ID.{{ item.id }} {{ item.name }} HP.{{ item.hp }} </li> </ul>
new Vue({ el: '#app', data: { // あらかじめ空リストを用意しておく list: [] }, created: function () { axios.get('list.json').then(function (response) { // 取得完了したらlistリストに代入 this.list = response.data }.bind(this)).catch(function (e) { console.error(e) }) } })
ライフサイクルフックの created
を使って new 直後にjsonを非同期で取り込む、取り込む前は data.list の空配列[]
が一瞬だが、適用されている。ここが表示されるまでに、ローディングアニメーション処理とか入るとカッコよくなる。
[ { "id": 1, "name": "スライム", "hp": 100 }, { "id": 2, "name": "ゴブリン", "hp": 200 }, { "id": 3, "name": "ドラゴン", "hp": 500 } ]
SECTION 11 DOMを直接参照する $el と $refs
DOMにアクセスするには、インスタンスプロパティ$el
,$refs
を使用する。
但し、ライフサイクルフックのmounted
以降でないと使えない
$el の使い方
テンプレートを囲んでいるルート要素は$el を使って参照できる。
例えば<canvas>
要素などにアクセスしたい時などに使用する。
var app = new Vue({ el: '#app' mounted: function() { console.log(this.$el) // <div id="app"></div> } })
$refsの使い方
<div id="app"> <p ref="hello">hello</p> <!-- p要素にhelloと名を付けた --> </div>
以下の様にアクセス
new Vue({ el: '#app', mounted: function() { console.log(this.$refs.hello) // p要素のDOMとなる } })
$el や$refsは一時的な変更です!
これらは仮想DOMではないので描画処理の最適化をしない
操作の都度描画するので注意
<div id="app"> <button v-on:click="handleClick">カウントアップ</button> <button v-on:click="show=!show">表示/非表示</button> <span ref="count" v-if="show">0</span> </div>
new Vue({ el: '#app', data: { show: true }, methods: { handleClick() { var count = this.$refs.count if (count) { count.innerText = parseInt(count.innerText, 10) + 1 } } } })
カウントアップに対して、表示・非表示ボタンがあるが、count up した状態で、非表示・再表示すると、カウントが0にもどってしまう。これは、DOMに対して加算をしたのみなので、DOMが消えると、値も消えてしまうため。 vue.js での指定なら仮想DOMなのでこうはならない。
SECTION12 テンプレート制御ディレクティブ
ディレクティブ | 作用 |
---|---|
v-pre | テンプレートのコンパイルをスキップする XSS対策に有効 |
v-once | 一度だけバインディングを行う |
v-text | Mustash {{ }} の代わりにテキストコンテンツを描画 |
v-html | HTMLタグをそのまま描画する |
v-cloak | インスタンスの準備が終わると取り除かれる |
v-pre
XSS対策などで使う
<a v-bind:href="#" v-pre> hello {{ message }} </a> <!-- これは以下のよう生だし描画される --> <a v-bind:href="#" v-pre> hello {{ message }}</a>
v-once
描画されたあとに 指定したプロパティの値が変わってもDOMは更新されない
v-text
Mustash {{ kore }} を使わないで書くパターンがある場合に使える
var app = new Vue({ el: '#app' data: { message: 'Hello!' } })
こんな風にmessage
でバインドできる。
<span v-text="message"></span>
v-html
v-pre (XSS対策)とは逆に、htmlのタグ等、をそのまま出してしまう奴
自分がコントロールできない外部やユーザー要因のデータ部分に使うと脆弱性ありありなので使い所には注意が必要。
var app = new Vue({ el: '#app' data: { message: 'hello<strong>Vue.js!</strong>' } })
こんな風にhtmlがそのまま出力する事ができる。
<span v-html="message"></span> <!-- これは以下のよう描画される --> <span>hello<strong>Vue.js!</strong></span>
v-cloak
インスタンスの準備ができると自動的に取り除かれる。コンパイル前のテンプレートが表示されるのを防げる
CSSに以下の様なスタイル定義をする
[v-cloak] { display: none}
以下のやつで画面読み込み時に#app
要素を隠せる。
インスタンス生成でv-cloak
属性が外れてフェードイン表示される。
@keyframes cloak-in { 0% { opacity: 0; } } #app { animation: cloak-in 1s; } #app[v-cloak] { opacity: 0; }
仮想DOMとは?
超要約するとDOMツリーの上にもう一枚仮想DOMのツリーを作ってVue.jsでは基本的に仮想側の操作をする。 DOM自体が変わったり、変えたりした際は非同期で本来のDOMを変更しているので、タイムラグや、DOMの入れ替え時に反映されない事がある。
例えば v-if
,v-else
で分岐表示させた際等にこれが起こりうる。そのため、分岐の各要素に異なる key
を付けて別物ということを認識させる事でちゃんと描画されるようになる。
jQuery などのDOMライブラリとの併用
も、一応可能だが、Vue.jsがDOMを直接いじる $el
,$refs
が同様の事ができるので、併用はできるけど、まあ、無意味化しつつあるよね。ということらしい。
まとめ
- 使用したいデータはdataオプションに登録しよう
- 操作するリストには不変でユニークなkey属性を設定しよう
- 配列インデックスを使った更新はVue.setを使う
- 関数の呼び出し方ではthisは変化することがある
- $elと$refsはmounted以降で使う