LoginSignup
2
1

More than 1 year has passed since last update.

フォームコンポーネントを作りながら理解するCompositionApi

Last updated at Posted at 2021-12-01

はじめに

おはようございます。こんにちは。こんばんは。
Watatakuです。

今回はフォームコンポーネントを作りながらVue3で登場したcompositionApiについて解説していく記事です。
またcompositionApiを使うためにVue3にアップグレード(しなくても良かった)したのでVue2とVue3の違いについてもお話しできればと思います。
今回作ったやつ。⬇︎
vue3-components.png

コード

環境(Vueのバージョン)

  • optionsApi: ver.2.6.11
  • compositionApi: ver.3.0.0

書かないこと

optionApiの書き方、解説。

Vue Composition APIとは

以下の2点のために策定された関数ベースのAPIです。

Vue Composition APIのメリット
1.コードの可読性・再利用性の改善
2.型インターフェースの改善

今までのVue2でのOptions APIでは1つのコンポーネントが複数の役割を持った際にコードが肥大化し、可読性が著しく低下するという問題がありました。

そこでComposition APIでは関心事によってコードを分割し、分割したコードを簡単にコンポーネントに注入できるようになっています。

詳しくはこちら

テキストボックス

  • optionsApi
App.vue
<template>
  <div class="contents">
    <TextInput
      v-model="text"
      type="text"
      name="text"
      placeholder="テキストボックス"
      :value="text"
    />
  </div>
</template>

<script>
import TextInput from "./components/TextInput.vue";

export default {
  components: {
    TextInput,
  },
  data() {
    return {
      text: "",
    };
  },

};
</script>
TextInput.vue
<template>
  <input
    :type="this.type"
    :placeholder="this.placeholder"
    :name="this.name"
    :value="this.value"
    @input="updateValue"
  />
</template>

