意気揚々とAtomic Designを導入したら、v-modelとmethodsに躓いたお話

## Atomic Designとは
パーツ・コンポーネント単位で定義していく、UIデザイン手法です。

最小の単位「原子」(Lv1)からデザインし、「分子」(Lv2)、「生体」(Lv3)、「テンプレート」(Lv4)、「ページ」(Lv5)の順にデザイン作業を進めていきます。

今回はフォームを作成しました。

| Atoms          | Template |
| -------------- | -------- |
| FormInput.vue  | Form.vue |
| FormButton.vue |          |

Atomsに**フォームの入力欄**の`FormInput`、**フォームの送信ボタン**の`FormButton`の作成をします。
Templateに、Atomsを使って作成したフォームを`Form`として作成します。

以下の記事にもあるように、完璧な線引きは難しいので私は適当にAtomsとTemplateを使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/1a2c07e0-091a-29a4-e786-66f7e3b863da.png)


https://qiita.com/rhirayamaaan/items/7f990e146ec01f2e7e08

https://note.com/tabelog_frontend/n/n07b4077f5cf3


## `FormInput.vue`を作成する
`FormInput.vue`では、v-modelでの値の渡し方に躓きました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/7a567529-2c2e-5b73-6f06-aa8cadfb7c6d.png)


ありがちなミスですが、親から受け取った`value`を`v-model`にしました。
ただこれではうまくいきません。

```vue:Atoms/FormInput.vue
<template>
  <input v-model="value" />
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, PropType } from '@nuxtjs/composition-api';

export default defineComponent({
  props: {
    value: {
      type: String,
      required: true,
    },
  },
});
```

何故なら、`value`は親から渡された値なので、**読み取り専門**になるからです。
子で値を上書きしても、その値が親まで伝わりません。


### `v-model`を使った値の渡し方
答えはこの記事にありました。
要約すると、`v-model`は`:value`と`@input`の糖衣構文だから分解しなさいという内容です。

https://qiita.com/ozone/items/b75efe5c449cbc469b1e#v-model%E3%82%92%E4%BD%BF%E3%81%86%E8%A6%81%E7%B4%A0%E3%82%92%E3%83%A9%E3%83%83%E3%83%97%E3%81%99%E3%82%8B

ということで実際にコードを書いてみましょう。
まずは親からです。

```vue:Template/Form.vue
<template>
  <div class="form-container">
    <FormInput :type="'text'" :placeholder="'姓'" :value="firstName" @input="firstName = $event" />
    <FormInput :type="'text'" :placeholder="'名'" :value="lastName" @input="lastName = $event" />
    <FormInput
      :type="'email'"
      :placeholder="'メールアドレス'"
      :value="email"
      @input="email = $event"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from '@nuxtjs/composition-api';

// components
import FormInput from '@/components/Atoms/FormInput.vue';

export default defineComponent({
  components: {
    FormInput,
  },
  setup() {
    const firstName = ref<string>('');
    const lastName = ref<string>('');
    const email = ref<string>('');

    return {
      firstName,
      lastName,
      email,
    };
  },
});
</script>
```

次に子です。

```vue:Atoms/FormInput.vue
<template>
  <div>
    <input
      class="form-input"
      :type="type"
      :placeholder="placeholder"
      :value="value"
      @input="$emit('input', $event.target.value)"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, PropType } from '@nuxtjs/composition-api';

// types
import { FormInputType } from './types/FormInput.type';

export default defineComponent({
  props: {
    type: {
      type: String as PropType<FormInputType>,
      default: 'text',
      validator: function(value: string) {
        return ['text', 'email', 'password'].indexOf(value) !== -1;
      },
    },
    placeholder: {
      type: String,
      required: true,
    },
    value: {
      type: String,
      required: true,
    },
  },
});
</script>
```

```typescript:Atoms/Types/FormInput.type.ts
export type FormInputType = 'text' | 'email' | 'password';
```

これで無事に動きました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/3a1b4148-9d0e-20b1-7881-80cd85b68149.png)


## `FormButton.vue`を作成する
`FormButton.vue`では、methodsの使い方に躓きました。
方法としては以下の2つです。
①`Vuex`を使う
②`$emit`や`props`を使う

VuexとTypeScriptの相性が悪いので、①はなし。
なので②の方法を考えました。

### `$emit`を使わずに作る
以下の記事を参考にしました。
TypeScriptを使うので`props`の成約をつけられるというメリットでこちらを採用しました。

https://techblog.roxx.co.jp/entry/2020/07/09/092955

ということで実際にコードを書いてみましょう。
まずは親からです。

```vue:Template/Form.vue
<template>
  <div class="form-container">
    <FormButton :click="submit" @submit="submit" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from '@nuxtjs/composition-api';

// components
import FormButton from '@/components/Atoms/FormButton.vue';

export default defineComponent({
  components: {
    FormButton,
  },
  setup() {
    const submit = async (): Promise<void> => {
      try {
        await window.liff.sendMessages([
          {
            type: 'text',
            text: firstName.value,
          },
          {
            type: 'text',
            text: lastName.value,
          },
          {
            type: 'text',
            text: email.value,
          },
        ]);
        await window.liff.closeWindow();
      } catch (err) {
        window.alert(err);
        await window.liff.closeWindow();
      }
    };

    return {
      submit,
    };
  },
});
</script>
```

次に子です。

```vue:Atoms/FormButton.vue
<template>
  <div class="form-button">
    <button class="button" @click="childClick">送信</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, PropType } from '@nuxtjs/composition-api';

// type
import { ClickType, Props } from './types/FormButton.type';

export default defineComponent({
  props: {
    click: {
      type: Function as ClickType,
      required: true,
    },
  },
  setup(props: Props) {
    // methods
    const childClick = () => {
      props.click();
    };

    return {
      childClick,
    };
  },
});
</script>
```

```typescript:Atoms/Types/FormButton.type.ts
export type ClickType = {
  (): void;
};

export type Props = {
  click: Function;
};
```

これで完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/838372/03f43b39-f4c2-6df8-afd1-ae712aa27ce4.png)