商品が削除される時に何が起こっているのか?

はじめに

例えば、トマト水煮を削除するには、

チェックボックスを押す。

削除された。

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

フロントエンド

要素の検証で確認すると、チェックボックスは、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>

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です