Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

vuex2.xその0003 vuex入門的な感じでToDo管理アプリケーションを作る

参考にさせて頂いたページ

はじめに

APIにリクエストする非同期な処理を想定して、actionsからcommitしています。 実際にAPIとか作ったらもう少し違う実装が適切かもしれないです。

ES2015が使えない環境下でのことを考慮するなど個人的事情からlodashを使用していたり、とはいえ全体的にはES2015で書いていますがそこはそっとスルーでお願いします。

今回のバージョン

  • Vue.js v2.1.10
  • vuex v2.3.0

サンプルソースコード

例として以下の通りです。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<style>
.is-done {
  color: #999999;
  text-decoration: line-through;
}
</style>

<h1>タスク管理アプリケーション</h1>
<div id="app">
</div>

<script src="js/lodash.js"></script>
<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
/**
 * store
 */
const store = new Vuex.Store({
  strict: true,
  state: {
    latestItemId: null,
    items: []
  },
  getters: {

  },
  actions: {
    addItem({commit, rootState}, content) {
      // 実際はここでAPIにリクエストしてDBのレコード追加が成功したらcommit
      const newItemId = rootState.latestItemId + 1;
      commit('addItem', {id: newItemId, content: content, isDone: false});
    },
    toggleItem({commit, rootState}, id) {
      // 実際はここでAPIにリクエストしてDBの当該レコードの更新が成功したらcommit
      const index = _.findIndex(rootState.items, function(o) { return o.id == id; });
      commit('toggleItem', index);
    },
    deleteItem({commit}, id) {
      // 実際はここでAPIにリクエストしてDBの当該レコードの削除が成功したらcommit
      commit('deleteItem', id);
    },
    getItems({commit}) {
      // 実際はここでAPIにリクエストしてDBからタスクリストを取得します
      const items = [
        {id: 0, content: '起きる', isDone: false},
        {id: 1, content: '朝食を食う', isDone: false},
        {id: 2, content: '昼食を食う', isDone: true},
        {id: 4, content: '夕食を食う', isDone: true},
        {id: 5, content: '風呂に入る', isDone: false},
        {id: 10, content: '寝る', isDone: false},
      ];
      const latestItemId = items[items.length - 1].id;
      commit('getItems', {latestItemId: latestItemId, items: items});
    }
  },
  mutations: {
    addItem(state, newItem) {
      state.latestItemId = newItem.id;
      state.items.push(newItem);
    },
    toggleItem(state, index) {
      state.items[index].isDone = !state.items[index].isDone;
    },
    deleteItem(state, id) {
      state.items = _.filter(state.items, function(item) {
        return item.id !== id;
      });
    },
    getItems(state, newState) {
      _.assign(state, newState);
    }
  }
});

/**
 * addItem
 * タスク新規追加フィールドのコンポーネント
 */
const addItem = {
  data() {
    return {
      content: '',
    }
  },
  template: `
    <section>
      <h2>タスクを新しく追加</h2>
      <div>
        <input type="text" v-model="content">
        <input type="submit" value="追加" @click="addItem">
      </div>
    </section>
  `,
  methods: {
    addItem() {
      if (!this.content) return;
      this.$store.dispatch('addItem', this.content);
      this.content = '';
    }
  }
};

/**
 * item
 * タスク一つ分のliタグのコンポーネント
 */
const item = {
  props: ['item'],
  template: `
    <li>
      <span :class="{'is-done': isDone}">id {{item.id}} : {{item.content}}</span>
      <button @click="toggleItem" v-text="buttonLabel"></button>
      <button @click="deleteItem">削除する</button>
    </li>
  `,
  computed: {
    id() {
      return this.item.id;
    },
    isDone() {
      return this.item.isDone;
    },
    buttonLabel() {
      return this.item.isDone ? '未完了に戻す' : '完了にする';
    },
  },
  methods: {
    toggleItem() {
      this.$store.dispatch('toggleItem', this.id);
    },
    deleteItem() {
      if (!confirm("削除しますか?")) return;
      this.$store.dispatch('deleteItem', this.id);
    },
  }
};

