Table of Contents
Last updated: 2024-06-26

Web extensions


Web module


ShareAspace web can be extended with custom web UI functionality in three different ways:

  • Custom ShareAspace web module.
  • Custom create and edit forms.
  • Custom web controls in OOTB ShareAspace forms.

Extensions are loaded as within IFRAMES inside ShareAspace web.

Module configuration

In order to setup a custom ShareAspace web module a a configuration object must be setup in the clients.options array of the ShareAspace Space template.

There are a couple of different options when configuring this module.

  • "Plain module" - The full module canvas can be utilized by the custom web module.
  • Module with OOTB action ribbon and/or OOTB read view - The canvas of the custom web module is framed by a ShareAspace action ribbon at the top and/or a ShareAspace read view to the right.

The custom module can operate in two modes.

  • Input dependent - The module requires an input object and will not be available as a tile icon or as an option in the user's module menu list.
  • Non input dependent - The module can appear as a tile and will appear the the user's module menu list. It is still possible to configure the module to accept input objects in this mode.
Tip

It is possible to configure a name-value parameter list on the module. These parameters will be passed to the extension module once loaded.

Example: Input dependent module

  • Accepting Document SoftType as input.
  • Requires a registered extension on the collection level with id ExtensionPlain.
{
    "$id": "ExtensionPlain",
    "$moduleRef": "Extension",
    "prioritized": false,
    "settings": {
      "acceptTypes": "[{\"order\":0,\"acceptType\":\"Document\"}]",
      "documentationRef": "https://docs.shareaspace.com",
      "endpoint": "ExtensionPlain",
      "inputDependent": "True",
      "parameters": "[]",
      "title": "Extension plain"
    }
}

Example: Non input dependent module with OOTB action ribbon and read view

  • Accepting Document SoftType as input.
  • Activates ShareAspace action ribbon for selected object. Will have an Edit action requiring the selected object to have an edit view with id dress.
  • Activates ShareAspace read view for selected object. Requires selected object to have a configured read view with id read.
  • Requires a registered extension on the collection level with id Extension.
{
    "$id": "Extension",
    "$moduleRef": "Extension",
    "prioritized": false,
    "settings": {
      "acceptTypes": "[{\"order\":0,\"acceptType\":\"Document\"}]",
      "actions": "[{\"order\":0,\"title\":\"Edit\",\"type\":\"update\",\"view\":\"dress\"}]",
      "activatedFeatures": {
        "outputView": true,
        "ribbon": true
      },
      "baseTypes": "Document",
      "documentationRef": "https://docs.shareaspace.com",
      "endpoint": "Extension",
      "externalActions": "[]",
      "externalMultiActions": "[]",
      "externalStaticActions": "[]",
      "inputDependent": "True",
      "multiActions": "[]",
      "outputViews": "[{\"order\":0,\"title\":\"Read\",\"showEmptyValue\":false,\"view\":\"read\"}]",
      "parameters": "[]",
      "staticActions": "[]",
      "title": "Extension"
    }
}

Register extension

In order for custom modules to be loaded they must be registered as extensions on the collection level. See documentation.

Module extensions can be registered as part of the metadata exchange for Nova extensions or registered as either External endpoint or Trusted external endpoint.

If user impersonation is required (having a JSON Web Token, JWT for the current user being handed over to the module) the extension must be registered as a Trusted external endpoint.

When registering an external module, Category should be set to Client and Method should be set to GET.

Startup

Once a module has been loaded in ShareAspace web the web application will send a message to the extension.

window.addEventListener("message", (event) => {
    var data = event.data;

    if (data.type === "setup") {
      route = data.data.apiUrl;
      extensionId = data.data.extensionId;
    }
  });

The name of the initial setup message is typed as setup using the contract described below.

type ExtensionModuleSetupEvent = {
  type: 'setup'
  data: {
    /** The identifier of the extension we are expecting to run. */
    extensionId: string

    /** The id of the space currently in context. */
    spaceId: string

    /** The API URL to communicate with the ShareAspace backend. */
    apiUrl: string

    /**
     * The unique identifier of the iframe this is need when communicating
     * back to ShareAspace.
     **/
    frameId: string

    /**
     * Access token that must be used to communicate with the ShareAspace API.
     * This property can be undefined if impersonate is not defined.
     */
    accessToken?: string

    /**
     * The current ShareAspace information filter encoded as a string.
     */
    informationFilter?: string

    /**
     * One to many ObjectId's to start from, can also be undefined if there were not
     * input objects.
     */
    inputObjects?: Array<string>

    /**
     * These are the parameters defined in the extension configuration.
     * Can be undefined if there is no parameters defined in the configuration.
     */
    parameters?: Record<string, string>
  }
}

