Image
Seitenanfang
Navigation
Mai 25, 2019

Tutorial: Ajax-Warenkorb als OXID-Modul

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 (https://github.com/proudcommerce/docker-oxid6).

Im ersten Schritt legen wir den benötigten Modul-Ordner im OXID an:

Anschließend wird der Modul-Ordner dem Autoloader bekannt gemacht.
Hierfür wird ein Eintrag im Abschnitt psr-4 unter autloader-dev hinzugefügt:

  "autoload-dev": {
    "psr-4": {
      "NetAdvising\\AjaxBasket\\": "./source/modules/na/ajaxbasket"
    }
  }

Nachem dies erfolgt ist, muss Composer aktualisiert werden:

composer dump-autoload

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 OXID unter: https://docs.oxid-esales.com/developer/en/6.0/modules/skeleton/metadataphp/version20.html.

<?php

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

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

Das Modul ist soweit vorbereitet und sollte im Backend wie folgt sichtbar sein:

Die eigentliche Arbeit kann nun beginnen.

Hinzufügen einer Java Script-Datei

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.

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

 'blocks'      => array(
        array(
            '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:

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}]

Um zu überprüfen, ob der Block korrekt erweitert wird, empfehle ich das Modul Oxid Module Internals. Dies analysiert Module und stellt womögliche Fehler und deren Ursachen visuell dar. Bezogen werden kann dieses Modul unter: https://github.com/OXIDprojects/oxid-module-internals, oder via Composer.

composer require oxid-community/moduleinternals

Nachdem Modules Internals installiert wurde, überprüfen wir unser Modul auf den überschriebenen Block.

Grün signalisiert, dass der Block erfolgreich überschrieben wurde. Taucht hierbei ein Fehler auf, so ist es naheliegend, dass Pfade fehlerhaft sind, Klassennamen falsch geschrieben, oder der Namespace des Moduls nicht bekannt ist.

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

Da unser Block korrekt überschrieben wurde und unsere JavaScript-Datei angelegt wurde, überprüfen wir nun, ob diese auch tatsächlich geladen wird:

Die JavaScript-Datei wird erfolgreich geladen, sodass wir zur eigentlichen Entwicklung übergehen können.

OXID bietet im Standard bereits eine Ajax-Funktionalität an, welche sich in der Datei out/wave/src/js/widgets/oxajax.min.js befindet. Nachteil dieser Funktionalität ist, dass diese nur HTML anfordert und zurückliefert. 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: .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) {
    na_oxAddToBasket = {
        _create: function() {
            var b = this,
                c = (b.options, b.element);

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

        submitForm: function(form) {
            var 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.html);
                        a("div#naItemAddedModal").modal("show");
                        a("span#basketCounter").text(data.itemAmount);
                        a("div.minibasket-menu-box").html(data.minibasket);
                    }
                }
            })
        }
    }, a.widget("ui.na_oxAddToBasket", na_oxAddToBasket)
}(jQuery);

