Magento 2.0 modul fejlesztés lépésről lépésre – 4. rész (Knockout JS)

Az eddigi cikkeinkben, mely három részből állt, megismerkedtünk egy példa Magento 2.0 modul felépítésével. A mostani cikkünkben a Knockout.js használatával ismerkedünk, mely a Magento 2.0 rendszerben sok helyen van használva.



Tartalom:

  • Mi az a Knockout.js?
  • Miért kell / érdemes használni és mire jó?
  • Alap példa modul felépítése
  • Block-ok és layout létrehozása
  • Knockout JS használata PHTML template fájlban
  • Saját CustomerData osztály létrehozása és implementálása
  • Knockout JS használata HTML template fájlként

 

Mi az a Knockout.js?

A Knockout.js egy nyílt forráskódú egyszerűsített dinamikus javascript, mely egy Model-View-View Model (MVVM) rendszer/framework.

 

Miért kell / érdemes használni és mire jó?

Természetesen nem kell használni, viszont mivel a Magento 2.0 szerves részét képezi a full page cache használata miatt is, így a saját modulunkban megvalósítandó üzleti logika alapján célszerű elgondolkozni a használatán a fejlesztés megkezdése előtt. Anélkül, hogy különálló bonyolult javascript-ek készítenénk saját modulunkban, egy könnyű megoldással férhetünk hozzá customer, product, order stb. adatokhoz a frontenden és kezelhetjük azokat.

 

 

Alap példa modul felépítése

 

A jelenlegi cikkben is a már korábban használt alap példa modult fogjuk használni. A Magento 2.0-ban a Knockout JS jelentős szerepet kap, így érdemes vele megismerkedni.

 

Mivel korábbi cikkünk az alap modul felépítésére már kitért, most csak a modul felépítése kerül bemutatásra.

 

magento 2 modul szerkezet

 

Block-ok és layout létrehozása

 

A példához egy block-ot hozunk létre és két külön template fájlt, amiben két különböző módszerrel használjuk a Knockout JS-t. Az első példában magában a template fájlban használjuk a Knockout JS-t, míg a másodikban ennek segítségével egy másik HTML template fájlt implementálunk vagy úgy is fogalmazhatnánk, hogy töltünk be. A két külön példához első lépésben egy layout fájlt hozunk létre, és aszerint, hogy melyik block-ot használjuk, most az egyszerűség kedvéért csak kikommentezzük a megfelelőt.

Ehhez szükségünk lesz egy default.xml fájlra, ami az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/view/frontend/layout/default.xml. A fájl tartalma:

 

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
     <body>
         <referenceBlock name="sidebar.additional">
             <!-- First sample -->
             <block class="Aion\Sample\Block\Sample" name="aion.sample.knockout.sidebar" template="Aion_Sample::sidebar.phtml" after="wishlist_sidebar"/>
             <!-- Second Sample -->
             <!--<block class="Aion\Sample\Block\Sample" name="aion.sample.knockout.sidebar.second" template="Aion_Sample::second-sidebar.phtml" after="wishlist_sidebar"/>-->
         </referenceBlock>
     </body>
 </page>

 

A layout fájlban a példa kedvéért a saját block-unk és a hozzá tartozó template fájl az oldalsáv (sidebar) alatti részen fog megjelenni (<referenceBlock name=”sidebar.additional”>).

 

A következő lépésben a block osztály kerül létrehozásra, ami a fenti layout-ban is látható. Az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/Block/Sample.php. A fájl tartalma:

 

<?php
 namespace Aion\Sample\Block;
 use Magento\Framework\View\Element\Template;
 use Aion\Sample\Helper\Data as DataHelper;
 
 class Sample extends Template
 {
     /**
      * @var DataHelper
      */
     protected $_helper;
 
     /**
      * @param Template\Context $context
      * @param DataHelper $dataHelper
      * @param array $data
      */
     public function __construct(
         Template\Context $context,
         DataHelper $dataHelper,
         array $data = []
     ) {
         $this->_helper = $dataHelper;
         parent::__construct($context, $data);
         $this->_isScopePrivate = true;
     }
 
     /**
      * Get extension helper
      *
      * @return DataHelper
      */
     public function getExtensionHelper()
     {
         return $this->_helper;
     }
 
     /**
      * Sample items for example
      *
      * @return array
      */
     public function getInitSecondItems()
     {
         $sampleData = $this->_helper->getSampleProductNames();
 
         return $sampleData;
     }
 }

 

A block-ban látható getInitSecondItems() függvény a második példában fog szerepet kapni, így erre később térünk ki.

 

Knockout JS használata PHTML template fájlban

 

