🙌

Nuxt.js+Vuetifyでダイアログで使えるフォームを作るまで

2022/04/24に公開

Nuxt.js+Vuetifyでダイアログで値を編集して更新できる感じのフォームを実装する。

完成イメージ

プロフィールを表示・編集ができる画面を実装する。

  • 表示部分:取得したプロフィールを表示。編集ボタンを押下した時に編集フォームを開設、
  • 編集フォーム:更した値を「変更する」を押下した時点で表示部分に反映させてダイアログを閉じる。

表示部分

編集フォーム


構成

.
├── domain
│   └── components
│       └── profile
│           ├── ProfileCard.vue - プロフィール表示部分
│           ├── ProfileEditDialog.vue - プロフィール編集ダイアログ
│           └── ProfileEditForm.vue - プロフィール編集フォーム
├── domain
│   └── profile
│       └── model
│           ├── Address.ts - 住所
│           ├── Contact.ts - 連絡先
│           └── Profile.ts - プロフィール
└── pages
    └── profile
        └── index.vue - プロフィール画面

コンポーネントの親子関係

  • 親から順にindex.vue > ProfileCard.vue > ProfileEditDialog.vue > ProfileEditForm.vueの関係になる。


props・emit

親子間でデータの受け渡しをするので、props・emitを意識する。

props:親→子に値を渡す。
emit:子→親に値を渡す。

データ受け渡しのイメージ

  • プロフィールの表示:index.vue -(props)→ ProfileCard.vue
  • プロフィール編集画面の表示:ProfileCard.vue -(props)→ ProfileEditDialog.vue -(props)→ ProfileEditForm.vue
  • プロフィールの更新結果を表示に反映:ProfileEditForm.vue -(emit)→ ProfileEditDialog.vue -(emit)→ ProfileCard.vue

ProfileEditDialog→ProfileEditForm.vue、ProfileEditDialog.vue→ProfileCard.vueの間では、値をディープコピーして渡す。

理由は以下。

  1. Vue.jsは双方向データバインディングである。[1]
  2. Vuetifyでは、Vue.jsが用意している「.lazy」が機能しない。(通常は.lazyを使用する。) [2]

ソース

domain/components/profile

ProfileCard.vue
<template>
  <div>
    <v-card class="ma-auto pa-4" max-width="400" tile>
      <v-card-title
        >Profile
        <profile-edit-dialog
          :initial-data="state.profile"
          @submitted="submitted"
        />
      </v-card-title>
      <v-divider></v-divider>
      <v-card-subtitle class="pb-1 font-weight-bold">名前</v-card-subtitle>
      <v-card-text class="pt-0 pb-1">
        {{ state.profile.lastName + ' ' + state.profile.firstName }}
      </v-card-text>
      <v-card-subtitle class="pt-0 pb-1 font-weight-bold"
        >電話番号</v-card-subtitle
      >
      <v-card-text class="pt-0 pb-1">
        {{ state.profile.contact.phoneNumber }}
      </v-card-text>
      <v-card-subtitle class="pt-0 pb-1 font-weight-bold"
        >メールアドレス</v-card-subtitle
      >
      <v-card-text class="pt-0 pb-1">
        {{ state.profile.contact.mail }}
      </v-card-text>
      <template v-for="(address, index) in state.profile.addresses">
        <v-card-subtitle class="pt-0 pb-1 font-weight-bold" :key="index + '_1'"
          >住所{{ index + 1 }}</v-card-subtitle
        >
        <v-card-text class="pt-0 pb-1" :key="index + '_2'">
          {{
            address.prefacture +
            address.municpality +
            address.blockNumber +
            address.building
          }}
        </v-card-text>
      </template>
    </v-card>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType, reactive } from '@nuxtjs/composition-api'
import Profile from '~/domain/profile/model/Profile'

type Props = {
  initialData: Profile
}

export default defineComponent({
  props: {
    initialData: {
      type: Object as PropType<Profile>,
    },
  },
  setup(props: Props) {
    const state = reactive({
      profile: props.initialData,
    })
    const submitted = (p: Profile) => {
      state.profile = p
    }
    return { state, submitted }
  },
})
</script>
  • submittedで表示部分を更新する。
ProfileEditDialog.vue
<template>
  <div>
    <v-dialog v-model="editDialog" max-width="500">
      <template v-slot:activator="{ on, attrs }">
        <v-btn text rounded v-bind="attrs" v-on="on">
          <v-icon>{{ mdiPencil }}</v-icon>
          編集
        </v-btn>
      </template>
      <v-card class="ma-auto pa-4">
        <profile-edit-form v-model="state.profile" />
        <v-card-actions>
          <v-btn color="primary" @click="submit">変更する</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  PropType,
  reactive,
  ref,
  SetupContext,
} from '@nuxtjs/composition-api'
import Profile from '~/domain/profile/model/Profile'
import { mdiPencil } from '@mdi/js'
import clone from 'just-clone'

