Overview

In the general sense, an ACE UI Widget is a user interface component that knows how to present itself in browsers by using web standards such as HTML, CSS and JavaScript. A widget exists solely in the browser, and lives inside a ACE UI application. A widget operates on one or more domain objects to read and write data. A widget can communicate with the Web Service API to read external data (such as user data, categorization taxonomies, etc). In a manner of speaking, a widget acts as the link between the user and the domain object tree, connecting user input with one or more domain objects.

In the technical sense, a widget consists of one or more files attached to a content in ACE. The widget gets loadedlazily by the UI Framework when it is required (typically when it is included in a template that the UI Framework wants to present). The widget itself is written using JavaScript, but may use HTML and CSS to provide custom markup and styles. The UI Framework makes heavy use of AngularJS when it comes to widgets. Frankly, a ACE UI widget is AngularJS directive.

Usages of a widget

Widgets are referred to in ACE UI Templates. A ACE UI template may be used in any ACE UI application, so the widget does not know where it may be used. It knows it lives inside a ACE UI application, but it does not know which one or exactly what it looks like. A widget must be constructed with this in mind, making little to no assumptions about its context. To allow easier cross-application widget development, there is a fixed set of libraries and frameworks that will always be present in a ACE UI application.

Defining a widget

Defining a widget requires you to create a content in system where the widget files are stored. When the UI Framework wants to display a specific widget, it reads widget alias from template and retrieves the widget fileds. The widget alias should be prefixed with _widget/. The widget content should be a aceUIWidgetContent type.

Files on the widget content

Once the UI Framework has built the external ID for the widget content, it goes through a two-step process in order to try to find the actual widget file(s):

  1. manifest.json: The UI Framework searches for a file called manifest.json on the widget content. The manifest file, in turn, points to one or more JavaScript files on the content. This is the recommended way of defining a widget. For detailed information on the manifest.json file, see The widget manifest file.
  2. widget.js: If no manifest.json file is found, the UI Framework falls back to searching for a file called widget.js. If your widget only consists of one JavaScript file, and it does not have any external RequireJS dependencies, this option is a simpler alternative than #1. Once he UI Framework has gotten a hold of one or more widget JavaScript files, it evaluates them and registers them as loaded. The UI Framework does this lazily for every widget that needs to be loaded.

Typically, a widget might consist of four files:

  • manifest.json: The widget manifest, contains the widget definition
  • widget.js: The widget JavaScript file
  • template.html: The AngularJS template for the widget
  • style.css: The associated styles for the widget

An example widget content.json

{
  "importerMetadata": {
    "alias": "_widget/aceUITextArea"
  },

  "system": {
    "contentType": "aceUIWidgetContent"
  },

  "operations": [
    {
      "_type": "aceAssignToViews",
      "views": ["acePublic"]
    }
  ],

  "aspects": {
    "aceFiles" : {
      "files": {
        "manifest.json": {
          "fileUri": "file:contentfiles/TextArea-manifest.json.file",
          "filePath": "manifest.json"
        },

        "style.css": {
          "fileUri": "file:contentfiles/TextArea-style.css",
          "filePath": "style.css"
        },

        "widget.js": {
          "fileUri": "file:contentfiles/TextArea-widget.js",
          "filePath": "widget.js"
        },

        "template.html": {
          "fileUri": "file:contentfiles/TextArea-template.html",
          "filePath": "template.html"
        }
      }
    },

    "aceUIWidget": {
      "name": "ACE UI Widget - TextArea"
    }
  }
}

The content import is very straight-forward. The important take-aways are:

  • The alias must be prefixed with _widget/
  • The content must be of type aceUIWidgetContent
  • There must be a manifest.json file or a widget.js file

An example widget JavaScript file

/**
 * This function call, atex.aceui.register(...), is the actual widget
 * registration which makes it available to the UI Framework.
 */
