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

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

Gitの履歴変更で盛大に失敗した話をメモベースで書く

GitHubに公開リポジトリを使ってアプリを作っているが、上げちゃいけないモノをアップしてしまったのをきっかけに色々やったメモです。要はバットノウハウ、失敗談、こんな風になっちゃだめですよという自分への戒めです。

そもそもの経緯

branchを二つ作って、個別に新機能を開発をしていた。

branchB(ゆっくり目の作業)で .gitignoreを編集した後 branchA(急ぎ目の作業)をcheckoutして別作業を続ける。

branchBでしていた .gitignore の設定がなくなった為、 branchAでgit add . すると 隠したいはずの追加ファイルがステージングに上がり、そのまま気付かずコミットしてしまう。(ぎゃー)

しばらくしてその事実に気付き、リモートリポジトリの履歴から該当ファイルを削除しようと以下の記事を見つつ試みる

Git リポジトリに上がっているファイルを履歴ごと消すには? - Qiita

Git ファイルの履歴を完全に削除する · GitHub

ところが、コマンドの意味を理解しないままガチャガチャと操作(ぎゃー)

禁断の git push -fを良くわからないまま実行する(ぎゃー)

何が起こったか?

まずはこいつを見てくれ、どう思う?

f:id:sakamata:20190302161920p:plain

すごく…複雑です。

盛大にやらかして色々やってこりゃ駄目だ終わった状態のリポジトリである。 画面にはローカルとリポートのbranch両方が表示されて同じコミットのコメントが複数にコピーされてしまっている。

何が起こったかを自分なりに理解した範囲で解説。

本当はもっと根本を理解してからまとめて述べた方が、為になると思うのだけど、自分がやりたいのはアプリの制作であってツールに振り回された顛末を詳しく分析することじゃない。従ってこんな風にややこしい自体にハマるGitという複雑なツールを理解できない俺が悪いのか、ツールが悪いのかという議論はしないし、受け付けない。(けど、こういう事じゃない?という見解は良かったらくれると助かります。)

git filter-branch --tree-filter "rm -f [消したいファイルパス]" HEAD

このコマンドですべての履歴から特定のファイルの履歴を消せるとあったので消したが、 上記サイトを参考に実行した、このコマンドかこの後行った操作が、どうも過去の履歴に渡るコミット時のhash値を変えるという操作だったらしい。そもそも変更されたファイルの状態からhashを生成しているっぽいので、もしかして履歴が変わればhash値もブロックチェーン的に変わる感じかな?

で、Gitのそもそも論の記事がある。

P117~P146あたりにrebase絡みでコミットのリビジョンについての解説がある。で、おそらく状況としては、履歴の書き換えをすることで、rebaseに近いhash値の書き換えが発生、それに気づかないまま、強制pushであるgit push -fを怖さも知らずに実行し、ローカル、リモート、それぞれで古い履歴を消さないまま、良くわからず複数のbranchで履歴の削除を繰り返した結果、複数の履歴が存在し、収集が付かなくなった。という事の様だ。

こえーよ、Gitまじこえーよ。理解してない人間が使っちゃいけないオーバーテクノロジーだよ。

そもそもの問題の発生は .gitignore ファイルを編集した状態で別branchをcheckoutした事が原因でこんなことになるとは思いもよらなんだ。カスケード破壊ってやつですか。

という事で、理解するのは諦めてこの後、何をしたかという話ですが…。

実は、履歴の変更をしようと決めた時点で、リモートリポジトリ非公開にしてから潰すつもりだったので、ローカルリポジトリにバックアップを取って、上みたいになるまで安心して弄り倒して壊しまくってました。

現在はバックアップを取ったリポジトリのbranchを整理してから master branch で該当ファイルの履歴からの削除を行いました。ヤバいファイルを履歴から完全に削除したのですが、どうもこれで コミット時のhash値は、初期のオリジナルの物とは変わってしまいました。どうもそれは仕方のない事らしいというのが昨日今日で調べた範囲で判明している事です。(そんなこと無いって見解と知識が欲しい)チーム開発でこれをやったら終わりですが、ほとんど個人でやっていたので、リセットさえすりゃ簡単な事態であった事は確かです。 しかし、これチームで履歴の編集をやらかす事態が発生すると、チーム全員に一旦 Cloneでやり直しをさせるって事になるのか…。これは申し訳ない事態だ。