type Props = {
  initialData: Profile
}

export default defineComponent({
  props: {
    initialData: {
      type: Object as PropType<Profile>,
    },
  },
  setup(props: Props, context: SetupContext) {
    const editDialog = ref(false)
    const close = () => (editDialog.value = false)
    const state = reactive({
      profile: clone(props.initialData),
    })
    const submit = () => {
      context.emit('submitted', clone(state.profile))
      close()
    }
    return { state, editDialog, mdiPencil, submit }
  },
})
</script>
  • ProfileEditForm.vueにはstate.profileをディープコピーした値を渡す。
  • submitが発火した時にsubmittedにstate.profileをディープコピーした値を引数にして渡す。
  • ディープコピーにはjust-cloneを使用する。[3]
ProfileEditForm.vue
<template>
  <div v-if="form">
    <v-card-title>Profile</v-card-title>
    <v-card-text>
      <v-form>
        <v-container>
          <div class="mb-2 text-subtitle-1 font-weight-bold">名前</div>
          <v-row no-gutters>
            <v-col class="mr-3">
              <v-text-field label="" v-model="form.lastName"></v-text-field>
            </v-col>
            <v-col class="ml-3">
              <v-text-field label="" v-model="form.firstName"></v-text-field>
            </v-col>
          </v-row>
          <div class="mb-2 text-subtitle-1 font-weight-bold">連絡先</div>
          <v-text-field
            label="電話番号"
            v-model="form.contact.phoneNumber"
          ></v-text-field>
          <v-text-field
            label="メールアドレス"
            v-model="form.contact.mail"
          ></v-text-field>
          <div v-for="(address, index) in form.addresses" :key="address.id">
            <div class="mb-2 text-subtitle-1 font-weight-bold">
              住所 {{ index + 1 }}
            </div>
            <v-row no-gutters>
              <v-col class="mr-3">
                <v-text-field
                  label="都道府県"
                  v-model="address.prefacture"
                ></v-text-field>
              </v-col>
              <v-col class="ml-3">
                <v-text-field
                  label="市区町村"
                  v-model="address.municpality"
                ></v-text-field>
              </v-col>
            </v-row>
            <v-row no-gutters>
              <v-col class="mr-3">
                <v-text-field
                  label="番地"
                  v-model="address.blockNumber"
                ></v-text-field>
              </v-col>
              <v-col class="ml-3">
                <v-text-field
                  label="建物"
                  v-model="address.building"
                ></v-text-field>
              </v-col>
            </v-row>
          </div>
        </v-container>
      </v-form>
    </v-card-text>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  PropType,
  reactive,
  ref,
  SetupContext,
} from '@nuxtjs/composition-api'
import Profile from '~/domain/profile/model/Profile'
import { mdiPencil } from '@mdi/js'

type Props = {
  value: Profile
}

export default defineComponent({
  props: {
    value: {
      type: Object as PropType<Profile>,
    },
  },
  setup(props: Props) {
    const form = reactive(props.value)
    const editDialog = ref(false)
    return { form, editDialog, mdiPencil }
  },
})
</script>

domain/profile/model

Address.ts
type Address = {
    id: number
    prefacture: string
    municpality: string
    blockNumber: string
    building: string
}

export default Address
Contact.ts
type Contact = {
    phoneNumber: string
    mail: string
}

export default Contact
Profile.ts
import Address from "./Address"
import Contact from "./Contact"

type Profile = {
    id: number
    firstName: string
    lastName: string
    contact: Contact
    addresses: Address[]
}

export default Profile

pages/profile

index.vue
<template>
  <div>
    <profile-card :initial-data="profile" />
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from '@nuxtjs/composition-api'
import Profile from '~/domain/profile/model/Profile'

export default defineComponent({
  setup() {
    const profile = reactive<Profile>({
      id: 1,
      firstName: '太郎',
      lastName: '山田',
      contact: {
        phoneNumber: '08011112222',
        mail: 'xxx@example.com',
      },
      addresses: [
        {
          id: 1,
          prefacture: '東京都',
          municpality: '新宿区',
          blockNumber: '新宿1-1-1',
          building: '新宿ビル1階',
        },
        {
          id: 2,
          prefacture: '東京都',
          municpality: '渋谷区',
          blockNumber: '渋谷2-2-2',
          building: '渋谷ビル2階',
        },
      ],
    })
    return { profile }
  },
})
</script>
  • ここにAPIからの取得処理を記載する。
脚注
  1. フォーム入力バインディング ↩︎

  2. v-text-field v-model lazy doesn't work #1810 ↩︎

  3. https://github.com/angus-c/just#just-clone ↩︎

Discussion