<div class="utility-10 p-2" id="js-utility-10">
<div class="field has-addons">
<div class="control"><input class="input" id="js-utility-10-form" type="text" placeholder="例:タオル" /></div>
<div class="control"><button class="button is-primary" id="js-utility-10-form-btn">検索</button></div>
</div>
<div id="js-utility-10-items"></div>
</div><!-- 商品詳細モーダル-->
<div class="modal" id="js-utility-10-modal">
<div class="modal-background js-utility-10-modal-close"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title" id="js-utility-10-modal-title"></p><button class="js-utility-10-modal-close delete" aria-label="close"></button>
</header>
<section class="modal-card-body content mb-0">
<figure class="image is-2by1"><img id="js-utility-10-modal-img" /></figure>
<p id="js-utility-10-modal-detail"></p>
<p><span class="is-size-3" id="js-utility-10-modal-price"></span>円</p>
</section>
<footer class="modal-card-foot">
<div class="field has-addons">
<div class="control"><input class="input" id="js-utility-10-modal-input" type="number" value="0" step="1" min="0" /></div>
<div class="control"><button class="button is-primary" id="js-utility-10-modal-btn">カートに入れる</button></div>
</div>
</footer>
</div>
</div><!-- 商品カートボタン--><button class="utility-10__cart-btn button is-medium" id="js-utility-10-cart-btn"><span class="icon"><i class="fas fa-shopping-cart fa-lg"></i></span></button><!-- 商品カートモーダル-->
<div class="modal" id="js-utility-10-cart-modal">
<div class="modal-background js-utility-10-cart-modal-close"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">商品カート</p><button class="js-utility-10-cart-modal-close delete" aria-label="close"></button>
</header>
<section class="modal-card-body content mb-0" id="js-utility-10-cart-modal-body"></section>
<footer class="modal-card-foot">合計料金<span class="pl-4" id="js-utility-10-cart-modal-total"></span>円</footer>
</div>
</div>
#js-utility-10.utility-10.p-2
.field.has-addons
.control
input#js-utility-10-form.input(type='text', placeholder='例:タオル')
.control
button#js-utility-10-form-btn.button.is-primary 検索
#js-utility-10-items
// 商品詳細モーダル
#js-utility-10-modal.modal
.modal-background.js-utility-10-modal-close
.modal-card
header.modal-card-head
p#js-utility-10-modal-title.modal-card-title
button.js-utility-10-modal-close.delete(aria-label='close')
section.modal-card-body.content.mb-0
figure.image.is-2by1
img#js-utility-10-modal-img
p#js-utility-10-modal-detail
p
span#js-utility-10-modal-price.is-size-3
| 円
footer.modal-card-foot
.field.has-addons
.control
input#js-utility-10-modal-input.input(type='number', value='0', step='1', min='0')
.control
button#js-utility-10-modal-btn.button.is-primary カートに入れる
// 商品カートボタン
button#js-utility-10-cart-btn.utility-10__cart-btn.button.is-medium
span.icon
i.fas.fa-shopping-cart.fa-lg
// 商品カートモーダル
#js-utility-10-cart-modal.modal
.modal-background.js-utility-10-cart-modal-close
.modal-card
header.modal-card-head
p.modal-card-title 商品カート
button.js-utility-10-cart-modal-close.delete(aria-label='close')
section#js-utility-10-cart-modal-body.modal-card-body.content.mb-0
footer.modal-card-foot
| 合計料金
span#js-utility-10-cart-modal-total.pl-4
| 円
/* No context defined. */
$BLOCK_NAME: '.utility-10';
// 変数
#{ $BLOCK_NAME } {
&__cart-btn {
position: fixed;
bottom: 8px;
right: 8px;
z-index: 10;
}
}
'use strict';
export const utility10 = () => {
const utility = new Utility10();
utility.init();
}
type Product = {
uid: string,
name: string,
price: number,
tags: string[],
detail: string,
imgUrl: string,
thumbUrl: string,
}
type CartItem = {
uid: string,
name: string,
number: number,
price: number,
}
class Utility10 {
el: HTMLElement;
formEl: HTMLInputElement;
formBtnEl: HTMLElement;
itemsEl: HTMLElement;
modalEl: HTMLElement;
modalImgEl: HTMLElement;
modalTitleEl: HTMLElement;
modalDetailEl: HTMLElement;
modalPriceEl: HTMLElement;
modalInputEl: HTMLInputElement;
modalBtnEl: HTMLElement;
modalCloseEls: HTMLCollection;
cartBtnEl: HTMLElement;
cartModalEl: HTMLElement;
cartModalBodyEl: HTMLElement;
cartModalTotalEl: HTMLElement;
cartModalCloseEls: HTMLCollection;
searchUrl: string;
cartItems: CartItem[];
constructor() {
this.el = document.getElementById('js-utility-10');
this.formEl = <HTMLInputElement>document.getElementById('js-utility-10-form');
this.formBtnEl = document.getElementById('js-utility-10-form-btn');
this.itemsEl = document.getElementById('js-utility-10-items');
this.modalEl = document.getElementById('js-utility-10-modal');
this.modalImgEl = document.getElementById('js-utility-10-modal-img');
this.modalTitleEl = document.getElementById('js-utility-10-modal-title');
this.modalDetailEl = document.getElementById('js-utility-10-modal-detail');
this.modalPriceEl = document.getElementById('js-utility-10-modal-price');
this.modalInputEl = <HTMLInputElement>document.getElementById('js-utility-10-modal-input');
this.modalBtnEl = document.getElementById('js-utility-10-modal-btn');
this.modalCloseEls = document.getElementsByClassName('js-utility-10-modal-close');
this.cartBtnEl = document.getElementById('js-utility-10-cart-btn');
this.cartModalEl = document.getElementById('js-utility-10-cart-modal');
this.cartModalBodyEl = document.getElementById('js-utility-10-cart-modal-body');
this.cartModalTotalEl = document.getElementById('js-utility-10-cart-modal-total');
this.cartModalCloseEls = document.getElementsByClassName('js-utility-10-cart-modal-close');
if (location.origin === 'https://zakzakst.github.io') {
// GitHubの場合
this.searchUrl = '/parts/data/utility10.json';
} else {
// ローカル環境の場合
this.searchUrl = '/data/utility10.json';
}
this.cartItems = [];
}
/**
* 初期化
*/
init(): void {
if (!this.el) return;
this.onClickFormBtn();
this.onClickItems();
this.onClickModalBtn();
this.onClickModalClose();
this.onClickCartBtn();
this.onClickCartModalClose();
}
/**
* 検索結果データの取得
* @param keyword 検索キーワード
* @returns 検索にヒットした商品データ
*/
getSearchResult(keyword: string): Promise<Product[] | null> {
return new Promise(resolve => {
fetch(this.searchUrl)
.then((res) => {
return res.json();
})
.then((data) => {
// 検索キーワードに一致するデータを取得
const filteredData = data.filter((item: Product) => {
// 型番が一致するかの判定(完全一致)
if (item.uid === keyword) return true;
// 商品名が一致するかの判定(部分一致)
if (item.name.indexOf(keyword) !== -1) return true;
// タグが一致するかの判定(部分一致)
if (item.tags.length) {
const filteredTags = item.tags.filter(tag => {
return tag.indexOf(keyword) !== -1;
});
if (filteredTags.length) return true;
}
// 検索に一致しない場合
return false;
});
resolve(filteredData);
})
.catch(error => {
console.log(error);
resolve(null);
});
});
}
/**
* 検索結果の表示
* @param data 表示する商品データ
*/
showSearchResult(data: Product[]): void {
// 検索結果の初期化
this.itemsEl.innerHTML = '';
if (data.length) {
// 検索結果がある場合
data.forEach(item => {
const imgUrl = item.thumbUrl || 'https://bulma.io/images/placeholders/128x128.png';
const markup = `
<a class="box utility-10__item" data-uid="${item.uid}">
<article class="media">
<div class="media-left">
<figure class="image is-64x64">
<img src="${imgUrl}" alt="${item.name} サムネイル画像">
</figure>
</div>
<div class="media-content">
<p class="is-size-4">${item.name}</p>
<p>${item.price}円</p>
</div>
</article>
</a>
`;
this.itemsEl.insertAdjacentHTML('beforeend', markup);
});
} else {
// 検索結果がない場合
this.itemsEl.textContent = 'キーワードに対応する商品が見つかりませんでした。';
}
}
/**
* 商品詳細の表示
* @param uid 商品UID
*/
async showItemDetail(uid: string): Promise<void> {
// 商品データを反映
const data = await this.getItemDetail(uid);
const imgUrl = data.imgUrl || 'https://bulma.io/images/placeholders/640x320.png';
this.modalEl.dataset.uid = data.uid;
this.modalEl.dataset.name = data.name;
this.modalEl.dataset.price = String(data.price);
this.modalImgEl.setAttribute('src', imgUrl);
this.modalTitleEl.textContent = data.name;
this.modalDetailEl.textContent = data.detail || '詳細情報は登録されていません。';
this.modalPriceEl.textContent = String(data.price);
// 既にカートに入っている商品の場合、商品数を反映
const cartTarget = this.cartItems.find(item => {
return item.uid === data.uid;
});
const targetCartNumber = cartTarget ? cartTarget.number : 0;
this.modalInputEl.value = String(targetCartNumber);
// モーダルを表示
this.modalEl.classList.add('is-active');
}
/**
* 商品詳細の非表示
*/
hideItemDetail(): void {
// モーダルを非表示
this.modalEl.classList.remove('is-active');
// 商品データを初期化
this.modalEl.dataset.uid = null;
this.modalEl.dataset.name = null;
this.modalEl.dataset.price = null;
this.modalImgEl.removeAttribute('src');
this.modalTitleEl.textContent = '';
this.modalDetailEl.textContent = '';
this.modalPriceEl.textContent = '';
this.modalInputEl.value = String(0);
}
/**
* 商品詳細データの取得
* @param uid 商品UID
* @returns 対象商品の詳細データ
*/
getItemDetail(uid: string): Promise<Product | null> {
return new Promise(resolve => {
fetch(this.searchUrl)
.then((res) => {
return res.json();
})
.then((data) => {
// 検索キーワードに一致するデータを取得
const targetData = data.find((item: Product) => {
return item.uid === uid;
});
resolve(targetData);
})
.catch(error => {
console.log(error);
resolve(null);
});
});
}
/**
* カートの表示
*/
showCart(): void {
if (this.cartItems.length) {
// カートに商品が登録されている場合
let tableBodyMarkup = '';
let total = 0;
this.cartItems.forEach(item => {
tableBodyMarkup += `
<tr>
<th>${item.name}</th>
<td class="has-text-right">× ${item.number}</td>
</tr>
`;
total += item.price * item.number;
});
const markup = `
<table class="table is-striped">
<tbody>
${tableBodyMarkup}
</tbody>
</table>
`;
this.cartModalBodyEl.insertAdjacentHTML('beforeend', markup);
this.cartModalTotalEl.innerHTML = String(total);
} else {
// カートに商品が登録されていない場合
this.cartModalBodyEl.innerHTML = 'カートに登録した商品はありません。';
this.cartModalTotalEl.innerHTML = '---';
}
this.cartModalEl.classList.add('is-active');
}
/**
* カートの非表示
*/
hideCart(): void {
this.cartModalEl.classList.remove('is-active');
this.cartModalBodyEl.innerHTML = '';
this.cartModalTotalEl.innerHTML = '';
}
/**
* フォームボタンクリック時のイベント設定
*/
onClickFormBtn(): void {
this.formBtnEl.addEventListener('click', async () => {
const formVal = this.formEl.value;
const searchResult: Product[] = await this.getSearchResult(formVal);
this.showSearchResult(searchResult);
});
}
/**
* 商品クリック時のイベント設定
*/
onClickItems(): void {
this.itemsEl.addEventListener('click', e => {
e.preventDefault();
const target = <HTMLElement>e.target;
const targetItem = <HTMLElement>target.closest('.utility-10__item');
// 商品以外の要素がクリックされた場合、処理を終了
if (!targetItem) return;
// 商品UIDを取得
const uid = targetItem.dataset.uid;
// 商品詳細を表示
this.showItemDetail(uid);
});
}
/**
* カートに入れるボタンクリック時のイベント設定
*/
onClickModalBtn(): void {
this.modalBtnEl.addEventListener('click', () => {
const number = Number(this.modalInputEl.value);
const uid = this.modalEl.dataset.uid;
const name = this.modalEl.dataset.name;
const price = Number(this.modalEl.dataset.price);
if (number > 0) {
// 商品数が0より大きい場合、対応する商品をカートに追加
this.cartItems.push({
uid,
name,
number,
price,
});
} else {
// 商品数が0以下の場合、対応する商品をカートから削除
const targetCartItem = this.cartItems.find(item => {
return item.uid === uid;
});
this.cartItems.splice(this.cartItems.indexOf(targetCartItem), 1);
}
this.hideItemDetail();
});
}
/**
* モーダルを閉じる要素クリック時のイベント設定
*/
onClickModalClose(): void {
[...this.modalCloseEls].forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
this.hideItemDetail();
});
});
}
/**
* カートを表示ボタンクリック時のイベント設定
*/
onClickCartBtn(): void {
this.cartBtnEl.addEventListener('click', e => {
e.preventDefault();
this.showCart();
});
}
/**
* カートを閉じる要素クリック時のイベント設定
*/
onClickCartModalClose(): void {
[...this.cartModalCloseEls].forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
this.hideCart();
});
});
}
}
検索フォームに入力されたキーワードに対応するデータを JSON データ( https://zakzakst.github.io/parts/data/utility10.json )から取得して
ショッピングカートを表示するスクリプト。
スクリプト内では都度「utility10.json」を読み込んでいる。こちらはデータ量が少ないため、「一度読み込んで変数に入れて置き、そのデータを利用する」ほうが効率がいい。
しかし現実のショッピングカートは商品データ全体を読み込むとデータ量が大きくなりすぎる。
今回はデータ量が多い場合を想定して、都度データを読み込む方法で実装した。
※データの読み込み関数は全て以下の流れで行っているが、現実ではデータの絞り込みはそれぞれ API にパラメータを渡してサーバー側で行う想定。