netAdvising

solid. digital. experience.

Tutorial: Ajax-Warenkorb als OXID-Modul


Alexander
Alexander
4 Nov 2021
OXID Development

In unserem ersten Beitrag möchten wir die Modul-Entwicklung in OXID anhand eines praktischen Moduls erläutern. Voraussetzungen ist eine funktionierende OXID-Instanz, wobei die Version keinerlei Rolle spielen soll. Verwendet wird eine Docker-Composition von ProudCommerce. Im ersten Schritt legen wir den benötigten Modul-Ordner im OXID an:

Screenshot

Im zweiten Schritt erstellen wir die metadata.php. Hierbei handelt es sich um beschreibende Informationen zum Moduls. Oxid entnimmt aus ihr die zu erweiternden Klassen, Blöcke, Controller und vieles mehr. Einen genaueren Überblick liefert die OXID Hilfe.

<?php

/**
 * Metadata version
 */
$sMetadataVersion = '2.0';

/**
 * Module information
 */
$aModule = [
    'id'          => 'ajaxbasket',
    'title'       => 'AJAX Warenkorb',
    'description' => [
        'de' => 'Fügt Artikel via Ajax zum Warenkorb hinzu.',
        'en' => 'Add articles to basket via ajax.',
	],
    'thumbnail'   => 'logo.webp',
    'version'     => '1.0.0',
    'author'      => 'netAdvising',
    'url'         => 'http://www.netadvising.de',
    'email'       => 'info@netadvising.de',
    'extend'      => [],
    'controllers' => [],
    'templates'   => [],
    'events'      => [],
    'blocks'      => [],
    'settings'    => []
];

Anschließend wird das Modul mit einer composer.json versehen, da es via Composer eingebunden wird:

{
	"name": "netadvising/ajaxbasket",
	"description": "Ajax Basket for Tutorial",
	"type": "netadvising-module",
	"version": "v1.0.0",
	"keywords": ["OXID", "modules", "tutorial", "Ajax", "basket"],
	"homepage": "https://wwww.netadvising.de",
	"license": ["GPL-3.0-only", "proprietary"],
	"autoload": {
		"psr-4": {
			"NetAdvising\\AjaxBasket\\": "../../../source/modules/na/ajaxbasket"
		}
	}
}

Das Modul wird nun in die Komposition eingebunden.

"repositories": {
  "netadvising": {
    "type": "path",
    "url": "source/modules/na/*"
  }
}

Anschließend wird der Modul-Ordner der Komposition wie folgt bekannt gemacht:

"require": {
  "oxid-esales/oxideshop-metapackage-ce": "v6.3.1",
  "netadvising/ajaxbasket":"^v1.0.0"
}

Das Modul muss nun installiert werden. Hierfür werden die folgenden Befehle ausgeführt:

# install module to composition
composer update

# install module to oxid
vendor/bin/oe-console oe:module:install-configuration source/modules/na/ajaxbasket/

# write module configuration to database
vendor/bin/oe-console oe:module:apply-configuration

Das Modul sollte nun im Backend sichtbar sein und die eigentliche Arbeit kann nun beginnen: Screenshot

Um das Handling mittels JavaScript abzubilden, binden wir eine JavaScript-Datei in den Header ein. Ein günstiger Block wäre hierbei base_js, in der Template-Datei base.tpl: Screenshot

Wir erweitern den Block base_js um einen eigenen Block. Dies wird in der Metadata wie folgt hinterlegt:

'blocks' => [
    [
        'template'  => 'layout/base.tpl',
        'block'     => 'base_js',
        'file'      => 'Application/views/blocks/base_js.tpl'
	]
]

Wir erstellen im Modul-Ordner den Order Application/views/blocks und erstellen in diesem Ordner wiederum eine neue Datei namens base_js.tpl:

Screenshot

Die base_js.tpl wird nach den OXID-JavaScript-Dateien eine Modul-Datei einbinden, wobei [{$smarty.block.parent}] kennzeichnet, dass der aktuelle Block den übergeordneten Block erweitert. Wird diese Zeile entfernt, würde der originale Block gänzlich ersetzt werden.

[{$smarty.block.parent}]
[{oxscript include=$oViewConf->getModuleUrl("ajaxbasket", "out/src/js/ajaxbasket.js") priority=1}]

