FirstSpirit Guide - Eingabekomponenten für den ContentCreator

Veröffentlicht am 18.11.2019

In einigen Fällen kann es innerhalb eines FirstSpirit Projektes sinnvoll sein eigens entwickelte Eingabekomponenten zur Verfügung zu stellen, da sich nicht alle Szenarien mit den Standard-Eingabekomponenten abbilden lassen oder die Pflege damit zu komplex wäre. Für solche Fälle hat e-Spirit die Möglichkeit geschaffen das System über Module mit eigenen Eingabekomponenten zu erweitern. In diesem Blog-Artikel beleuchten wir die notwendigen Schritte, um eine Eingabekomponente zur Verwendung im ContentCreator zu entwickeln.

Peter Zeidler

Research and Development

Anlegen der Persistenzklasse

Bevor wir in die Implementierung einsteigen zunächst der Hinweis, dass der gesamte Code auf Bitbucket verfügbar sind und man ihn direkt in seiner IDE mitlesen kann.

In unserem Beispiel wird in der Eingabekomponente ein Objekt gespeichert das lediglich einen String beinhaltet. Hier wären aber auch komplexere Objekte möglich.

package de.aboutcontent.blogpost.input;
import de.espirit.common.tools.Strings;
public class Input {
	private String text = null;
	public Input() {
	}
	public Input(String text) {
		this.text = text;
	}
	public boolean isEmpty() {
		return Strings.isEmpty(getText());
	}
	public String getText() {
		return text;
	}
	public void setText(String text) {
		this.text = text;
	}
}

Definition der Eingabekomponente

Die Eingabekomponte wird durch unterschiedliche Klassen in der module.xml repräsentiert. Hierzu muss es in dieser Datei einen eigenen Eintrag geben. In unserem Beispiel haben wir den Fokus auf den ContentCreator geleget und implementieren daher keine Klassen, um diese auch im SiteArchitect nutzen zu können.

<public>
	<name>GOM_INPUT</name>
	<description>BlogPost ContentCreator Input Component.</description>
	<class>de.espirit.firstspirit.module.GadgetSpecification</class>
	<configuration>
		<gom>de.aboutcontent.blogpost.input.GomInput</gom>
		<factory>de.aboutcontent.blogpost.input.InputWebPluginGadgetFactory</factory>
		<value>de.aboutcontent.blogpost.input.InputValueEngineerFactory</value>
		<scope unrestricted="yes"/>
	</configuration>
	<resources>...</resources>
</public>

Was hierbei im Vergleich zu anderen “PUBLIC”-Komponenten auffällt ist, dass als Klasse nicht eine unserer eigenen Klassen angegeben ist sondern die Klasse de.espirit.firstspirit.module.GadgetSpecification, die für die Registrierung unserer Eingabekomponente zuständig ist. Hierzu wird in der Definition der Komponenten das Element configuration erwartet, in der wir die einzelnen Elemente unserer Eingabekomponte angeben. Zusätzlich müssen wir noch den Scope eintragen in dem unsere Eingabekomponente verwendet werden kann. In unserem Fall reicht es uns aus, wenn diese überall verwendet werden darf. Eine Beschreibung über die möglichen Scopes findet sich in der FirstSpirit Dokumentation.

Registrieren der Komponente als valides FirstSpirit Formularelement

Durch diese Klasse wird die XML-Repräsentation innerhalb der Formular-Definition gesteuert. Hierzu benötigen wir eine Klasse, welche die von Firstspirit bereitgestellte abstrakte Klasse AbstractGomFormElement erweitert. Hier müssen wir als Erstes die Methode getDefaultTag() implementieren, die den Namen des Standard XML-Tags benennt den unsere Eingabekomponente bekommen soll.

Soll unsere Eingabekomponente jedoch über eine komplexere XML-Struktur verfügen, so wird bei den Kind-Elementen daraus der Tag-Name des jeweiligen Unter-Elementes ermittelt. In unserem Fall wird stattdessen einfach der Name des PUBLIC-Elements aus der module.xml genutzt.

@Override
protected String getDefaultTag() {
	return "GOM_INPUT";
}

Zusätzlich zu dieser Methode müssen wir für alle Attribute die wir in der XML-Definition angeben jeweils passende Getter und Setter bereitstellen. Damit wir in unserem Beispiel die Möglichkeit schaffen das der Inhalt nicht von Redakteuren bearbeitet werden kann, möchten wir das Attribut editable mit einem booleschen Wert ( YesNo) pflegbar machen und benötigen hierzu den Setter setEditable, sowie den Getter getEditable().

