vue3(要素の追加、v-for)

<script setup lang="ts">
import { computed } from 'vue';
import ErrorMessage from '@admin/Components/Parts/ErrorMessage.vue';
import HeadLabel from '@admin/Components/Parts/Input/HeadLabel.vue';
import SectionTitle from '../Parts/SectionTitle.vue';
import { Errors } from '@inertiajs/core';
import InputText from '@admin/Components/Parts/Input/InputText.vue';
import DescriptionForInputHtml from '../Parts/DescriptionForInputHtml.vue';
import DescriptionForSelectList from '../Parts/DescriptionForSelectList.vue';
import PrimaryButton from '@admin/Components/Parts/Button/PrimaryButton.vue';
import SecondaryButton from '@admin/Components/Parts/Button/SecondaryButton.vue';
import InputRadio from '@admin/Components/Parts/Input/InputRadio.vue';
import InputHtmlEditor from '@admin/Components/Parts/Input/InputHtmlEditor.vue';

export interface EthicType {
  description: string;
  descriptionHtml: string;
  descriptionEn: string;
  descriptionEnHtml: string;
  categories: { name: string; requiredChild: number | null }[];
  consentAcquisitions: { name: string }[];
}
export type ConsentAcquisitionIdType = number;
export type EthicCategoryRequiredChildType = {
  label: string;
  value: number;
};

const ethic = defineModel<EthicType>({ required: true });
defineProps<{
  ethicCategoryRequiredChildren: EthicCategoryRequiredChildType[];
  consentAcquisitionId: ConsentAcquisitionIdType;
  errors: Errors;
}>();

const maxCategories = 20;
const haveReachedMaxCategories = computed(() => {
  return ethic.value.categories.length >= maxCategories;
});

const maxConsentAcquisitions = 20;
const haveReachedMaxConsentAcquisitions = computed(() => {
  return ethic.value.consentAcquisitions.length >= maxConsentAcquisitions;
});

const addCategoryRow = () => {
  ethic.value.categories.push({ name: '', requiredChild: null });
};
const deleteCategoryRow = (index: number) => {
  ethic.value.categories.splice(index, 1);
};
const addConsentAcquisitionRow = () => {
  ethic.value.consentAcquisitions.push({ name: '' });
};
const deleteConsentAcquisitionRow = (index: number) => {
  ethic.value.consentAcquisitions.splice(index, 1);
};
</script>

<template>
  <section role="group" aria-labelledby="ethic">
    <SectionTitle id="ethic" value="倫理委員会" />
    <div class="mx-auto w-80">
      <table class="mb-40 table-row-header">
        <tbody>
          <tr>
            <th class="w-30"><HeadLabel :required="true">説明文(日本語)</HeadLabel></th>
            <td>
              <DescriptionForInputHtml />
              <InputHtmlEditor
                aria-label="説明文"
                v-model:html="ethic.descriptionHtml"
                v-model:json="ethic.description"
                class="field-auto"
              />
              <ErrorMessage :value="errors['ethic.description']" />
              <ErrorMessage :value="errors['ethic.descriptionHtml']" />
            </td>
          </tr>
          <tr>
            <th class="w-30"><HeadLabel :required="true">説明文(英語)</HeadLabel></th>
            <td>
              <DescriptionForInputHtml />
              <InputHtmlEditor
                aria-label="説明文"
                v-model:html="ethic.descriptionEnHtml"
                v-model:json="ethic.descriptionEn"
                class="field-auto"
              />
              <ErrorMessage :value="errors['ethic.descriptionEn']" />
              <ErrorMessage :value="errors['ethic.descriptionEnHtml']" />
            </td>
          </tr>
        </tbody>
      </table>
      <div role="group" aria-label="カテゴリー">
        <DescriptionForSelectList>「倫理審査カテゴリー」の選択肢を編集してください。</DescriptionForSelectList>
        <table class="mb-20 table-column-header">
          <thead>
            <tr>
              <th>操作</th>
              <th class="w-80">倫理審査カテゴリー/子項目の設定</th>
            </tr>
          </thead>
          <tbody v-for="(category, key) in ethic.categories" :key="key" :aria-label="`選択肢${key + 1}`">
            <tr>
              <td class="text-center">
                <SecondaryButton v-if="key !== 0" @click="deleteCategoryRow(key)">削除</SecondaryButton>
              </td>
              <td>
                <table class="table-row-header table-inner">
                  <tbody>
                    <tr>
                      <th class="w-15rem"><HeadLabel :required="true">倫理審査カテゴリー</HeadLabel></th>
                      <td>
                        <InputText aria-label="名称" v-model="category.name" />
                        <ErrorMessage :value="errors[`ethic.categories.${key}.name`]" />
                      </td>
                    </tr>
                    <tr>
                      <th><HeadLabel :required="true">子項目</HeadLabel></th>
                      <td>
                        <div class="flex flex-column">
                          <template
                            v-for="(requiredChild, requiredChildKey) in ethicCategoryRequiredChildren"
                            :key="requiredChildKey"
                          >
                            <InputRadio :value="requiredChild.value" v-model="category.requiredChild">{{
                              requiredChild.label
                            }}</InputRadio>
                          </template>
                        </div>
                        <ErrorMessage :value="errors[`ethic.categories.${key}.requiredChild`]" />
                      </td>
                    </tr>
                  </tbody>
                </table>
              </td>
            </tr>
          </tbody>
        </table>
        <div>
          <PrimaryButton @click="addCategoryRow" :disabled="haveReachedMaxCategories">選択肢追加</PrimaryButton>
        </div>
      </div>

      <div role="group" aria-label="同意の取得" class="ethic-box">
        <template v-if="ethic.categories.some((category) => category.requiredChild === consentAcquisitionId)">
          <DescriptionForSelectList>「同意の取得」の選択肢を編集してください。</DescriptionForSelectList>
          <table class="mb-20 table-column-header">
            <thead>
              <tr>
                <th class="w-20">操作</th>
                <th>同意の取得選択肢</th>
              </tr>
            </thead>
            <tbody>
              <tr
                v-for="(consentAcquisition, key) in ethic.consentAcquisitions"
                :key="key"
                :aria-label="`選択肢${key + 1}`"
              >
                <td class="text-center">
                  <SecondaryButton v-if="key !== 0" @click="deleteConsentAcquisitionRow(key)">削除</SecondaryButton>
                </td>
                <td>
                  <div class="flex items-center">
                    <span class="w-10"><HeadLabel :required="true" /></span>
                    <InputText aria-label="名称" v-model="consentAcquisition.name" />
                  </div>
                  <ErrorMessage :value="errors[`ethic.consentAcquisitions.${key}.name`]" />
                </td>
              </tr>
            </tbody>
          </table>
          <div>
            <PrimaryButton @click="addConsentAcquisitionRow" :disabled="haveReachedMaxConsentAcquisitions"
              >選択肢追加</PrimaryButton
            >
          </div>
        </template>
      </div>
    </div>
  </section>
