UI Module

Objective


UI Modules are graphical interfaces that offer the ability to send and retrieve information to the services APIs. They are located in soajs.dashboard under folder ui/modules/. SOAJS UI Dashboard is built on top of AngularJS which makes it a Single Page Applications (SPAs). The modules inside the UI are built to be loaded on demand and their controllers are registered upon loading.


This space will show you:

  1. How to create a module
  2. How to install a module
  3. Module Controllers
  4. Module Service
  5. Module Directives
  6. Module Permissions
  7. Full Example



Creating a Module


Start by installing the SOAJS dashboard.

Navigate to soajs.dashboard/ui/modules/ folder

  1. Create your module by adding a folder myModule
  2. Create the installation file and name it install.js

Folder Schema:

$ cd soajs.dashboard/ui/modules/ && ls -l
# output
drwxr-xr-x  5 root  wheel   170 May 29 19:19 myModule

$ ls -l myModule
# output
drwxr-xr-x  3 root  wheel   102 May 29 19:07 directives
-rw-r--r--  1 root  wheel   484 May 29 19:19 install.js
-rw-r--r--  1 root  wheel  1335 May 29 19:17 controller.js
-rw-r--r--  1 root  wheel  1335 May 29 19:17 service.js

$ ls -l myModule/directives
# output
-rw-r--r--  1 root  wheel  116 May 29 18:37 list.tmpl





Installing a Module


Every module has an installation file install.js, that tells the main application about the location of the module's scripts, directives & services, the permissions to be applied and which menu the module is accessible from. Installing a UI module does not mean that its information is stored in the database, the UI only loads the information and adds it to its navigation granting the user access to that module.

The installation file contains an array of entries that point to the module's pages. Following the same concept of SPAs, a module should have at least One page meaning One entry in the array. The following table explains the content of one entry in the installation array:

NameTypeMandatoryDescription
idStringYESHTML Id of a module's page
labelStringYESModule page label used by the Menu(s) & the Tracker
urlStringYESModule's page link ( should always start with "#/" )
scriptsArrayYESThe location of the javascript files this page requires, each entry refers to one file
tplPathStringYESThe location of the module's page default html directive
checkPermissionObjectNOThis object contains the module's page permission, the user's ACL is matched against this entry to determine if the page is accessiblew
pillarObjectYESDefine which pillar this module belongs to as well as the position inside that pillar.
iconStringNOAn optional icon that shows up in the menu(s), given to the default page of the module.
trackerBooleanNOIf set to true, then breadcrumbs are enabled and displayed above the module
ancestorArrayNOIf tracker is set to true, displays the list of parent breadcrumbs based on this array, up to the module's page
mainMenuBooleanNOIf set to true, the module has an entry in the main menu
footerMenuBooleanNOIf set tot true, the module has an entry in the footer menu
userMenuBooleanNOIf set tot true, the module has an entry in the user menu (this menu shows up for logged in members only)
guestMenuBooleanNOIf set tot true, the module has an entry in the guest menu (this menu shows up for non logged in members only)
install.js sample
var myModuleNav =[
    {
        'id': 'myModules',
        'label': 'My Module',
        'url': '#/mymodule',
        'scripts': ['modules/myModule/controller.js'],
        'tplPath': 'modules/myModule/directives/list.tmpl',
        'checkPermission':{
            'service':'mymodule',
            'route':'/list'
        },
        'pillar': {
            'name': 'operate',
            'label': 'Operate',
            'position': 4
        },
        'icon': '',
        'mainMenu': true,
        'tracker': true,
        'ancestor': ['Home']
    }
];

//add the module to the dashboard navigation
navigation = navigation.concat(myModuleNav);

Once you create the install.js file, edit the soajs.dashboard/ui/index.html file and point out its location in the <head> tag. Refresh the page and the Dashboard UI will pick up your module's location, once the main controller starts, it will load your module.

Note

Add the entry of modules/myModule/install.js before the dashboard main controller entry app/controller.js.