Communication from ShareAspace to extension

There are two more message types that ShareAspace Web will send to loaded extensions.

Object altered

ShareAspace web will send a object-altered message to the extension if an action taken from the module action ribbon has updated one or more SoftType instances.

type ObjectAlteredEvent {
  type: 'object-altered'
  data: {
      /**
       * An array with ObjectId's for altered SoftType instances.
       */
      selectedObjects: Array<{
          $oid: string
      }>
  }}

Refresh

ShareAspace web will send a refresh message to the extension if a user has clicked the refresh button on the module tab in ShareAspace web.

type RefreshEvent {
  type: 'refresh'
}

Communication from extension to ShareAspace

An extension can communicate back to ShareAspace by posting message to its parent (i.e. ShareAspace web).

window.parent.postMessage(
      {
        type: "...",
        data: {
          ...
        },
      },
      apiUrl // The ShareAspace API url provided by the setup message.
    );

Selection changed

Selection changed would typically be used when a user has selected a new SoftType instance within the module. By sending the selection-changed message ShareAspace web can (if the features are active in the configuration) switch the context object(s) for the action ribbon and the read view.

type SelectionChangedEvent = {
  type: 'selection-changed'
  data: {
    /**
     * The frame id for the extension, provided by the setup message.
     */
    frameId: string
    /**
     * An array with ObjectId's for the selected object(s)
     */
    selectedObjects: Array<{
      $oid: string
    }>
  }
}

Open in

Open in lets the extension module tell ShareAspace web to open another module in ShareAspace web for a provided SoftType instance.

type OpenInEvent = {
    /**
     * One to many ObjectId(s) of SoftType instances to be passed into the module to be opened.
     */
    inputObject: string | string[];
    /**
     * The SoftType id of the SoftType instance to be passed into the module to be opened.
     */
    softTypeId: string;
    /**
     * The id of the extension requesting a new module to be opened.
     */
    extensionId: string;
    /**
     * The id of the module to be opened.
     */
    moduleId: string;
};

Web form


The custom web form allows for a configuration where it is possible to have complete control of the rendering of:

  • a ShareAspace edit form.
  • a ShareAspace read form (modal).
  • a ShareAspace external action form.

ShareAspace will rely on the extension to provide:

  • Form validation information in order to control whether the submit button should be active or not.
  • A payload to be:
    • Passed as a POST message for a ShareAspace SoftType instance update.
    • Passed as a payload to a custom external extension.

Configuration

In order to setup a custom ShareAspace web form an externalStaticAction must be configured on a module configuration under the clients.options array of the ShareAspace Space template.

{
    "$id": "ModuleId",
    "$moduleRef": "Generic",
    "prioritized": true,
    "settings": {
    ...
    "externalStaticActions": "[{\"order\":0,\"title\":\"External edit\",\"viewExtensionId\":\"ExternalExternal\",\"viewType\":\"Submit\",\"submitButtonText\":\"Save\",\"\":\"Cancel\"}]",
    ...
    }
}
Parameter Type Description
order number Starts at 0. Controls the ordering of configured ribbon actions.
title string Name of ribbon action button.
viewExtensionId string Id of the form extension to be loaded.
viewType string Allowed values Submit/Read. Controls if Submit and Cancel buttons should be rendered.
submitButtonText string Submit button text.
cancelButtonText string Cancel button text.
actionExtensionId string Id of registered extension that hosts the form.
titleIconPath string Path an image that will be displayed before the title of the modal form that opens when clicking the action.
iconRelativePath string Icon to be used for the ribbon action button.
hideSubmitButton boolean Hides submit button if set to true.

Startup

Once a form has been loaded in ShareAspace web the web application will send a message to the form extension. ShareAspace web expects a setup-response message back (more information to follow).

The name of the initial setup message is typed as setup using the contract described below.

