商品が削除される時に何が起こっているのか?
はじめに
例えば、トマト水煮を削除するには、

チェックボックスを押す。
削除された。

この理由をソースコードドリブンで解明していこう。
フロントエンド

要素の検証で確認すると、チェックボックスは、inputタグになっている。
frontend/src/components/ShoppingList.vueでは、以下のあたりが関係してきそうだ。
<template>
<div>
<section v-for="section in sections" :key="section.id" class="section">
<draggable
v-model="section.items"
item-key="id"
@end="onReorder(section)"
class="list"
>
<template #item="{ element: item }">
<div class="item">
<label class="checkbox">
<input type="checkbox" @change="toggleItem(item)" />
</label>
<div class="item-main">
<div class="name">{{ item.name }}</div>
<div class="qty" v-if="item.qty">{{ item.qty }}</div>
</div>
<button class="delete" @click="hardDelete(item)">×</button>
</div>
</template>
</draggable>
</section>
</div>
</template>
@change=”toggleItem(item)”
<template>
<div>
<section v-for="section in sections" :key="section.id" class="section">
<draggable
v-model="section.items"
item-key="id"
@end="onReorder(section)"
class="list"
>
<template #item="{ element: item }">
<div class="item">
<label class="checkbox">
<input type="checkbox" @change="toggleItem(item)" />
</label>
</div>
</template>
</draggable>
</section>
</div>
</template>
@changeとは何か?
HTML要素のchangeイベントが発生した時に、指定したメソッドや式を実行するための構文。
今回は、チェックボックスにチェックをいれると、toggleItem(item)が発火する。
toggleItem(item)
toggleItem関数は以下のように定義されている。
<script setup>
async function toggleItem(item) {
// 画面から即消す(ハード削除の挙動)
for (const s of sections) {
const i = s.items.findIndex(it => it.id === item.id)
if (i >= 0) { s.items.splice(i, 1); break }
}
saveLocal(sections)
if (!offline.value && !(''+item.id).startsWith('tmp-item-')) {
await axios.post('/api/items/' + item.id + '/toggle/', { hard_delete: true })
} else {
pushQueue({ type: 'toggleItem', payload: { id: item.id, hard_delete: true } })
}
}
</script>
まず行われるのは、画面上の削除。
<script setup>
async function toggleItem(item) {
// 画面から即消す(ハード削除の挙動)
for (const s of sections) {
const i = s.items.findIndex(it => it.id === item.id)
if (i >= 0) { s.items.splice(i, 1); break }
}
saveLocal(sections)
if (!offline.value && !(''+item.id).startsWith('tmp-item-')) {
await axios.post('/api/items/' + item.id + '/toggle/', { hard_delete: true })
} else {
pushQueue({ type: 'toggleItem', payload: { id: item.id, hard_delete: true } })
}
}
</script>
次に、ローカルストレージに保存される。
<script setup>
async function toggleItem(item) {
// 画面から即消す(ハード削除の挙動)
for (const s of sections) {
const i = s.items.findIndex(it => it.id === item.id)
if (i >= 0) { s.items.splice(i, 1); break }
}
saveLocal(sections)
if (!offline.value && !(''+item.id).startsWith('tmp-item-')) {
await axios.post('/api/items/' + item.id + '/toggle/', { hard_delete: true })
} else {
pushQueue({ type: 'toggleItem', payload: { id: item.id, hard_delete: true } })
}
}
</script>
そして、オンラインの場合、サーバーに削除リクエストを送信する。
<script setup>
async function toggleItem(item) {
if (!offline.value && !(''+item.id).startsWith('tmp-item-')) {
await axios.post('/api/items/' + item.id + '/toggle/', { hard_delete: true })
} else {
pushQueue({ type: 'toggleItem', payload: { id: item.id, hard_delete: true } })
}
}
</script>
そして、オフラインの場合、アプリ起動IPに接続できた時に送るためにキューに保存する。
<script setup>
async function toggleItem(item) {
if (!offline.value && !(''+item.id).startsWith('tmp-item-')) {
await axios.post('/api/items/' + item.id + '/toggle/', { hard_delete: true })
} else {
pushQueue({ type: 'toggleItem', payload: { id: item.id, hard_delete: true } })
}
}
</script>
item
引数のitemについて解説していこう。