<head>
    <!-- html head code goes here -->

    <!-- my module install.js file-->
    <script src="modules/myModule/install.js"></script>

    <!-- html head code goes here -->

    <!-- dashboard main controller file-->
    <script src="app/controller.js"></script>

    <!-- html head code goes here -->
</head>



Module Controllers


As mentioned above, a module is loaded on demand. The dashboard's main controller uses the $controllerProvider service of AngularJS to register the module's controllers when the module is loaded. The controller allows the module to control and apply the two way data binding between the module's scope and its html directives. Once a module's controller is registered, it gains access to the global scope of the application.

Controller Code Sample:

var myModule = soajsApp.components;
myModule.controller('myModuleCtrl', ['$scope', function($scope) {
    //module's code goes here
}]);


The module's controller and the main application's controller behave the same way; they both provide the scope and use dependency injections. You can learn more about controllers on AngularJs. Once your controller is defined, you can create your functions and implement the business logic of the module.

var myModule = soajsApp.components;
myModule.controller('myModuleCtrl', ['$scope', function($scope) {
    //module's code goes here
    $scope.listEntries = function(){
        //invokes an api and returns an array of data objects
        $scope.entries = APIresponse;
    };

    $scope.getOneEntry = function(entryId){
        //...
    };

    $scope.listEntries();
}]);





Module Service


AngularJS provides the ability to use services by injecting them in the controller as dependencies. You can either inject a native AngularJS service or a custom service you created. Similar to how you create a module controller, the dashboard's main controller uses the $provide service to offer you the ability to register your custom services with the $injector. Once registered, you can inject them as dependencies in your module's controller.

Service Code Sample

var myModuleService = soajsApp.components;
myModuleService.service('myModuleSrv', ['$timeout', '$http', function($timeout, $http){
    return {
        'getEntriesFromAPI': function(opts, callback){
            var config = {
                url: opts.url,
                method: opts.method
            };
            $http(config).success(function(response, status, headers, config) {
                $timeout(function(){
                    return callback(null, response);
                }, 500);
            }).error(function(errData, status, headers, config) {
                $timeout(function(){
                    return callback(errData);
                }, 500);
            });
        }
    };
}]);



Module Directives


Directives are markers on a DOM element that tell the HTML compiler of AngularJS to attach the behavior of your Module to the DOM element or even transform it. Consider the code above where the controller calls list entries, and the later after invoking some API returns an array of data objects representing records you want to print; use the directives to print these records.

Directive Code Sample:

<section ng-controller="myModuleCtrl">
    <ul>
        <li ng-repeat="entry in entries">
            <b>{{entry.name}}</b><br/>
            <p>{{entry.description}}</p>
        </li>
    </ul>
</section>

The above code snippet loops in the array of data fetch by listEntries function in the controller, and for each entry, it prints the name and description as instructed. Also notice that for the directive to function, you need to provide it with the name of the controller of your module (line 1).



Module Permissions


SOAJS Access Level, is applied on the service APIs. This means that Dashboard UI should read the user ACL, and display what is allowed accordingly. Because your service and your module are separate, you need to define the permissions of the module separately.

UI module permissions:

var permissions = {
    'list': ['myService', '/list'],
    'add': ['myService', '/add']
};
  1. The object keys are the names of the operations we will use in the UI interface.
  2. The first entry in the array, is the name that matches your service.
  3. The second entry in the array, is the name of the API in your service.

If the ACL provides access to myService/list, then the first entry in the above object will resolve to TRUE.

SOAJS dashboard offers the ability that compares the above permissions object with the ACL. The result is stored in the scope of the module so you can use it in your module's code or directive(s). A predefined method in the Dashboard UI named constructModulePermissions handles this task.

Parameter NameParameter OrderDescription
Scope1The UI Module Scope
Access2An Object in the scope to store the result of the ACL check
Permissions3The UI Module permissions

Example:

var permissions = {
    'list': ['myService', '/list'],
    'add': ['myService', '/add']
};

//create an entry in your module scope
$scope.access = {};

//call the method and compare the permissions with the ACL
//allowed permissions are then stored in scope.access
constructModulePermissions($scope, $scope.access, permissions);