Damit Firstspirit weiß, dass wir das Attribut in der Formulardefinition pflegbar machen möchten, müssen wir den Setter mit der Annotation GomDoc beschreiben. Da das Bearbeiten unserer Eingabekomponente im späteren Formular im Standard aktiviert ist, geben wir zusätzlich noch die Annotation Default mit an. Damit der Default-Wert auch im Formular angezeigt wird, initialisieren wir die entsprechende Variable in unser Klasse mit diesem.

private YesNo editable = YesNo.YES;
@GomDoc(description = "Component has read-only-mode", since = "1.0")
@Default("true")
public YesNo getEditable() {
	return editable;
}
/**
 * Set value of gom attribute 'editable'.
 *
 * @param value
 * @see #getEditable()
 * @see #editable()
 */
public void setEditable(final YesNo editable) {
	this.editable = editable;
}
/**
 * Convenience method to get the value of gom attribute 'editable'. Default
 * value is 'true'
 * 
 * @return true/false
 * @see #getEditable()
 * @see #setEditable(de.espirit.firstspirit.access.store.templatestore.gom.YesNo)
 */
public YesNo editable() {
	return editable;
}

Erläuterung - FirstSpirit Gadget Factories

Pro Eingabekomponente können mehrere Factory-Elemente angegeben werden, die die Interfaces SwingGadgetFactory bzw. WebPluginGadgetFactory implementieren. Da wir uns in unserem Beispiel nur um die Implementierung für den ContentCreator kümmern, kommt es bei dem Versuch unsere Eingabekomponente im SiteArchitect anzuzeigen innerhalb der Formulardarstellung zu folgendem Fehler:

FirstSpirit Site Architect zeigt Fehler
Die implementierte Factory-Klasse ist nur für die Erzeugung von Instanzen innerhalb des ContentCreators zuständig.
public WebPluginGadget<HashMap<String, Serializable>> create(GadgetContext<GomInput> context) {
	return new InputWebPluginGadget(context);
}

Über die Methode getControllerName() wird FirstSpirit bekannt gegeben unter welchem Namen wir die Erzeugung einer neuen Instanz im Javascript registriert haben. Die Registrierung dieser Funktion muss über das Javascript selbst erfolgen, welches wir über Firstspirit in den ContentCreator laden lassen. Diesem Teil der Implementierung werden wir uns im späteren Verlauf dieses Tutorials annehmen.

public String getControllerName() {
	return "InputWebGadget";
}

Zusätzlich erfährt der ContentCreator über die Methoden getScriptUrls() und getStylesheetUrls() welche Dateien für unsere Eingabekomponente einmalig in den ContentCreator geladen werden müssen. Diese Dateien müssen wir dem ContentCreator genauso wie unsere Implementierungen über eine WebApp zur Verfügung stellen.

public List<String> getScriptUrls() {
	return Collections.singletonList("input/input.js");
}
public List<String> getStylesheetUrls() {
	return Collections.singletonList("input/input.css");
}

Das HTML zur Verfügung stellen mittels WebPluginGadget

Durch das Interface WebPluginGadget müssen wir Methoden bereitstellen, die das HTML unserer Eingabekomponente für FirstSpirit zur Verfügung stellt. In unserem Beispiel liegt das HTML für die Eingabekomponente in einer eigenen Datei, die in den Ressourcen unseres Moduls gespeichert ist.

public String getView() {
	return getResource("/input.html");
}
private static String getResource(final String filename) {
	try {
		final InputStream stream = InputWebPluginGadget.class.getResourceAsStream(filename);
		if (stream != null) {
			return Streams.toString(stream, "UTF-8");
		}
	} catch (final IOException ignored) {
	}
	return "";
}
<div class="inputWidget">
	<input id="inputWidget_text" type="text"/>
</div>

Desweiteren müssen wir innerhalb der Methode getWidgetConfiguration() die aktuelle Konfiguration der Eingabekomponente so aufbereiten, dass diese mit an das Javascript für die Erzeugung der Instanz im Browser übergeben werden kann.

public HashMap<String, Serializable> getWidgetConfiguration() {
	final HashMap<String, Serializable> configuration = new HashMap<>();
	configuration.put("label", context.getGom().label(context.getDisplayLanguage().getAbbreviation()));
	configuration.put("editable", context.getGom().editable() == YesNo.YES);
	return configuration;
}

Zum Schluss muss an dieser Stelle noch der Bereich AspectHandling implementiert werden. In unserem Beispiel nehmen wir hier das Interface SerializingValueHolder welches für die Übergabe des Werts der Eingabekomponente zwischen der JavaScript Instanz, dem WebPluginGadget und Firstspirit zuständig ist. Hierzu implementieren wir die Methoden, durch die der Wert zwischen der JavaScript Instanz und dem WebPluginGadget übergeben werden.