atex.aceui.register('ng-directive', 'widgetName', [], function() {

  /**
   * The return value here is nothing but an AngularJS directive factory.
   */
  return [function() {
    return {
      replace: false,
      restrict: 'AE',

      /**
       * These are scope parameters that always gets passed to widgets.
       */
      scope: {

        /**
         * The 'config' property is the corresponding 'config' object coming from the template.
         */
        'config': '=',

        /**
         * The 'baseUrl' property will point to the path where this widget is loaded from.
         * This is useful when the widget wants to include other files on the widget content
         * (such as a CSS file).
         */
        'baseUrl': '@',

        /**
         * This is the name of the field as expressed in the template.
         */
        'fieldName': '@?',

        /**
         * The 'domainObject' property is the corresponding 'domainObject' object coming
         * from the template.
         */
        'domainObject': '=',

        /**
         * The 'domainObjects' property is the corresponding 'domainObjects' object comingh
         * from the template.
         */
        'domainObjects': '=',

        /**
         * The 'widgetId' property is a unique widget ID generated for this widget instance
         * by the UI Framework.
         */
        'widgetId': '@',

        /**
         * The 'mode' property is used let to the widget behave differently depending on what
         * mode the application is using for the current content editor.
         *
         * Typical modes might be 'edit' and 'view'.
         */
        'mode': '@'
      },

      /**
       * atex.aceui.baseUrl will point to the path where this widget is loaded from.
       * In this case, 'template.html' is just another file on the widget content.
       */
      templateUrl: atex.aceui.baseUrl + '/template.html',

      controller: function($scope) {
        // ...
      },

      link: function(scope, element, attrs) {
        // ...
      }
    };
  }];
});

The widget JavaScript file must make a function call to atex.aceui.register. This function call is what actually registers the widget in the UI Framework. When registering widgets, these are the supported paramaters to atex .aceui.register():

  1. String: When registering a widget, this must be 'ng-directive'
  2. String: The name of your widget (the AngularJS directive name, so the same rules apply here)
  3. Array of strings: RequireJS dependencies for the widget
  4. Function: The widget registering function(the return value of this function must be an AngularJS directive factory) . If the widget has specified RequireJS dependencies in the previous argument, this function will be called with arguments that match the dependencies.

Using AngularJS dependency injection in a widget

Since widgets in the UI Framework are AngularJS directives, they have access to the dependency injection mechanism of AngularJS. This, for example, allows a widget to specify that it wants to use the AngularJS service ContentService which is defined in the UI Framework. Using the ContentService in a widget enables it to read, write and update content in the Polopoly system.

Example

atex.aceui.register('ng-directive', 'widgetName', [], function() {

  /**
   * This is where the dependency is specified and injected by AngularJS.
   */
  return ['ContentService', function(ContentService) {
    return {
      replace: false,
      restrict: 'AE',

      scope: {
        'config': '=',
        'baseUrl': '@',
        'domainObject': '=',
        'domainObjects': '=',
        'widgetId': '@',
        'mode': '@'
      },

      controller: function($scope) {

        /**
         * A scope method that reads a content using the ContentService.
         */
        $scope.readContent = function(id) {
          ContentService.get({ id: id }).then(function(response) {
            // do something with the response...
          });
        };

      },

      link: function(scope, element, attrs) {
        // ...
      }
    };
  }];
});