type ExternalFormSetupEvent = {
  type: 'setup'
  data: {
    /** The id of the space currently in context. */
    spaceId: string

    /** The API URL to communicate with the ShareAspace backend. */
    apiUrl: string

    /**
     * The unique identifier of the iframe this is need when communicating
     * back to ShareAspace.
     **/
    frameId: string

    /**
     * Access token that must be used to communicate with the ShareAspace API.
     * This property can be undefined if impersonate is not defined.
     */
    accessToken?: string

    /**
     * ObjectId of the object selected when calling the external form.
     */
    selectedObjectId: string

    /**
     * If there is a object in context when calling the external form this is the ObjectId
     * of this object. If there is no object in context this is undefined.
     */
    contextObjectId?: string

    /**
     * The if an entry of a context object is selected this is the id of that entry.
     * If there is no entry selected this is undefined.
     */
    entryId?: string
  }
}

Communication from ShareAspace to extension

When ShareAspace web expects the resulting payload from the form ShareAspace web will pass a get-payload message to the extension form. ShareAspace expects a get-payload-response message back from the extension (details in the following section).

Get payload

type GetPayloadEvent = {
  type: 'get-payload'
}

Communication from extension to ShareAspace

The form can communicate needed height real estate back to ShareAspace web using the ifram-height message.

IFRAME height

type IFrameHeightEvent {
    type: 'iframe-height',
    data: {
        /**
         * The required pixel height of the custom form (before scrolling is enabled).
         */
        height: string|number
    }
}

Setup response

The response to the initial setup message.

type SetupResponseEvent {
    type: 'setup-response',
    data: {
        /**
         * The frame id for the extension, provided by the setup message.
         */
        frameId: string
        /**
         * Describes if user input in the external form as fulfilled all requirements (will enable submit button if set to true).
         */
        isValid: boolean,
        /**
         * The resulting form payload. Will be passed by ShareAspace web to the ShareAspace REST API or to the registered external extension (acting on the submit action).
         */
        payload: any
    }
}

Payload response

The response to the get-payload message.

type GetPayloadResponseEvent {
    type: 'get-payload-response',
    data: {
        frameId: string
        isValid: boolean,
        payload: any
    }
}

Validation

Validation can be used by the external form to keep ShareAspace up-to-date with user input changes in the form, e.g. a user has taken action to/from valid form input requirements.

type ValidChangedEvent {
    type: 'valid-changed',
    data: {
        isValid: boolean
    }
}

Web form control


A web custom web form control can be configured within a ShareAspace web view configuration. The control is bound to a port on the input/output schema of the view. The web form control can message ShareAspace in order to:

  • State if the user input has validated correctly.
  • Provide ShareAspace with the user input.
  • Notify ShareAspace if a user has altered a value.
  • Control the required height of the custom control within the view.

The ShareAspace form can aske the extension control for its value.

Startup

type ExternalContentSetupEvent = {
  type: 'setup'
  data: {
    /** The id of the space currently in context. */
    spaceId: string

    /** The API URL to communicate with the ShareAspace backend. */
    apiUrl: string

    /**
     * The unique identifier of the iframe this is need when communicating
     * back to ShareAspace.
     **/
    frameId: string

    /**
     * Access token that must be used to communicate with the ShareAspace API.
     * This property can be undefined if impersonate is not defined.
     */
    accessToken?: string

    /**
     * The ObjectId of the content configured to be displayed in external content.
     */
    objectId: string
  }
}

Communication from ShareAspace to extension

When ShareAspace web expects the resulting payload from the form ShareAspace web will pass a get-payload message to the extension form. ShareAspace expects a get-payload-response message back from the extension (details in the following section).

Get payload

type GetPayloadEvent = {
  type: 'get-payload'
}

Communication from extension to ShareAspace

Resize IFRAME

The form can communicate needed height real estate back to ShareAspace web using the resize-iframe message.

type ResizeIFrameEvent {
  type: 'resize-iframe',
  data: {
      height: string,
      frameId: string,
      visible: string,
  }
}

Setup response

The response to the initial setup message.

type SetupResponseEvent {
    type: 'setup-response',
    data: {
        /**
         * The frame id for the extension, provided by the setup message.
         */
        frameId: string
        /**
         * Describes if user input in the external form as fulfilled all requirements (will enable submit button if set to true).
         */
        isValid: boolean,
        /**
         * The resulting form payload. Will be passed by ShareAspace web to the ShareAspace REST API or to the registered external extension (acting on the submit action).
         */
        payload: any
    }
}

Payload response

The response to the get-payload message.

type GetPayloadResponseEvent {
    type: 'get-payload-response',
    data: {
        frameId: string
        isValid: boolean,
        payload: any
    }
}

Validation

Validation can be used by the external form to keep ShareAspace up-to-date with user input changes in the form, e.g. a user has taken action to/from valid form input requirements.