Im nächsten Schritt erstellen wir unsere JavaScript-Datei unter den oben genannten Pfad.

Die JavaScript-Datei wird erfolgreich geladen, sodass wir anschließend die Logik implementieren können.

OXID bietet im Standard bereits eine Ajax-Funktionalität an, welche sich in der Datei out/flow/src/js/widgets/oxajax.min.js befindet. Nachteil dieser Implementierung ist, dass ausschließlich HTML anfordert und zurückliefert wird. Möchten wir jedoch einen Artikel in den Warenkorb packen, müssen mehrere Elemente auf der Seite aktualisiert werden, wie die Menge der im Warenkorb befindlichen Artikel und der Preis im Warenkorb.

Daher modifizieren wir das Script, sodass es auch mit einem JSON-Objekt umgehen kann.

!(function ($) {
	naOxAjax = {
		loadingScreen: {
			start: function (a, b) {
				var c = Array();
				return (
					$(a).each(function () {
						var a = document.createElement('div');
						if (
							((a.innerHTML =
								'<div class="loadingfade"></div><div class="loadingicon"></div><div class="loadingiconbg"></div>'),
							$('div', a).css({
								position: 'absolute',
								left: $(this).offset().left - 10,
								top: $(this).offset().top - 10,
								width: $(this).width() + 20,
								height: $(this).height() + 20
							}),
							b && b.length)
						) {
							var d = Math.round(b.offset().left - 10 - $(this).offset().left + b.width() / 2),
								e = b.offset().top,
								f = Math.round(
									e -
										10 -
										$(this).offset().top +
										(b.last().offset().top - e + b.last().height()) / 2
								);
							$('div.loadingiconbg, div.loadingicon', a).css({
								'background-position': d + 'px ' + f + 'px'
							});
						}
						$('div.loadingfade', a)
							.css({
								opacity: 0
							})
							.animate(
								{
									opacity: 0.55
								},
								200
							),
							$('body').append(a),
							c.push(a);
					}),
					c
				);
			},
			stop: function (a) {
				$.each(a, function (a, b) {
					$('div', b).not('.loadingfade').remove(),
						$('div.loadingfade', b)
							.stop(!0, !0)
							.animate(
								{
									opacity: 0
								},
								100,
								function () {
									$(b).remove();
								}
							);
				});
			}
		},
		updatePageErrors: function (a) {
			if (a.length) {
				var b = $('#content > .status.error');
				if (
					(0 == b.length &&
						($('#content').prepend("<div class='status error corners'>"),
						(b = $('#content > .status.error'))),
					b)
				) {
					b.children().remove();
					var c;
					for (c = 0; c < a.length; c++) {
						var d = document.createElement('p');
						$(d).append(document.createTextNode(a[c])), b.append(d);
					}
				}
			} else $('#content > .status.error').remove();
		},
		ajax: function (a, b) {
			var c = this,
				d = {},
				e = '',
				f = '';
			'FORM' == a[0].tagName
				? ($('input, textarea', a).each(function () {
						switch (this.type) {
							case 'hidden':
							case 'password':
							case 'text':
							case 'number':
							case 'textarea':
								return void (d[this.name] = this.value);
								break;
							case 'checkbox':
								if (this.checked) return void (d[this.name] = 1);
								else return void (d[this.name] = 0);
								break;
							case 'radio':
								if (this.checked) return void (d[this.name] = this.value);
								break;
						}
				  }),
				  (e = a.attr('action')),
				  (f = a.attr('method')))
				: 'A' == a[0].tagName && (e = a.attr('href')),
				b.additionalData &&
					$.each(b.additionalData, function (a, b) {
						d[a] = b;
					});
			var g = {},
				h = Array();

			for (var i in d) d.hasOwnProperty(i) && h.push(i);
			h.sort().forEach(function (a) {
				g[a] = d[a];
			});

			var j = null;
			var z = b.dataType;

			b.targetEl && (j = c.loadingScreen.start(b.targetEl, b.iconPosEl)),
				f || (f = 'get'),
				jQuery.ajax({
					data: g,
					url: e,
					type: f,
					dataType: z,
					timeout: 3e4,
					beforeSend: function (a, b) {
						b.url = b.url.replace('&&', '&');
					},
					error: function (a, d, e) {
						j && c.loadingScreen.stop(j), b.onError && b.onError(a, d, e);
					},
					success: function (a) {
						j && c.loadingScreen.stop(j),
							void 0 != a.debuginfo && a.debuginfo && $('body').append(a.debuginfo),
							void 0 != a.errors && void 0 != a.errors.default
								? c.updatePageErrors(a.errors.default)
								: c.updatePageErrors([]),
							b.onSuccess && b.onSuccess(a, d);
					}
				});
		},
		reportJSError: function (a) {
			'undefined' != typeof console && void 0 !== console.error && console.error(a);
		},
		evalScripts: function (container) {
			var self = this;
			try {
				$('script', container).each(function () {
					try {
						if ('' != this.src && 0 == $('body > script[src="' + this.src + '"]').length)
							return $('body').append(this), document.body.appendChild(this), !0;
						eval(this.innerHTML);
					} catch (e) {
						self.reportJSError(e);
					}
					$(this).remove();
				});
			} catch (e) {
				self.reportJSError(e);
			}
		}
	};
	$.widget('ui.naOxAjax', naOxAjax);
})(jQuery);