private Input value = null;
public HashMap<String, Serializable> getSerializedValue() {
	HashMap<String, Serializable> result = new HashMap<>();
	result.put("TEXT", value.getText());
	return result;
}
public void setSerializedValue(HashMap<String, Serializable> serialization) {
	if (serialization != null && serialization.containsKey("TEXT")) {
		value = new Input((String) serialization.get("TEXT"));
	} else {
		value = new Input();
	}
}

Zu guter Letzt werden die Methoden zur Übergabe des Wertes zwischen unserem WebPluginGadget und Firstspirit, sowie einer Methode über die Firstspirit erfährt, ob es sich um einen leeren Wert handelt, implementiert. Bei diesen Methoden handelt es sich um sehr einfachen Code ohne große Logik.

public Input getValue() {
	return value;
}
public void setValue(Input value) {
	this.value = value;
}
public boolean isEmpty() {
	return value == null || value.isEmpty();
}

Javascript für den ContentCreator

Das Javascript welches wir in den ContentCreator geladen haben, muss über einen bestimmte Satz an Funktionen verfügen, um die folgenden Aufgaben zu behandeln:

Es muss einen neuen Prototypen erstellen von dem für jede Verwendung unserer Eingabekomponente eine neues Objekt erstellt wird. Beim Aufruf des Konstruktors wird der WebPluginGadgetHost übergeben. Dieser stellt uns Methoden zur Verfügung mit denen wir uns die Elemente unserer Eingabekomponente, die wir über das HTML-Snippet durch die Methode getView(), in das DOM des ContentCreators haben schreiben lassen, zurückliefern lassen können. Da wir die Eingabekomponente potentiell öfters in einem Formular verwenden können, werden die darin enthaltenen IDs von Firstsprit ersetzt. Hierdurch können wir nicht mehr über die Original ID innerhalb des Documents auf die entsprechenden Elemente zugreifen. Über die Methode getElementById(String id) des Hosts, erhalten wir stattdessen das DOM-Element der Instanz. Zusätzlich verfügt er noch über eine Methode onModification(), mit der wir dem ContentCreator mitteilen können, dass sich der Wert geändert hat.

(function () {
    /**
     * Constructor of the WebPluginGadget controller object.
     *
     * @param host WebPluginGadgetHost current component instance
     * @param configuration component configuration via de.aboutcontent.blogpost.input.InputWebPluginGadget.getWidgetConfiguration()
     * @constructor
     */
    function CreateInstance(host, configuration) {
        const self = this;
        self.host = host; // WebPluginGadgetHost
        self.configuration = configuration; // Widget configuration
        self.editable = configuration.editable;
        self.readonly = false;
        self.value = undefined;
        self.input = undefined;

Nach dem Erzeugen der Instanz und der Integration unseres HTML-Snippets im DOM wird von FirstSpirit in unserem erzeugten Objekt die Funktion onLoad() aufgerufen. Innerhalb der Funktion registrieren wir einen EventListener auf dem Textfeld unserer Eingabekomponente, so dass wir bei einer erfolgten Eingabe den eingegebenen Wert erhalten. Gleichzeitig können wir den ContentCreator über die Änderung informieren. Zusätzlich setzen wir einen eventuell bereits vorhandenen Wert in das Textfeld und aktivieren/deaktivieren die Bearbeitungsmöglichkeiten unserer Eingabekomponente.

self.onLoad = function () {
    self.input = self.host.getElementById('inputWidget_text');
    self.input.addEventListener("input", function() {
        self.value["TEXT"] = self.input.value;
        self.host.onModification(); // Notify the ContentCreator that a changed has occured
    });
    if (self.value) {
        self.input.value = self.value["TEXT"];
        self.input.readOnly = !self.editable || self.readonly;
    }
};

Zusätzlich werden die Funktionen getValue(), setValue(value) und isEmpty() benötigt, um den Wert unserer Eingabekomponente mit dem ContentCreator auszutauschen.

/**
 * Returns the current value of the input component.
 *
 * @returns undefined | {text: (string)}
 */
self.getValue = function () {
    return self.value;
};
/**
 * Set the value saved in FirstSpirit.
 *
 * @param value FirstSpirit saved value
 */
self.setValue = function (value) {
    self.value = value;
    if (self.input) {
        self.input.value = value["TEXT"];
    }
};
/**
 * Check if the component value is empty.
 *
 * @returns {boolean}
 */
self.isEmpty = function () {
    return !self.value || !self.value.text;
};

Da unsere Eingabekomponente durch FirstSpirit automatisch über ein Label verfügt, welches nicht in unserem HTML-Snippet ausgegeben wird, benötigen wir noch die Funktion getLabel(), über die der ContentCreator erfährt was er als Label anzeigen soll. Im WebPluginGadget haben wir das Label in die Konfiguration geschrieben, damit wir nun in unserem Javascript darauf zugreifen und ausgeben lassen können.

/**
 * Return the component's label
 *
 * @returns {string}
 */
self.getLabel = function () {
    return self.configuration.label;
};

Da wir in unserer Eingabekomponente die Möglichkeit geschaffen haben die Bearbeitung des Wertes durch Redakteure zu unterbinden müssen wir jetzt die Methode setEditable(editable) implementieren, über die wir den in der Formulardefinition angegebenen Wert für das Attribut durch den ContentCreator erhalten.

/**
 * Accept or prevent input.
 *
 * @param editable new status
 */
self.setEditable = function (editable) {
    self.readonly = !editable;
    if (self.input) {
        self.input.readonly = !self.editable || self.readonly;
    }
};

Zu guter Letzt müssen wir die Instanz des Prototypen in der Applikation registrieren.

}
    window.InputWebGadget = CreateInstance;
})();

