Backend Live - Build Your Own Extension
In case the available extensions don’t quite fit your use cases, here’s some information on how to build your own. I think you’ll be quite pleased how simple it is to bring backend functionality to the frontend.
Create your module
The fastest way to create your Magnolia Module is by using Magnolia’s Maven Archetype.
-
Open a terminal to the location you wish to create your module, and run:
mvn archetype:generate -DarchetypeGroupId=info.magnolia.maven.archetypes -DarchetypeArtifactId=magnolia-module-archetype -DarchetypeVersion=RELEASE (1)
1 If you want to create a project webapp
first, just remove the-DarchetypeArtifactId=magnolia-module-archetype
from the command to select the type of project from a list. -
Choose your options and confirm, here is a quick demonstration:
Define value for property 'module-class-name': MyJSModule Define value for property 'magnolia-bundle-version': 6.2.17 # Must be 6.2.17 and higher Define value for property 'groupId': info.magnolia.backendlive Define value for property 'artifactId': backend-custom-js-module Define value for property 'version' 1.0-SNAPSHOT: : Define value for property 'package' info.magnolia.backendlive: : Define value for property 'module-name' backend-custom-js-module: :
-
Cleanup the
pom.xml
so that it looks something like this:backend-custom-js-module/pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>info.magnolia.backendlive</groupId> <artifactId>backend-custom-js-module</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>backend-custom-js-module Magnolia Module</name> <dependencies> <dependency> <groupId>info.magnolia</groupId> <artifactId>magnolia-core</artifactId> </dependency> <dependency> <groupId>info.magnolia.javascript-models</groupId> <artifactId>magnolia-module-javascript-models</artifactId> <version>${js-models.version}</version> <1> </dependency> </dependencies> </project>
1 ${js-models.version}
refers to the current version of Javascript Models 2.x i.e.{js-models-version
}.I suggest you remove the generated hybrid module
as it is best to work from a purelight-module
. Just deletesrc/main/resources/backend-custom-js-module
and create a folder in yourmagnolia.resources.dir
location. I will refer to thislight-module
asbackend-custom-js-module
. -
Add a dependency in your Magnolia Module Definition file
src/main/resources/META-INF/magnolia/backend-custom-js-module.xml
to Javascript Models like this:src/main/resources/META-INF/magnolia/backend-custom-js-module.xml<module> ... <dependencies> ... <dependency> <name>javascript-models</name> <version>${js-models.version}/*</version> </dependency> </dependencies> ... </module>
Backend
For the backend code, we are going to follow the general structure for registry items in Magnolia, meaning we will have a Definition
class and an Implementation
class. This can vary from registry to registry, please refer to the Magnolia Documentation when creating something new. For this example, we will create a new Action.
-
Create your
Definition
class first.You can choose the base class or interface that you wish, we will be using
info.magnolia.ui.api.action.ConfiguredActionDefinition
. You will also want to use the Registry annotation so that we can use$type
in our YAML files. Create the Java class:info.magnolia.backendlive.actions.MyJavascriptActionDefinition
with the following content:src/main/java/info/magnolia/backendlive/actions/MyJavascriptActionDefinition.javapackage info.magnolia.backendlive.actions; import info.magnolia.rendering.renderer.ContextAttributeConfiguration; import info.magnolia.ui.api.action.ActionType; import info.magnolia.ui.api.action.ConfiguredActionDefinition; import lombok.Getter; <1> import lombok.Setter; <1> import java.util.List; import java.util.Map; @Getter @Setter @ActionType("myJsAction") <2> public class MyJavascriptActionDefinition extends ConfiguredActionDefinition { <3> private String modelPath; <4> private Map
parameters; <5> private List exposedComponents; <6> public MyJavascriptActionDefinition() { setImplementationClass(MyJavascriptAction.class); <7> } } 1 Lombok makes generating getters
andsetters
easier.2 Annotation to allow the use of $type
in YAML definition.3 Extend the existing Action Definition. 4 Specify the path to the Javascript class. 5 Expose parameters that can be used in the Javascript class via this.parameters
.6 Expose components not specified in the Javascript Models configuration ( /modules/javascript-models/config/engineConfiguration/exposedComponents
).7 Point to the implementation class where we will build the business logic of the execute
function. -
Now we need to create the
info.magnolia.backendlive.actions.MyJavascriptAction
class with the following content:src/main/java/info/magnolia/backendlive/actions/MyJavascriptAction.javapackage info.magnolia.backendlive.actions; import info.magnolia.module.jsmodels.JavascriptObject; import info.magnolia.module.jsmodels.factory.JavascriptObjectFactory; import info.magnolia.ui.ValueContext; import info.magnolia.ui.api.action.AbstractAction; import info.magnolia.ui.api.action.ActionExecutionException; import org.graalvm.polyglot.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.script.ScriptException; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class MyJavascriptAction extends AbstractAction
implements JavascriptObject { <1> private static final Logger log = LoggerFactory.getLogger(MyJavascriptAction.class); private final JavascriptActionDefinition definition; private Value jsObject; <2> @Inject protected MyJavascriptAction(JavascriptActionDefinition definition, ValueContext> item, JavascriptObjectFactory jsObjectFactory) { <3> super(definition); this.definition = definition; try { Map localBindings = new HashMap<> (); <4> localBindings.put("parameters", definition.getParameters()); localBindings.put("content", item.getSingleOrThrow()); localBindings.put("definition", getDefinition()); localBindings.put("def", getDefinition()); localBindings.put("log", log); jsObject = jsObjectFactory.createJavascriptObject(definition.getModelPath(), localBindings, definition.getExposedComponents()); <5> mergeJavascriptObject(); <6> } catch (ScriptException | IOException e) { log.warn("Error occurred during evaluation of the script (script path: [{}]). Error Message: {}", JavascriptObjectFactory.resolvePath(definition), e.getMessage()); } } @Override <7> public void execute() throws ActionExecutionException { if (getJsObject().hasMember("execute") && getJsObject().getMember("execute").canExecute()) { <8> getJsObject().getMember("execute").execute(); } else { log.error("You must specify an execute method within {}.", JavascriptObjectFactory.resolvePath(definition)); } } @Override public Value getJsObject() { <9> return jsObject; } } 1 Extend the AbstractAction
and ImplementJavascriptObject
.2 The compiled Javascript class instance stored here. 3 The constructor needs the YAML definition and the JavascriptObjectFactory
to compile the JS.4 Create your local bindings, so you can access these objects in the JS class via this
.5 Compile the JS class and get the instance. 6 Merging the object with the Java class gives you access to the Java public methods within your JS class. 7 Override all the mandatory functions, more on this below. 8 Check if the JS class has defined the execute
method, and invoke it if so.9 Mandatory to define getter
as it is used in theJavascriptObject
interface. -
Now that you have the code, you can start using this in your frontend. Rather you can read Javascript Actions Extension documentation for the usage.
A couple notes about the implementation code.
The localBindings
at Line 29 gives you access to these instances by calling the first parameter with this
. When you call:
this.log.warn('WARNING');
You are actually writing to the Magnolia log file directly. You can add whatever objects you wish to have access to in the Javascript. |
Any abstract method will need to be defined in this implementation class, but is simply a wrapper to the "expected" implementation. If you expect the frontend developers to define it, you can throw an exception if they don’t. If you wish to have some default behavior to be invoked if the Javascript class doesn’t define it, you can add that to the else
statement. If you just want to fall back to the extended class logic if nothing is specified, then you need not add anything at all, as the Line 36 ` mergeJavascriptObject();` will add that function to be available to Javascript.
This is an example of a void
method, and therefore doesn’t require a return object. It also doesn’t take in any parameters. To learn more about how to handle this, see the GraalVM Value API execute definition.