Now that your module is aware of what is allowed, apply the ACL either in your controller's code or the directive(s) where needed.

Apply the ACL in the Controller:

var myModule = soajsApp.components;
myModule.controller('myModuleCtrl', ['$scope', function($scope) {
    var permissions = {
        'list': ['myService', '/list'],
        'add': ['myService', '/add']
    };

    //create an entry in your module scope
    $scope.access = {};

    //call the method and compare the permissions with the ACL
    //allowed permissions are then stored in scope.access
    constructModulePermissions($scope, $scope.access, permissions);

    $scope.listEntries = function(){ };

    //if scope.access.list is allowed, call listEntries
    if($scope.access.list) {
        $scope.listEntries();
    }
}]);

Apply the ACL in a directive:

<section ng-controller="myModuleCtrl">
    <!-- if scope.access.add is allowed, show button-->
    <input type="button" value="Add New Entry" ng-click="addEntry();" ng-if="access.add" class="btn btn-primary btn-sm"/>
</section>



Wrapping It Up


Let's build a module, that has the following:

  1. One Page Module containing the list of data entries
  2. On the same page above the list, show Add New Entry button
  3. When Add New Entry is clicked, show a form above the list (do not redirect to another page)
  4. When form is submitted, post the values to a remote API.

Hints:
- We will use service.js to connect to remote APIs to fetch and send data.
- We will also use the Grid UI to display the data in directive list.tmpl and the Form UI to generate a form for the user.


Step 1: create the module installation file as instructed above


install.js
var myModuleNav = [
    {
        //main information
        'id': 'myModules',
        'label': 'My Module',
        'url': '#/mymodule',
        'scripts': ['modules/myModule/controller.js', 'modules/myModule/service.js'],
        'tplPath': 'modules/myModule/directives/list.tmpl',
        //permissions information
        'checkPermission': {
            'service': 'myService',
            'route': '/list'
        },
        'pillar': {
            'name': 'operate',
            'label': 'Operate',
            'position': 4
        },
        //menu & tracker information
        'icon': '',
        'mainMenu': true,
        'tracker': true,
        'ancestor': ['Home']
    }
];
navigation = navigation.concat(myModuleNav);



Step 2: Create the controller with 3 functions: listEntries - addEntry - viewEntry.

  • listEntries: Calls the service to get the data from remote API, constructs the configuration of the Grid UI, invokes buildGrid to print the data
  • viewEntry: Invoked through grid when View Item button is pressed.
  • addEntry: Invoked when Add New Entry was pressed, generates form and attaches submit and reset operations.
    • Reset: empties the form and closes the modal overlay
    • Submit: calls the services to post the data to the remote API
controller.js
"use strict";
var myModule = soajsApp.components;
myModule.controller('myModuleCtrl', ['$scope', '$modal', 'myModuleSrv', function($scope, $modal, myModuleSrv) {
    $scope.$parent.isUserLoggedIn();

    //define the permissions
    var permissions = {
        'list': ['myService', '/list'],
        'add': ['myService', '/add']
    };

    $scope.access = {};
    //call the method and compare the permissions with the ACL
    //allowed permissions are then stored in scope.access
    constructModulePermissions($scope, $scope.access, permissions);

    //function that lists the entries in a grid
    $scope.listEntries = function() {
        var opts = {
            "url": "http://dashboard-api.soajs.org:4000/myService/list",
            "method": "get"
        };
        myModuleSrv.getEntriesFromAPI(opts, function(error, response) {
            if(error) {
                $scope.$parent.displayAlert('danger', error.message);
            }
            else {
                myModuleSrv.printGrid($scope, response.data);
            }
        });
    };

    //function that prints one data record to the console
    $scope.viewEntry = function(oneDataRecord) {
        console.log(oneDataRecord);
    };

    //function that adds a new entry by using form & modal
    $scope.addEntry = function() {
        if($scope.access.add) {

            var submit = function(formData) {  //operation function, returns the data entered in the form
                var opts = {
                    url: "http://dashboard-api.soajs.org/myService/add",
                    method: "post",
                    data: formData
                };
                myModuleSrv.sendEntryToAPI(opts, function(error) {
                    if(error) {
                        $scope.$parent.displayAlert('danger', error.message);
                    }
                    else {
                        $scope.$parent.displayAlert('success', "Your entry has beend added.");
                        $scope.form.formData = {};
                        $scope.modalInstance.close();
                        $scope.listEntries();
                    }
                });
            };

            myModuleSrv.buildForm($scope, $modal, submit);
        }
    };

    //if scope.access.list is allowed, call listEntries
    if($scope.access.list) {
        $scope.listEntries();
    }
}]);

