ハコヅミを作る #9:一番の売り、コンテナの一括チェック
前回(#8)で、チェックリストでギアを1つずつチェックできるようになりました。 今回は、ハコヅミを作ろうと思ったきっかけでもある、コンテナ単位の一括チェックです。
#1で書いたとおり、キャンプの荷物は、ギアをコンテナにまとめて入れておき、コンテナごと車に積みます。コンテナを積んだら、中のギアもまとめてチェックされてほしい。Notionではできなかったことです。
見た目
チェックリストの画面は、#8の時点で、すでにコンテナごとにまとめて並べていました。 今回は、各コンテナの見出しの左にチェックボックスを置きました。

実際の画面(上のほうを切り出したもの)。カーゴ 30L-1 はまとめてチェック済み、カーゴ 50L-1 は「一部」
画面は、この記事の時点のコードを手元で動かして撮ったものです。データは#7でNotionから取り込んだ、実際のギアを入れています。
コンテナのチェック状態は、保存しない
一番考えたのは、コンテナのチェック状態をどこに持つかです。
コンテナにも「チェック済み」の列を用意する方法もあります。ただそうすると、ギアを1つずつチェックした場合と、コンテナでまとめてチェックした場合とで、状態が食い違うおそれがあります。 たとえば、コンテナにはチェックが付いているのに、中のギアが1つだけ外れている、といった状態です。
そこで、コンテナのチェック状態は保存せず、中のギアのチェック状態から毎回決めることにしました。テーブルは1つも変えていません。
// none:対象のギアがない(すべて今回不要)ので押せない
export type ContainerCheckState = 'checked' | 'mixed' | 'unchecked' | 'none';
export function containerCheckState(items: { checked: boolean; notNeeded: boolean }[]) {
const { checked, target } = campProgress(items); // 今回不要を除いて数える
if (target === 0) return 'none';
if (checked === target) return 'checked';
return checked === 0 ? 'unchecked' : 'mixed';
}
| 中のギア(今回不要を除く) | コンテナ |
|---|---|
| すべてチェック済み | チェック |
| 一部だけチェック済み | 「一部」 |
| どれもチェックされていない | 未チェック |
| 対象がない(すべて今回不要) | 押せない |
こうしておけば、ギアを1つずつ全部チェックしたときも、コンテナは自然に「チェック」になります。状態の持ち主が1か所なので、食い違いようがありません。
押したときの決めごと
コンテナのチェックボックスを押したときの動きは、Issueの段階で細かく決めました。
- チェックを付けると、そのコンテナの今回不要でないギアをすべてチェックする
- チェックを外すと、すべて外す
- 今回不要のギアは変えない。積まない前提のギアなので、コンテナごと積んでもチェックは付けない
- 「一部」の状態で押したら、全部チェックにする。「コンテナを積んだ」という操作なので、チェックする側に倒した
サーバー側では、そのキャンプの項目のうち、そのコンテナに入っているギアで、今回不要でないものを、まとめて更新します。
await db
.update(campItems)
.set({ checked: input.value })
.where(
and(
eq(campItems.campId, campId),
eq(campItems.notNeeded, false),
inArray(campItems.gearId, gearsInContainer) // そのコンテナに入っている自分のギア
)
);
「一部」の表示:indeterminate
チェックボックスには、チェックと未チェックの間の「一部」の表示があります。HTMLの indeterminate です。
ただし、これはHTMLの属性ではなく、DOMのプロパティなので、<input indeterminate> のように書いても効きません。JavaScriptで要素に直接設定する必要があります。
Svelte 5 では、{@attach} を使って要素に直接設定しました。
<script lang="ts">
// 「一部」の状態は HTML の属性ではなく DOM のプロパティ(indeterminate)で表す
function indeterminate(state: ContainerCheckState) {
return (node: HTMLInputElement) => {
node.indeterminate = state === 'mixed';
};
}
</script>
<input
type="checkbox"
checked={group.checkState === 'checked'}
disabled={group.checkState === 'none'}
{@attach indeterminate(group.checkState)}
/>
{@attach} は、状態が変わると自動で設定し直してくれるので、ギアを1つチェックするたびに「一部」の表示も追いかけて変わります。
E2Eテストでも、この「一部」の状態を、要素の indeterminate を読んで確かめています。
戻し忘れに気づく「中身をすべて見る」
一括チェックには、弱点があります。「コンテナの中身が正しい」ことが前提だという点です。 前回のキャンプで使ったランタンを、コンテナに戻し忘れていたら、一括チェックしても気づけません。
そこで、コンテナの見出しの下に「中身をすべて見る」を置きました。開くと、そのコンテナに入っている自分のギアが、すべて出ます。
- チェックリストにないギア(プランに入っていないもの)も出す
- それには「(このチェックリストにない)」、廃棄済なら「・廃棄済」と添える

「中身をすべて見る」を開いたところ。プランに入っていない食器などには注記が付く
積み込みの前にさっと開いて、「入っているはずのものがあるか」を目で確かめる使い方を想定しています。別の画面に移らず、その場で開閉できる形にしました。
テストの思い込み
この機能のE2Eで、1つだけ落ちたテストがありました。「中身をすべて見る」のギアの並び順を、予想で決め打ちしていたためです。
中身は製品名の日本語の並び順で並べていますが、「古いランタン」「予備のランタン」「ランタン」の並びは、予想と違っていました。漢字とカタカナの前後は、日本語の並びの規則次第です。
このテストで確かめたかったのは「全部出ること」と「チェックリストにないものが分かること」で、並び順ではありません。並び順に頼らない確かめ方に直しました。 テストで何を確かめたいのかをはっきりさせておくと、こうした思い込みに振り回されずに済みます。
おわりに
- コンテナのチェック状態は保存せず、中のギアから決める(食い違いが起きない)
- 「一部」は
indeterminate。属性ではなくプロパティなので、{@attach}で設定する - 一括チェックの前提(中身が正しい)を、「中身をすべて見る」で補う
これで、ハコヅミを作ろうと思ったきっかけの機能ができました。MVPの機能は、これですべてそろいです。次回は、MVPができるまでを振り返ります。