This page explains in detail how to use JavaScript models for templates. JavaScript models can be done via Light development, thus enabling fast development and deployment without the need for Java, Maven or WAR deployment. JavaScript models represent a type of Magnolia Resources

If you want to use JavaScript models, make sure that your bundle contains the magnolia-module-javascript-models module. For further information see JavaScript Models module - Installing.

Defining, referencing and using a JavaScript model

Base recipe: Using a convention for naming and location of the model

1. Set the model class property in the template definition.

rhino.yaml
templateScript: /your-light-module/templates/pages/rhino.ftl
renderType: freemarker
modelClass: info.magnolia.module.jsmodels.rendering.JavascriptRenderingModel
2. Create the model file in the same location as the template definition and give it the name <template-name>.js . Create a JavaScript "class" with properties and methods in the file and create an instance at the end of the file.
rhino.js
var Dumbo = function () {
	this.name = "John";
    this.getRandomNumber = function () {
        return Math.ceil(100 * Math.random());
    }
};
new Dumbo();
3. Use the model in the template script.
rhino.ftl
<div>Hey ${model.name}, your happiness level is at ${model.getRandomNumber()}%.</div>

Reference the model in the script and use the properties or methods defined within the model.

Files involved:


your-light-module/
└── templates
    └── pages
        ├── rhino.ftl
        ├── rhino.js
        └── rhino.yaml

This recipe works for both page and component templates.

Using the modelPath and class property

With this approach you do not have to follow the naming convention. This can be handy to use the same model among multiple templates.

Instead of the property modelClass, you must set the properties class and modelPath. The value for the former must be info.magnolia.module.jsmodels.rendering.JavascriptTemplateDefinition and for the latter the path to the JavaScript model.

Example:

rhino.yaml
templateScript: /your-light-module/templates/pages/rhino.ftl
renderType: freemarker
class: info.magnolia.module.jsmodels.rendering.JavascriptTemplateDefinition
modelPath: /your-light-module/templates/common/baseModel.js

Nashorn, JavaScript and the Java API

When you write a JavaScript model, you obviously write a JavaScript code. However, you will encounter objects which come from the Java world. For instance this is true when you work with Magnolia's built-in rendering context objects or when using templating functions.

When using objects originating in Java within a JavaScript model, it is helpful to know their public methods, which you can also use within the JavaScript code. To get familiar with these objects, it helps to have a look at the Java API pages which list the public methods and the corresponding return types. On this page you will find many links to Java docs. Follow the links to get an understanding of these Java-origin objects.

Examples: AggregationState (for the state object), ContentMap (for the content object), SimpleTranslator (for the i18n object) etc.

Most context objects are JavaBeans, which means you can access their properties with the "dot" operator or a getter method in a template script or in a JavaScript model. Both expressions are valid and return the same value.

state.channel.name;
state.getChannel().getName();
Both the lines above have the same meaning.

Using built-in rendering context objects

In this section we take a look at the Magnolia built-in objects modelcontentdefctxstate and i18n . These objects can be used directly within a template script. Below you will see how to access their properties within a JavaScript model.

(info) The code snippets starting with var are in JavaScript and are intended to be used within JavaScript models.

model

The model itself provides the following built-in properties:

  • parent: Model of the parent component or template.
  • root: Top root model of the rendering process.
  • content: Content node bound to the model in the rendering context provided as a ContentMap.
  • node: Content node bound to the model in the rendering context provided as a Node.
  • definition: The renderable definition (template, area or component) bound to the model. Same as def.

Within a FreeMarker template script, you reference this property as follows:

[#assign parentTemplateDefintion = model.parent.definition /]
Combining a method of the parent model ( #getPageInfo ) and built-in model properties:
 ${model.parent.getPageInfo(model.parent.content)}
In a JavaScript model class, you must use this instead of model :
var parentTemplateDefintion = this.parent.definition;

var DemoModel = function () {
    /**
     * Whithin the model JS file, when using model properties, you must use <code>this</code> to reference the model.
     */
    this.demonstrateBuiltInProperties = function () {
        var res = "<pre>";
        res += "Current Template name = " +this.definition.name +"\n";
        res += "Parent Template name = " +this.parent.definition.name +"\n";
        res += "Current node path = " +this.content['@path']  +"\n";
        res += "Parent node path = " +this.parent.content['@path']  +"\n";
        res += "Page info by using method of the parent model: "+ this.parent.getPageInfo(this.parent.content)+"\n";
        res += "</pre>";
        return res;
    }
};
 
new DemoModel();

Note the usage of the parent property, which is a handy pointer to the parent model. With parent you have access to all properties and methods defined on the parent model as well as to the built-in properties.

content

Current content node in the rendering context provided as a ContentMap.

In a page template, current node is the page node (mgnl:page). In a component template, current node is the component node (mgnl:component). It is the contextual "root" content node. The current node is exposed as a ContentMap object, which means it carries the properties of the underlying Node.

example
var pageTitle = content.title;
var nodePrimaryType = content["jcr:primaryType"];
var nodeType = content["@nodeType"]; // same as above)
var name = content["@name"];
var id = content["@id"];
var path = content["@path"];
var depth = content["@depth"];
The content object is a "map". You can access its properties via the "dot notation" (line 1) or the "brackets notation" (line 2). The latter is required if the property's name contains a colon or a dot.  

The content object also maps some always-existing properties with the spacial delimiter @ . These properties are @name@id (same as jcr:uuid), @path, @depth, and @nodeType (same as jcr:primaryType).

def –  template definition

Current 

$webResourceManager.requireResource("info.magnolia.sys.confluence.artifact-info-plugin:javadoc-resource-macro-resources") RenderableDefinition
. Use def to access the properties of the template definition such as title, or custom parameters. It is a JavaBean, which means you can access its properties with the "dot" operator or a getter method.
example
var templateTitle = def.title;
var myColor = (def.parameters && def.parameters.color && ""!=def.parameters.color) ? def.parameters.color : "red";

ctx – context

Context represents the environment in which the current process runs. The type is ContextIt is a WebContext when the script is executed from a Web page and SimpleContext for instance when the script generates a mail from within a workflow or scheduled job.

The Context interface provides access to:

  • user (User)
  • locale (java.util.Locale)

    Please note that ${ctx.locale} is different from ${cmsfn.language()}, the former referring to the locale currently used by the user, the latter to the locale currently used by the site. See also AdminCentral and public locales.

In addition, WebContext provides access to:

WebContext properties are null if the execution is not a Web page request.

example
var userName = ctx.user.name;
var locale = ctx.locale;
var contextPath = ctx.contextPath;
var servletContext = ctx.servletContext;

state – aggregation state

The current AggregationState. Only set if ctx is of type WebContext. (See above.) It is a shortcut for ctx.aggregationState.

Provides access to many properties as:

  • channel (Channel)
  • originalURI
  • currentURI
  • queryString
  • mainContentNode (javax.jcr.Node)
  • templateName
  • locale (the same as ctx.locale)
  • etc.

Read the javadoc (AggregationState) for all the properties.

Please note that the values of all the properties are HTML-escaped by default. Should you need it, the raw (unescaped) data can still be accessed in the following manner:

${state.unwrap().originalURI}

However, be warned that this may expose your webapp to XSS attacks.

example
var currentURI = state.currentURI;
var queryString = state.queryString;
var channelName = state.channel.name;

i18n – simple translator for localized content

The i18n is an object of the SimpleTranslator type. It provides access to all the message bundles which have been loaded into the system. Use the #translate method to get a localized value identified by a key. You can provide more String arguments to the method in order to replace the placeholders ({}) in the translation.

Example:

message-bundle snippet
javascript-model-samples.frontend.footer.arbitraryWisdom = You make your own luck
javascript-model-samples.frontend.footer.happinessLevel = Random happiness level: {0}%
JavaScript model snippet
var localizedText = i18n.translate("javascript-model-samples.frontend.footer.arbitraryWisdom");
var localizedText2 = i18n.translate("javascript-model-samples.frontend.footer.happinessLevel", "95");

Enabling and using templating functions for JS models

Templating functions were built mainly to be used within template scripts directly, but they can also be very handy within JavaScript models. In the default configuration of magnolia-module-javascript-models, templating functions are not enabled. 

To give more power to your JavaScript models, enable the templating functions to access and search JCR content, such as cmsfn , damfn , searchfn and similar.

Enabling templating functions

Edit the configuration of the module with the Configuration app. For each templating function you want to use, add an entry at /modules/javascript-models/config/engineConfiguration/exposedComponents .

The stock configuration of the module shows just the version of the module. You have to create the subtree starting with the folder /modules/javascript-models/config .

Using templating functions in a JavaScript model

Once a templating function is enabled, use it in the JavaScript code of the model. 

Example: Breadcrumb navigation using cmsfn#ancestors

JS snippet from the model class
    this.renderBreadcrumbs = function (pageNode) {
        var res = i18n.translate('javascript-model-samples.frontend.breadcrumbs.start.label');
        cmsfn.ancestors(pageNode).forEach(function (anchestorPage) {
            res += '<span><a href="' + cmsfn.link(anchestorPage) + '">' + evalBreadcrumbItitle(anchestorPage) + '</a></span><span>&gt;&gt;</span>';
        });
        res += '<span>' + evalBreadcrumbItitle(pageNode) + '</span>';
        return res;
    };
 
    function evalBreadcrumbItitle(pageNode) {
        return (pageNode.navigationTitle) ? pageNode.navigationTitle : (pageNode.title) ? pageNode.title : pageNode["@name"];
    }//-
The method #renderBreadcrumbs expects a page node as the argument.

Here is the method call within a page template script:

<div class="breadcrumbs">
  ${model.renderBreadcrumbs(content)}
</div>

Creating a custom form processor with JavaScript

JavaScript can also be used for processing forms created by Magnolia's Form Module (see also Form module creating a custom form processor). Below is the form definition and the JavaScript based form processor for a form in order to create a new contact in the contacts workspace. 

The complete example is available on bitbucket.

YAML definition of the form component:

/javascript-model-samples/raw/templates/components/form.yaml
i18nBasename: info.magnolia.module.form.messages
dialog: form:form
description: paragraph.form.description
modelClass: info.magnolia.module.form.templates.components.FormModel
renderType: freemarker
title: Form with Javascript Form Processor
templateScript: /form/components/form.ftl
class: info.magnolia.module.form.templates.components.FormParagraph
formProcessors:
  - name: saveContact
    enabled: true
    class: info.magnolia.module.jsmodels.form.JavascriptFormProcessor
    formProcessorScriptPath: /javascript-model-samples/templates/js/formProcessors/saveContact.js
parameters:
  formEnctype: multipart/form-data
areas:
  fieldsets:
    description: areas.components.form.fieldsets.description
    type: list
    title: areas.components.form.fieldsets.title
    enabled: true
    templateScript: /form/generic/listArea.ftl
    availableComponents:
      formGroupFields:
        id: form:components/formGroupFields
Notes:

  • Line 12: The class property must have the value info.magnolia.module.jsmodels.form.JavascriptFormProcessor , this definition class enables the usage of a JavaScript based form processor (whose path is defined on the next line).
  • Line 13: The formProcessorScriptPath property contains the path to the JavaScript of the processor.

JavaScript-based form processor:

/javascript-model-samples/raw/templates/js/formProcessors/saveContact.js
/**
 * Form processor that creates new contact in contacts workspace.
 * @constructor
 */
var Processor = function () {

    this.process = function (content, parameters) {
        this._saveContact(parameters, ctx.getPostedForm().getDocument("photo"))
    }

    /**
     * Formats contact name.
     *
     * @param firstName
     * @param lastName
     * @returns {string}
     * @private
     */
    this._contactName = function (firstName, lastName) {
        var contactName = firstName.substring(0, 1) + lastName.replace(/ /g,"");
        return contactName.toLowerCase();
    }

    /**
     * Creates resource node under contact containing the uploaded photo.
     *
     * @param contact
     * @param document
     * @private
     */
    this._createBinary = function (contact, document) {
        var resource = contact.addNode("photo", "mgnl:resource");
        resource.setProperty("fileName", document.getFileName());
        resource.setProperty("extension", document.getExtension());
        resource.setProperty("jcr:mimeType", document.getType());
        resource.setProperty("jcr:data", document.getStream());
    }

    /**
     * Saves contact into contacts workspace.
     * This method does not check for illegal characters or if generated name is unique - for sake of simplicity.
     *
     * @param values
     * @param document
     * @returns {boolean}
     * @private
     */
    this._saveContact = function (values, document) {
        try {
            var contact = ctx.getJCRSession("contacts").getRootNode().addNode(this._contactName(values.firstName, values.lastName), "mgnl:contact");
            contact.setProperty("firstName", values.firstName);
            contact.setProperty("lastName", values.lastName);
            contact.setProperty("organizationName", values.organization);
            contact.setProperty("email", values.email);
            this._createBinary(contact, document);
            contact.getSession().save();
            return true;
        } catch (e) {
            return false;
        }
    };

}

new Processor();

Loading scripts to the model

You can load other JavaScript files in the Javascript model file. This is handy when reusing JavaScript classes in several models. There are two possibilites to load a script. In both cases, load the script at the top of model file.

Loading a script as a Magnolia resource with loadScript

We recommend using loadScript if the JavaScript is a Magnolia resource. It can then load it independently of its origin. When referencing the script, the path is absolute and has to start with the module name.

Example:

loadScript("/javascript-model-samples/templates/js/utils.js");

Loading a script from the filesystem with load

A Nashorn built-in feature, load can be used to load a script with http . When using it for files of Magnolia modules, both the JavaScript model file and the file to be included must be available on the filesystem. Combine it with the built-in __PATH__ variable,  which is the absolute path to the parent directory of the current model file.

Examples:

load(__PATH__ + "/../js/utils.js");
load("https://cdnjs.cloudflare.com/ajax/libs/validate.js/0.11.1/validate.min.js");

A complete example

Below is a complete example showing how to use the included script.

Example:

The file to be loaded:

var Utils = function () {

    this.formatDate = function (calendarObject) {
        var date = new Date(calendarObject.getTimeInMillis());
        var datestring = date.getFullYear() + "-" +
            ("0" + (date.getMonth() + 1)).slice(-2) + "-" +
            ("0" + date.getDate()).slice(-2) + "-" +
            ("0" + date.getHours()).slice(-2) + ":" +
            ("0" + date.getMinutes()).slice(-2);

        return datestring;
    }
}
The model which is loading and using the above script: 
loadScript("/javascript-model-samples/templates/js/utils.js");

var MyModel = function () {
    var utils = new Utils();

    this.getPageInfo = function (pageNode) {
        var pageCreator = pageNode["mgnl:createdBy"];
        var modificationDate = utils.formatDate(pageNode["mgnl:lastModified"]);
        return i18n.translate("javascript-model-samples.frontend.footer.pageInfo.label", pageCreator, modificationDate);
    };

};
new MyModel();

  • Line 1: Load the external script with a JavaScript class.
  • Line 4: Create an instance of the external script's class.
  • Line 8: Use the instance method of the external class.

Exposing other components

You can expose any other component in the same way as enabling the templating functions, see JavaScript Models module - Exposing components. A component is a Java class (or an interface for which a type mapping exists within the framework) having a default constructor that can be resolved and instantiated by the IoC framework.

(warning) This is a very powerful mechanism and should be used carefully. Fortunately, its power can be limited by restricting access to the Java API.

Restricting access to the Java API

The module provides two ways to limit the "power" of a JavaScript model.

Disabling Nashorn extensions and Java syntax extensions

There are two Nashorn engine options to limit the power: 

  • --no-java turns off Java specific syntax extensions like "Java", "Packages", etc.
  • --no-syntax-extensions makes sure that only standard ECMAScript syntax is supported, disabling the Nashorn extensions.

Using class filter to exclude instantiation of some Java classes

To interpret JavaScript, Nashorn creates a compiled version of a JS model. When the script is executed, Nashorn instantiates Java classes. With a class filter you can exclude Java classes which you do not want to be instantiated. The class filter must implement  dk.nashorn.api.scripting.ClassFilter  .

The filter is applied when you explicitly use the types in the Nashorn code, for instance, with the expression Java.type("myType") . The filter is not applied when you assign an object to a Nashorn variable without explicitly mentioning the type.

If the filter prevents the usage of a class – technically speaking, if the filter returns false – Nashorn then handles this case as a java.lang.ClassNotFoundException which leads to a java.lang.RuntimeException .

Example:

Java class filter code:Nashorn code:

this.test = function () {
    var sytemTime = Java.type("java.lang.System").currentTimeMillis();
    return sytemTime;
};

this.test2 = function (myObject) {
    if (myObject instanceof Java.type("java.util.GregorianCalendar")) {
        return utils.formatDate(myObject)
    }else{
        return myObject;
    }
};
this.test3 = function (workspaceName) {
    var jcrSession = ctx.getJCRSession(workspaceName);
    return
};

  • The filter is applied when calling the #test and #test2 methods (see lines 2, 7 in the Nashorn code above).
  • The filter is not applied when calling the #test3 method, although on line 14 Nashorn assigns an object of the type javax.jcr.Session.
#trackbackRdf ($trackbackUtils.getContentIdentifier($page) $page.title $trackbackUtils.getPingUrl($page))