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.

  1. 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.
  2. 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: :
  3. 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 pure light-module. Just delete src/main/resources/backend-custom-js-module and create a folder in your magnolia.resources.dir location. I will refer to this light-module as backend-custom-js-module.
  4. 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.

  1. 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.java
    package 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 and setters 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.
  2. Now we need to create the info.magnolia.backendlive.actions.MyJavascriptAction class with the following content:

    src/main/java/info/magnolia/backendlive/actions/MyJavascriptAction.java
    package 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 Implement JavascriptObject.
    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 the JavascriptObject interface.
  3. 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.


Food for thought

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.

Feedback

Incubators

×

Location

This widget lets you know where you are on the docs site.

You are currently perusing through the Backend Live docs.

Main doc sections

DX Core Headless PaaS Legacy Cloud Incubator modules