A Knockout JS bemutatásához szükséges még a megfelelő javascript fájlok elkészítése is. Első lépésben definiáljuk a saját modulunkhoz tartozó javascript fájlt, amit a requirejs-config.js-ban teszünk meg. Ez az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/frontend/requirejs-config.js. A fájl tartalma:

var config = {
     map: {
         '*': {
             sample: 'Aion_Sample/js/view/sample-sidebar'
         }
     }
 };

 

Itt adjunk meg, hogy a modulunkban hol található a saját javascript fájlunk.

 

Természetesen szükség van a sample-sidebar.js fájlra is. Ez az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/frontend/web/js/view/sample-sidebar.js. A fájl tartalma:

 

define([
     'ko',
     'uiComponent',
     'Magento_Customer/js/customer-data',
     'mage/translate'
 ], function (ko, Component, customerData, $t) {
     'use strict';
     return Component.extend({
         // Second example
         defaults: {
             template: 'Aion_Sample/second-sidebar'
         },
         displayContent: ko.observable(true),
         initialize: function () {
             this._super();
             this.sample = customerData.get('sample');
             // Second example
             this.someText = $t('Sample content with template.');
             // Second example. foreach examples
             this._showItems();
             // Second example, foreach example
             this._showMonths();
             // Second example, other foreach example
             this._showCategories();
             // Second example and another foreach example
             this._showMyItems();
         },
         getInfo: function() {
             return this.sample().info || this.initFullname || customerData.get('customer')().fullname;
         },
         getCartItemsCountText: function () {
             return this.sample().cart_items_text;
         },
         getCartItemsCount: function () {
             return this.sample().cart_count;
         },
         getHint: function() {
             return this.sample().hint || this.initHint;
         },
         _showItems: function() {
             var self = this;
             if (typeof this.initSampleData !== "undefined") {
                 self.sampleItems = JSON.parse(this.initSampleData);
             }
         },
         _showMonths: function() {
             var self = this;
             self.months = [ 'Jan', 'Feb', 'Mar', 'etc' ];
         },
         _showCategories: function() {
             var self = this;
             self.categories = [
                 { name: 'Fruit', items: [ 'Apple', 'Orange', 'Banana' ] },
                 { name: 'Vegetables', items: [ 'Celery', 'Corn', 'Spinach' ] }
             ];
         },
         _showMyItems: function() {
             var self = this;
             self.myItems = [ 'First', 'Second', 'Third' ]
         }
     });
 });

 

A példában szereplő függvények előtt jelzésre került, hogy melyek tartoznak a később bemutatandó második példához. Ezen objektumok és függvények előtt a //Second example látható.

Nézzük meg egy kicsit a működést!

A javascript a Magento 2.0-ban lévő uiComponent objektumot terjeszti ki, majd definiálásra kerül a saját sample objektum is, mely a CustomerData része lesz (this.sample = customerData.get (‘sample’);). Még négy függvény kerül a példában definiálásra, melyeket a template fájl fogunk használni.

 

Függvények:

  • getInfo() – a vevő (customer) nevének megjelenítésért felelős.
  • getCartItemsCountText() – string példa szöveggel, melynek része a kosárban lévő termékek száma.
  • getCartItemsCount() – integer, ami a kosárban lévő termékek számát tartalmazza (nem az összmennyiséget, sum qty)
  • getHint() – string, csak információt tartalmaz

 

Az említett függvények által visszaadott adatokat a 4. pontban leírt Sample osztály fogja kezelni.

 

Nézzük a block-hoz tartozó template fájl tartalmát, ami az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/frontend/templates/sidebar.phtml. A fájl tartalma:

 

<?php
 $sampleHelper = $block->getExtensionHelper();
 $initFullName = $sampleHelper->getIsLoggedIn() ? $sampleHelper->getCustomerFullName() : __('You are logged out.');
 ?>
 <?php if ($sampleHelper->isEnabled()) : ?>
     <div class="block block-compare block-aion-sample" data-bind="scope: 'sample'">
         <div class="block-title">
             <strong><?php /* @escapeNotVerified */ echo __('Aion Sample Block'); ?></strong>
         </div>
         <div class="block-content">
             <strong class="subtitle" style="display: inline-block">
                 <?php /* @escapeNotVerified */ echo __('Customer Info:') ?>
             </strong>
             <p class="description">
                 <span data-bind="text: getInfo()"></span><br />
             </p>
             <p class="description">
                 <!-- ko if: getCartItemsCount() -->
                 <strong class="subtitle" style="display: inline-block">
                     <?php /* @escapeNotVerified */ echo __('Cart Info:') ?>
                 </strong>
                 <br />
                 <span data-bind="text: getCartItemsCountText()"></span>
                 <!-- /ko -->
             </p>
             <p class="hint"><small data-bind="text: getHint()"></small></p>
         </div>
     </div>
     <script type="text/x-magento-init">
     { "*": {
             "Magento_Ui/js/core/app": {
                 "components": {
                     "sample": {
                         "component": "Aion_Sample/js/view/sample-sidebar",
                         "initHint": "<?php echo __('(Refresh automatically after cart modification)') ?>",
                         "initFullname": "<?php echo $initFullName ?>"
                     }
                 }
             }
         }
     }
     </script>
 <?php endif; ?>

 