/**
 * itemList
 * タスク一覧のulタグのコンポーネント
 */
const itemList = {
  components: {item},
  template: `
    <section>
      <h2>登録されているタスク</h2>
      <ul>
        <item v-for="item in items" v-bind:item="item"></item>
      </ul>
    </section>
  `,
  computed: {
    items() {
      return this.$store.state.items;
    }
  },
  created() {
    this.$store.dispatch('getItems');
  }
};

/**
 * app
 * rootのコンポーネント
 */
const app = new Vue({
  el: '#app',
  store,
  components: {
    addItem,
    itemList,
  },
  template: `
    <div class="app">
      <add-item />
      <item-list />
    </div>
  `
});
</script>

</body>
</html>

Vuexとは

Storeを提供し、以下のようなルールに則った開発をすることでコードの見通しがよくなります。

  • Storeが持っているstateはmutationsによってのみ更新される
  • mutaionsは同期的でなくてはならず、非同期な処理はactionsが担う

正確な情報は公式ドキュメントを参照してください。

個人的まとめ

あくまでも個人的なもので、ES2015などを使用しない環境下のせいもあると思いますが、参考になればと思います。

  • VuexはStoreを提供するもので、コンポーネントは普通にVue.js単体で使っているときと同じ要領で書く
  • rootコンポーネントにstoreオプションを設定すると、子コンポーネントthis.$storeが参照できるようになる
  • コンポーネント内でstoreを参照するときはthis.$storeを使う
  • Storeはstrict: true,を設定した方がいい
  • Storeが持っているstateはmutationsによってのみ更新される
  • mutaionsは同期的でなくてはならず、非同期な処理はactionsが担う
  • mutationsが持っているメソッドは第一引数にstateを受け取る
  • actionsが持っているメソッドは第一引数にcontextまたは分割で{state, rootState, commit, dispatch, getters}を受け取る
  • mutaionを呼ぶときはcommitで、actionを呼ぶときはdispatch
  • getters, modules, plugins というキーもあるけど今回は使っておらず、いずれも大きい規模になるほど有用そう
  • actionのdispatchとpromiseについては「アクション · GitBook

Vuexに限らずVue.jsのコンポーネントのこと

  • コンポーネント内において、Storeが持っているstateの更新が反映されてほしい箇所はcomputedでreturnする
  • コンポーネント内において、親からもらったpropsの更新が反映されてほしい箇所は直接templateに書くか、computedでreturnする
  • アロー関数を使うとthisがコンポーネント自身を参照しないので理解して使う

vuex2.xその0002 mapStateのこと

参考にさせて頂いたページ

はじめに

storeを複数使うような規模のものを書いていないのでmapStateはまだ自分には必要なさそうですが、少し書いてみました。

サンプルソースコード

こんな感じかなー。と書いてみました。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<div id="app">
</div>

<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
const mapState = Vuex.mapState;

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: function(state){
      return state.count++;
    },
    decrement: function(state){
      return state.count--;
    }
  }
});

const Counter = {
  template: `
    <div>
      <div>{{ count }}</div>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </div>
  `,
  computed: mapState({
    // アロー関数は、コードをとても簡潔にできます!
    count: state => state.count,
    // 文字列を渡すことは、`state => state.count` と同じです
    countAlias: 'count',
    // `this` からローカルステートを参照するときは、通常の関数を使わなければいけません
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  }),
  methods: {
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
        this.$store.commit('decrement')
    }
  }
};

const app = new Vue({
  el: '#app',
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
});
</script>

</body>
</html>

このコードについて

上記のサンプルでは、公式ドキュメントにあったオブジェクトスプレッド演算子は使っていませんが、ローカルなcomputedも定義する場合はそのような記述が必要になるようです。

オブジェクトスプレッド演算子が使えない環境下では、Object.assign()とかlodashの_.assign()とか使う感じでしょうか。