トマト水煮のような一つ一つの商品であることはわかる。
ソース上でどうやってitemがtoggleItem関数に渡されていくか?
それを見ていこう。
<template>
<div>
<section v-for="section in sections" :key="section.id" class="section">
<draggable
v-model="section.items"
item-key="id"
@end="onReorder(section)"
class="list"
>
<template #item="{ element: item }">
<div class="item">
<label class="checkbox">
<input type="checkbox" @change="toggleItem(item)" />
</label>
<div class="item-main">
<div class="name">{{ item.name }}</div>
<div class="qty" v-if="item.qty">{{ item.qty }}</div>
</div>
<button class="delete" @click="hardDelete(item)">×</button>
</div>
</template>
</draggable>
</section>
</div>
</template>
v-for=”section in sections”
まずはここに注目していただきたい。
<template>
<div>
<section v-for="section in sections" :key="section.id" class="section">
</section>
</div>
</template>
sectionsからsectionを一つずつ取り出して処理している。
sections
sectionsは以下のようにスクリプトで定義されている。
<script setup>
const sections = reactive([])
</script>
以下のバローや無印のセクションのこと。

section
sectionは、バロー、または無印のセクション。
v-model=”section.items”
<template>
<div>
<section v-for="section in sections" :key="section.id" class="section">
<draggable
v-model="section.items"
item-key="id"
@end="onReorder(section)"
class="list"
>
<template #item="{ element: item }">
<div class="item">
<label class="checkbox">
<input type="checkbox" @change="toggleItem(item)" />
</label>
<div class="item-main">
<div class="name">{{ item.name }}</div>
<div class="qty" v-if="item.qty">{{ item.qty }}</div>
</div>
<button class="delete" @click="hardDelete(item)">×</button>
</div>
</template>
</draggable>
</section>
</div>
</template>
v-modelでsectionのitemsを紐づけている。
v-modelは、フォーム要素とVueの変数を双方向にバインディングするための構文。
items
createSection関数で空のitemsを定義して、createItem関数でitemsにデータを挿入している。
<script setup>
async function createSection() {
const title = (newSectionTitle.value || '').trim()
if (!title) return
if (!offline.value) {
await axios.post('/api/sections/', { list: listId, title, position: sections.length })
await fetchSectionsOnline()
} else {
// ローカル追加
const tmpId = 'tmp-sec-' + Date.now()
sections.push({ id: tmpId, list: listId, title, position: sections.length, items: [], _newName:'', _newQty:'' })
saveLocal(sections)
pushQueue({ type: 'createSection', payload: { title } })
}
newSectionTitle.value = ''
}
async function createItem(section) {
const name = (section._newName || '').trim(); if (!name) return
const qty = section._newQty || ''
if (!offline.value && !(''+section.id).startsWith('tmp-sec-')) {
await axios.post('/api/items/', { section: section.id, name, qty, position: section.items.length })
await fetchSectionsOnline()
} else {
const tmpId = 'tmp-item-' + Date.now()
section.items.push({ id: tmpId, section: section.id, name, qty, position: section.items.length })
saveLocal(sections)
pushQueue({ type: 'createItem', payload: { sectionId: section.id, name, qty } })
}
section._newName = ''
section._newQty = ''
}
</script>
<template #item=”{ element: item }”>
ここでは、elementにitemsの各要素が渡ってくる。
それをitemという名前で使用している。
#itemは名前付きスロット
通常のスロットは一つしか受け取れないが、名前付きスロットだと複数受け取れる。
スロットとは
コンポーネントを作る時に、中身を差し替え可能にするための仕組み。
普通のコンポーネントは、親からpropsを受け取って表示するが、スロットを使うとHTMLの中身そのものを親から渡せる。
商品が削除される流れのまとめ
チェックボックスが押される → toggleItem関数が発火する → 画面から商品が削除される

<template>
<div>
<draggable
v-model="section.items"
item-key="id"
@end="onReorder(section)"
class="list"
>
<template #item="{ element: item }">
<div class="item">
<label class="checkbox">
<input type="checkbox" @change="toggleItem(item)" />
</label>
</template>
</draggable>
</section>
<div v-if="offline" style="margin:8px 0; font-size:12px; opacity:.7;">オフラインモード(変更は後で同期)</div>
</div>
</template>
<script setup>
async function toggleItem(item) {
// 画面から即消す(ハード削除の挙動)
for (const s of sections) {
const i = s.items.findIndex(it => it.id === item.id)
if (i >= 0) { s.items.splice(i, 1); break }
}
saveLocal(sections)
if (!offline.value && !(''+item.id).startsWith('tmp-item-')) {
await axios.post('/api/items/' + item.id + '/toggle/', { hard_delete: true })
} else {
pushQueue({ type: 'toggleItem', payload: { id: item.id, hard_delete: true } })
}
}
</script>

コメントを残す