Lényegében itt látható a Knockout JS használata. Nézzük, részletesen hol használjuk, és hogyan működik.

 

<span data-bind=”text: getInfo()”></span>

A javascript fájlban definiált getInfo() függvény által visszaadott string fog megjelenni a <span> tag-ben.

 

Knockout JS:

 

<!– ko if: getCartItemsCount() –>

               <strong class=”subtitle” style=”display: inline-block”>

                               <?php /* @escapeNotVerified */ echo __(‘Cart Info:’) ?>

               </strong>

              <br />

              <span data-bind=”text: getCartItemsCountText()”></span>

<!– /ko –>

 

Amennyiben a javascript fájlban definiált getCartItemsCount() függvény értéke nem 0, akkor megjelenítésre kerül az if szekcióban lévő rész. A getCartItemsCountText() függvény pedig egy string-gel tölti ki a <span> tag-et.

 

<p class=”hint”><small data-bind=”text: getHint()”></small></p>

A getHint() függvény egy string-gel tölti ki a <small> tag-et.

 

Nézzük meg, hogy ezek után hogyan jelenik meg az elkészült block a Magento 2.0 sidebar részében.

 

magento 2 modul fejlesztés sidebar

 

Hogyan módosíthatjuk ezeket az adatokat különböző felhasználói interakciókra? Erre kapunk választ a következő pontban.

 

Saját CustomerData osztály létrehozása és implementálása

 

Ahhoz, hogy a fenti block-unk és annak tartalma interaktívan módosuljon, szükségünk van egy saját CustomerData osztályra, mely a javascript-ben definiált sample osztályt szolgálja ki adatokkal.

Az osztály az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/CustomerData/Sample.php. A fájl tartalma:

 

<?php
 namespace Aion\Sample\CustomerData;
 use Magento\Customer\CustomerData\SectionSourceInterface;
 use Aion\Sample\Helper\Data as DataHelper;
 /**
  * Sample section
  */
 class Sample implements SectionSourceInterface
 {
     /**
      * @var DataHelper
      *
     protected $_helper;
 
     /**
      * @param DataHelper $dataHelper
      */
     public function __construct(
         DataHelper $dataHelper
     ) {
         $this->_helper = $dataHelper;
     }
 
     /**
      * {@inheritdoc}
      */
     public function getSectionData()
     {
         $sampleData = $this->_getSampleData();

         return $sampleData;
     }
 
     /**
      * First sample data example
      *
      * @return array
      */
     protected function _getSampleData()
     {
         $sampleData = [
             'info' => __('You are logged out.')
         ];
         $isLoggedIn = $this->_helper->getIsLoggedIn();
         if ($isLoggedIn) {
             $sampleData = [
                 'info' => __('You are logged in as: %1', $this->_helper->getCustomerFullName())
             ];
         }
 
         $cartItemsCount = $this->_helper->getCartItemCount();
         $sampleData = array_merge(
             $sampleData,
             [
                 'cart_items_text' => __('You have %1 item(s) in your cart', $cartItemsCount),
                 'cart_count' => (int)$cartItemsCount,
                 'hint' => __('(Refresh automatically after cart modification)')
             ]
         );
 
         return $sampleData;
     }
 }

 

Az osztályban a getSectionData() függvény szolgálja ki adatokkal a javascript-ben definiált sample osztályt. Itt fontos megjegyezni, hogy a két osztályt célszerű azonos néven elnevezni a PHP és Javascript kódban.

A _getSampleData egy tömbbel tér vissza, melynek kulcsai megegyeznek a javascript kódban hasznát értékekkel. Itt implementálhatjuk a szükséges üzleti logikát. A példa kedvéért itt csak a customer teljes nevének átadása és kosár elemek számolása történik meg.