Weiterhin fügen wir das Java-Script-Widget für den Ajax-Warenkob hinzu.

!(function (a) {
	let na_oxAddToBasket = {
		_create: function () {
			const b = this,
				c = (b.options, b.element);

			c.click(function () {
				const form = a(this).closest('form');
				b.submitForm(form);
				return false;
			});
		},

		submitForm: function (form) {
			const addData = {};
			addData.doAjax = true;
			naOxAjax.ajax(form, {
				dataType: 'json',
				additionalData: addData,
				onSuccess: function (data) {
					if (data.success === true) {
						a('div#naItemAddedModal').find('div#naItemAddedModalinnerBody').html(data.contentModal);
						a('div#naItemAddedModal').modal('show');
						a('span#basketCounter').text(data.itemAmount);

						const $contents = $('a.fixed-header-link').first().contents();
						$contents[$contents.length - 1].nodeValue = ' ' + data.itemAmount;

						a('div.minibasket-menu-box').html(data.contentMiniBasket);
					}
				}
			});
		}
	};
	a.widget('ui.na_oxAddToBasket', na_oxAddToBasket);
})(jQuery);

OXID verwendet die sogenannte Widget Factory von jQuery UI. Da wir möglichst nahe am OXID-Standard bleiben wollen, nutzen wir diese Vorgehensweise ebenfalls.

Um die neue Aktion auf den Warenkorb-Button zu binden, müssen wir die entsprechenden Blöcke überschreiben.

'blocks' => [
    [
        'template'  => 'layout/base.tpl',
        'block'     => 'base_js',
        'file'      => 'Application/views/blocks/base_js.tpl'
	],
    [
        'template'  => 'page/details/inc/productmain.tpl',
        'block'     => 'details_productmain_tobasket',
        'file'      => 'Application/views/blocks/details_productmain_tobasket.tpl'
	],
	[
		'template'  => 'widget/product/listitem_grid.tpl',
		'block'     => 'widget_product_listitem_grid_tobasket',
		'file'      => 'Application/views/blocks/widget_product_listitem_grid_tobasket.tpl'
	],
	[
		'template'  => 'widget/product/listitem_infogrid.tpl',
		'block'     => 'widget_product_listitem_infogrid_tobasket',
		'file'      => 'Application/views/blocks/widget_product_listitem_infogrid_tobasket.tpl'
	],
	[
		'template'  => 'widget/product/listitem_line.tpl',
		'block'     => 'widget_product_listitem_line_tobasket',
		'file'      => 'Application/views/blocks/widget_product_listitem_line_tobasket.tpl'
	],
],

Der Warenkorb-Button befindet sich in folgenden Template-Dateien:

  1. details_productmain_tobasket (Produkt-Detail-Seite)
  2. widget_product_listitem_grid_tobasket (Produkt-Liste / Grid)
  3. widget_product_listitem_infogrid_tobasket (Produkt-Liste / Grid)
  4. widget_product_listitem_line_tobasket (Produkt-Liste / Zeile)

