ITHub

Építsünk webshopot Vue.js-szel

Építsünk webshopot Vue.js-szel
Rajcsányi Zoltán
Rajcsányi Zoltán
| ~14 perc olvasás

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:
enter image description here

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ó url
  • favourite = kedvencnek jelölve, logikai érték
  • price = ár
  • quantity = 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.