Szükséges még definiálni a CustomerData osztályt is, hogy a Magento 2.0 rendszer „tudjon” róla. Ezt a di.xml fájlban kell megadnunk, ami az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/etc/frontend/di.xml. A fájl tartalma:

 

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
     <type name="Magento\Customer\CustomerData\SectionPoolInterface">
         <arguments>
             <argument name="sectionSourceMap" xsi:type="array">
                 <item name="sample" xsi:type="string">Aion\Sample\CustomerData\Sample</item>
             </argument>
         </arguments>
     </type>
 </config>

 

A következő fontos lépés, hogy a Sample osztályban definiált getSectionData() függvény mikor fusson le, milyen felhasználói interakcióra. Ehhez szükséges egy xml fájlban megadni ezen controller-ek listáját.

 

Ezt a sections.xml fájlban kell definiálnunk, ami az alábbi helyen található a példa modulunkban: app/code/Aion/Sample/etc/frontend/sections.xml. A fájl tartalma:

 

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Customer:etc/sections.xsd">
     <action name="checkout/cart/add">
         <section name="sample"/>
     </action>
     <action name="checkout/cart/delete">
         <section name="sample"/>
     </action>
     <action name="checkout/cart/updatePost">
         <section name="sample"/>
     </action>
     <action name="checkout/cart/updateItemOptions">
         <section name="sample"/>
     </action>
     <action name="checkout/sidebar/removeItem">
         <section name="sample"/>
     </action>
     <action name="checkout/sidebar/updateItemQty">
         <section name="sample"/>
     </action>
 </config>

 

Az xml-ben látható, milyen controller action-öket definiáltunk. Ezek a termék kosárba helyezése, törlése, termék számának frissítése stb.

De mit is jelent pontosan ez?

Miután elkészültünk a fenti Sample osztállyal, az említett getSectionData() függvény le fog futni ezen controller action-ök futása esetén, és a frontend-en megjelenő block-unk automatikusan ajax-szal frissülni fog a getSectionData() függvény által visszaadott adatokkal. Mindez anélkül történik, hogy bármilyen ajax függvényt és kezelést definiáltunk, írtunk volna a javascript fájlunkban.

 

Bejelentkezett felhasználóval így néz ki a block-unk miután egy tetszőleges terméket a kosárba helyeztünk:

 

magento 2 modul fejlesztés kosár

 

 

Az ajax hívásban visszakapott objektum tartalma:

 

magento 2 modul fejlesztés ajax tartalom

 

 

 

Knockout JS használata HTML template file-ként

 

Az előző példában a phtml fájlban használtuk közvetlenül a Knockout JS-t. A második esetben is ezt fogjuk alkalmazni, azonban egy külön html template fájlt használunk az adatok megjelenítésére.

Ehhez első lépésben egy második phtml példa fájlt hozunk létre. A fájl a modulunkban az app/code/Aion/Sample/view/frontend/templates/second-sidebar.phtml. A fájl tartalma:

 

<?php
 $sampleHelper = $block->getExtensionHelper()
 ?>
 <?php if ($sampleHelper->isEnabled()) : ?>
     <div class="block block-compare block-aion-sample" data-bind="scope: 'sample'">
         <div class="block-title">
             <strong><?php /* @escapeNotVerified */ echo __('Aion Second Sample Block'); ?></strong>
         </div>
         <div class="block-content">
             <!-- ko template: getTemplate() -->
             <!-- /ko -->
         </div>
     </div>
     <script type="text/x-magento-init">
     {
         "*": {
             "Magento_Ui/js/core/app": {
                 "components": {
                     "sample": {
                         "component": "Aion_Sample/js/view/sample-sidebar",
                         "initSampleData": "<?php echo addslashes(json_encode($block->getInitSecondItems())) ?>"
                     }
                 }
             }
         }
     }
     </script>
 <?php endif; ?>

 

Az első példában bemutatott phtml fájlhoz képest a különbség jól látható. A tartalmi részben kerül implementálásra a html template behívása:

<!– ko template: getTemplate() –>

<!– /ko –>

 

Itt fontos visszatérni a cikk elején bemutatott sample-sidebar.js fájlra és figyelembe venni a //Second sample kód részeket, mert a második példánkat ezek kezelik. Emellett az 1. pontban részletezett default.xml fájlban a második block-ot használjuk csak:

<block class=”Aion\Sample\Block\Sample” name=”aion.sample.knockout.sidebar.second” template=”Aion_Sample::second-sidebar.phtml” after=”wishlist_sidebar”/>

Az elsőt kikommentezzük.

 

A következő lépésben létrehozzuk a HTML fájlt, ami az app/code/Aion/Sample/view/frontend/web/template/second-sidebar.html. A fájl tartalma:

 