Wir erstellen hierfür folgende neue Dateien innerhalb unseres bestehenden Block-Ordner, mit folgendem Inhalt:

Application/views/blocks/details_productmain_tobasket.tpl

[{$smarty.block.parent}]
[{oxscript add="$('button#toBasket').na_oxAddToBasket();"}]

Application/views/blocks/widget_product_listitem_grid_tobasket.tpl

<div class="actions text-center">
    <div class="btn-group">
        [{if $blShowToBasket}]
            [{oxhasrights ident="TOBASKET"}]
                <button type="submit" class="btn btn-outline-dark hasTooltip addtoBasket" aria-label="[{oxmultilang ident="TO_CART"}]" data-placement="bottom" title="[{oxmultilang ident="TO_CART"}]" data-container="body">
                    <i class="fa fa-shopping-cart"></i>
                </button>
                [{oxscript add="$('button.addtoBasket').na_oxAddToBasket();"}]
            [{/oxhasrights}]
            <a class="btn btn-primary" href="[{$_productLink}]" >[{oxmultilang ident="MORE_INFO"}]</a>
        [{else}]
            <a class="btn btn-primary" href="[{$_productLink}]" >[{oxmultilang ident="MORE_INFO"}]</a>
        [{/if}]
    </div>
</div>

Application/views/blocks/widget_product_listitem_infogrid_tobasket.tpl

<div class="actions">
    <div class="btn-group">
        [{if $blShowToBasket}]
            [{oxhasrights ident="TOBASKET"}]
                <button type="submit" aria-label="[{oxmultilang ident="TO_CART"}]" class="btn btn-outline-dark hasTooltip addtoBasket" data-placement="bottom" title="[{oxmultilang ident="TO_CART"}]" data-container="body">
                    <i class="fa fa-shopping-cart"></i>
                </button>
                [{oxscript add="$('button.addtoBasket').na_oxAddToBasket();"}]
            [{/oxhasrights}]
            <a class="btn btn-primary" href="[{$_productLink}]" >[{oxmultilang ident="MORE_INFO"}]</a>
        [{else}]
            <a class="btn btn-primary" href="[{$_productLink}]" >[{oxmultilang ident="MORE_INFO"}]</a>
        [{/if}]
    </div>
</div>

Application/views/blocks/widget_product_listitem_line_tobasket.tpl

[{if $blShowToBasket}]
    [{oxhasrights ident="TOBASKET"}]
    <div class="form-group">
        <div class="input-group">
            <input id="amountToBasket_[{$testid}]" type="text" name="am" value="1" size="3" autocomplete="off" class="form-control amount">
            <span class="input-group-append">
                <button id="toBasket_[{$testid}]" type="submit" aria-label="[{oxmultilang ident="TO_CART"}]" class="btn btn-primary hasTooltip addtoBasket" title="[{oxmultilang ident="TO_CART"}]" data-container="body">
                    <i class="fa fa-shopping-cart"></i>
                </button>
                [{oxscript add="$('button.addtoBasket').na_oxAddToBasket();"}]
                [{if $removeFunction && (($owishid && ($owishid==$oxcmp_user->oxuser__oxid->value)) || (($wishid==$oxcmp_user->oxuser__oxid->value)) || $recommid)}]
                    <button triggerForm="remove_[{$removeFunction}][{$testid}]" type="submit" class="btn btn-danger removeButton hasTooltip" title="[{oxmultilang ident="REMOVE"}]">
                        <i class="fa fa-times"></i>
                    </button>
                [{/if}]
            </span>
        </div>
    </div>
    [{/oxhasrights}]
[{else}]
    <a class="btn btn-primary" href="[{$_productLink}]">[{oxmultilang ident="MORE_INFO"}]</a>

    [{if $removeFunction && (($owishid && ($owishid==$oxcmp_user->oxuser__oxid->value)) || (($wishid==$oxcmp_user->oxuser__oxid->value)) || $recommid)}]
        <button triggerForm="remove_[{$removeFunction}][{$testid}]" type="submit" class="btn btn-danger btn-block removeButton">
            <i class="fa fa-times"></i> [{oxmultilang ident="REMOVE"}]
        </button>
    [{/if}]
