Edited at

Vue.jsとOnsenUIを使ったオレオレなショッピングカートチュートリアル


はじめに

JSビギナーが、Vue.jsとOnsenUIを理解するためにオレ向けチュートリアルとして何か作ってみようと思い立ちました。Vue.js使うので、リアクティブがわかりやすいショッピングカートでいきます。


できあがり

shoppers.gif


できるまでの話

出来上がるまでに考えたことなど、つらつらと書いていきます。Vue.jsというよりは意外と、JSそのものの知識がなくてアレでした。JSってやや使いづらいっすね。Onsenの話は少なめになってしまいました。


プロダクトメニューを作る

プロダクトのメニューはとりあえず、こんな感じ。アイコンは fontawsome さまさまで。


メニュー

      products: [ 

{ id: 1, name: "Adventure's Guide", price: 20, icon: "fa-book" },
{ id: 2, name: "Magical Looking Gem", price: 100, icon: "fa-gem" },
{ id: 3, name: "Broken Compass", price: 10, icon: "fa-compass" },
{ id: 4, name: "Map Human Heart", price: 30, icon: "fa-map" },
{ id: 5, name: "Flying Broom", price: 50, icon: "fa-broom" },
{ id: 6, name: "Everyday's IPA Beer", price: 7, icon: "fa-beer" },
{ id: 7, name: "Medicinal Plants", price: 10, icon: "fa-leaf" },
{ id: 8, name: "Extremely Dried Fish", price: 15, icon: "fa-fish" },
]


メニューから購入するプロダクトと個数を選んでもらう

リストをタップされた時に何番目のプロダクトがクリックされたのかを認識する必要があるので、以下のようにしてみた。引数 id はHTML側で渡しています。もっとうまい方法あるのだろうか?


addCart

  methods: {

addCart: function (id) {
var selectedProdct = this.products.find((prod) => {
return (prod.id === id);
});
this.cart.push(selectedProdct)
}
}

HTML側は、以下のようにv-for で回して、描くリストアイテムの @click="addCart(prod.id)"のところに引数を渡すようにしています。Onsen UIclass が少しうるさいですが、貼り付けます。


メニュー表示

   <ons-list-item class="list__item" v-for="prod in products" :key="prod.id">

<div class="left list__item__left">
<!-- <div class="product-icon"></div> -->
<ons-icon v-bind:icon="prod.icon" size="28px"></ons-icon>
</div>
<div class="center list__item__center">
<span class="list-item__title">{{ prod.name }}</span>
<span class="list-item__subtitle">{{ prod.price }} Gold</span>
</div>
<div class="right list__item__right">
<span class="toolbar-button">
<ons-icon class="icon" icon="fa-plus" @click="addCart(prod.id)"></ons-icon>
</span>
<span class="notification badgeToolbar product-count">{{ countInTheCartById(prod.id) }}</span>
<span class="toolbar-button">
<ons-icon class="icon" icon="fa-minus" @click="delCart(prod.id)"></ons-icon>
</span>
</div>
</ons-list-item>


選んだものをカートに入れる

カートは配列にして、とりあえず products の中の各オブジェクトをクリックするたびにそのままaddCartすることにしました。( addCartは英語的に変ですが )


カートからアイテムを削除する

カートの中のプロダクトの数を減らす部分です。カートの中は、同じプロダクトが複数入ることがあるので例えばこんな感じになるわけです。


カートの中身

cart = [

{"id":1,"name":"Adventure's Guide","price":20,"icon":"fa-book"},
{"id":2,"name":"Magical Looking Gem","price":100,"icon":"fa-gem"},
{"id":1,"name":"Adventure's Guide","price":20,"icon":"fa-book"},
{"id":1,"name":"Adventure's Guide","price":20,"icon":"fa-book"}
]

こんな配列から、id=1を1つだけカートから削除したい。ちょっとググると、arrayのidを指定した要素削除は delete とか splice がでてくるのですが、要素を削除したあとに代わりに undefined をいれてしまって、配列の長さは変えない(よってあとでカートの中身を数える時におかしくなる)とか、指定したidを持つ要素を全て削除してしまうとか、気の利いたメソッドがなくてややめんどくさいところでした。

(配列は削除じゃなくて、ES6ならfilterで新しいarrayを生成した方が良いとの意見もあるようです)

このあたりが参考になります:[1],[2],[3]

結局 Array.prototype.some() をつかってこんな実装にしてみました。


delCart

  methods: {

delCart: function (id) {
this.cart.some (function(element, i) {
if (element.id == id) {
vm.cart.splice(i, 1)
return true
}
});
}
}

some()true を返すとそこでループが break するという謎仕様です。とりあえず動きます。これで、「1つ削除したらすぐにループから抜ける」ことができました。少し調べたところforbreak できるが、forEach はできない(return trueせよ) など、JSあるあるな一貫性のない言語設計のように思えました。謎は深まりますが、深追いはやめておきます。

このあたりが参考になります:[1], [2]


カートの中身を集計する

カートの中身を集計して、プロダクトAをX個、プロダクトBをY個という具合にみせようと思いました。map/reduce などでエレガンドに書けないだろうか?と思ったが、いまのところいい感じにはできていない。思いつくままに自己流でやってみた。

まず、カートに各プロダクトが何個づつ入っているか数えることにする。


countInTheCartById

  computed: {

productCountById: function () {
count = {}
this.cart.forEach(function(item) {
count[item.id] = (count[item.id])? count[item.id] + 1 : 1 ;
});
return count
// => プロダクトid毎の個数 eg.) {1: 3, 2: 2, 4: 1, 5: 1, 7: 2, 8: 3}
}
}

id毎に個数を数えるのは、このあたりを参考にしました:[1]

これとメニュー products をマージして cart とは別に、computedCart をつくってみた。カートにないプロダクトもあるので、そのidはスキップされます。


メニュー

      computedCart: [ 

{ id: 1, name: "Adventure's Guide", price: 20, icon: "fa-book", count: 3 },
{ id: 2, name: "Magical Looking Gem", price: 100, icon: "fa-gem", count: 2 },
{ id: 4, name: "Map Human Heart", price: 30, icon: "fa-map", count: 1 },
{ id: 5, name: "Flying Broom", price: 50, icon: "fa-broom", count: 1 },
{ id: 7, name: "Medicinal Plants", price: 10, icon: "fa-leaf", count: 2 },
{ id: 8, name: "Extremely Dried Fish", price: 15, icon: "fa-fish", count: 3 },
]

メソッド的にはこんな感じ。カートのボタンを押したら viewCart() が発火する。


viewCart

  methods: {

viewCart() {
var countForCheckOut = []
for (id in this.productCountById){
this.products.find(function(item) {
if (item.id == id) {
item["count"] = vm.productCountById[id]
countForCheckOut.push(item)
}
});
}
this.computedCart = countForCheckOut
}
}

ただ、合計金額だけはこの computedCart より cart から全部足し算した方が簡単なので、reduce つかってやってみました。スッキリ書けて、いいですね、reduce


totalPriceInTheCart

  computed: {

totalPriceInTheCart: function () {
return total = this.cart.reduce((sum, x) => sum + x.price, 0)
}
}

reduce についてはこのあたりを参考にしました:[1], [2]


購入履歴に残す

ここはまだ実装していません。でも、ここまでくれば、それほど手間なくできるでしょう。


ソース

今回つくった雑なソースはここ


今後

バックエンドとのやり取りを作っていこうかなと思っています。メニュー表や購入履歴はバックエンドに置くような感じで。