<!-- ko if: displayContent() -->
     <p data-bind="text: someText"></p>
 
     <h4>Items Form Block Class</h4>
     <ul data-bind="foreach: sampleItems">
         <li>
             <span data-bind="text: $data"> </span>
         </li>
     </ul>
 
     <h4>Months</h4>
     <ul data-bind="foreach: months">
         <li>
             <span data-bind="text: $data"> </span>
         </li>
     </ul>
 
     <h4>Categories</h4>
     <ol data-bind="foreach: { data: categories, as: 'category' }">
         <li>
             <ul data-bind="foreach: { data: items, as: 'item' }">
                 <li>
                     <span data-bind="text: category.name"></span>:
                     <span data-bind="text: item"></span>
                 </li>
             </ul>
         </li>
     </ol>
 
     <h4>My Items</h4>
     <ul>
         <!-- ko foreach: myItems -->
         <li>Item name: <span data-bind="text: $data"></span></li>
         <!-- /ko -->
     </ul>
 
 <!-- /ko -->
 <!-- ko ifnot: displayContent() -->
     <p class="empty-text" data-bind="text: $t('Content is empty.')"></p>
 <!-- /ko -->

 

A sample-sidebar.js-t elemezve nézzük át a HTML template fájl működését. Elsőként definiálva maga a template fájl és egy true érték, mely az if feltételben szerepel. Természetesen ezt a szükséges üzleti logika szerint lehet módosítani.

 

        // Second example

        defaults: {

            template: ‘Aion_Sample/second-sidebar’

        },

        displayContent: ko.observable(true),

 

A példa adatokat szolgáló függvények automatikusan lefutnak az inicializálás során:

 

           // Second example

            this.someText = $t(‘Sample content with template.’);

            // Second example. foreach examples

            this._showItems();

            // Second example, foreach example

            this._showMonths();

            // Second example, other foreach example

            this._showCategories();

            // Second example and another foreach example

            this._showMyItems();

 

Ezek közül ‒ a példa céljából ‒ csak egyetlen adatai érkeznek a block osztályból (app/code/Aion/Sample/Block/Sample.php). Az átadása a phtml fájlban történik:

 

“initSampleData”: “<?php echo addslashes(json_encode($block->getInitSecondItems())) ?>”

 

A getInitSecondItems() függvény a helper osztályban van megvalósítva:

 

/**
  * Get sample product names
  *
  * @return array
  */
 public function getSampleProductNames()
 {
     $sampleData = [];
     /* @var $product Product */
     $product = $this->_productFactory->create();
     /* @var $collection Collection */
     $collection = $product->getCollection();
     $collection->setVisibility($this->_catalogProductVisibility->getVisibleInCatalogIds());
     $collection->addStoreFilter()->addAttributeToSelect(
         ['name']
     );
     $collection->getSelect()->orderRand('e.entity_id');
     $collection->setPageSize(
         5
     )->setCurPage(
         1
     )->toArray(['name']);
 
     /* @var $item Product */
     foreach ($collection as $item) {
         $sampleData[] = $item->getName();
     }
 
     return $sampleData;
 }

 

A látható termékek közül véletlenszerűen kiválaszt ötöt és ezek neveit egy tömbként adja át.

 

A sample-sidebar.js fájlban inicializált többi függvény egyszerű példa adatokat hoz létre, hogy a HTML template fájlban ezeken végig iterálva segítse a működés megértését. A második példa block-unk mindezek után így jelenik meg a forntend-en:

 

magento 2 modul fejlesztés frontend

 

ÖsszegzésA cikkben megpróbáltuk bemutatni a Knockout JS használatát egyedi modul fejlesztéshez. A Magento 2.0-ban nagyon sok helyen van alkalmazva customer, order, cart, wishlist és egyéb adatok megjelenítése céljából a frontenden.

 

Sági Attila

Sági Attila

backend fejlesztő

Attila Magento Developer Plus tanúsítvánnyal rendelkező, rendkívül tapasztalt szenior fejlesztő. 2008-ban kezdett el egyedi Magento áruházakat és modulokat fejleszteni. Az AionHill csapat egyik első tagjaként már a Magento 2 hivatalos megjelenését megelőzően a korábbi publikus alfa- és bétaváltozatok alá is készített modulokat. Attila a fejlesztési projekteken túl belső oktatásokat is tart a többi fejlesztőkollégának a Magento 2-ről.


SZÜKSÉGE VAN EGY MEGBÍZHATÓ, PROFI MAGENTO FEJLESZTŐ PARTNERRE?

Kérjük, keressen bennünket, ha bármilyen kérdése, igénye lenne új vagy meglévő webáruház készítésével, megújításával kapcsolatban!

Next