Custom Modules

Overview

With custom modules it’s possible for an external agency/developer to create a personalized module that would be fully integrated into the Styla Editor. This means that content will be manageable via the Editor UI just like a core module. It will thus be possible to create a complete custom DOM structure with custom styling and even custom Javascript.

Custom Modules Definition

Any Custom Modules definition needs to be stored in the domain configuration. This is done in the Organization Manager.

Sample custom modules definition in the Organization Manager

A single domain can have an unlimited number of Custom Modules definition. Each definition is a row in the customModules array.
Each module requires the following information:

type This is the so-called “slug” of the module. It will be used as a reference when a module is stored in the Styla DB. Therefore it needs to be a unique identifier (cannot be the same as another module for the same domain) and cannot contain spaces or special characters. The slug is case-insensitive, and will anyway be converted to all uppercase by our system. We will also automatically prepend your domain name followed by a dot.
E.g. my-domain.PRODUCT.
title This is what will be shown in the Editor UI, e.g. when dragging and dropping a new module. This can be a descriptive name and can contain spaces.
schema A URL pointing to a JSON file containing the custom module’s data structure definition (*).
template A URL to a handlebars template file that will be used for rendering the DOM structure of the module (*).
js [Optional] A URL pointing to a javascript file that would be executed in order to extend the functionality of the module (*).

(*): The files you provide can be hosted anywhere, for example on an S3 bucket or on a customer’s server. It is important to make sure that the files remain accessible by the Styla Editor and your publishing domain by setting the appropriate CORS response headers in the chosen hosting. In addition, If you expect a high traffic on your site, we recommend using a CDN for distributing these assets.

Schema