</template>

✅ Vue.js の v-for における key の適切な使い方

Vue.js の v-for では、リストをレンダリングする際に :key を指定することで、効率的な更新が可能になります。key の適切な使い方を理解することで、パフォーマンスの最適化やバグの防止ができます。

🚀 v-for の基本的な書き方

Vue.js の v-for は、配列の各要素を繰り返し描画するために使用されます。基本的な構文は以下の通りです。

<tr v-for="item in items" :key="item.id">
  <td>{{ item.name }}</td>
</tr>

ここでのポイント

• v-for=”item in items” → items 配列の各要素を item としてループする

• :key=”item.id” → id を key に指定することで、効率的な更新が可能になる

✅ key には何を指定すべき?

1. 一意の識別子 (id) を key にする(ベストプラクティス)

<tr v-for="item in items" :key="item.id">
  <td>{{ item.name }}</td>
</tr>

📌 一意の id を key に指定すると、Vue が要素の変更を適切に判断できる。

2. インデックス (index) を key にする(非推奨)

<tr v-for="(item, index) in items" :key="index">
  <td>{{ item.name }}</td>
</tr>

🚨 インデックスを key にすると、以下の問題が発生する可能性がある

要素の追加・削除時に、意図しない UI の再描画が発生する

ユーザー入力を含むリストでは、データがずれる可能性がある

🚀 v-for の key に index を使うべきでない理由

以下の配列があるとします:

items = [
  { id: 1, name: "Item A" },
  { id: 2, name: "Item B" },
  { id: 3, name: "Item C" }
];

✅ 正しい方法 (key に id を使用)

<tr v-for="item in items" :key="item.id">
  <td>{{ item.name }}</td>
</tr>

→ id を key にしているので、並び順が変わっても適切に処理される!

🚨 問題のある方法 (key に index を使用)

<tr v-for="(item, index) in items" :key="index">
  <td>{{ item.name }}</td>
</tr>

この場合、もし items の順番が変わると、Vue は index に基づいて要素を再描画 するため、意図しない UI のバグが発生する可能性があります。

📌 問題点

1. 削除や追加があると、インデックスが振り直され、意図しない再描画が発生

2. フォーム要素(input など)を含む場合、データが入れ替わることがある

🎯 v-for の key の選び方まとめ

指定する key適用すべきケース推奨度
item.id(一意のID)データに一意の id がある場合(推奨!)✅ ベスト
indexリストの並びが変わらない場合のみ⚠️ 非推奨

📌 Vue.js では key に id を指定するのがベスト! index を使うと意図しない UI のバグが発生する可能性があるので注意!🚀

コメント

タイトルとURLをコピーしました