Step 3: create the directive to display the Add new Entry button and the Grid UI

list.tmpl
<section ng-controller="myModuleCtrl">
    <div>
        <!-- if scope.access.add is allowed, show button-->
        <input type="button" value="Add New Entry" ng-click="addEntry();" ng-if="access.add" class="btn btn-primary btn-sm"/>
        <!-- UI Grid directive -->
        <nglist></nglist>
    </div>
</section>

Step 4: create a custom service

service.js
"use strict";
var myModuleService = soajsApp.components;
myModuleService.service('myModuleSrv', ['$timeout', '$http', function($timeout, $http) {

    function callAPI(config, callback) {
        $http(config).success(function(response, status, headers, config) {
            $timeout(function() {
                return callback(null, response);
            }, 500);
        }).error(function(errData, status, headers, config) {
            $timeout(function() {
                return callback(errData);
            }, 500);
        });
    }

    return {
        'getEntriesFromAPI': function(opts, callback) {
            var config = {
                url: opts.url,
                method: opts.method,
                headers:{
                    'Content-Type': 'application/json'
                }
            };
            callAPI(config, callback);
        },

        'sendEntryToAPI': function(opts, callback) {
            var config = {
                url: opts.url,
                method: opts.method,
                data: opts.data,
                json: true,
                headers:{
                    'Content-Type': 'application/json'
                }
            };
            callAPI(config, callback);
        },

        'printGrid': function($scope, response) {
            var options = {
                'grid': {
                    recordsPerPageArray: [20, 50, 100, 200],
                    'columns': [
                        {'label': 'Title', 'field': 'title'},
                        {'label': 'Created', 'field': 'created'}
                    ],
                    'defaultLimit': 20
                },
                'defaultSortField': '',
                'data': response,
                'left': [
                    {
                        'icon': 'search',
                        'label': 'View Item',
                        'handler': 'viewEntry'
                    }
                ]
            };
            buildGrid($scope, options);
        },

        'buildForm': function($scope, $modal, submitAction) {
            var config = {
                "timeout": $timeout,
                "form": {
                    "entries": [
                        {
                            'name': 'title',
                            'label': 'Title',
                            'type': 'text',
                            'placeholder': 'My Entry...',
                            'value': '',
                            'tooltip': 'Give your entry a title',
                            'required': true
                        },
                        {
                            'name': 'description',
                            'label': 'Description',
                            'type': 'textarea',
                            'placeholder': 'My Description...',
                            'value': '',
                            'tooltip': 'Give your entry a description',
                            'required': true
                        }
                    ]
                },
                "name": 'addEntry',
                "label": 'Add New Entry',
                "actions": [
                    {
                        'type': 'submit',               //button type
                        'label': 'Add Entry',          //button label
                        'btn': 'primary',               //button class name (AngularJs's Bootstrap)
                        'action': submitAction
                    },
                    {
                        'type': 'reset',
                        'label': 'Cancel',
                        'btn': 'danger',
                        'action': function() {
                            //reset the form and close modal
                            $scope.modalInstance.dismiss('cancel');
                            $scope.form.formData = {};
                        }
                    }
                ]
            };

            //call buildForm
            buildFormWithModal($scope, $modal, config);
        }
    }
}]);

The above code is also provided in a sample Module that includes a basic service example myService associated with a UI module myModule. You can obtain this code by downloading the module from GitHub.