JavaScriptでなんちゃってECサイトの実装

ローカルストレージを使用したショッピングカート機能を作りました。
js勉強中でどんなものを作ったらいいか悩んでいる方など、よかったら参考にしてみてください。
GitHubでもソースを公開しています。
https://github.com/SuenagaRyoko/ec_sample

[DEMO画面](https://suenagaryoko.github.io/ec_sample/)

##ディレクトリ構成

以下のような構成になっています。
index.htmlが商品画面、confirm.htmlが購入確認画面となります。
html5reset-1.6.1.cssはリセットCSS（初期で入っているスタイルをリセットするものです）になります。以下のサイトのものを使用しています。
http://html5doctor.com/html-5-reset-stylesheet/

```
.
├── index.html
├── confirm.html
├── css
│   ├── confirm.css
│   ├── html5reset-1.6.1.css
│   ├── style.css
├── img
│   ├── bicycle.png
│   ├── bread.png
│   ├── cake.png
│   ├── cart.png
│   ├── denim.png
│   ├── diamond.png
│   ├── dumbbell.png
│   ├── funnel.png
│   ├── glasses.png
│   ├── painting.png
│   └── wine.png
└── js
    ├── confirm.js
    └── script.js
```

##実装する処理
この記事では、押さえていてほしいメインの処理だけ解説したいと思います。
また、記事では分かりやすく見せるために、GitHubのソースと少し変えています。
HTML/CSSについては、[DEMO画面](https://suenagaryoko.github.io/ec_sample/)より検証ツールにて確認してもらった方がわかりやすいかと思います。

- カートボタンクリックでカートアイコンの数字を変更
- カートボタンクリックで押したボタンをアクティブにする
- カートの解除
- カートに入れた商品の名前と価格をローカルストレージに保存する
- 購入確認画面にてカートに入れた商品の表示

##カートボタンクリックでカートアイコンの数字を変更
カートボタンを押したらカートアイコンの個数を増やす処理です。


```javascript:script.js
var cart_btns = document.querySelectorAll('.js_cart_btn'),//カートボタン
cart_cnt_icon = document.getElementById('js_cart_cnt'),//カートの個数アイコン
cart_cnt = 0;//カートのアイテム数

// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {
    cart_cnt++;
    if( cart_cnt >= 1 ){
      cart_cnt_icon.parentNode.classList.remove('hidden');
    }
    cart_cnt_icon.innerHTML = cart_cnt;
  });
});
```

###クリックイベント
ボタンがクリックされた時の処理は以下のように囲ってあげます。
.addEventListenerは要素に対してイベントを登録するメソッドです。
カートボタンが複数あるのでボタンの数だけループさせてイベントを登録しています。
forEachは配列をループさせる構文です。
引数のcart_btnではボタン1つ１つを取得、indexでは要素が何番目なのかを取得しています。

```javascript:script.js
// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {
    //クリックされた時の処理を記述
  });
});
```

###カウント表示
カートボタンが押された時にカウントアップする処理を作っていくのですが、
カートの商品が0の時にカウントを表示しないようにしています。


```html:index.html
<div class="cart">
    <!-- カートのカウント部分のclassに「hidden」をつけて、
    初期状態では表示しないようにしています -->
    <div class="cart_cnt hidden">
        <span id="js_cart_cnt"></span>
    </div>
  <a class="cart_icon" href="confirm.html">
      <img src="img/cart.png" alt="">
  </a>
</div>
```

```css:style.css
.hidden{
    display: none;
}
```

それを踏まえた上で以下のようなソースになります。

```javascript:script.js
// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    //ボタンが押されたらカウントを増やす
    cart_cnt++;
    //カウントが1以上なら、カウントアイコンからclass「hidden」を外す
    if( cart_cnt >= 1 ){
      cart_cnt_icon.parentNode.classList.remove('hidden');
    }
    //カウントアイコンにカウントを出力
    cart_cnt_icon.innerHTML = cart_cnt;

  });
});
```

この状態だとカートの解除ができないのでボタンを押すと無限にカウントが増えてしまい、
商品以上の数になってしまいます。
そこについては後ほど修正していきます。


##カートボタンクリックで押したボタンをアクティブにする

###ボタンのカラー変更

こちらもカートボタンをクリックした時の処理なので、カウントと同じ処理の中に書きます。
ちなみに以下がカートボタンの初期状態になります。

```html:index.html
<button class="item_cart_btn js_cart_btn" data-name="自転車" data-price="30000">カート</button>
```

```css:style.css
.item_wrapper .item_list .item .item_btnarea .item_cart_btn {
  padding: 4px 1.2rem;
  font-size: 1.2rem;
  color: #fff;
  background-color: #62e2dc;
  border: none;
  border-radius: 20px;
  outline: none;
}
```

カートボタンのclassに「.item_cart_btn_active」というclassをつけることでボタンの色を変更します。
ボタンの背景色を上書きする形になります。
(今回の場合は、アクティブクラスの詳細度が初期のものと同じで、初期のクラスの後に書いてあるので、実は!importantなしでも上書きできます。)

```css:style.css
.item_wrapper .item_list .item .item_btnarea .item_cart_btn_active {
  background-color: #f5d883 !important;
}
```

ボタンを押された時に、アクティブクラスを付与する処理がこちらです。

```javascript:script.js
// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    //カウント処理(中略)

    //カートボタンにアクティブクラスを付与
    cart_btn.classList.add('item_cart_btn_active');

  });
});

```

##カートの解除

先ほどカウント処理のところで、カウントが無限に増えてしまうと書きました。
ボタンの色も今の状態だと、1度押したらアクティブのままです。
なのでカート解除の処理をここで作りたいと思います。

###ボタンがクリックされているか判定

まずカートボタンをクリックした時にすでに押されているのか、押されていないのかを判断しなければいけないので、クリック管理用の配列を作ります。

```javascript:script.js

var clicked = [];//クリックされたボタンのindexを格納

```

押されたボタンのindexを配列「clicked」に追加していきます。
序盤でもちらっと説明しましたが、forEachの引数のindexで何番目のボタンかを取得できています。
indexを配列に追加していくことで、押されたボタンの情報を保管できます。

```javascript:script.js

// カートボタンのクリックイベント登録時にforEachの引数でindexを取得しています
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    //ボタンクリック管理用の配列にindexを追加
    clicked.push(index);

  });
});

```

このままだと押されたボタンが押された回数分配列に追加されてしまうので、重複してしまいます。
なので、対象のボタンのindexが配列に含まれていない時だけ追加し、すでに配列に含まれていたら削除します。

```javascript:script.js

// カートボタンのクリックイベント登録時にforEachの引数でindexを取得しています
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    // カートボタンがすでに押されているかの判定
    if (clicked.indexOf(index) >= 0) {

      //ボタンのindexが配列に含まれていたら、配列から削除
      for (var i = 0; i < clicked.length; i++) {
        if(clicked[i] == index){
          clicked.splice(i, 1);
        }
      }

    }else if(clicked.indexOf(index) == -1){

      //ボタンのindexが配列に含まれていなかったら、配列に追加
      clicked.push(index);

    }

  });
});

```

これでボタンが押されているか、押されていないかの判定ができるようになりました。

###ボタンがクリックされていない場合

ボタンが配列に含まれていない場合は、序盤に作った以下の処理を入れます。

- カートアイコンのカウントを増やす
- カートボタンをアクティブにする


```javascript:

// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    // カートボタンがすでに押されているかの判定
    if (clicked.indexOf(index) >= 0) {

      //カートボタンがクリックされている場合の処理(中略)

    }else if(clicked.indexOf(index) == -1){

      //ボタンのindexが配列に含まれていなかったら、配列に追加
      clicked.push(index);

      //カートアイコンのカウントを増やす
      cart_cnt++;
      if( cart_cnt >= 1 ){
        cart_cnt_icon.parentNode.classList.remove('hidden');
      }
      cart_cnt_icon.innerHTML = cart_cnt;

      //カートボタンをアクティブにする
      cart_btn.classList.add('item_cart_btn_active');
    }

  });
});

```

###ボタンがすでにクリックされている場合

すでにカートボタンが押されている場合、以下の処理が必要です。

- カートアイコンのカウントを減らす
- カートボタンを非アクティブにする

ソースで書くとこのようになります。

```javascript:script.js

// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    // カートボタンがすでに押されているかの判定
    if (clicked.indexOf(index) >= 0) {

      //カートアイコンの数を減らす
      cart_cnt--;
      //0の時はカートアイコンのカウントを表示させない
      if(cart_cnt == 0){
        cart_cnt_icon.parentNode.classList.add('hidden');
      }
      cart_cnt_icon.innerHTML = cart_cnt;

      //カートボタンを非アクティブにする
      cart_btn.classList.remove('item_cart_btn_active');

    }else if(clicked.indexOf(index) == -1){

      //カートボタンがクリックされていない場合の処理(中略)

    }

  });
});

```

ここまでで画面の動きは作ることができました。
次はデータをローカルストレージに保管します。

##カートに入れた商品の名前と価格をローカルストレージに保存する

まず、ローカルストレージを簡単に説明すると、ブラウザにデータを保管できる機能です。
詳しく知りたい方はこちらの記事がわかりやすかったので、よかったら読んでみてください。
https://www.granfairs.com/blog/staff/local-storage-01

###商品名・価格の取得
まずローカルストレージにデータを保存するには、商品の名前、価格を取得しなければなりません。
今回はカートボタンにカスタムデータ属性を持たせて取得します。
商品の表示部分はこのようなhtmlになっています。
data-name="商品名"、data-price="価格"となるようにあらかじめ指定します。


```html:index.html

<li class="item">
  <div class="item_img">
      <img src="img/painting.png" alt="">
  </div>
  <div class="item_textbox">
    <div class="item_title"><h2 class="js_get_title">風景画</h2></div>
    <div class="item_price"><span class="yen_symbol">¥</span><span class="js_get_price">500000</span></div>
    <div class="item_btnarea">
      <!-- カートボタンの属性にdata-nameとdata-priceを持たせています -->
      <button class="item_cart_btn js_cart_btn" data-name="風景画" data-price="500000">カート</button>
      <a class="item_buy_btn" href="confirm.html">購入</a>
    </div>
  </div>
</li>

```

jsでは、カートボタンがクリックされていない時に、カスタムデータ属性を取得します。

```javascript:script.js

// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {
    
    // カートボタンがすでに押されているかの判定
    if (clicked.indexOf(index) >= 0) {

      //他処理(中略)

    }else if(clicked.indexOf(index) == -1){

      //カスタムデータ属性から商品名と価格を取得
      var name = cart_btn.dataset.name,//商品の名前を取得
      price = Number(cart_btn.dataset.price);//商品の値段を取得

      //他処理(中略)

    }

  });
});

```

###ローカルストレージにデータを保存

データが取得できたら、データ保存用の配列を作って、ローカルストレージに保管します。
一緒にカートボタンのindexも保管しています。(記事の中の説明上では特に必要ありません)
ローカルストレージに保管する際は、JSON.stringifyでエスケープします。

```javascript:script.js

var save_items = [];//ローカルストレージ保存用の配列

// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    // カートボタンがすでに押されているかの判定
    if (clicked.indexOf(index) >= 0) {

      //他処理(中略)

    }else if(clicked.indexOf(index) == -1){

      //カスタムデータ属性から商品名と価格を取得
      var name = cart_btn.dataset.name,//商品の名前を取得
      price = Number(cart_btn.dataset.price);//商品の値段を取得

      //データ保存用の配列に商品データを追加
      save_items.push({
        id: index,
        name: name,
        price: price
      });

      //他処理(中略)

    }

    // ローカルストレージに商品データを保管(上書き)
    localStorage.setItem("items",JSON.stringify(save_items));

  });
});

```

###ローカルストレージからデータを削除

カートボタンがすでに押されている商品の場合、データを削除しなければなりません。
ローカルストレージを更新する際は上書きする形になるので、配列「save_items」のデータを削除してそれをもう一度localStorage.setItemで保存します。

```javascript:script.js

// カートボタンを押した際の処理
cart_btns.forEach(function (cart_btn,index) {
  cart_btn.addEventListener('click',function () {

    // カートボタンがすでに押されているかの判定
    if (clicked.indexOf(index) >= 0) {

      //クリック管理用の配列から対象のボタンのindexを削除
      for (var i = 0; i < clicked.length; i++) {
        if(clicked[i] == index){
          clicked.splice(i, 1);

          //データ保管ようの配列から対象の商品データを削除
          save_items.splice(i, 1);
        }
      }

    }else if(clicked.indexOf(index) == -1){

      //カスタムデータ属性から商品名と価格を取得
      var name = cart_btn.dataset.name,//商品の名前を取得
      price = Number(cart_btn.dataset.price);//商品の値段を取得

      //データ保存用の配列に商品データを追加
      save_items.push({
        id: index,
        name: name,
        price: price
      });

      //他処理(中略)

    }

    // ローカルストレージに商品データを保管(上書き)
    localStorage.setItem("items",JSON.stringify(save_items));

  });
});

```

これでローカルストレージへのデータ保存と削除ができたので、次は確認画面で取得したデータを表示させます。

##購入確認画面にてカートに入れた商品の表示

カートの商品を表示させるには、ローカルストレージの商品の個数分以下のようなDOMを生成しなければいけません。

```html:confirm.html

<li>
  <h2>ジョウロ</h2>
  <div class="price">2000</div>
</li>

```


jsでのローカルストレージの配列の取り出しと、要素の生成を行うのですが何回もfor分で要素を生成すると処理が重くなってしまいます。
DocumentFragmentを使用すると負荷を少なくできるのでそちらを使っていきます。

DocumentFragmentの詳細については以下のサイトをご確認ください。
> DocumentFragment インターフェイスは、親ノードを持たない最小限の文書オブジェクト (文書の断片) を表します。これは Document の軽量版として使用され、標準の文書のようにノードで構成される文書構造の区間を格納します。重要な違いは、文書の断片はアクティブな文書ツリー構造の一部ではないため、断片に対して変更を行っても、文書に影響したり、再フローを起こしたり、変更が行われたときに性能上の影響を及ぼしたりすることがないことです。
DocumentFragment - Web API | MDNより引用
https://developer.mozilla.org/ja/docs/Web/API/DocumentFragment


```javascript:script.js

var items = JSON.parse(localStorage.getItem("items")),//ローカルストレージの商品データの配列
ele = document.getElementById('js_shopping_list'),//カートの商品を追加する要素
fragment = document.createDocumentFragment(),//DOMの追加処理用のフラグメント

if (items) {
  // カート商品の数分、要素を生成
  for (var i = 0; i < items.length; i++) {
    var li = document.createElement('li'),
    h2 = document.createElement('h2'),
    price = document.createElement('div');

    //生成した要素にクラスを追加
    price.classList.add('price');
    
    //要素に商品データを追加
    h2.appendChild(document.createTextNode(items[i].name));
    price.appendChild(document.createTextNode(items[i].price));

    //商品名と価格の要素をliに追加
    li.appendChild(h2);
    li.appendChild(price);
    fragment.appendChild(li);

  }
}

// 作成した要素の追加
ele.appendChild(fragment);

```

見た感じわかりづらいですが、ローカルストレージにデータがある場合。
h2と.priceの要素に対してデータを追加し、
データを追加したh2と.priceをliに追加することで入れ子構造を作っています。

またローカストレージからデータを取得する際には、
データを格納する際にJSON.stringifyでエスケープされているので、
JSON.parseでオブジェクトに戻してあげます。

これで商品データを画面に表示がすることができるかと思います。

###購入金額の合計を表示

最後に合計金額を表示します。
以下では、for文の中で商品の金額を足していき、要素の出力しています。

```javascript:script.js

var total = 0,//商品の合計金額
total_ele = document.getElementById('js_total');//商品の合計金額表示用の要素

if (items) {
  // カート商品の数分、要素を生成
  for (var i = 0; i < items.length; i++) {

    //要素の生成(中略)

    // 合計金額を加算
    total = total + items[i].price;
  }
}

```

ここまででソースについての説明は終わります。
元のソースでは、ここで紹介した内容の他にも、
サイド商品ページを表示した際にボタンやカートアイコンのカウントを保持したり、
購入確定ボタンを押した際にアラートを出したりなどしていたりするので、
よかったら見てみてください。

長くなってしまいましたが、ここまで読んでいただきありがとうございました。