Építsünk webshopot Vue.js-szel
Az első két cikkben megismerkedtünk a Vue.js keretrendszer alapjaival, majd felépítettünk egy interaktív galériát, megismertük a HTML űrlap és a keretrendszer kapcsolatát, készítettünk Vue komponenst, ami kommunikált a Vue egyedünkkel, végül két komponens között is kommunikációt végeztünk egy úgynevezett busz segítésével.
Ebben a cikkben felépítünk egy egyszerű webshopot.
Mit fogunk pontosan elkészíteni?
A terv így néz ki:
Felül a kívánságlistát látjuk. A kedvencnek jelölt termék miniatűrjei helyezkednek itt el. A miniatűrre klikkelve törölhetjük a terméket ebből a dobozból. A kívánságlista teljes törlése a kuka ikonra történő kattintással lehetséges.
A második blokkban a kosár helyezkedik el. Működése hasonló a kívánságlistához, azzal a különbséggel, hogy itt egy termék többször is szerepelhet annak függvényében, hogy hányszor dobták azt be a kosárba. A miniatűrre kattintva eggyel csökken a termék mennyisége.
Az alsó részen helyezkednek el a termékek. Termékenként dobozokba van helyezve a termékhez kötődő információ és a szükséges funkciók. A fejlécben helyezkedik el a termék neve, illetve ott lehet a kedvenceink közé rakni az adott terméket. A termék neve alatt szerepel egy termékfotó, majd alatta a termék részletes leírása. A láblécben, alul láthatjuk a termék árát, a mennyiségét (opcionálisan), egy új terméket tudunk még a kosárhoz adni, illetve a kosárban lévő összes ilyen típusú terméket is törölhetjük a kuka ikonnal.
Adatok betöltése
A termékekhez tartozó adatokat a getAllProducts()
függvény fogja visszaadni. A függvény statikusan egy objektumtömbként tárolja a termékek adatait az alábbi formában:
function getAllProducts() {
return [
{
img: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/211306/sw01.png',
title: 'Guavian Security Soldier',
info: 'Guavian soldiers undergo cybernetic augmentation, with a mechanical reservoir and pump injecting chemicals into their bloodstreams to enhance speed and aggression. They are silent in battle, communicating via high-frequency datastreams. Han Solo ran afoul of the Guavians for failing to repay their 50,000-credit loan.',
favourite: false,
price: 0.75,
category: 'Star Wars',
quantity: 1,
}
...
]
Komponensek nélküli megvalósítás
Először egyetlen Vue egyed mögé építem fel a logikát, a cikk második felében pedig komponensek felhasználásával fogom átírni a kódot.
A Vue egyed
A termékekhez tartozó adatokat a korábban létrehozott getAllProducts()
függvény adja vissza az adat data: {}
jellemzőben. Származtatott értékként definiálom a kosár értékét, a kedvencek számát, illetve a termékek halmazát a kívánságlistában és a kosárban. A kívánságlista és a kosár törlése metódusként definiálom. A Vue egyed váza ilyen lesz:
new Vue({
el: '#app',
data: {
items: getAllProducts() // termékek betöltése
},
computed: {
total() { ... }, // kosár értéke
favourites() { ... }, // kedvencek száma
itemsOnFavourites() { ... }, // kedvenc termékek listája
itemsOnBasket() { ... }, // kosárban lévő termékek listája
},
methods: {
dropFavourites() { ... }, // kívánságlista ürítése
dropBasket() { ... } // kosár ürítése
}
})
A vázlatban a függvény típusú tulajdonságot ECMAScript 6 formátumra rövidítettem: total() { ... } == total: function() { ... }
Származtatott értékek
Kosárban lévő termékek értéke összesen
total: function() {
var total = 0;
this.items.forEach(function(item) {
total += item.price*item.quantity;
});
return Math.round(total*100)/100;
},
Kívánságlista elemeinek a száma (ennyi kedvencnek jelölt temrék van)
favourites: function() {
var counter = 0;
this.items.forEach(function(item) {
counter += item.favourite?1:0;
});
return counter;
}
Kosárban lévő termékek
itemsOnBasket: function() {
var iob = [];
this.items.forEach(function(item) {
if (item.quantity > 0) {
iob.push(item);
}
});
return iob;
},
Kívánságlistában lévő termékek (kedvencnek jelölve)
itemsOnFavourites: function() {
var iof = [];
this.items.forEach(function(item) {
if (item.favourite) {
iof.push(item);
}
});
return iof;
},
Metódusok
Összes kedvenc törlése
dropFavourites: function() {
this.items.forEach(function(item) {
item.favourite = false;
});
}
Kosár ürítése
dropBasket: function() {
this.items.forEach(function(item) {
item.quantity = 0;
}
});
HTML kód
A kívánságlista
A kívánságlista blokkja kissé máshogy fog kinézni annak függvényében, hogy tartalmaz terméket, vagy üres.
<template v-if="favourites > 0">
<div>
<span><i class="fa fa-heart"></i> {{ favourites }}</span>
<div v-for="iof in itemsOnFavourites"
:style="{'background-image': 'url('+iof.img+')'}"
@click="iof.favourite=false">
</div>
<div class="mini-drop" @click="dropFavourites()">
<i class="fa fa-trash"></i>
</div>
</div>
</template>
<template v-else>
<span class="empty"><i class="fa fa-heart-o"></i></span>
</template>
Amikor a kosár üres, akkor egy span
elemet használunk, egyébként pedig div
elem közé zárjuk a szerkezetet. Mivel a feltétel esetében fontos, hogy a v-else
ág egy adott DOM szinten és azonos DOM elemben helyezkedjen el a v-if
-el, ezért a Vue által javasolt <template></template>
elemet felhasználva tujduk a problémát feloldani.
Ez így nem jó:
<div>
<span v-if="a==b"> ... </span>
<div v-else> ... </div>
</div>
A stílus felépítése dinamikusan is lehetséges a Vue-ban a :style="..."
direktíva segítségével.
A kosár
A kosárhoz tartozó html
kód nem tartalmaz új ismeretet.
<div v-if="total > 0">
<span><i class="fa fa-dollar"></i>{{ total }}
<i class="fa fa-shopping-basket"></i>
</span>
<template v-for="iob in itemsOnBasket">
<div v-for="idx in iob.quantity"
:style="{'background-image': 'url('+iob.img+')'}"
@click="iob.quantity--">
</div>
</template>
<div @click="dropBasket()"><i class="fa fa-trash"></i></div>
</div>
<div v-else>
<i class="fa fa-shopping-basket"></i>
</div>
A termékek listája
A programunk komplexebb része a termék adatlapja, hiszen kedvencnek jelölhetjük a termékünket, egy darabot hozzáadhatunk a kosárhoz, vagy az összeset törölhetjük is belőle. Az egysoros műveleteknek köszönhetően az összes logika belerakható a html
kódba (nem szükséges függvényeket írnunk).
<div v-for="item in items"
class="card"
:class="{selected: item.quantity > 0, favourite: item.favourite}">
<h1 @click="item.favourite=!item.favourite">
{{ item.title }}
<span class="favourite">
<i class="fa"
:class="{
'fa-heart': item.favourite===true,
'fa-heart-o': item.favourite === false
}"></i>
</span>
</h1>
<div class="text-center img">
<img :src="item.img" alt="">
</div>
<div class="info">
<p>{{ item.info }}</p>
</div>
<div class="form-control text-right">
<div class="price"><i class="fa fa-dollar"></i>{{ item.price }}</div>
<div class="quantity"
v-if="item.quantity > 0">
x{{ item.quantity }}
</div>
<button @click="item.quantity=0"
:disabled="item.quantity === 0"
:class="{disabled: item.quantity === 0}">
<i class="fa fa-trash"></i>
</button>
<button @click="item.quantity++">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
A :class
direktívában amennyiben objektumot definiálunk, például a :class="{'a': logikai_kif_1, 'b': logikai_kif_2 }"
értékkel, úgy több css
osztályt is meg tudunk határozni, hogy látható legyen, vagy sem. Baloldalon mindig az osztály neve, míg a jobb oldalon a láthatósága szerepel az objektumban.
A webshop működés közben
See the Pen WebShop with Vue.js - part 1/4 by Rajcsányi Zoltán (@rajcsanyiz) on CodePen.
Építkezzünk komponensekkel
Most pedig írjuk át a programunkat az alábbi komponensekkel:
<mini>
elem: Mini termékfotók, amik a kosárban és a kívánságlistában fognak szerepelni.<favourites>
elem: Kívánságlista<basket>
elem:Kosár<lego>
elem: Termék adatlap
A Vue egyed
A Vue egyedünk nem változott abban, hogy a termékek itt kerülnek betöltésre. Illetve a termékek szűrt listája is itt generálódik a számított favourites
és itemsOnBasket
metódusokkal.
new Vue({
el: "#app",
data: { items: getAllProducts() },
computed: {
// Kívánságlista termékei
favourites: function() {
var favs = [];
this.items.forEach(function(item) {
if (item.favourite) {
favs.push(item);
}
});
return favs;
},
// Kosár termékei
itemsOnBasket: function() {
var iob = [];
this.items.forEach(function(item) {
if (item.quantity > 0) {
iob.push(item);
}
});
return iob;
}
}
});
A kívánságlista komponens
A kívánságlista html
kódja nagyon egyszerű lesz:
<favourites :items="favourites"></favourites>
A mini elem
Azért, hogy a logika egyszerűbb legyen, bevezetek egy <mini>
html
elemet, amit a kívánságlistában és a kosárban használok fel a későbbiekben. A mini
termékfotóra klikkelve a @remove
esemény fog kiváltódni, azaz a kiválasztott termék kikerül a kívánságlistából, vagy csökken a kosárba helyezett mennyisége.
<script type="text/x-template" id="mini-template">
<div class="mini"
:style="{'background-image': 'url('+img+')'}"
@click="$emit('remove')"
title="remove">
</div>
</script>
Az esemény kiváltására úgyszintén inline
, egysoros kódot használtam: $emit('remove')
.
A JavaScript forráskódban a komponens definiálásánál egyedüli bemeneti paraméterként a fotóhoz tartozó url-t kell megadnunk, amit img
néven definiáltam.
Vue.component("mini", {
template: "#mini-template",
props: ["img"]
});
Kívánságlistához tartozó JavaScript kódrészlet
Az első megvalósításhoz képest a dropFavourites
metódus átkerült a Vue egyedből a <favourites>
komponensbe, mivel itt helyben van rá szükség. Így a kód tisztább lett, hiszen a komponens önmaga képes kezelni a hozzá tartozó feladatokat.
Vue.component("favourites", {
template: "#favourites-template",
props: ["items"],
methods: {
// összes kedvenc törlése a kívánságlistából
dropFavourites: function() {
this.items.forEach(function(item) {
item.favourite = false;
});
}
}
});
A kívánságlista sablonja
A mini elem definiálásakor az :img
direktívát felhasználva képes a program a megfelelő termékfotót a mini képhez rendelni. Az elem kivétele a kedvencekből a @remove
egyedi eseményt felhasználva történik és itt is szerencsések vagyunk, mert egyetlen parancshoz sincs szükség függvény deklarációjára: @remove="favourite.favourite=false"
Ettől függetlenül az összes kedvenc termék törléséhez már szükséges (célszerűbb) a metódus létrehozása, hiszen itt valamivel komplexebb kódról van már szó.
<script type="text/x-template" id="favourites-template">
<div class="alert alert-info text-center">
<template v-if="items.length > 0">
<span><i class="fa fa-heart"></i> {{ items.length }}</span>
<hr>
<mini v-for="favourite in items"
:img="favourite.img"
@remove="favourite.favourite=false"></mini>
<div class="mini-drop" title="drop all" @click="dropFavourites()">
<i class="fa fa-trash"></i>
</div>
</template>
<template v-else>
<span class="empty"><i class="fa fa-heart-o"></i></span>
</template>
</div>
</script>
JavaScript kód (Vue komponens definíció)
Vue.component("favourites", {
template: "#favourites-template",
props: ["items"],
methods: {
dropFavourites: function() {
this.items.forEach(function(item) {
item.favourite = false;
});
}
}
});
A kosár komponens
A komponens html
kódja hasonlóan egyszerű, mint előzőleg a kedvencek: <basket :items="itemsOnBasket"></basket>
Sablon
A kedvencekhez képest annyi az eltérés, hogy a mini fotók itt többszörösen jelenhetnek meg, hiszen, ha egy termékből egynél több kerül a kosárba, akkor az több tételként, ismétlődve fog szerepelni.
<script type="text/x-template" id="basket-template">
<div class="alert alert-info text-center">
<div v-if="total > 0">
<span>
<i class="fa fa-dollar"></i>{{ total }}
<i class="fa fa-shopping-basket"></i></span>
<hr>
<template v-for="iob in items">
<mini v-for="idx in iob.quantity"
:img="iob.img"
@remove="iob.quantity--" >
</mini>
</template>
<div class="mini-drop"
title="drop all"
@click="dropBasket()">
<i class="fa fa-trash"></i>
</div>
</div>
<div v-else>
<span class="empty"><i class="fa fa-shopping-basket"></i></span>
</div>
</div>
</script>
JavaScript (Vue komponens definíció)
Számított érték a kosárban lévő termékek összértéke, amit a sablonban előzőleg már fel is használtunk egy sztring interpoláció segítségével. ({{ total }}
). A kosár ürítéséhez a dropBasket()
metódust fogjuk felhasználni.
Vue.component("basket", {
template: "#basket-template",
props: ["items"],
computed: {
// kosárban lévő termékek értéke összesen
total: function() {
var total = 0;
this.items.forEach(function(item) {
total += item.price * item.quantity;
});
return Math.round(total * 100) / 100;
}
},
methods: {
// kosár ürítése
dropBasket: function() {
this.items.forEach(function(item) {
item.quantity = 0;
});
}
}
});
Termék komponens
Kicsit komplexebb kód tartozik a termék komponenshez, azonban a JavaScript definíció kifejezetten egyszerű:
Vue.component("lego", {
template: "#lego-template",
props: ["img", "favourite", "price", "quantity"]
});
A HTML
kód egy picit komplexebb:
<lego v-for="item in items"
:img="item.img"
:favourite="item.favourite"
:price="item.price"
:quantity="item.quantity"
@toggle-favourite="item.favourite=!item.favourite"
@add-quantity="item.quantity++"
@drop-quantity="item.quantity=0">
<template slot="title">{{ item.title }}</template>
{{ item.info }}
</lego>
A terméknek négy fő jellemzője van
img
= fotó urlfavourite
= kedvencnek jelölve, logikai értékprice
= árquantity
= a kosárba ennyi darab került a termékből
A termékkel három művelet végezhető:@toggle-favourite
= váltógomb a kedvencnek jelöléshez@add-quantity
= növeli egy termékkel a kosár tartalmát@drop-quantity
= minden ilyen terméket kivesz a kosárból
A komponens sablonfájlja
Bár elsőre kicsit komplexebbnek tűnhet a kód, valójában nem tartalmaz az eddigiekhez képest új ismeretet.
<script type="text/x-template" id="lego-template">
<div class="card"
:class="{
selected: quantity > 0,
favourite: favourite
}">
<h1 @click="$emit('toggle-favourite')" title="toggle favourite">
<slot name="title"></slot>
<span class="favourite">
<i class="fa"
:class="{
'fa-heart': favourite === true,
'fa-heart-o': favourite === false
}"></i>
</span>
</h1>
<div class="text-center img">
<img :src="img" alt="">
</div>
<div class="info">
<p><slot></slot></p>
</div>
<div class="form-control text-right">
<div class="price"><i class="fa fa-dollar"></i>{{ price }}</div>
<div class="quantity" v-if="quantity > 0">x{{ quantity }}</div>
<button @click="$emit('drop-quantity')"
:disabled="quantity === 0"
:class="{disabled: quantity === 0}">
<i class="fa fa-trash"></i>
</button>
<button @click="$emit('add-quantity')">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</script>
Ezzel készen is vagyunk, remélem tetszett a refaktorálás!
Próbáld ki itt:
See the Pen WebShop with Vue.js - part 2/4 by Rajcsányi Zoltán (@rajcsanyiz) on CodePen.
Zárszó
A következő cikkben a webshopot továbbfejlesztjük. Készítünk termék keresőt, szűrőt, lapozót. Használunk majd AJAX-ot és természetesen lesz backend is a rendszer mögött. Extraként pedig közkinccsé fogom tenni a GitHubon a Laravel keretrendszerrel készült PHP kódot és a Docker konténert azért, hogy a backenddel is tudjatok lokális környezetben játszani.