[{/if}]

Die Template-Anpassungen bewirken, dass ein Klick auf den Warenkorb-Button unseren Event (na_oxAddToBasket) triggert.

Ein Klick auf den Warenkorb-Button lädt die Seite nun nicht mehr neu, sondern ruft den Controller via Ajax auf: Screenshot

Im Ergebnis befindet sich der Artikel bereits im Warenkorb.

Nun wollen wir den Controller dazu bringen, dass dieser nicht mehr weiterleitet, sondern ein JSON-Objekt zurückliefert, welches unsere gewünschten Werte enthält.

Hierfür müssen wir die Klasse \OxidEsales\Eshop\Application\Component\BasketComponent anpassen, da diese die eigentliche Logik für das Hinzufügen eines Artikels in den Warenkorb enthält.

Wir erstellen die neue Klasse BasketComponent, im Namespace NetAdvising\AjaxBasket\Application\Component.

Die Klasse wird vorerst folgenden Inhalt enthalten:

<?php

namespace NetAdvising\AjaxBasket\Application\Component;

class BasketComponent extends BasketComponent_parent
{

}

Beim Erweitern von Klassen ist es wichtig, dass stets die Parent-Klasse erweitert wird.

/**
 * Basket content update controller.
 * Before adding article - check if client is not a search engine. If
 * yes - exits method by returning false. If no - executes
 * oxcmp_basket::_addItems() and puts article to basket.
 * Returns position where to redirect user browser.
 *
 * @param string $sProductId Product ID (default null)
 * @param double $dAmount    Product amount (default null)
 * @param array  $aSel       (default null)
 * @param array  $aPersParam (default null)
 * @param bool   $blOverride If true amount in basket is replaced by $dAmount otherwise amount is increased by
 *                           $dAmount (default false)
 *
 * @return mixed
 * @throws \OxidEsales\Eshop\Core\Exception\DatabaseConnectionException
 * @throws \OxidEsales\Eshop\Core\Exception\DatabaseErrorException
 */
public function toBasket($sProductId = null, $dAmount = null, $aSel = null, $aPersParam = null, $blOverride = false)
{
	if (
		Registry::getSession()->getId() &&
		Registry::getSession()->isActualSidInCookie() &&
		!Registry::getSession()->checkSessionChallenge()
	) {
		$this->getContainer()->get(LoggerInterface::class)->warning('EXCEPTION_NON_MATCHING_CSRF_TOKEN');
		Registry::getUtilsView()->addErrorToDisplay('ERROR_MESSAGE_NON_MATCHING_CSRF_TOKEN');
		return;
	}

	// adding to basket is not allowed ?
	$myConfig = $this->getConfig();
	if (Registry::getUtils()->isSearchEngine()) {
		return;
	}

	// adding articles
	if ($aProducts = $this->_getItems($sProductId, $dAmount, $aSel, $aPersParam, $blOverride)) {
		$this->_setLastCallFnc('tobasket');

		$database = \OxidEsales\Eshop\Core\DatabaseProvider::getDb();
		$database->startTransaction();
		try {
			$oBasketItem = $this->_addItems($aProducts);
			//reserve active basket
			if (Registry::getConfig()->getConfigParam('blPsBasketReservationEnabled')) {
				$basket = Registry::getSession()->getBasket();
				Registry::getSession()->getBasketReservations()->reserveBasket($basket);
			}
		} catch (\Exception $exception) {
			$database->rollbackTransaction();
			unset($oBasketItem);
			throw $exception;
		}
		$database->commitTransaction();

		// new basket item marker
		if ($oBasketItem && $myConfig->getConfigParam('iNewBasketItemMessage') != 0) {
			$oNewItem = new stdClass();
			$oNewItem->sTitle = $oBasketItem->getTitle();
			$oNewItem->sId = $oBasketItem->getProductId();
			$oNewItem->dAmount = $oBasketItem->getAmount();
			$oNewItem->dBundledAmount = $oBasketItem->getdBundledAmount();

			// passing article
			Registry::getSession()->setVariable('_newitem', $oNewItem);
		}

		// nA - AjaxBasket - AH - 2021/11/08 -> BEGIN
		$sItemId = key($aProducts);
		$this->sendAjaxResponse($sItemId);
		// nA - AjaxBasket - AH - 2021/11/08 <- END

		// redirect to basket
		$redirectUrl = $this->_getRedirectUrl();
		$this->dispatchEvent(new \OxidEsales\EshopCommunity\Internal\Transition\ShopEvents\BasketChangedEvent($this));

		return $redirectUrl;
	}
}