type ValidChangedEvent {
    type: 'valid-changed',
    data: {
        isValid: boolean
    }
}

Open module

type OpenModuleEvent {
  type: 'open-module',
  data: {
      inputObject: string,
      softTypeId: string,
      moduleId: string,
  }
}

Property update

If the value that the control is bound to changes its value, the form can notify ShareAspace web about this update by sending the property-update message. value can be any type and depends on the type that the control is managing.

type PropertyUpdateEvent {
  type: 'property-update',
  data: {
      frameId: string,
      value: any,
  }
}

Examples


Caution

Sample code provided is for demonstration and educational purposes only. The code is simplified in order to describe concepts and do not always follow best development practices. All production code should always follow development best practices. All sample code is supplied "AS IS" without any warranties or support.

Open in


<h3>Open in:</h3>

<div>
  Module id: <input id="moduleId" type="text" />
  Input object: <input id="inputObject" type="text" />
  SoftType id: <input id="softTypeId" type="text" />
</div>

<button onClick="openModule()">Open in module</button>

<script>
  let route = undefined;
  let extensionId = undefined;

  function openModule() {
    var payload = {
      extensionId: extensionId,
      moduleId: document.getElementById("moduleId").value,
      inputObject: document.getElementById("inputObject").value,
      softTypeId: document.getElementById("softTypeId").value,
    };
    window.parent.postMessage(payload, route);
  }

  window.addEventListener("message", (event) => {
    var data = event.data;

    if (data.type === "setup") {
      route = data.data.apiUrl;
      extensionId = data.data.extensionId;
    }
  });
</script>

Selection changed / Object altered


<h3>Send update:</h3>
<div>
  <input id="uuid" type="text" value="Paste ObjecId here..." />
  <input id="sendUuid" type="button" value="Send" />
</div>

<h3>Update received:</h3>
<div>
  <label id="updatedValue">Updates from SAs are reported here...</label>
</div>

<script type="text/javascript">
  var route = undefined;
  var frameId = undefined;

  window.addEventListener("message", (event) => {
    var data = event.data;
    if (data.type === "setup") {
      route = data.data.apiUrl;
      frameId = data.data.frameId;
    } else if (data.type === "object-altered") {
      document.getElementById("updatedValue").innerHTML =
        data.data.selectedObjects[0].$oid;
    }
  });

  document.getElementById("sendUuid").addEventListener("click", () => {
    var uuid = document.getElementById("uuid").value;
    var sasBaseUrl = route;

    window.parent.postMessage(
      {
        type: "selection-changed",
        data: {
          selectedObjects: [{ $oid: uuid }],
          frameId: frameId,
        },
      },
      sasBaseUrl
    );
  });
</script>

Form


<div>
  Id: <input id="id" type="text" /> Name: <input id="name" type="text" />
</div>

<script>
  var frameId = undefined;
  var route = undefined;

  function getPayload() {
    var output = {
      id: document.getElementById("id").value,
      name: document.getElementById("name").value,
    };

    return output;
  }

  function isValid() {
    var id = document.getElementById("id");
    var name = document.getElementById("name");

    return id.value == "foo" && name.value === "bar";
  }

  function listenToInputs() {
    document.getElementById("id").addEventListener("change", () => {
      window.parent.postMessage(
        JSON.stringify({
          type: "valid-changed",
          data: {
            isValid: isValid(),
            payload: getPayload(),
            frameId: frameId,
          },
        }),
        route
      );
    });

    document.getElementById("name").addEventListener("change", () => {
      window.parent.postMessage(
        {
          type: "valid-changed",
          data: {
            isValid: isValid(),
            payload: getPayload(),
            frameId: frameId,
          },
        },
        route
      );
    });
  }

  window.addEventListener("message", (event) => {
    var data = event.data;

    if (data.type === "setup") {
      frameId = data.data.frameId;
      route = data.data.apiUrl;

      // NOTE: We have removed the init and init-response
      let payload = {
        type: "setup-response",
        data: {
          isValid: isValid(),
          payload: getPayload(),
          frameId: frameId,
        },
      };

      window.parent.postMessage(payload, route);
      listenToInputs();
    } else if (data.type === "get-payload") {
      let payload = {
        type: "get-payload-response",
        data: {
          isValid: isValid(),
          payload: getPayload(),
          frameId: frameId,
        },
      };
      window.parent.postMessage(payload, route);
    }
  });
</script>