OXID verwendet die sogenannte Widget Factory von jQuery UI (https://jqueryui.com/widget/). 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 den entsprechenden Block überschreiben.

    'blocks'      => array(
        array(
            'template'  => 'layout/base.tpl',
            'block'     => 'base_js',
            'file'      => 'Application/views/blocks/base_js.tpl'
        ),
        array(
            'template'  => 'page/details/inc/productmain.tpl',
            'block'     => 'details_productmain_tobasket',
            'file'      => 'Application/views/blocks/details_productmain_tobasket.tpl'
        ),
    ),

Der Warenkorb-Button befindet sich in der Template-Datei productmain.tpl, innerhalb des Blocks details_productmain_tobasket. Wir erweitern daher diesen um einen eigenen Block.

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

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

Die untere Zeile bindet den Warenkorb-Button an unser Widget na_oxAddToBasket.

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

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 NaBasketComponent, im Namespace NetAdvising\AjaxBasket\Application\Component.

Die Klasse wird vorerst folgenden Inhalt enthalten:

<?php

namespace NetAdvising\AjaxBasket\Application\Component;

class NaBasketComponent extends NaBasketComponent_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
 */
public function toBasket($sProductId = null, $dAmount = null, $aSel = null, $aPersParam = null, $blOverride = false)
{
	// adding to basket is not allowed ?
	$myConfig = $this->getConfig();
	if (\OxidEsales\Eshop\Core\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 (\OxidEsales\Eshop\Core\Registry::getConfig()->getConfigParam('blPsBasketReservationEnabled')) {
				$basket = \OxidEsales\Eshop\Core\Registry::getSession()->getBasket();
				\OxidEsales\Eshop\Core\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
			\OxidEsales\Eshop\Core\Registry::getSession()->setVariable('_newitem', $oNewItem);
		}

		// nA - AjaxBasket - AH - 2019/05/25 -> BEGIN
		$sItemId = key($aProducts);
		$this->sendAjaxResponse($sItemId);
		// nA - AjaxBasket - AH - 2019/05/25 <- END

		// redirect to basket
		return $this->_getRedirectUrl();
	}
}

Wir überschreiben die Methode toBasket, da diese nun ein JSON-Objekt zurückgeben soll. Hierfür fügen wir in Zeile 58. einen eigenen Methoden-Aufruf 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
 *
 * @return JSON-Object
 */
protected function sendAjaxResponse($sItemId)
{
	$blisAjax = (boolean) \OxidEsales\Eshop\Core\Registry::getRequest()->getRequestParameter('doAjax');

	if($blisAjax == true) {
		$oSmarty = $this->_getSmarty();

		$oArticle = oxNew(\OxidEsales\Eshop\Application\Model\Article::class);

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

		$oxwArticleDetails = oxNew(\OxidEsales\Eshop\Application\Component\Widget\ArticleDetails::class);
		$oSmarty->assign("oxwArticleDetails", $oxwArticleDetails);

		if ($oSmarty->template_exists($this->_sItemAddedToBasketTemplate)) {
			$sHtmlContentModal = $oSmarty->fetch($this->_sItemAddedToBasketTemplate);
		}

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

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

		$oxViewConfig = oxNew(\OxidEsales\Eshop\Core\ViewConfig::class);
		$oSmarty->assign("oViewConf", $oxViewConfig);

		$oxViewConfig = oxNew(\OxidEsales\Eshop\Core\ViewConfig::class);
		$oSmarty->assign("oView", $oxViewConfig);

		if ($oSmarty->template_exists($this->_sItemAddedToBasketMinibasketTemplate)) {
			$sHtmlContentMiniBasket = $oSmarty->fetch($this->_sItemAddedToBasketMinibasketTemplate);
		}

		\OxidEsales\Eshop\Core\Registry::getUtils()->showMessageAndExit(
			json_encode(
				array(
					"success"       => true,
					"html"          => $sHtmlContentModal,
					"itemAmount"    => $iBasketItemsAmount,
					"minibasket"    => $sHtmlContentMiniBasket
				)
			)
		);
	}
}

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

{  
   "success":true,
   "html":null,
   "itemAmount":7,
   "minibasket":null
}

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

  • Warenkorb-Vorschau (minibasket)
  • Modalfenster mit Hinweis (html)
  • Anzahl der Warenkorb-Items (itemAmount)

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

'templates'   => array(
	'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 Wave-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'      => array(
	array(
		'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.

Der deutliche Vorteil ergibt sich hierbei für den User, dass diese Seite nicht mehr springt, aufgrund eines vollständigen Ladevorgangs.

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' => array(
	array(
		'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">×</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>

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 (https://docs.oxid-esales.com/developer/en/6.0/modules/skeleton/structure.html#modules-structure-language-files-20170316).

Der Inhalt der ajaxbasket_lang.php gestaltet sich wie folgt:

<?php

// -------------------------------
// RESOURCE IDENTIFIER = STRING
// -------------------------------
$aLang = array(
    '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="modal_wrapper">

    <div class="side_left"><img src="[{$oxwArticleDetails->getActPicture()}]" itemprop="image" class="img-ajaxmodal"></div>
    <div class="side_right">
        [{$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" id="productShortdesc" itemprop="description">[{$oDetailsProduct->oxarticles__oxshortdesc->rawValue}]</p>
            [{/if}]
        [{/oxhasrights}]
    </div>
</div>
<div style="clear: both;"></div>

Wir arbeiten mit CSS, um unser Modalfenster noch etwas zu gestalten. Dafür fügen wir noch eine eigene CSS-Datei per Modul ein:

'blocks'      => array(
	array(
		'template' 	=> 'layout/base.tpl',
		'block' 	=> 'base_style',
		'file' 		=> 'application/views/blocks/base_style.tpl'
	)
)

Die Template-Datei application/views/blocks/base_style.tpl wird analog der base_js.tpl erweitert:

[{$smarty.block.parent}]
[{ oxstyle include=$oViewConf->getModuleUrl("ajaxbasket", "out/src/css/ajaxbasket.css")}]

Unsere ajaxbasket.css enthält folgenden Inhalt:

div#naItemAddedModalinnerBody .rating-star-filled {
    color: #F60;
}

div#naItemAddedModalinnerBody div.side_right{
    float: right;
    width: 60%;
}

div#naItemAddedModalinnerBody div.modal_wrapper{
    width: 100%;
}

div#naItemAddedModalinnerBody div.side_left{
    float: left;
    width: 35%;
}

div#naItemAddedModalinnerBody p.shortdesc{
    margin-top: 20px;
}

div#naItemAddedModalinnerBody img.img-ajaxmodal{
    max-width: 200px;
}

Damit sieht unser Ergebnis wie folgt aus:

Ein vollständiges Beispiel findet Ihr im Demoshop unter: https://oxid-demo.netadvising.de/Kiteboarding/Kites/Kite-CORE-GTS.html

Der Quellcode kann unter https://git.netadvising.de/alex/oxid_ajaxbasket bezogen werden.

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