Wir überschreiben die Methode toBasket, da diese nun ein JSON-Objekt zurückgeben soll. Hierfür fügen wir einen eigenen Methoden-Aufruf (sendAjaxResponse()) ein.

Wichtig ist anzumerken, dass es immer das Ziel sein sollte, eine Methode möglichst am Ende, oder am Anfang anzupassen und mit parent::toBasket() zu arbeiten. Somit bleibt der eigentliche Code der Methode updatesicher. In diesem Fall ist das jedoch nicht möglich, da wir genau mitten im Code ansetzen müssen.

Die Methode sendAjaxResponse gibt unser JSON-Objekt zurück und muss zunächst implementiert werden:

/**
 * Sends Ajax-Response for addToBasket
 *
 * @param string $sItemId Product ID
 */
protected function sendAjaxResponse(string $sItemId): void
{
	$isAjax = (boolean) Registry::getRequest()->getRequestParameter('doAjax');

	if($isAjax === true) {
		$oSmarty = Registry::getUtilsView()->getSmarty();

		$oArticle = oxNew(Article::class);

		$oArticle->load($sItemId);
		$oSmarty->assign("oDetailsProduct", $oArticle);

		$oxwArticleDetails = oxNew(ArticleDetails::class);
		$oSmarty->assign("oxwArticleDetails", $oxwArticleDetails);

		$htmlContentModal = "";
		if ($oSmarty->template_exists($this->_itemAddedToBasketTemplate)) {
			$htmlContentModal = $oSmarty->fetch($this->_itemAddedToBasketTemplate);
		}

		$oBasket = $this->getSession()->getBasket();
		$oBasket->calculateBasket();
		//take params
		$iBasketItemsAmount = $oBasket->getItemsCount();

		$oSmarty->assign("oxcmp_basket", $oBasket);

		$oxViewConfig = oxNew(ViewConfig::class);
		$oSmarty->assign("oViewConf", $oxViewConfig);
		$oSmarty->assign("oView", $oxViewConfig);

		$htmlContentMiniBasket = "";
		if ($oSmarty->template_exists($this->_itemAddedToBasketMinibasketTemplate)) {
			$htmlContentMiniBasket = $oSmarty->fetch($this->_itemAddedToBasketMinibasketTemplate);
		}

		Registry::getUtils()->showMessageAndExit(
			json_encode(
				array(
					"success"               => true,
					"contentModal"          => $htmlContentModal,
					"itemAmount"            => $iBasketItemsAmount,
					"contentMiniBasket"     => $htmlContentMiniBasket
				)
			)
		);
	}
}

Nach erfolgter Anpassung liefert ein Klick auf den Warenkorb nun folgende Antwort:

{
	"success": true,
	"contentModal": null,
	"itemAmount": 7,
	"contentMiniBasket": null
}

Wir sehen, dass bereits ein JSON-Objekt als Antwort an das Frontend gelangt. Diverse Sachen müssen nun weiterführend befüllt werden:

  1. Warenkorb-Vorschau (minibasket)
  2. Modalfenster mit Hinweis (html)
  3. Anzahl der Warenkorb-Items (itemAmount)

Screenshot

Um das Markup für die Elemente zu hinterlegen, werden vorerst zwei Templates im Modul erstellt:

'templates' => [
	'na_item_added_to_basket_modal.tpl' => 'na/ajaxbasket/application/views/blocks/na_item_added_to_basket_modal.tpl',
	'na_item_added_to_basket_minibasket.tpl' => 'na/ajaxbasket/application/views/blocks/na_item_added_to_basket_minibasket.tpl',
],

Die Datei na_item_added_to_basket_minibasket.tpl lädt unseren Mini-Warenkorb nach und aktualisiert dessen Inhalt.

Da wir den eigentlichen Inhalt des Minibaskets lediglich erneut zeigen wollen, wird das Template wie folgt gestaltet:

[{oxid_include_dynamic file="widget/minibasket/minibasket.tpl"}]

Da unser JSON-Objekt nun den Inhalt der Template-Datei minibasket.tpl enthält, können wir den Warenkorb im Frontend mit folgender Zeile aktualisieren:

a('div.minibasket-menu-box').html(data.minibasket);

Im nächsten Schritt soll die Mengenanzeige aktualisiert werden. Da diese nicht klar selektiert werden kann, müssen wir das Flow-Template hier anpassen, wobei dies ebenfalls über unser Modul geschieht.

Wir erstellen einen neuen Block, welcher im Template widget/header/minibasket.tpl den Block dd_layout_page_header_icon_menu_minibasket_button überschreibt. In unserer metadata.php sieht dies dann wie folgt aus:

'blocks' => [
	[
		'template'  => 'widget/header/minibasket.tpl',
		'block'     => 'dd_layout_page_header_icon_menu_minibasket_button',
		'file'      => 'application/views/blocks/dd_layout_page_header_icon_menu_minibasket_button.tpl'
	]
]

Hierzu erstellen wir eine neue Template-Datei im Ordner application/views/blocks/dd_layout_page_header_icon_menu_minibasket_button.tpl und befüllen diese mit folgendem Inhalt:

<i class="fa fa-shopping-cart fa-2x" aria-hidden="true"></i>
<span id="basketCounter">[{if $oxcmp_basket->getItemsCount() > 0}][{ $oxcmp_basket->getItemsCount() }][{/if}]</span>

Damit ist es uns möglich den Counter mittels span#basketCounter zu selektieren.

Da wir bereits die kumulierte Artikel-Anzahl im JSON-Objekt zurückbekommen, übergeben wir dies wie folgt an das Frontend:

a('span#basketCounter').text(data.itemAmount);

Wir haben bereits jetzt eine vollständige Aktualisierung des Warenkorbs mittels Ajax implementiert:

Screenshot

Im letzten Schritt möchten wir dem User ein Modalfenster anzeigen, mit dem Hinweis, dass etwas in den Warenkorb gelegt wurde.

Hierfür haben wir bereits das Template na/ajaxbasket/application/views/blocks/na_item_added_to_basket_modal.tpl erstellt, welches den Inhalt des Modalfensters enthalten soll. Den Inhalt bestimmen wir später, denn vorher muss das leere Modalfenster noch in das Template eingebunden werden.

Wir erweitern dafür den Footer \source\Application\views\wave\tpl\layout\footer.tpl und überschreiben den Block footer_main.

'blocks' => [
	[
		'template'  => 'layout/footer.tpl',
		'block'     => 'footer_main',
		'file'      => 'application/views/blocks/footer_main.tpl'
	]
]

In der neu erstellten footer_main.tpl fügen wir den folgenden Quellcode ein:

<div class="modal fade basketFlyout" id="naItemAddedModal" tabindex="-1" role="dialog" aria-labelledby="basketModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h4 class="modal-title" id="basketModalLabel">[{oxmultilang ident="NA_ITEM_ADDED_MODAL_HEADER"}]</h4>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div id="naItemAddedModalinnerBody" class="basketFlyout"></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal">[{oxmultilang ident="DD_MINIBASKET_CONTINUE_SHOPPING"}]</button>
                <a href="[{oxgetseourl ident=$oViewConf->getSelfLink()|cat:"cl=basket"}]" class="btn btn-primary" data-toggle="tooltip" data-placement="top" title="[{oxmultilang ident="DISPLAY_BASKET"}]">
                    <i class="fa fa-shopping-cart"></i>
                </a>
            </div>
        </div>
    </div>
</div>
[{$smarty.block.parent}]

Dieses Template ist angelehnt, an das Modalfenster aus dem Flow-Theme.

Unser oben gezeigter Modal-Code enthält Sprach-Labels. Diese werden in OXID verwendet, um die Mehrsprachigkeit abzubilden und werden wie folgt eingebunden:

[{oxmultilang ident="NA_ITEM_ADDED_MODAL_HEADER"}]

Hierfür wird pro verwendeter Sprache eine Labeldatei angelegt:

Es gilt anzumerken, dass OXID im Application-Ordner nach den Labels sucht. Ist dieser vorhanden, müssen diese darin platziert werden. Existiert kein Application-Ordner, kann der Ordner Translations auch direkt im Modul-Verzeichnis platziert werden.

Der Inhalt der ajaxbasket_lang.php gestaltet sich wie folgt:

<?php

// -------------------------------
// RESOURCE IDENTIFIER = STRING
// -------------------------------
$aLang = [
    'charset'                      => 'UTF-8',
    'NA_ITEM_ADDED_MODAL_HEADER'   => 'Artikel hinzugefügt'
];

Um nun den Inhalt des Modalfensters zu gestalten, widmen wir uns der oben genannten Datei na_item_added_to_basket_modal.tpl. Hier hinterlegen wir folgenden Code:

<div class="container">
    <div class="row">
        <div class="col-4">
            <img src="[{$oxwArticleDetails->getActPicture()}]" itemprop="image" class="img-fluid img-ajaxmodal">
        </div>
        <div class="col-8">
            [{$oDetailsProduct->oxarticles__oxtitle->value}] [{$oDetailsProduct->oxarticles__oxvarselect->value}]<br>
            [{oxmultilang ident="ARTNUM" suffix="COLON"}] [{$oDetailsProduct->oxarticles__oxartnum->value}]
            <div>
                [{assign var="iRatingValue" value=$oxwArticleDetails->getRatingValue()}]

                [{if $iRatingValue}]
                    [{strip}]
                        <div class="hidden" itemtype="http://schema.org/AggregateRating" itemscope="" itemprop="aggregateRating">
                            <span itemprop="worstRating">1</span>
                            <span itemprop="bestRating ">5</span>
                            <span itemprop="ratingValue">[{$iRatingValue}]</span>
                            <span itemprop="reviewCount">[{$oxwArticleDetails->getRatingCount()}]</span>
                        </div>
                    [{/strip}]
                [{/if}]

                [{if !$oxcmp_user}]
                    [{assign var="_star_title" value="MESSAGE_LOGIN_TO_RATE"|oxmultilangassign}]
                [{elseif !$oxwArticleDetails->canRate()}]
                    [{assign var="_star_title" value="MESSAGE_ALREADY_RATED"|oxmultilangassign}]
                [{else}]
                    [{assign var="_star_title" value="MESSAGE_RATE_THIS_ARTICLE"|oxmultilangassign}]
                [{/if}]


                [{section name="starRatings" start=0 loop=5}]
                    [{if $iRatingValue == 0}]
                        <i class="fa fa-star rating-star-empty"></i>
                    [{else}]
                        [{if $iRatingValue > 1}]
                            <i class="fa fa-star rating-star-filled"></i>
                            [{assign var="iRatingValue" value=$iRatingValue-1}]
                        [{else}]
                            [{if $iRatingValue < 0.5}]
                                [{if $iRatingValue < 0.3}]
                                    <i class="fa fa-star rating-star-empty"></i>
                                [{else}]
                                    <i class="fa fa-star-half-o rating-star-filled"></i>
                                [{/if}]
                            [{assign var="iRatingValue" value=0}]
                            [{elseif $iRatingValue > 0.8}]
                                <i class="fa fa-star rating-star-filled"></i>
                                [{assign var="iRatingValue" value=0}]
                            [{else}]
                                <i class="fa fa-star-half-o rating-star-filled"></i>
                                [{assign var="iRatingValue" value=0}]
                            [{/if}]
                        [{/if}]
                    [{/if}]
                [{/section}]
            </div>
            [{oxhasrights ident="SHOWSHORTDESCRIPTION"}]
                [{if $oDetailsProduct->oxarticles__oxshortdesc->rawValue}]
                    <p class="shortdesc mt-4" id="productShortdesc" itemprop="description">[{$oDetailsProduct->oxarticles__oxshortdesc->rawValue}]</p>
                [{/if}]
            [{/oxhasrights}]
        </div>
    </div>
</div>

Damit sieht unser Ergebnis wie folgt aus:

Screenshot

In einem weiterführenden Beitrag werden wir das Deployment eines Moduls mittels GIT & Composer erläutern.