<script>
export default {
  props: {
    type: { type: String },
    placeholder: { type: String },
    name: { type: String },
    value: { type: String },
  },
  methods: {
    updateValue(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>
  • compositionApi
App.vue
<template>
  <TextInput
    v-model:modalValue="form.text"
    type="text"
    name="text"
    placeholder="テキストボックス"
    :value="form.text"
  />
</template>

<script>
import { reactive } from "vue";
import TextInput from "./components/TextInput.vue";

export default {
  name: "App",
  components: {
    TextInput,
  },
  setup() {
    const form = reactive({
      text: "",
    });

    return {
      form
    };
  },
};
</script>
TextInput
<template>
  <input
    :type="type"
    :placeholder="placeholder"
    :name="name"
    :value="value"
    @input="updateValue"
  />
</template>

<script>
export default {
  props: {
    type: { type: String, required: true },
    placeholder: { type: String, required: true },
    name: { type: String, required: true },
    value: { type: String, required: true },
  },
  emits: ["update:modalValue"],
  setup(_, context) {
    const updateValue = (e) => {
      context.emit("update:modalValue", e.target.value);
    };
    return { updateValue };
  },
};
</script>

optionApiとcompositionApiを見比べて大きく違うのはsetup(){}の有無ではないでしょうか?
このsetup(){}がCompositionApiを扱うためのキモになります。
このsetup(){}の中に、optionApiで言う「data, mounted, methods, ・・・・」を描くイメージです。

data()

まずはじめにdata()をみていきます。
結論から申し上げますと, "vue"からref or reactiveで宣言すればいいです。
そして、その変数をテンプレートで扱うためにreturnします。

// --------------- optionsApi ----------------------
data() {
    return {
      text: "",
    };
},

// アクセス
this.text

// -------------- compositionApi --------------
// reactiveで書く場合
setup() {
    const form = reactive({
      text: "",
    });

    return {
      form
    };
},

// アクセス
form.text

// refで書く場合
setup() {
   const text = ref("");

    return {
      text
    };
},

// アクセス
text.value

refとreactiveの違いについては下記記事がご参考になると思います。
参照

methods

optionsApiでは、メソッドはmethodsに書かなくてはなりませんでしたが、
compositionApiではこれもsetup(){}に記述します。
書き方は、setup(){}で普通にjavascript(typescriptを使うのであれば、typescript)でメソッド宣言するだけです。
あとはdata()同様、returnするだけ。

//optionApi
methods: {
    updateValue(e) {
      this.$emit("input", e.target.value);
    },
},

//compositionApi
setup(_, context) {
    const updateValue = (e) => {
      context.emit("update:modalValue", e.target.value);
    };
    return { updateValue };
},

propsとemit

  • props

propsの渡し方、受け取りかたは大きく変わってないです。
ただ、受け取ったpropsを用いてデータを加工したりする時(setup関数に渡さなければいけない時)に工夫が必要です。

props: {
    title: {
      type: String,
      required: true
    },
    count: {
      type: Number,
      default: 0
    }
},
setup (props) {
    const doubleCount = computed(() => props.count * 2)

    return { doubleCount }
}

setup(){}の第一引数がpropsになります。ちなみに後述しますが第ニ引数がコンテキスト。
コンテキストを用いてemitします。

  • emit

まずはじめに、Vue2とVue3で仕様が若干変わっていたので、作者は苦戦しました。ちなみにv-modelも若干仕様が変わっています。
詳しくはこちら。

emits: ["update:modalValue"],
setup(_, context) {
    const updateValue = (e) => {
      context.emit("update:modalValue", e.target.value);
    };
    return { updateValue };
}

Vue2との違いは事前にemitのイベントをemitsプロパティに指定します。

その後、propsの時にも少し触れたのですが、setup(){}の第二引数でコンテキストを指定し、
コンテキストのemitメソッドを使います。
ちなみにpropsが必要なく、emitだけを使いたい場合は上記のように(_, context)のように書きます。

他のcompositionApiの書き方

以上が、このアプリでのcompositionApiの説明でした。

おい、Watataku!!残りのライフサイクルとかwatchはどないすんねん!!
と言うことで、ここからはこのアプリとは関係ないですがライフサイクルとかwatch*を説明していきましょう。

結論、importしろ。これだけです。

  • ライフサイクル
// optionsApi
export default {
  created() {
    console.log("created");
  },

  mounted() {
    console.log("mounted");
  },
};

// compositionApi
import { onMounted } from "vue";

export default {
  setup() {
    console.log("created");

    onMounted(() => {
      console.log("mounted");
    });
  },
};

Vue3_Composition_API_🔊.png

それぞれのライフサイクルに対応したonXXX関数を使用する
beforeCreate・createdがsetupにまとめられている

  • computed
//optionsApi
export default {
  data() {
    return {
      count: 1,
    };
  },
  computed: {
    doubleCount() {
      return this.count * 2;
    },
  },
};

//compositionApi
import { ref, computed } from "vue";

export default {
  setup() {
    const countRef = ref(1);

    // computed関数を使用して計算プロパティを定義します
    const doubleCount = computed(() => {
      return countRef.value * 2;
    });

    return {
      countRef,
      doubleCount,
    };
  },
};
  • watch
//optionsApi
export default {
  data() {
    return {
      count: 1,
    };
  },

  watch: {
    count(count, prevCount) {
      console.log(count);
      console.log(prevCount);
    },
  },
};

//compositionApi
import { ref, watch } from "vue";

export default {
  setup() {
    const countRef = ref(1);

    // watch関数でリアクティブな変数を監視します
    watch(countRef, (count, prevCount) => {
      console.log(count);
      console.log(prevCount);
    });

    return {
      countRef,
    };
  },
};

watchの第1引数には監視対象、第2引数にはcallback関数を渡します。
callback関数の第1引数には変更後の値、第2引数には変更前の値が渡されます。

画像アップローダー

ここからはフォームコンポーネントの続きをやっていきます。
パスワードとテキストエリアに関してはテキストボックスと同じような作りなので省略します。

  • optionsApi
App.vue
<template>
  <div class="contents">
    <div class="imgContent">
      <ImagePreview :imageUrl="imageUrl" />
      <div class="module--spacing--largeSmall"></div>
      <UploadFile @fileList="setFileList" />
    </div>
  </div>
</template>

<script>
import ImagePreview from "./components/ImagePreview.vue";
import UploadFile from "./components/UploadFile.vue";

export default {
  components: {
    ImagePreview,
    UploadFile,
  },
  data() {
    return {
      imageUrl: "",
      fileList: null,
    };
  },
  methods: {
    setFileList(fileList) {
      this.fileList = fileList;
      const imageUrl = URL.createObjectURL(fileList[0]);
      this.imageUrl = imageUrl;
    },
  }
};
</script>

<style>
@media screen and (min-width: 1026px) {
  .imgContent {
    width: 90%;
    max-width: 700px;
    height: 35vh;
    margin: auto;
    margin-bottom: 10px;
    background-color: #ccc;
    padding-top: 5%;
  }
}
@media screen and (min-width: 482px) and (max-width: 1025px) {
  .imgContent {
    width: 90%;
    max-width: 700px;
    height: 20vh;
    margin: auto;
    margin-bottom: 10px;
    background-color: #ccc;
    padding-top: 5%;
  }
}
</style>
UploadFile.vue
<template>
  <label for="corporation_file" class="btn btn-success">
    画像を設定する
    <input
      type="file"
      class="file_input"
      style="display:none;"
      id="corporation_file"
      mulitple="multiple"
      @change="onDrop"
    />
  </label>
</template>

<script>
export default {
  methods: {
    onDrop(e) {
      const imageFile = e.target.files;
      if(imageFile) {
        this.$emit("fileList", imageFile);
      }
    },
  },
};
</script>

<style scoped>
label {
  background-color: #fff;
  padding: 1%;
  width: 40%;
  margin: 0 auto;
  box-shadow: 1px 1px 8px 0px #000;
  display: block;
}
</style>
ImagePreview.vue
<template>
  <div class="imagePreview">
    <img :src="this.imageUrl" width="50" height="50" alt />
  </div>
</template>

<script>
export default {
  props: ["imageUrl"],
};
</script>

<style scoped>
@media screen and (min-width: 1026px) {
  .imagePreview {
    height: 200px;
    width: 200px;
    background: rgb(240, 240, 240);
    overflow: hidden;
    border-radius: 50%;
    background-position: center center;
    background-size: cover;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 20px;
    position: relative;
  }

  .imagePreview img {
    height: 200px;
    width: 200px;
  }
}

@media screen and (min-width: 482px) and (max-width: 1025px) {
  .imagePreview {
    height: 100px;
    width: 100px;
    background: rgb(240, 240, 240);
    overflow: hidden;
    border-radius: 50%;
    background-position: center center;
    background-size: cover;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 20px;
    position: relative;
  }

  .imagePreview img {
    height: 100px;
    width: 100px;
  }
}

@media screen and (max-width: 481px) {
  .imagePreview {
    height: 50px;
    width: 50px;
    background: rgb(240, 240, 240);
    overflow: hidden;
    border-radius: 50%;
    background-position: center center;
    background-size: cover;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 20px;
    position: relative;
  }

  .imagePreview img {
    height: 50px;
    width: 50px;
  }
}
</style>

  • compositionApi
App.vue
<template>
   <div class="imgContent">
    <ImagePreview :imageUrl="form.imageUrl" />
    <div class="module--spacing--largeSmall"></div>
    <UploadFile @fileList="setFileList" />
  </div>
</template>

<script>
import { reactive } from "vue";
import ImagePreview from "./components/ImagePreview.vue";
import UploadFile from "./components/UploadFile.vue";

export default {
  name: "App",
    ImagePreview,
    UploadFile,
  },
  setup() {
    const form = reactive({
      imageUrl: "",
      fileList: null,
    });

    const setFileList = (fileList) => {
      form.fileList = fileList;
      const imgUrl = URL.createObjectURL(fileList[0]);
      form.imageUrl = imgUrl;
    };

    return {
      form,
      setFileList,
    };
  },
};
</script>

<style>
@media screen and (min-width: 1026px) {
  .imgContent {
    width: 90%;
    max-width: 700px;
    height: 35vh;
    margin: auto;
    margin-bottom: 10px;
    background-color: #ccc;
    padding-top: 5%;
  }
}
@media screen and (min-width: 482px) and (max-width: 1025px) {
  .imgContent {
    width: 90%;
    max-width: 700px;
    height: 20vh;
    margin: auto;
    margin-bottom: 10px;
    background-color: #ccc;
    padding-top: 5%;
  }
}
</style>
UploadFile.vue
<template>
  <label for="corporation_file" class="btn btn-success">
    画像を設定する
    <input
      type="file"
      class="file_input"
      style="display: none"
      id="corporation_file"
      mulitple="multiple"
      @change="onDrop"
    />
  </label>
</template>

<script>
export default {
  emits: ["fileList"],
  setup(_, context) {
    const onDrop = (e) => {
      const imageFile = e.target.files;
      if (imageFile) {
        context.emit("fileList", imageFile);
      }
    };
    return { onDrop };
  },
};
</script>

<style scoped>
label {
  background-color: #fff;
  padding: 1%;
  width: 40%;
  margin: 0 auto;
  box-shadow: 1px 1px 8px 0px #000;
  display: block;
}
</style>
ImagePreview.vue
<template>
  <div class="imagePreview">
    <img :src="imageUrl" alt />
  </div>
</template>

<script>
export default {
  props: {
    imageUrl: {
      type: String,
      required: true,
    },
  },
};
</script>

<style scoped>
@media screen and (min-width: 1026px) {
  .imagePreview {
    height: 200px;
    width: 200px;
    background: rgb(240, 240, 240);
    overflow: hidden;
    border-radius: 50%;
    background-position: center center;
    background-size: cover;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 20px;
    position: relative;
  }

  .imagePreview img {
    height: 200px;
    width: 200px;
  }
}

@media screen and (min-width: 482px) and (max-width: 1025px) {
  .imagePreview {
    height: 100px;
    width: 100px;
    background: rgb(240, 240, 240);
    overflow: hidden;
    border-radius: 50%;
    background-position: center center;
    background-size: cover;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 20px;
    position: relative;
  }

  .imagePreview img {
    height: 100px;
    width: 100px;
  }
}

@media screen and (max-width: 481px) {
  .imagePreview {
    height: 50px;
    width: 50px;
    background: rgb(240, 240, 240);
    overflow: hidden;
    border-radius: 50%;
    background-position: center center;
    background-size: cover;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 20px;
    position: relative;
  }

  .imagePreview img {
    height: 50px;
    width: 50px;
  }
}
</style>

セレクトボックス

  • optionsApi
App.vue
<template>
  <div class="contents">
    <SelextBox v-model="select" :options="optionsSelect" />
    <span v-if="this.select == ''">選択オプション:選択してください。</span>
    <span v-else>選択オプション: {{ select }}</span>
  </div>
</template>

<script>
import SelextBox from "./components/SelectBox.vue";

export default {
  components: {
    SelextBox,
  },
  data() {
    return {
      select: 0,
      selectValue: "",
      optionsSelect: [
        { label: "Vue.js", value: "Vue.js" },
        { label: "React", value: "React" },
        { label: "Angular", value: "Angular" },
      ],
    };
  },
};
</script>
SelectBox.vue
<template>
  <select name="select-box" @input="updateValue">
    <option value="0">選択してください</option>
    <option
      v-for="(option, index) in options"
      :key="index"
      :value="option.value"
      >{{ option.label }}</option
    >
  </select>
</template>

<script>
export default {
  props: {
    options: { type: Array, required: true },
  },
  methods: {
    updateValue(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>
  • compositionApi
App.vue
<template>
  <SelectBox
    v-model:select="form.select"
    name="selectbox"
    :options="form.optionsSelect"
  />
  <div class="module--spacing--verySmall"></div>
  <span v-if="form.select == ''">選択オプション:選択してください。</span>
  <span v-else>選択オプション: {{ form.select }}</span>
</template>

<script>
import { reactive } from "vue";
import SelectBox from "./components/SelectBox.vue";

export default {
  name: "App",
  components: {
    SelectBox,
  },
  setup() {
    const form = reactive({
      select: 0,
      selectValue: "",
      optionsSelect: [
        { label: "Vue.js", value: "Vue.js" },
        { label: "React", value: "React" },
        { label: "Angular", value: "Angular" },
      ],
    });

    return {
      form,
    };
  },
};
</script>
SelectBox.vue
<template>
  <select :name="name" @input="updateValue">
    <option value="0">選択してください</option>
    <option
      v-for="(option, index) in options"
      :key="index"
      :value="option.value"
    >
      {{ option.label }}
    </option>
  </select>
</template>

<script>
export default {
  props: {
    name: { type: String, required: true },
    options: { type: Array, required: true },
  },
  emits: ["update:select"],
  setup(_, context) {
    const updateValue = (e) => {
      context.emit("update:select", e.target.value);
    };
    return { updateValue };
  },
};
</script>

ラジオボタン

  • optionsApi
App.vue
<template>
  <div class="contents">
    <RadioButton v-model="checkName" :options="optionsRadio" />
    <span>選択オプション: {{ checkName }}</span>
  </div>
</template>

<script>
import RadioButton from "./components/RadioButton.vue";

export default {
  components: {
    RadioButton,
  },
  data() {
    return {
      checkName: "選択してね",
      optionsRadio: [
        { label: "hoge", value: "hoge" },
        { label: "bow", value: "bow" },
        { label: "fuga", value: "fuga" },
      ],
    };
  },
};
</script>
RadioButton
<template>
  <div>
    <label v-for="(option, index) in options" :key="index">
      <!-- ラジオボタンにはname属性必須 -->
      <input
        type="radio"
        :value="option.value"
        @change="updateValue"
        name="radio-button"
      />{{ option.label }}</label
    >
  </div>
</template>

<script>
export default {
  props: {
    options: { type: Array, required: true },
  },
  methods: {
    updateValue(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>
  • compositionApi
App.vue
<template>
  <RadioButton
    v-model:modalValue="form.checkName"
    :options="form.optionsRadio"
  />
  <span>選択オプション: {{ form.checkName }}</span>
</template>

<script>
import { reactive } from "vue";
import RadioButton from "./components/RadioButton.vue";

export default {
  name: "App",
  components: {
    RadioButton,
  },
  setup() {
    const form = reactive({
      checkName: "選択してね",
      optionsRadio: [
        { label: "hoge", value: "hoge" },
        { label: "bow", value: "bow" },
        { label: "fuga", value: "fuga" },
      ],
    });

    return {
      form,
    };
  },
};
</script>
RadioButton.vue
<template>
  <div>
    <label v-for="(option, index) in options" :key="index">
      <!-- ラジオボタンにはname属性必須 -->
      <input
        type="radio"
        :value="option.value"
        @change="updateValue"
        name="radio-button"
      />{{ option.label }}</label
    >
  </div>
</template>

<script>
export default {
  props: {
    options: { type: Array, required: true },
  },
  emits: ["update:modalValue"],
  setup(_, context) {
    const updateValue = (e) => {
      context.emit("update:modalValue", e.target.value);
    };
    return { updateValue };
  },
};
</script>

ボタン

チェックボックスにチェックを入れないとボタンがクリックできなくて、
そのボタンをクリックしたらモーダルが出現するものを実装します。(説明下手でごめんなさい。)

  • optionsApi
App.vue


<template>
  <div class="contents">
    <CheckBox v-model="checked" :checked="checked" />
    <label>同意</label><br />

    <Button :disabled="!checked" msg="モーダルが出ます" @push="click" />
    <Modal
      title="モーダルタイトル"
      detail="モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。"
      v-if="open"
      @close="open = false"
      @modal-click="modalClick"
    />
  </div>
</template>

<script>
import CheckBox from "./components/CheckBox.vue";
import Button from "./components/Button.vue";
import Modal from "./components/Modal.vue";

export default {
  components: {
    CheckBox,
    Button,
    Modal,
  },
  data() {
    return {
      checked: false,
      open: false,
    };
  },
  methods: {
    click() {
      this.open = true;
    },
    modalClick() {
      console.log("テキストボックスの入力内容:", this.text);
      console.log("パスワードの入力内容:", this.pass);
      console.log("テキストエリアの入力内容:", this.textarea);
      console.log("画像の名前:", this.fileList[0].name);
      alert("コンソールを見ろ!!");
      this.open = false;
      this.checked = false;
      // this.uploadImage();
    },
};
</script>
CheckBox.vue
<template>
  <input type="checkbox" @change="updateValue" :checked="this.checked" />
</template>

<script>
export default {
  props: ["checked"],
  methods: {
    updateValue(e) {
      this.$emit("input", e.target.checked);
    },
  },
};
</script>
Button.vue
<template>
  <button class="button" @click="push">
    {{ this.msg }}
  </button>
</template>

<script>
export default {
  props: ["msg"],
  methods: {
    push() {
      this.$emit("push");
    },
  },
};
</script>
Modal.vue
<template>
  <transition name="modal">
    <div class="overlay" @click="$emit('close')">
      <div class="panel" @click.stop>
        <h2>{{ this.title }}</h2>
        <div class="module--spacing--small"></div>
        <div class="modal-contents">
          <p>{{ this.detail }}</p>
        </div>
        <Button :disabled="false" msg="ボタン" @push="click" />
      </div>
    </div>
  </transition>
</template>

<script>
import Button from "./Button.vue";
export default {
  props: ["title", "detail"],
  components: {
    Button,
  },
  methods: {
    click() {
      this.$emit("modal-click");
    },
  },
};
</script>

<style scoped>
.overlay {
  background: rgba(0, 0, 0, 0.8);
  position: fixed;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 900;
  transition: all 0.5s ease;
}
.panel {
  width: 40%;
  text-align: center;
  background: #fff;
  padding: 40px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.modal-contents {
  text-align: left;
}
.modal-enter,
.modal-leave-active {
  opacity: 0;
}
.modal-enter .panel,
.modal-leave-active .panel {
  top: -200px;
}
</style>
  • compositionApi
App.vue
<template>
  <CheckBox v-model:change="form.checked" :checked="form.checked" />
  <label>同意</label><br />

  <Button :disabled="!form.checked" msg="モーダルが出ます" @push="handleOpen" />
  <Modal
    title="モーダルタイトル"
    detail="モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。モーダルの内容です。"
    v-if="form.open"
    @close="form.open = false"
    @modal-click="modalClick"
  />
</template>

<script>
import { reactive } from "vue";
import CheckBox from "./components/CheckBox.vue";
import Button from "./components/Button.vue";
import Modal from "./components/Modal.vue";

export default {
  name: "App",
  components: {
    CheckBox,
    Button,
    Modal,
  },
  setup() {
    const form = reactive({
      checked: false,
      open: false,
    });

    const handleOpen = () => {
      form.open = true;
    };
    const modalClick = () => {
      aleat("モーダルの中のボタンをクリックしました")
    };


    return {
      form,
      handleOpen,
      modalClick
    };
  },
};
</script>
CheckBox.vue
<template>
  <input type="checkbox" @change="updateValue" :checked="checked" />
</template>

<script>
export default {
  model: {
    prop: "checked",
    event: "change",
  },
  props: {
    checked: {
      type: Boolean,
      required: true,
    },
  },
  emits: ["update:change"],
  setup(_, context) {
    const updateValue = (e) => {
      context.emit("update:change", e.target.checked);
    };
    return { updateValue };
  },
};
</script>
Button.vue
<template>
  <button class="button" @click="push">
    {{ msg }}
  </button>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true,
    },
  },
  emits: ["push"],
  setup(_, context) {
    const push = () => {
      context.emit("push");
    };

    return { push };
  },
};
</script>
Modal.vue
<template>
  <transition name="modal">
    <div class="overlay" @click="handleClose">
      <div class="panel" @click.stop>
        <h2>{{ title }}</h2>
        <div class="module--spacing--small"></div>
        <div class="modal-contents">
          <p>{{ detail }}</p>
        </div>
        <Button :disabled="false" msg="ボタン" @push="click" />
      </div>
    </div>
  </transition>
</template>

<script>
import Button from "./Button.vue";
export default {
  props: {
    title: {
      type: String,
      required: true,
    },
    detail: {
      type: String,
      required: true,
    },
  },
  components: {
    Button,
  },
  emits: ["close", "modal-click"],
  setup(_, context) {
    const handleClose = () => {
      context.emit("close");
    };
    const click = () => {
      context.emit("modal-click");
    };
    return { handleClose, click };
  },
};
</script>

<style scoped>
.overlay {
  background: rgba(0, 0, 0, 0.8);
  position: fixed;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 900;
  transition: all 0.5s ease;
}

.panel {
  width: 40%;
  text-align: center;
  background: #fff;
  padding: 40px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.modal-contents {
  text-align: left;
}

.modal-enter,
.modal-leave-active {
  opacity: 0;
}

.modal-enter .panel,
.modal-leave-active .panel {
  top: -200px;
}
</style>

まとめ

以上。

フォームコンポーネントを作りながらcompositionApiについて書かせていただきました。
ご参考になれば幸いです。
もし間違いなどがあれば、ご教授お願いします。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1