Developing custom Input components for FirstSpirit

Published on 18 Nov 2019

In some cases, when developing FirstSpirit projects the standard input components do not offer all of the features that are required for your specific scenario or are simply too complex to handle a problem. For this reason, e-Spirit has provided means to extend the standard functionalities using your own custom implementations for input components. In this article, we will develop a simple custom input component to be used in the FirstSpirit ContentCreator.

Peter Zeidler

Research and Development

Creating the persistence object

Before going into the details, take a note that the whole code is available on BitBucket for you to view in your favorite IDE.

In our example the input component will simply handle an object that contains a String. However, far more complex objects could be created here.

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

Defining the input component

The input component has to be registered within the module.xml file and is represented by multiple classes. For our example we focused on the ContentCreator and have therefore not implemented the classes needed to use the component in the FirstSpirit SiteArchitect.

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

What immediately comes to mind when comparing our PUBLIC component to others, is that we are not referencing one of our own classes but rather an internal FirstSpirit class called de.espirit.firstspirit.module.GadgetSpecification which is responsible for registering our input component. To properly configure this process, we must provide multiple elements that belong to our custom implementation within the <configuration> tag. Furthermore, we have to provide the scope in which our implementation can be executed. A list of all available scopes can be found in the official FirstSpirit documentation.

Registering the component as a valid FirstSpirit form element

The following class is used to register our XML representation to be available within a form definition. To do that, our class must extend the AbstractGomFormElement class provided by FirstSpirit. In this class, our first task is to implement the getDefaultTag() method to provide the default XML tag that our input component is identified by. This does not change the behaviour of the root element of our component.

Should our input component require a more complex XML structure, the name of the corresponding child element is being used as the identifier of the XML tag. In our example, we will simply use the unique name of the PUBLIC component we registered in the module.xml file.

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

In addition to this method, we have to implement all required getters and setters for our XML attributes. To enable editors to make the component read-only, we provide the attribute editable which can hold a boolean value. For that reason, we need the methods setEditable and the corresponding getter getEditable. To let FirstSpirit know that we would like to provide the attribute within our form definition, we have to annotate our setter method with GomDoc. As we would like to make editing possible by default, we provide the annotation Default as doing so will show the default value in our form after we have initialised our variable with said value.

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

Explanation - FirstSpirit Gadget Factories

For every input component we can provide multiple factory elements that implement the interfaces SwingGadgetFactory or WebPluginGadgetFactory. Due to the fact that in our implementation we only handle the ContentCreator, we receive the following error message when trying to display the input component's form within the FirstSpirit SiteArchitect:

FirstSpirit Site Architect showing error
The factory class implemented is only responsible for creating the instances inside of the ContentCreator and will therefore fail to load in the FirstSpirit SiteArchitect.
public WebPluginGadget<HashMap<String, Serializable>> create(GadgetContext<GomInput> context) {
	return new InputWebPluginGadget(context);
}

Using the method getControllerName() we can register the name of the Javascript controller that is responsible for creating new components in FirstSpirit. The registration of the controller must happen in the Javascript snippet that we are loading into the ContentCreator itself. We will look at the Javascript code later on in this tutorial.

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

We use the methods getScriptUrls() and getStylesheetUrls() to load all required files for our input component into the ContentCreator.

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

Providing the HTML using a WebPluginGadget

Using the interface WebPluginGadget, we must provide methods for FirstSpirit to load the required HTML of our input component. In this case, the HTML is saved in its own file which has been linked and distributed within the FirstSpirit modules own resources.

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>

In the method getWidgetConfiguration() we have to provide the current configuration of the component. In order to pass the data to the Javascript that runs in the browser to create an instance, we have to serialize this data first.

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

The next step is to implement AspectHandling within the interface. For our example we use the interface SerializingValueHolder which will be responsible for transfer the values between the Javascript instance, the WebPluginGadget and FirstSpirit itself.

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();
	}
}

The final step is implementing the methods to transfer values between the WebPluginGadget and FirstSpirit. These functions are fairly simply without any complex logic behind them.

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

Javascript for the ContentCreator

The javascript we loaded into the ContentCreator must have certain functions that are responsible for the following tasks:

Javascript must create a new prototype that will create a new object for each use of our input component. We pass the WebPluginGadgetHost as a constructor parameter. This host provides us with JavaScript methods that allow us to return the elements of our input component, which we have previously provided by using the getView() method. Since we can potentially use the input component more often in a form, the IDs contained therein are replaced by Firstsprit. This means that we can no longer access the corresponding elements via the original ID within the document. Using the getElementById(String id) method of the host, we obtain the DOM element of the current instance. In addition, it provides an onModification() method that lets us notify the ContentCreator that the value in our input component has changed.

(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;

After creating the instance and integrating our HTML snippet in the DOM, FirstSpirit calls the onLoad() function in our created object. Within the function, we register an EventListener on the text field of our input component, so that we receive the input value when something has been entered. At the same time we can inform the ContentCreator about the change. In addition, we put any existing value in the text box and enable/disable the editing capabilities of our input component.

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

In addition, the functions getValue(), setValue(value) and isEmpty() are needed to exchange the value of our input component with the ContentCreator.

/**
 * 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;
};

Since our input component automatically receives a label from Firstspirit, which is not shown in our HTML snippet, we need the function getLabel(), through which the ContentCreator receives information on what should be displayed as a label. In the WebPluginGadget we already provided a label in the configuration so that we can now access and output it in our Javascript.

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

Since we have made it possible to prevent the editing of the value by editors, we now have to implement the setEditable (editable) method, which returns the value for the attribute specified by the ContentCreator in the form definition.

/**
 * 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;
    }
};

Finally, we need to register the instance of the prototype in the application.

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

FirstSpirit ValueEngineerFactory - Preparing the data

So far, we have only been working with the form definition and functionality of our input component. What is still missing is the possibility to process the recorded values so that Firstspirit can save and read them again. To do this, a ValueEngineer is generated via the implementation of the ValueEngineerFactory interface.

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();
    }
}

Engineering values

Because Firstspirit internally stores all values as XML, our implementation of the ValueEngineer interface must take care of converting the captured value from the input component into XML nodes for Firstspirit to process and store.

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

At the same time, we need to be able to recover and display the value from the stored XML nodes of our input component.

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

Fortunately, e-Spirit provides a set of auxiliary methods that are self-explanatory by their name to handle that problem.

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

In addition to the methods we have implemented so far, we would usually still need to implement the AspectHandling methods. In our example, however, we will not go into detail because they are not needed for the example shown here. A list of possible Aspects can be found in the official FirstSpirit API documentation.

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

Final steps

In order for the ContentCreator to access our implementations and resources, we need to deploy them to the ContentCreator using a WebApp. For this we need a configuration file called web.xml and a corresponding entry in the module.xml file.

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

Conclusion

As soon as you begin to understand the interaction of the individual components, it is not so difficult to develop your own input component for the ContentCreator. But before you do it, you should definitely think twice about whether it's worth the effort and if your problem cannot be solved with an existing input component instead.

The sources for this example are available on Bitbucket.

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