vuex2.xその0001 公式ドキュメントにあるカウンターアプリを作ってみる

参考にさせて頂いたページ

公式ドキュメント

ソースコード

はじめに

色々な事情でwebpackなどを使用できない環境下でスタンドアローンビルドのvuexを使用するような想定で書いています。

サンプルソースコード

例として以下の通りです。 storeにはstrict: true,を設定した方が良さそうだなと思っています。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/style.css">
<link rel="shortcut icon" href="">
</head>
<body>

<div id="app">
</div>

<script src="js/vue.js"></script>
<script src="js/vuex.js"></script>
<script>
/**
 * store
 */
const store = new Vuex.Store({
  strict: true,
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++ ,
    decrement: state => state.count-- ,
  }
});

/**
 * Counterコンポーネント
 */
const Counter = {
  template: `
    <div>
      <div>{{ count }}</div>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </div>
  `,
  computed: {
    count(){ return this.$store.state.count; },
  },
  methods: {
    increment({commit}) {
      this.$store.commit('increment')
    },
    decrement({commit}) {
        this.$store.commit('decrement')
    }
  }
};

/**
 * rootコンポーネント
 */
const app = new Vue({
  el: '#app',
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter />
    </div>
  `
});
</script>

</body>
</html>

railsその0002 vagrant+rails5の環境でmodelやcontrollerのファイルを編集しても反映されない問題を解決する

参考にさせて頂いたページ

vagrantrsyncのエラーについて

vagrant + rails のときに起こる問題について

vagrant_1.8.4を使用していたときのこと

このときの環境(インストーラーなど)

ホストマシン環境

ゲストマシン環境

rsyncが無いのでMSYS2をインストールしてpacmanrsyncをインストールする

Vagrantfileの中で type: "rsync",を設定するにはrsyncをインストールする必要があり、

これまで git for windows の Git BASHvagrant を使ったりしていましたが、このままだと都合が悪いので、

表題の問題を解決するためにMSYS2をインストールしたり以下のことをしました。

MSYS2については別途「Windows10にmsys2をインストールして使ってみる - Motomichi Works Blog」に今回の件以外のことも含めて書きました。

Vagrantfileのconfig.vm.synced_folderの設定をする

例として以下の通りですが、同期するディレクトリのパスはご自分の環境に合わせて読み替えてください。

config.vm.synced_folder "./my_app", "/var/www/rails_project/my_app",
  type: "rsync",
  rsync__args: %w(--verbose --archive --delete -z --copy-links --times),
  rsync__exclude: %w(.git/ log/ tmp/ vendor/)

vagrant upしてみるとエラー

以下のようなエラーが出ました。同期せずに止まっているようなので仮想環境内のディレクトリには何もありません。

There was an error when attempting to rsync a synced folder.
Please inspect the error message below for more info.

Host path: /c/Users/motomichi/Desktop/all/git_repos_all/github/MotomichiWorks/rails_practice_0001/my_app/
Guest path: /var/www/rails_project/my_app

Command: "rsync" "--verbose" "--archive" "--delete" "-z" "--copy-links" "--times" "--chmod=ugo=rwX" "--no-perms" "--no-owner" "--no-group" "--rsync-path" "sudo rsync" "-e" "ssh -p 2222 -o ControlMaster=auto -o ControlPath=C:/Users/Public/Documents/Wondershare/CreatorTemp/ssh.926 -o ControlPersist=10m -o StrictHostKeyChecking=no -o IdentitiesOnly=true -o UserKnownHostsFile=/dev/null -i 'C:/Users/motomichi/.vagrant.d/boxes/centos67box-20170505/0/virtualbox/vagrant_private_key'" "--exclude" ".vagrant/" "--exclude" ".git/" "--exclude" "log/" "--exclude" "tmp/" "--exclude" "vendor/" "/c/Users/motomichi/Desktop/all/git_repos_all/github/MotomichiWorks/rails_practice_0001/my_app/" "vagrant@127.0.0.1:/var/www/rails_project/my_app"

Error: This rsync lacks old-style --compress due to its external zlib.  Try -zz.

Continuing without compression.

Warning: Permanently added '[127.0.0.1]:2222' (RSA) to the list of known hosts.
mux_client_request_session: read from master failed: Connection reset by peer
Failed to connect to new control master
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(226) [sender=3.1.2]

これに関しては、「VirtualBox - ローカルとvargrentをrsyncしたくvagrantfileを編集したところ、vagrant up中に以下のエラーメッセージが発生しました。何が原因かアドバイスを頂けないでしょうか?(36200)|teratail」を参考にさせて頂いて解決しました。

私の環境では以下のファイルを編集しました。

C:\HashiCorp\Vagrant\embedded\gems\gems\vagrant-1.8.4\plugins\synced_folders\rsync\helper.rb

以下の3行をコメントアウトしました。

          "-o ControlMaster=auto " +
          "-o ControlPath=#{controlpath} " +
          "-o ControlPersist=10m " +

念のためいつでも元に戻せるようにしておくのが良さそうですね。

ホストマシンのmodelやcontrollerを編集しても仮想環境内に反映されない問題を解決する

まず vagrant reload をすると仮想環境内に同期はされるようになりました。

まだこれだけではrailsサーバーを一度止めてから、rails sしなおさないと編集内容が反映されません。

Rails developmentモードなのにコードの変更が反映されない – KeruuWeb」を参考にさせて頂いて解決しました。

vagrant reload

をしたあとで、一度以下を実行します。

vagrant rsync

次に以下を実行するとリアルタイムに同期されるようになります。

vagrant rsync-auto

このウィンドウはそのままにして、別ウィンドウでMSYS2を起動して、

vagrant ssh

railsのあるディレクトリでいつも通り

rails s

して確認すると解決されていました。

終了するときは、

ctrl + c で rsync-auto を止めて、いつも通り vagrant halt で止めます。

vagrant_1.9.7を使用していたときのこと

このときの環境(インストーラーなど)

ホストマシン環境

ゲストマシン環境

rsync-autoしたけど同期できないときのメッセージ

おおよそはvagrant_1.8.4を使用していたときと同じような手順を実行して、rsync-autoを実行しました。

$ vagrant rsync-auto

以下のように表示されました。

==> default: Not syncing C:/Users/motomichi/Desktop/all/git_repos_all/github/MotomichiWorks/rails_practice_0001/my_app as it is not part of the current workin
g directory.
==> default: Doing an initial rsync...
==> default: Rsyncing folder: /cygdrive/c/Users/motomichi/Desktop/all/git_repos_all/github/MotomichiWorks/rails_practice_0001/ => /vagrant
==> default: Watching: C:/Users/motomichi/Desktop/all/git_repos_all/github/MotomichiWorks/rails_practice_0001
==> default: Rsyncing folder: /cygdrive/c/Users/motomichi/Desktop/all/git_repos_all/github/MotomichiWorks/rails_practice_0001/ => /vagrant

以前どうだったか覚えていないのですが、任意で指定したパスはNot syncingと表示されてしまい同期されませんでした。

rsync-autoで同期できない問題を回避

任意でディレクトリ指定するのを諦めて、デフォルトの.と/vagrantだけ指定することにして、シンボリックリンクを作成しました。

Vagrantfileを以下のように記述しました。

  config.vm.synced_folder ".", "/vagrant",
    type: "rsync",
    rsync__args: %w(--verbose --archive --delete -z --copy-links --times),
    rsync__exclude: %w(.git/ log/ tmp/ vendor/)

私の場合のディレクトリパスでいうと以下のようにコマンドを実行しました。

まず仮想環境内に残っていたファイルを削除します。

$ vagrant ssh
$ su
(パスワード入力)vagrant
# cd /var/www/rails_project/
# rm -rf my_app/
exit

シンボリックリンクを作成します。

$ ln -s /vagrant/my_app

仮想環境内でここまでやったら、以下のコマンドを実行します。

$ vagrant rsync
$ vagrant rsync-auto

Windows10にmsys2をインストールして使ってみる

参考にさせて頂いたページ

ダウンロード

環境変数を継承する

その他

インストール

64bit版の現在の最新版である以下のインストーラーを使用した

  • msys2-x86_64-20161025.exe

Windows環境変数を継承させる

スタートメニューの「MSYS2 64bit > MSYS2 MSYS」を選択すると起動できますが、Windowsコマンドプロンプトなどから使えるコマンドが見つからないので、環境変数を継承させる必要があるみたいです。

スタートメニューを右クリックして「システム > システムの詳細設定 > 環境変数」からユーザー環境変数の方の「新規」を選択して、変数名と変数値を設定して、MSYS2を起動しなおしたら色々とコマンドが使えるようになりました。

MSYS2 で PATH が引き継がれない」を参考にさせて頂いて、以下のように設定しました。

  • 変数名 : MSYS2_PATH_TYPE
  • 変数値 : inherit

Desktopに移動する

cd /c/Users/xxxxxx/Desktop/

rsync vim git opensshをインストール

pacman -S rsync vim git openssh

JavaScript学習日記 その0005 ブラウザバック時の挙動と対策について考えてみる

参考にさせて頂いたページ

まずa.htmlとb.htmlを作成する

a.html

a.htmlはbody内に以下のように記述しました。 フィールドのvalueにdefaultを入れています。 submitするとgetでb.htmlへ遷移します。

フィールドを編集してからsubmitして、b.htmlからブラウザバックしたときのイベント発火とフィールドのvalueについて検証してみます。

<h1>Page a</h1>

<form action="b.html">
  <input type="text" name="example" value="default">
  <input type="submit" value="送信">
</form>

<script>

alert('hoge');

window.onload = function(){
  alert('window.onload');
};

window.onpageshow = function(){
  alert('window.onpageshow');
};
</script>

b.html

b.htmlはbody内に以下のように記述しました。 ただh1タグがあるだけです。

<h1>Page b</h1>

今日の検証環境

Windows10の2017年4月15日現在の以下のブラウザで試しています。

a.htmlでフィールドをeditedに変更してsubmitしたあとブラウザバックしてみる

alertが実行される場合は〇、ブラウザバックしたときにフィールドのvalueは編集を保持しているかは以下の通りでした。

ブラウザ hoge window.onload window.onpageshow value
Chrome edited
Firefox × × edited
IE edited
Edge edited

a.htmlでフィールドをeditedに変更してページを更新してみる

submitせずに更新してみるパターンです。 ページ更新なのでalertは全て実行されますが、FirefoxIE、Edgeはページを一度更新してもvalueにeditedが残りました。 そのままもう一度更新すると、IEとEdgeはdefaultに戻り、Firefoxは何度更新してもeditedが残りました。 一度目の更新のあとで、残っているeditedをそのままにしてsubmitしたとき、パラメータはもちろんb.html?example=editedですが、もう一度ブラウザバックするとIEとEdgeではvalueはdefaultに戻りました。 さすがにフォーム入力途中でページ更新はイレギュラーな行動だと思いますが。

ブラウザ hoge window.onload window.onpageshow value
Chrome default
Firefox edited
IE edited ※一度だけ保持
Edge edited ※一度だけ保持

a.htmlにwindow.onunload = function(){};を記述してみる

追記して以下のようにしました。

<h1>Page a</h1>

<form action="b.html">
  <input type="text" name="example" value="default">
  <input type="submit" value="送信">
</form>

<script>
window.onunload = function(){};

alert('hoge');

window.onload = function(){
  alert('window.onload');
};

window.onpageshow = function(){
  alert('window.onpageshow');
};
</script>

Firefoxでブラウザバックしたときにhogeとwindow.onloadのalertが実行されるようになりました。 window.onunload = function(){};を書くことで、ブラウザバック時に各alert()の実行と、編集したvalueにeditedが残るというように、4つのブラウザで同じ結果を得られるようでした。 ただ、何度更新してもvalueの編集内容が保持されるFirefoxについてはどうしたものかと思います。

ブラウザ hoge window.onload window.onpageshow value
Chrome edited
Firefox edited
IE edited ※一度だけ保持
Edge edited ※一度だけ保持

jsでテキストを変更してからsubmitして、ブラウザバックしてみる

たとえばbuttonタグを配置して、以下のようなjQueryのコードを書きます。

$(function(){
  $('button').click(function(){
    $(this).text('clicked');
    $('[name="example"]').val('clicked');
  });
});

buttonをクリックして、テキストとvalueがclickedに変わったあと、submitしてブラウザバックしたときFirefoxでは以下のようになります。

  • window.onunload = function(){};が記述されていないa.htmlではclickedが保持されました。
  • window.onunload = function(){};が記述されているa.htmlではtext()で変更した箇所は元に戻っていましたが、val()で入力したvalueはあくまでもフィールドの入力値なので保持されました。

ChromeIE、Edgeではwindow.onunload = function(){};を書いても書かなくても、ブラウザバックしたときのテキストはbuttonに戻っていて、valueはclickedを保持していました。

おわりに

Macの標準的なブラウザであるSafariを試せていません。

MaciOSAndroidを試せていませんが。

対応としてはwindow.onunload = function(){};を記述して以下のような状態にするのが多くの場合第一候補な気がしました。

  • フォームの入力値はユーザの変更を保持
  • jQueryvalueを入力した箇所はフォームの入力値なのでclickedを保持
  • jQueryでのテキスト変更は無かったことにする

ということになるようでした。

vue.js 2.x その0003-02 トランジション開始前、進行中、完了後、途中キャンセルのタイミングで任意のjs処理を実行する

参考にさせて頂いたページ

公式ページ

以前書いた記事

はじめに

以前簡単なトランジションのサンプルを作ってみました。 それを発展させて、enterとleaveの各4タイミングでjsを実行するサンプルを書いてみました。

  • enter開始前
  • enter進行中
  • enter完了後
  • enter途中キャンセル
  • leave開始前
  • leave進行中
  • leave完了後
  • leave途中キャンセル

サンプルコード

<style>
.hoge-enter-active,
.hoge-leave-active {
  transition: opacity 2s
}
.hoge-enter,
.hoge-leave-to /* hoge-leave-to クラスはバージョン 2.1.8 以降でのみ利用可能 */ {
  opacity: 0
}
</style>

<div id="transition-app">
  <h1>Vue.jsのサンプル 静的なページ</h1>
  <button v-on:click="fade">click here</button>
  <transition
    name="hoge"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:after-enter="afterEnter"
    v-on:enter-cancelled="enterCancelled"
    v-on:before-leave="beforeLeave"
    v-on:leave="leave"
    v-on:after-leave="afterLeave"
    v-on:leave-cancelled="leaveCancelled"
  >
    <div v-show="state.contentIsShow">トグルするコンテンツ</div>
  </transition>
<!--/#transition-app--></div>

<script src="js/vue.js"></script>
<script>
  var fadeApp = new Vue({
    el: '#transition-app',
    data: {
      state: {
        contentIsShow: true,
      },
    },
    methods: {
      fade: function(){
        this.state.contentIsShow = !this.state.contentIsShow;
      },
      beforeEnter: function(){
        console.log('beforeEnter')
      },
      enter: function(){
        console.log('enter')
      },
      afterEnter: function(){
        console.log('afterEnter')
      },
      enterCancelled: function(){
        console.log('enterCancelled')
      },
      beforeLeave: function(){
        console.log('beforeLeave')
      },
      leave: function(){
        console.log('leave')
      },
      afterLeave: function(){
        console.log('afterLeave')
      },
      leaveCancelled: function(){
        console.log('leaveCancelled')
      },
    },
  });
</script>