The edit schema file contains the definition of the dynamic data of the module and the corresponding UI for editing it in the module edit panel.
In order to define which fields will be available (with a related fields definition) we use the React JSON Schema Form specification (https://github.com/rjsf-team/react-jsonschema-form).

Since the Editor UI provides different tabs for managing content, we support 2 main blocks as children of editSchema:

content This is what will be rendered in the content tab. Here is where you define the fields that will be used by the content manager to create content (e.g. headlines or paragraph text) required
settings This is what will be rendered in the settings tab. Fields like “number of products per view” or “module padding” usually belong in this tab optional (if missing, the Settings tab won’t be available)

Each of these blocks (content and settings) support the following fields as children:

data This is the data definition. Here you will define what the fields are called, what type of field they are, what their default values are, etc. required
ui This is the UI definition. For each data element defined in the data block you will be able to specify which UI element should be used for rendering. optional (if missing, the default UI element for field type will be used)

Example

If we want to create a very simple module where we can add a headline and then have a slider control to set the font-size, we can write it like this:

{
    "content": {
        "data": {
            "properties": {
                "headerTitle": {
                    "title": "Title",
                    "placeholder": "Enter title here",
                    "type": "string"
                }
            },
            "type": "object"
        }
    },
    "settings": {
        "data": {
            "properties": {
                "fontSize": {
                    "title": "Font size",
                    "type": "number",
                    "default": 12,
                    "minimum": 9,
                    "maximum": 24
                }
            }
        },
        "ui": {
            "fontSize": {
                "ui:field": "SliderField",
                "ui:options": {
                    "marks": {
                        "9": "9",
                        "12": "12",
                        "14": "14",
                        "20": "20",
                        "24": "24"
                    },
                    "step": 1
                }
            }
        }
    }
}

As you can see we don’t have a UI block for headerTitle, inside the content object. This is because we just want to render a standard text input, so there’s no need to declare a specific UI element for it. We do that with fontSize, however, since we want to use SliderField and also specify which steps are allowed. Default, maximum and minimum, on the other hand, are defined in the data part.

SliderField is one of the UI widgets provided by Styla. Here is the full list:

UI field name Description Data Type
sliderField Shows a draggable slider for selecting a number value inside a given range. Supports a “step” parameter and only allowing a specific set of values as well. number
LinkField Shows the enriched link field, with support for selection of internal vs. external and the style selection dropdown Object

{
 link: string,
 text: string,
 stylePreset: string
}
				
RichTextField Shows the Rich Text Editor field. Object, using the Quill delta specifications: https://quilljs.com/docs/delta/
ImageField Image Uploader Object

{
 url: string,
 width: number,
 height: number 
}
				
InputField A regular input field. As mentioned before, will be used as default for strings so it’s not necessary to explicitly set it string

Arrays

We support arrays as well, which is very useful when we need a repeater field (e.g. an image gallery module). To define an array use this structure:

In data:
{
    "images": {
        "title": "Gallery Images",
        "type": "array",
        "items": {
            "type": "object",
            "properties": {
                "isShown": {
                    "title": "Is image shown",
                    "type": "boolean",
                    "default": true
                },
                "image": {
                    "title": "Image",
                    "type": "object"
                }
            }
        }
    }
}


In content:
{
    "ui": {
        "images": {
            "items": {
                "image": {
                    "ui:field": "ImageField"
                }
            }
        }
    }
}

This will render a repeater field where the user can upload an unlimited number of images, order them and they will also be able to toggle visibility, using the isShown field. Please note that we didn’t explicitly set a UI type for this switch. This is not necessary, as by default all boolean fields will be rendered as UI toggle buttons. We did, however, specify ImageField as a field for the image, in order to use the image uploader.


Dropdowns

We support dropdowns as well. In this case it is enough to just define the field as an enum:

"imageSize": {
    "title": "Image Size",
    "placeholder": "Select Image Size",
    "enum": [
        "small",
        "medium",
        "large"
    ]
}

This will automatically be rendered as a dropdown selection with the 3 options.

Template

This is where the HTML structure of the module is defined. To allow dynamic capabilities we support Handlebars syntax ( https://handlebarsjs.com/ ).
By using the Handlebars variables you will be able to access every piece of information that was created through the editor UI.

For example, to access the variable called “headerTitle”:

<div>{{content.headerTitle}}</div>

In a similar way you can access something that was defined inside settings:

<div style=”padding: {{settings.paddingSize}}”>hello</div>

This is of course a very simple example, but even complex layouts can be achieved by using the Handlebars capabilities, such as iterations, conditional rendering, etc.
For this we suggest taking a look at their official documentation:
https://handlebarsjs.com/guide/builtin-helpers.html

Updating this file will automatically affect the associated module in all the active pages where the module is used. Always keep this in mind when modifying the template.

JS

If your module requires some dynamic functionality that can only be achieved with Javascript, you can develop a script that will be executed right after your custom module is mounted.

The JS snippet needs to be a function that will accept one parameter, which will be the custom module main wrapping DOM element. This will be useful in order to be able to target the correct element without having to rely on id or class names. This will additionally allow the safe use of multiple instances of the module on the same page without causing conflicts.
In addition to that we can as well return a cleanup function, which will be called when the element is unmounted. This can be useful e.g. for removing click listeners etc.

This is a very basic example:

function ( wrapperElem ) {
    console.log( 'hello', wrapperElem );

    return function () {
        console.log( 'this is a cleanup function' );
    }
} 

The cleanup function is optional and can be omitted.

Full Example

In this example we are going to create a “Social Sharing” module:

  • The final result is a Social Sharing element, where the final user can click on a specific service and share the current page on the selected Social Media platform
  • Content creators need to be able to upload images with the Social Media logos. In addition they need to be able to rearrange the items, type a title which will be shown on mouseover, disable an element without deleting it.
  • Content creators need to be able to set a title for the whole module
  • Content creators need to be able to adjust the spacing between the icons and the icon size via a simple UI


For the Video Demo of the final result click here: https://monosnap.com/file/J7466WoiTDSEYyQy6Z2whFcXM0lvDD

Data Structure (Schema)

We will first define a standard text field which we will use as a call to action phrase naming it headerTitle. In addition to this we will have a repeater field which we will call icons. Each of these elements will contain the following data:

  • isShown: a boolean to hide/show the icon
  • image: the icon image. We will use the Styla image uploader for this.
  • title: the title that will be shown on mouseover on the icon
  • identifier: an identifier of the type of Social Network. This will be used by the JS file in order to determine how the destination URL will be constructed.


We also want to support a couple of settings:

  • iconsSize: the size of the icons in pixels
  • iconsPadding: the spacing between icons in pixels


For these 2 settings we want to use the SliderField widget and also set some predefined values on the slider. 

The final content of schema.json will therefore look like this:

{
    "data": {
        "properties": {
            "headerTitle": {
                "title": "Action description",
                "placeholder": "Example: Share now",
                "type": "string"
            },
            "icons": {
                "title": "Social icons",
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "isShown": {
                            "title": "Is shown",
                            "type": "boolean",
                            "default": true
                        },
                        "image": {
                            "title": "Image",
                            "placeholder": "enter the image URL here...",
                            "type": "object"
                        },
                        "title": {
                            "title": "Title",
                            "placeholder": "enter a Title here...",
                            "type": "string"
                        },
                        "identifier": {
                            "title": "Service Name",
                            "placeholder": "select a service from the list",
                            "enum": [
                                "facebook",
                                "linkedin",
                                "pinterest",
                                "twitter",
                                "whatsapp"
                            ]
                        }
                    }
                }
            }
        },
        "type": "object"
    },
    "ui": {
        "icons": {
            "items": {
                "image": {
                    "ui:field": "ImageField"
                }
            }
        }
    }
},
"settings": {
    "data": {
        "properties": {
            "iconsSize": {
                "title": "Icons size",
                "type": "number",
                "default": 50,
                "minimum": 25,
                "maximum": 100
            },
            "iconsPadding": {
                "title": "Space between icons",
                "type": "number",
                "default": 16,
                "minimum": 1,
                "maximum": 32
            }
        }
    },
    "ui": {
        "iconsSize": {
            "ui:field": "SliderField",
            "ui:options": {
                "marks": {
                    "25": "25",
                    "50": "50",
                    "75": "75",
                    "100": "100"
                },
                "step": 5
            }
        },
        "iconsPadding": {
            "ui:field": "SliderField",
            "ui:options": {
                "marks": {
                    "1": "1",
                    "4": "4",
                    "8": "8",
                    "12": "12",
                    "16": "16",
                    "20": "20",
                    "24": "24",
                    "32": "32"
                },
                "step": 1
            }
        }
    }
}