For more information about AngularJS dependency injection, please refer to the [AngularJS documentation] (https://docs.angularjs.org/).

Using RequireJS to specify dependencies in a widget

The UI Framework provides a mechanism for widgets to specify JavaScript dependencies using RequireJS. This allows widgets, and the UI Framework, to be sure that its dependecies have been fully loaded before the widget code gets executed. It also allows the UI Framework to only load dependencies once, even if the same dependency is defined multiple times.

The actual dependency declaration is done in the widget manifest. However, for a dependency to actually load, the widget registration function call must include a reference to the dependency.

Example manifest.json that defines a RequireJS dependency

{
  "name": "A widget",
  "description": "A widget that requires some-library.",
  "scripts": ["widget.js"],

  "requires": [
    {
      "name": "some-library",
      "path": "lib/some-library.js"
    }
  ]
}

Example widget.js that uses a RequireJS dependency

/**
 * This is where the dependency is specified and injected by the UI Framework.
 */
atex.aceui.register('ng-directive', 'widgetName', ['some-library'], function(someLibrary) {
  return ['ContentService', function(ContentService) {
    return {
      replace: false,
      restrict: 'AE',

      scope: {
        'config': '=',
        'baseUrl': '@',
        'domainObject': '=',
        'domainObjects': '=',
        'widgetId': '@',
        'mode': '@'
      },

      controller: function($scope) {
        // ...
      },

      link: function(scope, element, attrs) {
        // ...
      }
    };
  }];
});
  • If the required library defines an AMD module, RequireJS makes sure that the dependency is only injected into the function scope of the widget registration function (the last argument to atex.aceui.register). However, if the library does not specify an AMD module, the library becomes attached to the global object (window). If possible, this is something you generally you want to avoid.

For more information about RequireJS, please refer to the RequireJS documentation.

Interacting with the Domain Object

Widgets interact with the data shuttle using one or more domain objects.

Reading data

Data is read from the data shuttle using the getData method on a domain object:

controller: function($scope) {
  ...

  // Use the domain object from the 'domainObject' property if such has been
  // configured, otherwise try to get the 'data' one from the 'domainObjects' configuration.
  $scope.domainObject = $scope.domainObject || $scope.domainObjects['data'];

  $scope.data = $scope.domainObject.getData();

  ...
}

Writing data

Data is written using the setData method on a domain object:

link: function(scope, element, attrs) {
  ...

  scope.$watch('data', function(newValue, oldValue) {
    if (newValue !== oldValue) {
      scope.domainObject.setData(newValue);
      scope.domainObject.changed();
    }
  });

  ...
}

Reacting to data changes from a domain object

Not all changes to data comes from the widget itself. In some cases, the data edited by the widget might be changed by some other widget, or by some domain object. Such changes are detected by listening to the onecms:changed event on the domain object, the widget might then decide to reset its data:

 link: function (scope, element, attrs) {
   ...

   scope.domainChangeFinalizer = scope.domainObject.on('aceui:changed', function (event, modifierId) {
     // The listener function is called with a single string parameter, modifierId, which is the ID of the object that
     // initiated the data change. For example, a widget can use this parameter to avoid reacting to changes it has made itself.

     if (modifierId !== scope.widgetId) {
       scope.data = scope.domainObject.getData();
     }
   });

   scope.$on('$destroy', function() {
     if (typeof scope.domainChangeFinalizer !== 'undefined') {
       scope.domainChangeFinalizer();
     }
   });

   ...
 }

Accessibility

Development of widgets should be done with accessibility in mind. For example it should be possible to navigate the widgets components by using the keyboard and it should make sense when a user uses screen readers working within an application. Basically it means using proper html tags to describe the different elements (interactive elements should have a label element associated etc.). The use of aria- attributes on html elements is also encouraged.

Capture focus event

Sometimes an application want to 'activate' a widget for some reason, but since the widgets internals are unknown to the application it is up to the widget itself to define what should happen when this occurs, the widget can do this by capturing the focus DOM event:

controller: function ($scope, $element) {
  $element.on('focus', function (event) {
    $element.find('#my-important-first-input').focus();
  });
}

Modes

Widgets can behave differently depending on what mode the application is currently using for the content editor. The mode is passed to the widget from the UI Framework with the mode scope parameter. If modes are not supported by the application, the default mode will be 'edit'.

Mode Usage
Edit The widgets is in edit mode and it should be possible to edit the object/objects on the widget
View The widget should be in view mode and changes to the object/objects should not be possible

The widget manifest file

Although not required, it is recommended that a widget contains a manifest file called manifest.json. The file contains a JSON object with the following properties:

Property Usage Type Presence
"name" A human-readable name of the widget String Optional
"description" A human-readable description of the widget String Optional
"scripts" References to one or more JavaScript files the widget consists of Array of strings Required
"requires" Dependencies of this widget Array of require objects Optional

A require object consists of the following properties:

Property Usage Type Presence
"name" A name for this dependency String Required
"path" Reference to a JavaScript file String Required

An annotated example of a manifest.json file

{
  "name": "CKEditor widget",                        // Human-readable name name of widget
  "description": "A rich text editor (CKEditor).",  // Human-readable description of widget
  "scripts": ["widget.js"],                         // Reference to 'widget.js' on the widget content. Files are loaded in the same way the array is arranged

  "requires": [                                     // RequireJS dependencies
    {
      "name": "ckeditor",                           // 'ckeditor' is the name for this dependency
      "path": "ckeditor/ckeditor.js"                // Reference to 'ckeditor/ckeditor.js' on the widget content
    }
  ]
}