<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 のバグが発生する可能性があるので注意!🚀
コメント