Vue3 teleport と emits オプションを試してみる

## Prologue

[WEB+DB PRESS Vol.120](https://gihyo.jp/magazine/wdpress/archive/2021/vol120) で Vue3の特集がされていました。
内容は初学者でもわかりやすいチュートリアル形式になっているので、これから Vue を始める方でもおすすめだと思います。
その中に Vue3 で出た新しい機能について書かれていたので、今回はそこにフォーカスして試してみました。

## 参考

[WEB+DB PRESS Vol.120](https://gihyo.jp/magazine/wdpress/archive/2021/vol120)

## 環境

- macOS: v10.15.6
- node.js: v12.18.2
- terminal: iTerm
- エディタ: VS Code
- パッケージマネージャ: `yarn`



## `<teleport>` コンポーネント

Vue の組み込みコンポーネントで、テンプレートに記述したコンテンツをページ内の任意の箇所に移動させることができます。

=> これによりz-index の回避や header の調整などが可能になる...

実感を得るために、実際に動かしてみます。

## サンプルプロジェクトを作成

プロジェクト名は `vue3-prj` として作成していきます。
version は Vue3 を選択しますが、それ以外はお好みで大丈夫かと思います。

```
vue create vue3-prj 


Vue CLI v4.5.4
┌─────────────────────────────────────────────┐
│                                             │
│    New version available 4.5.4 → 4.5.10     │
│   Run yarn global add @vue/cli to update!   │
│                                             │
└─────────────────────────────────────────────┘

? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? No


Vue CLI v4.5.4
✨  Creating project in /Users/mi**/mii_work/vue3-prj.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...

yarn install v1.22.4
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...


success Saved lockfile.
info To upgrade, run the following command:
$ brew upgrade yarn
✨  Done in 57.46s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.22.4
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
✨  Done in 13.49s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project vue3-prj.
👉  Get started with the following commands:

 $ cd vue3-prj
 $ yarn serve
```

`/components/Modal.vue` を作成し、シンプルなモーダルを作成します。

``` JS:/components/Modal.vue
<template>
  <button @click="openModal">Open</button>
  <teleport to="body">
    <div v-if="isOpenModal" class="modal-content">
      <p>
        This is Modal!!!!!
      </p>
      <p>message: {{ message }}</p>
      <button @click="closeModal">Close</button>
    </div>
  </teleport>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  name: "Modal",
  props: {
    message: {
      type: String
    }
  },
  setup() {
    const isOpenModal = ref(false);
    const openModal = () => {
      return (isOpenModal.value = true);
    };
    const closeModal = () => {
      return (isOpenModal.value = false);
    };

    return {
      openModal,
      closeModal,
      isOpenModal
    };
  }
});
</script>
```

こちらを `/views/Home.vue` の以下の位置に設置します。

``` JS:/views/Home.vue
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <Input @send="emitSend" />
    <Modal :message="message" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import Input from "@/components/Input.vue";
import Modal from "@/components/Modal.vue";

export default defineComponent({
  name: "Home",
  components: {
    Input,
    Modal
  },
  setup() {
    const message = ref("");
    const emitSend = (value: string) => {
      message.value = value;
    };
    return {
      emitSend,
      message
    };
  }
});
</script>
```

body に宛先を向けた場合、モーダルを開くボタンをクリックすると、DOM 上では画像の位置に表示されます。

![スクリーンショット 2021-01-07 13.57.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558989/7f481f9f-ea6e-4089-0eb4-dfd752cb628d.png)



### emits オプション

> コンポーネントがどのように機能するかをより適切に文書化するために、発行されたすべてのイベントを定義することをお勧めします。

参考: 
- https://v3.vuejs.org/guide/component-custom-events.html#defining-custom-events
- https://v3.ja.vuejs.org/api/options-data.html#emits


記法としては以下の2通りあるとのことなので、こちらも試してみます。

1. カスタムイベント名を配列で列挙する方法
2. オブジェクトで定義してデータの有効性をチェックする関数を追加、データの有効性をチェックする方法


`/components/Input.vue` を作成し、イベントを発火させます。
コンポーネントの設置箇所は上記 `/views/Home.vue` 内に記載していますのでご参考ください。
 
``` JS:/components/Input.vue
<template>
  <div class="input-content">
    <input v-model="message" type="text" />
    <button @click="sendText">emit</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  name: "Input",
  emits: {
    send(message: string) {
      return message.length > 4
    }
  },
  setup(props, { emit }) {
    const message = ref('');
    const sendText = () => {
      emit("send", message.value);
    };

    return {
      sendText,
      message
    };
  }
});
</script>

```

引数 `message` は string 型、長さは4文字以下だとエラーになるように記載しました。
挙動は以下のようになります。

#### warning

- emits オプションを指定していない場合 -> warning が出ます

```
runtime-core.esm-bundler.js?5c40:38 [Vue warn]: Component emitted event "send-text" but it is neither declared in the emits option nor as an "onSend-text" prop.
```
推奨レベルなので warning として出るのかな、と思います。

- emits オプションを指定して、オプションの条件に添わない値（3文字など）を入力すると warning が出ます。

```
runtime-core.esm-bundler.js?5c40:38 [Vue warn]: Invalid event arguments: event validation failed for event "send".
```

## Epilogue

`emits` オプションはもう少しがっつり弾かれるのかとおもったのですが、warning で出ていました。 
文書化が目的だからでしょうか。コードや可読性の向上以外にも使えそうなので、引き続き調べてみます。
`fragments` も活用してみましたが、特に意識しないで書けること、もちろんエラーにもならないので、個人的には書きやすくなったなと思いました。
`teleport` はもう少し複雑な構成だとより恩恵を感じるのかなと感じました。ID 指定などもできるそうなので、試してみたいと思います。
今回はそれぞれ感触を確かめるに留まりましたが、時間があれば実践的なコードを作ってみようと思います。

何かご指摘などありましたらご連絡ください。