Once we have this we can already store it in a file, upload it in a web host (or S3 bucket) and then create the Custom Module configuration in the Styla Organization Manager:

{
    "customModules": [
        {
            "type": "SOCIAL_SHARING",
            "title": "Social Sharing Module",
            "schema": "https://custom-url.com/schema.json"
        }
    ]
}

After this, the module will already be available for use within the Editor. Of course there will not be any preview (since the template part is still missing) but we can already test the Editor part and make sure that it looks as we intended.

After populating the module with dummy data we can even check the generated JSON (by clicking on Edit JSON on the bottom left of the module) to double-check the data structure. This can be particularly useful when creating the handlebars template.

HandleBars Template (Template)

As mentioned above, the single icon is a repeater field, since the user can add an unlimited number of these items. Therefore we need to rely on the iteration functionality provided by Handlebars. In addition to that, we also want to access the settings from each element of this iteration. For this we will use the shortcut @root.

The final template will look like this:

<style>

   :not(#\20) .stylaApp .socialSharing {

       text-align: center;

   }


   :not(#\20) .stylaApp .socialSharing>span {

       display: block;

       font-size: 20px;

       font-weight: bold;

       font-family: Helvetica Neue;

       margin-bottom: 20px;

   }


   :not(#\20) .stylaApp .socialSharing>a {

       text-decoration: none;

   }

</style>

<div class="socialSharing">

   <span>{{ content.headerTitle }}</span>


   {{#each content.icons}}

       {{#if isShown}}

           <a data-service-name="{{identifier}}" href="#">

               <img

                   style="padding-left: {{ @root.settings.iconsPadding }}px; padding-right: {{ @root.settings.iconsPadding }}px; width: {{ @root.settings.iconsSize }}px; height: {{ @root.settings.iconsSize }}px;"

                   src="{{ image.url }}"

                   alt="{{ image.alt }}"

                   title="{{ title }}"

              />

           </a>

       {{/if}}

   {{/each}}

             

</div>

You will probably notice this snippet in the <style> block:

   :not(#\20) .stylaApp


This is needed in order to obtain the correct level of specificity without incurring the Styla CSS resetting logic.

We also used conditional rendering to show/hide the icon based on the value of isShown.

As you can see the field called identifier is output with an attribute called data-service-name. This will be used by the JS to construct the destination URL.

Once this is done we can save this in a .handlebars file and add it to our domain configuration in the Organization Manager.

{
    "customModules": [
        {
            "type": "SOCIAL_SHARING",
            "title": "Social Sharing Module",
            "schema": "https://custom-url.com/schema.json"
            "template": "https://custom-url.com/template.handlebars"
        }
    ]
}

Javascript (JS)

The module is now rendered in the preview but we don’t have any functionality. What we want is to open the correct destination link for sharing, and we also want to pass the current location to this URL. We will do this in Javascript.

As mentioned above, our function will receive the module DOM element as a parameter (wrapperElem). We will use this information to iterate through every child <a> element (the single icon) and append a click listener to it. Then, on click, we will read the data-service-name attribute in order to understand which is the type of service. Finally we will construct the destination URL based on the service and open the link. The document location and the meta information will be scraped directly from the page via document.querySelector:

function(wrapperElem) {
    function linkOpener(event) {
        var serviceName = event.currentTarget.getAttribute("data-service-name");
        var url = escape(document.location.href);
        var title = escape(document.title);
        var description = document.querySelector("meta[name='description']") && escape(document.querySelector("meta[name='description']").getAttribute("content"));
        var destinationLink = "";
        if (serviceName === "facebook") {
            destinationLink = "https://facebook.com/share.php?u=" + url;
        }
        if (serviceName === "pinterest") {
            destinationLink = "https://pinterest.com/pin/create/button/?url=" + url + "&amp;description=" + title;
        }
        if (serviceName === "twitter") {
            destinationLink = "https://twitter.com/share?url=" + url + "&amp;text=" + title;
        }
        if (serviceName === "linkedin") {
            destinationLink = "http://www.linkedin.com/shareArticle?mini=true&url=" + url + "&title=" + title;
        }
        if (serviceName === "whatsapp") {
            destinationLink = "https://api.whatsapp.com/send?text=" + url;
        }
        window.open(destinationLink, "_blank");
    }
    var links = wrapperElem.querySelectorAll("div a");
    for (var i = 0; i < links.length; i++) {
        var children = links[i].addEventListener("click", linkOpener);
    }
}

We can now store this script in a .js file and update our domain configuration in the Organization Manager:

{
    "customModules": [
        {
            "type": "SOCIAL_SHARING",
            "title": "Social Sharing Module",
            "schema": "https://custom-url.com/schema.json"
            "template": "https://custom-url.com/template.handlebars"
            "js": "https://custom-url.com/script.js"
        }
    ]
}