By the framework
Im letzten Beitrag dieser Reihe hatte ich die gruppierten Einträge einer Liste auf- und zuklappbar gemacht. Die Entwicklungen und Erweiterungen fanden allesamt nachträglich und außerhalb der von SAPUI5 gestellten Möglichkeiten des Frameworks statt. Dieses Mal möchte ich die gleiche Funktionalität in erweiterte Controls übertragen. Die Idee ist, dass sämtliche Entwicklung in einer Library ausgelagert werden. Dort können sie eigenständig und ohne Koppelung an einen Controller funktionieren. Am Ende dieses Eintrags möchte ich ein Resümee ziehen, welche Methode die bessere ist.
Meine Liste
SAPUI5 ist offen (zumindest in Form von OpenUI5) und es ist möglich sämtliche Controls erweitern zu können. Genau dies habe ich für diesen Beitrag gemacht. Anstatt der sap.m.List des vorherigen Betriags soll nun eine lib.acando.CollabseableList entstehen. Am Ende des Refactorings ersetze ich mit dieser eigenen Liste die vorherige des original Coding und habe sofort die Gruppierfunktionalitäten.
Den Beginn der eigenen Liste zeigt das folgende Coding.
/* lib/acando/CollapseableList.js */ sap.ui.define(['sap/m/List'], function(List) { "use strict"; var CollapseableList = List.extend("lib.acando.CollapseableList", { renderer: {} // Benutze sap.m.ListRenderer } ); return CollapseableList; }, /* bExport= */ true);
Mittels “extend” erweitere ich die Standard sap.m.Liste unter “lib.acando.CollapseableList”. Wichtig war es, einen leeren Eintrag unter “renderer” stehen zu haben. Jedes Control im SAPUI5 Framework besetzt eine eigene Renderer Klasse. Diese überträgt die Daten und das Verhalten des Controls in HTML und CSS. Lasse ich nun in meiner Erweiterung den renderer mittels “{}” undefiniert, so wird der Renderer des Eltern-Controls (sap.m.List) verwendet. Ohne eine Eingabe würde das Framework automatisch einen Renderer für die lib.acando.CollapseableList suchen, aber nie finden.
In der View, wo ich die CollapseableList einsetzen möchte, musste ich einen neuen XML Namespace “custom” definieren.
<mvc:View displayBlock="true" xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc" controllerName="opensap.myapp.controller.App" xmlns:core="sap.ui.core" xmlns:l="sap.ui.layout" xmlns:f="sap.ui.layout.form" xmlns:custom="opensap.myapp.lib.acando" >
Hier traf ich auf die erste große Hürde: Der neue Namespace custom muss für die Tags des Controls selbst genutzt werden, für die Aggregationen (z.B. headerToolbar oder items), aber nicht für die in diesen Aggregationen definierten, nicht erweiterten, Controls (z.B. Toolbar oder Item).
Damit es die folgen Generationen einfacher haben, so musste es in meinem Fall aussehen:
<custom:CollapseableList [...] > <custom:headerToolbar> <Toolbar> [...] </Toolbar> </custom:headerToolbar> [...]
Die letztendlich richtig definierte CollapseableList lief dann und verhielt sich wie eine gewöhnliche sap.m.List, wie sie auch vorher im ursrpünglichen Coding war. Nun war es an der Zeit, die eigenen Features nach und nach in mein eigenes Control zu übertragen.
Refactoring Teil a) Liste
An dieser Stelle hätte ich es mir einfach machen können. Die schnellste Lösung wäre es wohl gewesen, einfach das Coding aus dem Controller der Fiori in die CollapseableList zu übertragen. Tatsächlich habe ich dies auch in Form eines Zwischenschrittes getan. Für ein wirklich gutes Refactoring wollte ich aber noch mehr erreichen:
- Die neue Liste soll sich weiterhin automatisch mit den überarbeiteten Gruppieren “bestücken”
- Aktionen wie auf- und zuklappen soll nun von einen eigenen Gruppierer übernommen werden
- Der neue Gruppierer soll nicht mehr beim Pfeil im Titel “schummeln”, sondern das Symbol als eigenständigen Wert behandeln
- Die Status “offen” und “zu”, sowie die gruppierten Einträge an sich, sollen über API-Funktionalitäten von SAPUI5 realisiert werden.
- Die Trenner sollen ebenfalls nicht mehr ein nachträgliches click-Event haben, sondern dies direkt im Coding implementieren.
Als erstes habe ich also einen neuen Gruppierer erstellt, den “CollapseableGroupHeaderListItem” (ich musste dem Marketing versprechen den Namen noch mal zu ändern).
Diesen neuen Grupiierer habe ich in meinen Liste inkludiert. Dabei verweist ‘./CollapseableGroupHeaderListItem’ auf eine Datei, die sich im gleichen Ordner befindet. Praktisch, wenn man eine Bibliothek entwickelt die flexibel in andere Anwendungen integrierbar sein soll.
sap.ui.define(['sap/m/List', './CollapseableGroupHeaderListItem'], function(List, CollapseableGroupHeaderListItem) { "use strict";
Mittels onAfterRendering() habe ich die Abhängigkeit aus dem “onUpdate” Event der View gelöst.
// Control CollapseableList.prototype.onAfterRendering = function() { CollapseableList.prototype._enhanceListGrouper(this); }; // View der Fiori <custom:CollapseableList id="productsList"updateFinished="_enhanceListGrouper"[...] >
Beim Erstellen der Liste werden über das SAPUI5-Framework in der function addItemGroup() die Gruppierer in die Einträge aufgenommen. Diese function habe ich überschreiben und, anstatt des bisherigen Gruppierers, immer meinen eigenes Control “CollapseableGroupHeaderListItem” eintragen lassen.
CollapseableList.prototype.addItemGroup = function(oGroup, oHeader, bSuppressInvalidate) { oHeader = new CollapseableGroupHeaderListItem({ title: oGroup.key }); oHeader._bGroupHeader = true; this.addAggregation("items", oHeader, bSuppressInvalidate); return oHeader; };
Die _enhanceListGrouper() konnte ich extrem entschlacken, da all die vorherigen Anforderungen wie der Status, das Zählen, das click-Event, usw. in den ebenfalls neuen Gruppierer ausgelagert worden. Das einzige was blieb, ist das Hinzufügen der Einträge.
CollapseableList.prototype._enhanceListGrouper = function(oParaList) { var oList = oParaList; var that = this; var oGrouper;var iCount = 0;var aItems = oList.getItems(); for (var i = 0; i < aItems.length; i++) { if (that._isGrouper(aItems[i])) { oGrouper = aItems[i];oGrouper.aItems = []; oGrouper.open = true; oGrouper.attachBrowserEvent("click", that._handleGrouperClick, that); iCount = 0;} else { oGrouper.addGroupedItem(aItems[i]);iCount++; oGrouper.setCount(iCount);} } };
Der Check ob ein Gruppierer vorliegt wurde ebenfalls angepasst. Hier wäre es vermutlich noch besser gewesen, diese Prüfung über Etwas wie “aItem[i].isGrouper” ebenfalls noch auszulagern.
CollapseableList.prototype._isGrouper = function(oItem) { return oItem.getMetadata()._sClassName === "lib.acando.CollapseableGroupHeaderListItem"; };
Das restliche Coding konnte ich in die “CollapseableGroupHeaderListItem” auslagern! Diese möchte ich als nächstes zeigen.
Refactoring Teil b) Gruppierer
Der eigene Gruppierer erweitert in erster Linie das Standard Control sap.m.GroupHeaderListItem. Interessant war es, wie schnell und einfach ich über das Framework den open-Status implementieren konnte. Jeder Wert eines SAPUI5 Controls wird unter metada/properties definiert. Hier konnte ich einfach meinen Eigenschaft “open” eintragen und konfigurieren. Auf Basis dieses Eintrags stehe mir zur Laufzeit Funktionen wie “getOpen()” oder “setOpen()” zur Verfügung
function(GroupHeaderListItem) { "use strict"; var CollapseableGroupHeaderListItem = GroupHeaderListItem.extend("lib.acando.CollapseableGroupHeaderListItem", { metadata: { properties: { open: { type: "boolean", group: "Appearance", defaultValue: true } }, [...]
Ebenfalls sehr einfach war es die gruppierten Eintrage erfassen zur können. Das Framework unterscheidet hier zwischen so genannten “aggregations” und “associations”. Die aggregations sind quasi der Besitz eines Controls und diesem direkt zugeordnet. Anfangs wollte ich die Einträge der Liste über eine aggregation dem Gruppierer zuordnen. Dies hatte aber den ungewollten Effekt, dass sie wiederum aus der Liste entfernt wurden ¯\_(ツ)_/¯ . Die Lösung war es, über die associations zu gehen. Diese regeln nicht den “Besitz” zwischen den Controls, sondern legen nur die entsprechenden IDs der Controls als Referenz ab. Die korrekte Konfiguration in den metadata brachte mir auch hier passende Funktionen wie “addGroupedItem()” zur Laufzeit:
metadata: { [...] associations: { groupedItems: { type: "sap.m.ListItemBase", multiple: true, singularName: "groupedItem" } }
Als nächstes ging es um die Darstellung vom Gruppierer. Wie schon oben beschriebenen, haben SAPUI5 Controls eigene Renderer, um die Daten in HTML & CSS übertragen zu können. An dieser Stelle hätte ich jetzt einen eigenen Renderer schreiben können oder selektiv den vorhandenen Überschreiben. Ich habe den Quellcode des GroupHeaderListItemRenderer studiert und erkennt, dass nur die renderLIContent() überschrieben werden musste. Dies war auch direkt über den renderer-Key der metdata-Konfiguration möglich.
metadata: { [...] renderer: { renderLIContent: function(rm, oLI) { [...] } }
Die renderLIContent() habe ich mehr oder weniger direkt übernommen, aber an zwei wichtigen Stellen geändert:
- Vorangestellt zum Titel stelle ich mittels getIcon() den Pfeil dar.
- Anstatt die Zahl über die (nicht benutzte) getCount() zu ermitteln zähle ich die unter “getGroupedItems()” gemachten Einträge.
Ich hätte an dieser Stelle eigentlich so ziemlich Alles an der Ausgabe ändern können, aber die beiden Eingriffe haben in meinem Fall gereicht.
renderLIContent: function(rm, oLI) { var sTextDir = oLI.getTitleTextDirection(); rm.write("<span class='sapMGHLITitle'"); if (sTextDir !== sap.ui.core.TextDirection.Inherit) { rm.writeAttribute("dir", sTextDir.toLowerCase()); } rm.write(">"); rm.writeEscaped(oLI.getIcon() + " " + oLI.getTitle()); rm.write("</span>"); var iCount; var aGroupedItems = oLI.getGroupedItems(); if (aGroupedItems) { iCount = aGroupedItems.length; } if (iCount) { rm.write("<span class='sapMGHLICounter'>"); rm.writeEscaped(" (" + iCount + ")"); rm.write("</span>"); } }
Das Pfeil-Icon schreibe ich also nicht mehr in den Anfang vom Titel rein, sondern stelle es parallel im Renderer dar. Ich habe auch die Ermittlung des richtigen Icons zur Laufzeit ausgelagert. Die function getIcon() gibt mir den passenden Pfeil auf Basis des aktuellen Wertes der zuvor definierten Eigenschaft “open”.
CollapseableGroupHeaderListItem.prototype.getIcon = function() { return this.getOpen() ? "▼" : "►"; };
An dieser Stelle blieb noch das Handling des Klickens auf einen der Gruppierer. Fürchterlich viel ändern musste ich nicht. Es war halt nicht mehr nötig über “attachEvent” eine nachträgliche Verarbeitung eines Click zu implementieren. Ich konnte einfach das vom Standard angeboten “onclick” implementieren. Die weitere Änderung war, dass ich von den Einträgen – da als association abgelegt – nur die IDs hatte. Die tatsächlichen Controls musste ich mir auf Basis der ID vom sap.ui.Core holen. Das Setzen vom neue Status lief ebenfalls einfach ab. Neu war hier, dass ich nun nicht mehr den Pfeil umschreibe. Dieser wird ja nun über getIcon() erst ausgegeben.
CollapseableGroupHeaderListItem.prototype.onclick = function() { this._toggleGrouperState(); this._toggleGrouperItems(); }; CollapseableGroupHeaderListItem.prototype._toggleGrouperItems = function() { var aItems = this.getGroupedItems(); var oCore = sap.ui.getCore(); for (var i = 0; i < aItems.length; i++) { var oItem = oCore.byId(aItems[i]); oItem.$().slideToggle(); } }; CollapseableGroupHeaderListItem.prototype._toggleGrouperState = function() {var sNewIcon; if (oGrouper.open) { oGrouper.open = false; sNewIcon = this.icons.RIGHT; } else { oGrouper.open = true; sNewIcon = this.icons.DOWN; } var sNewText = sNewIcon + oGrouper.getTitle().slice(1); oGrouper.setTitle(sNewText);var bNewState = !this.getOpen(); this.setOpen(bNewState); };
Fertig!
Damit war das Refactoring abgeschlossen. Die neuen Funktionalitäten liefen nun komplett über die eigenen Controls. Die Fiori war nicht mehr direkt beeinflusst. Die eigenen Controls haben wie erhofft funktioniert.
Da die Controls ja den Anspruch haben eine library zu sein, habe ich diese schnell in eine andere Fiori kopiert und dort die View fix angepasst. Auch hier haben die Controls funktioniert.
An der Stelle sind mir nur zwei Punkte aufgefallen: noch funktioniert die Liste nicht mit einem “growing” attribut. Ferner gibt es viele Fiori, die automatisch den 1. Eintrag einer Liste auswählen und damit direkt die Detail-View befüllen möchten. In meinem Fall kam es hier anfangs zu einem Fehler, der da 1. Eintrag nun mal ein Gruppierer ist, der keinen Datenpfad besitzt. Dies ist aber ein bekanntes Problem bei gruppierten Listen und trifft meine Entwicklung nicht alleine. Hier müssten die Fiori besser prüfen oder ich überschreibe die “getItems()”.
Insgesamt bin ich sehr zufrieden mit dem Ergebnis und was einem das SAPUI5 Framework hier an Möglichkeiten bietet. Leider hat die Dokumentation an dieser Stelle nur den Charakter einer Starthilfe. Das Meiste habe ich mir durch studieren des öffentlichen Quellcodes der anderen Controls aneignen müssen. Trotzdem kann ich die Entwicklung / Erweiterung einer Controls auf Basis der tollen Möglichkeiten nur empfehlen!
Im nächsten Beitrag wird es bunt, da behandle ich eigene Styles in Fiori.
Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.