Laravelの場合はせいぜい .envファイルのコピーと storage/log の chmod 777 位の手間ですが、他のプロジェクトでは色々面倒な初期設定も他のメンバーにしてもらわんといけないかもなので、履歴を書き換えるような事態は発生させてはいけませんね。

閑話休題。現在は奇麗になったリポジトリを再度リネームしてGitHubに上げたのと、開発環境から本番環境にpushするだけでデプロイできるようにサーバー側も再設定しました。(本当はSeleniumを挟んでデプロイ出来るような環境をそろそろ作りたいのですが)

今回のリポジトリの再設定の際は、これまで sourceTreeに頼りっぱなしでコマンドラインをあまり使ってませんでしたが、VPSサーバーにpushする際の.ssh/config やGit configをそこそこ設定したり、testでcommitした際は全てコマンドラインで作業を行い。以前よりはGUIに頼らず操作ができる様になってきた感じです。ですが、やはり分岐しまくったbranchの状況を追うにはGUIでの可視化が便利ですね。

という訳で履歴管理ツールに悩まされるなんていうアホな事態はとっとと終わらせてモノを作ります。

【輪読会資料】基礎から学ぶ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')

parentsMethodchilds-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 データの監視と加工 のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

さすがに輪読担当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)がしていること
getwidthの値を取得して
sethalfWidthの値に代入する
つまり値を順繰りに回しているループしないか不安になるが、これで値をインタラクティブで双方向に連携ができる

算出プロパティのキャッシュ機能

算出プロパティとメソッド(関数)の違い
算出プロパティ  リアクティブなデータをキャッシュ化してしまう。

<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
    })
  }
})

実行頻度の制御

フォーム入力などで監視を使うと、高頻度で値が変わるので、非同期通信が度々発生してパフォーマンスに影響する。その為setTimeoutLodashなどを利用してウォッチャの実行頻度を制御すると良い。

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からリポジトリ一覧を取得するかんたんなアプリのデモ
axiosAJAX(非同期通信)の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

で、帰ったJSONlistに入れて、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

フックの関数による省略方法

第二引数に関数を入れると bindupdate にフックして同じ処理を呼び出せる。

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

また、詳細は省きますがwindowshostsファイルにもローカルドメインと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には書いて無い訳です。従来はたまたま偶然に望みのアプリが出ていた。という訳です。

そこで、どうすれば意図したアプリを外部から接続するか調べましたが、上手く行かず結論として上記の様なかなりその場しのぎの方法となりました。

readouble.com

環境の共有
共同作業者やクライアントと、現在作業中の内容を共有したい場合もあるでしょう。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 イベントとフォーム入力の受け取り のメモです。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

CHAPTER3 イベントとフォーム入力の受け取り

ちなみに Weeybleで去年同じ本の輪読会で使ったドキュメント、およびソースコードは以下にありました。

GitHub yasugahira0810/vuejs_chapter3

書籍用のサイトのCHAPTER3記述ページ(サンプルコード有り)

これまでの輪読会資料

CAHPTER 1
【輪読会資料】基礎から学ぶVue.js CHAPTER1 Vue.jsとフレームワークの基礎知識 読書メモ - 作りたいものがありすぎる

CHAPTER 2
【輪読会資料】基礎から学ぶVue.js CHAPTER2 データの登録と更新 - Qiita

SECTION 13 イベントハンドリング

イベントハンドラ
これまでのサンプルのボタンに出てた v-onの事をここでは解説する

イベントに紐づける処理の内容をこの本では「イベントハンドラと呼び
イベントハンドラとイベントを紐づけることを「ハンドル」と呼ぶ
イベントはmousewheelIE9では動かないものもあるので注意

<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,colorHTML5の入力タイプも使える

横スライドレンジの数値が出る奴

<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を使ってイベント検知が出来る

以下はjQueryvalメソッドと絡めた例

<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

ユニットテストとブラウザテストは別物、と意識した方が良いですね。