FirstSpirit ValueEngineerFactory - Daten vorbereiten

Bis jetzt haben wir uns um die Formulardefinition und die Funktionalität unserer Eingabekomponente gekümmert. Was jetzt noch fehlt ist die Möglichkeit die erfassten Werte so aufzubereiten, dass FirstSpirit sie speichern und wieder lesen kann. Hierzu wird über die Implementierung des Interfaces ValueEngineerFactory ein ValueEngineer erzeugt.

package de.aboutcontent.blogpost.input;
import de.espirit.firstspirit.client.access.editor.ValueEngineer;
import de.espirit.firstspirit.client.access.editor.ValueEngineerContext;
import de.espirit.firstspirit.client.access.editor.ValueEngineerFactory;
public class InputValueEngineerFactory implements ValueEngineerFactory<Input, GomInput> {
    public Class<Input> getType() {
        return Input.class;
    }
    public ValueEngineer<Input> create(ValueEngineerContext<GomInput> context) {
        return new InputValueEngineer();
    }
}

ValueEngineer

Da Firstspirit intern alle Werte als XML speichert, muss unsere Implementierung des Interfaces ValueEngineer sich darum kümmern, dass der erfasste Wert aus der Eingabekomponente in XML-Nodes umgewandelt wird, damit diese von Firstspirit verarbeitet und gespeichert werden können.

public List<Node> write(Input value) {
    return Collections.singletonList(Node.create("TEXT", value.getText()));
}

Gleichzeitig müssen wir aus den gespeicherten XML-Nodes unserer Eingabekomponente den Wert wiederherstellen und anzeigen können.

public Input read(List<Node> nodes) {
    for (final Node node : nodes) {
        final String nodeName = node.getName();
        if ("TEXT".equals(nodeName)) {
            return new Input(node.getText());
        }
    }
    return null;
}

Glücklicherweise gibt es auch hier einige Hilfsmethoden die von ihrem Namen her selbsterklärend sind.

public boolean isEmpty(Input value) {
    return value.isEmpty();
}
public Input copy(Input original) {
    return new Input(original.getText());
}
public Input getEmpty() {
    return new Input();
}

Zusätzlich zu den Methoden die wir bisher implementiert haben müssen wir noch die Methoden zum Thema AspectHandling implementieren. In unserem Beispiel werden wir jedoch nicht näher darauf eingehen, da sie für das hier gezeigte Beispiel nicht benötigt werden. Eine Liste der möglichen Aspects findet sich in der offiziellen FirstSpirit Dokumentation.

public <T> T getAspect(ValueEngineerAspectType<T> aspect) {
    return null;
}

Abschließende Arbeiten

Damit der ContentCreator auf unsere Implementierungen und Ressourcen zugreifen kann, müssen wir diese mit einer WebApp in den ContentCreator deployen. Hierzu benötigen wir als Erstes eine Konfigurationsdatei namens web.xml und einen entsprechenden Eintrag in der module.xml.

<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>BlogPost ContentCreator Input Component</display-name>
</web-app>
<web-app scopes="Global,Project">
    <name>GomInputContentCreatorExtensions</name>
    <displayname>BlogPost ContentCreator Input Component ContentCreator Extensions</displayname>
    <web-resources>
        ${module.resources.global.legacy.web}
        <resource target="/input/">input.css</resource>
        <resource target="/input/">input.js</resource>
    </web-resources>
    <web-xml>web.xml</web-xml>
 </web-app>

Fazit

Sobald man das Zusammenspiel der einzelnen Komponenten verstanden hat ist es nicht mehr so schwierig eine eigene Eingabekomponente für den ContentCreator zu entwickeln. Bevor man es jedoch macht, sollte man sich definitiv gut überlegen, ob sich die Aufwände dafür lohnen.

Die Sourcen zu diesem Beispiel sind auf Bitbucket verfügbar.

facebook icon twitter icon xing icon linkedin icon
© 2019 aboutcontent GmbH