Module 3 - Creating extensions in JavaScript

Before you begin this module, you should complete Module 1 - Building the application. In this module, you will build JavaScript components using AngularJS code to customize the visual design of your application.

If your interest is only in updating the visual components is not necessary to have completed Module 2: Creating extensions in Java.

Creating a reusable view component

In this module, we will walk through a powerful capability of Innovation Suite - the ability to code a custom visual component that can be integrated into applications using Innovation Studio.

  1. If you have not done so already, download and and unzip the latest sample code archive.

  2. For this Lesson, we will add the simplest possible component: one that simply displays one of its configuration parameters as text. We will call it a "Custom Label" component. Let's start with the work-central-app sample code found under "Module 3 - Create Re-usable View Components" in the sample code archive.

  3. The process of copying the sample code into your work-central-app project is the same as that described in Preparing for Eclipse in the earlier step about custom Java code: copy over the "bundle" folder from the sample code into the "work-central-app" project source code folder, and let the operating system guide you in merging the files. In fact, the only file overwritten is ext.module.js.

    images/docs/download/attachments/668422861/image2016-11-22_12_56_21.png

  4. Let's examine the code that was copied over. You can use any IDE or text editor you like. We will stick with Eclipse for now, but many Javascript developers prefer other tools. Find the view-components folder for work-central-app.

    projects\work-central-app\bundle\src\main\webapp\scripts\view-components

    A view component source typically has at least these 6 source files. .

    images/docs/download/attachments/668422861/Capture.PNG

  5. A view component is really nothing more than a pair of AngularJS Directives with some additional declarations so it will work with the View Designer in Innovation Studio. Please see the Developer Guide for a full explanation of how to develop View Components. For this tutorial we will just note a few things about the sample code.

    First, we will define a "design time directive" that controls how it appears in the View Designer canvas area.

    File: custom-label-design.directive.html

    <h3>Customize your Label Here</h3>

  6. The Javascript for the directive is simple enough if you are familiar with AngularJS. Note the long-winded names of the objects - they are a direct transformation of the bundle id and component name into "camel-case". This is actually an important convention for ALL AngularJS object names to avoid conflicts with other Javascripts that may be introduced later. The fact that it is camel-case is due to the way AngularJS maps these into hyphenated identifiers elsewhere. Please remember that you have little control over scripts outside of this component so good code hygiene here is critical. Similarly, note the namespaced HTML file for the template. All such files are resolved by name alone once the component is "compiled" for deployment, so this is important to avoid conflicts.

    File: custom-label-design.directive.js

    (function () {
        'use strict';
        angular.module('com.example.work-central-app.view-components.custom-label').directive('comExampleWorkCentralAppCustomLabelDesign', function () {
            return {
                restrict: 'E',
                templateUrl: 'scripts/view-components/custom-label/com-example-work-central-app-custom-label-design.directive.html',
                scope: {
                    rxConfiguration: '='
                }
            };
        });
    })();
    
  7. The runtime rendering of the component is simply displaying the configuration parameter, present in the $scope, which is declared in the controller.

    File: custom-label.directive.html

    <div>
        <h3>{{customLabel}}</h3>
    </div>
    
  8. The javascript for the runtime directive is similar to the design time directive. The biggest difference is the injection of the configuration parameter "customLabel". The same naming conventions apply.

    File: custom-label.directive.js

    (function () {
        'use strict';
        angular.module('com.example.work-central-app.view-components.custom-label').directive('comExampleWorkCentralAppCustomLabel',
            function () {
                return {
                    restrict: 'E',
                    templateUrl: 'scripts/view-components/custom-label/com-example-work-central-app-custom-label.directive.html',
                    scope: {
                        rxConfiguration: '='
                    },
                    link: function ($scope) {
                        var _config;
                        var init = function () {
                            _config = $scope.rxConfiguration.propertiesByName;
                            $scope.customLabel = _config.label;
                        };
                        $scope.$watch('rxConfiguration', init);
                    }
                };
        });
    })();
    
  9. The "config" of the directive is where configuration parameters are declared. Innovation Studio uses this to provide a simple UI for binding this configuration value to an expression in the View Designer. The important thing to note here is that the "type" and "designType" properties must agree with the "camel-case" version of the directive names in the directives shown above. This is a quirk of Angular JS, represented by the following table:

    Directive Actual Name of Directive (Camel-Case) Configured Reference (hyphenated)
    Runtime comExampleWorkCentralAppCustomLabel com-example-work-central-app-custom-label
    Design Time comExampleWorkCentralAppCustomLabelDesign com-example-work-central-app-custom-label-design

    If you do not take care in your component that these names "match", the component will not be rendered properly. Please see the documentation for a full description of these configuration properties. The important ones here are that isConfig controls whether it appears in the property inspector for configuration, and isProperty controls whether it is available for use in expressions elsewhere in the View.

    File: custom-label.config.js
    
    (function () {
        'use strict';
        angular.module('com.example.work-central-app.view-components.custom-label').config(function (rxViewComponentProvider) {
            rxViewComponentProvider.registerComponent([
                {
                    name: 'Custom Label',
                  group: 'Util Components',
                    icon: 'image_square',
                    type: 'com-example-work-central-app-custom-label', 
                    designType: 'com-example-work-central-app-custom-label-design', 
                   bundleId: 'com.example.work-central-app',
                    propertiesByName: [
                      {
                        name: 'label',
                        type: 'string',
                        isConfig: true,
                        isProperty: true,
                     isRequired: false
                      }
                    ]
                }
            ]);
        });
    })();
    
  10. The last of the view component files is the module itself. This is where the identity of the component is declared, allowing it's $scope to be manipulated in its own namespace.

    File: custom-label.module.js

    (function () {
        'use strict';
        angular.module('com.example.work-central-app.view-components.custom-label', [
            'com.bmc.arsys.rx.standardlib.security',
            'com.bmc.arsys.rx.standardlib.view-component'
        ]);
    })();
    
  11. The component needs to be "published" by including a reference to its module. This is a bit different for the case of a view component in an application bundle (vs. a library bundle).

    Because this happens to be an application bundle, the reference is placed in the ext.module.js file. NOTE: this is NOT the case for view components in a library bundle, which will be covered with the google map example below.

    projects\work-central-app\bundle\src\main\webapp\scripts\ext\ext.module.js

    The reference is shown in bold. This is the only file that should have been overwritten by copying over the sample code.

    angular.module('com.example.work-central-app-ext', [
        'com.example.work-central-app.view-components.custom-label'
    ]);
    

    These dependencies are automatically linked to the application as a whole in the application module file generated by the archetype.

  12. Deploy and Test in Innovation Studio

    projects\work-central-app> mvn clean install -Pexport -Pdeploy

    images/docs/download/attachments/668422861/worddav339bd92449b2a183e045d93e4b216c64.png

  13. Preview the view or run the application to see your custom text

    images/docs/download/attachments/668422861/worddava8e22f1d39784d59ef6d26c271686f46.png

Embedding a third party JavaScript component

The component used for this example will display a Google Map for a given address. The address can be mapped to some other field on the UI so that the map automatically stays in sync with location data that is present as part of an application.

In this particular case, because the component uses 3rd-party Javascript libraries, the structure of the code is a bit more complicated than a stand-alone component would be. To get a high level picture of the structure of the code in terms of a development project, most of the files affected are highlighted here. Take a moment to understand where the various files need to go in order to have a re-usable view component. Please refer to the documentation that describes step-by-step details for creating components.

NOTE: the additional code under the lib folder is not a standard part of building a component but is needed in this case because of the use of 3rd-party supporting code for Google Maps.

Establishing a library for deployment

NOTE: This project embeds the Google Map API in a new library. This API cannot be embedded more than once in any given project. Before generating the new project, make sure there is not already some bundle on your server that already uses Google Map API. An example of this is the Lunch Order application bundle that is pre-deployed in some sample environments.

  1. Follow the documented steps to create a library project, as was done previously for work-order-lib using the archetype.

  2. Accept defaults when appropriate but use these specific values when asked:

    |Archetype Prompt|Value| |'groupId'|com.example| |'artifactId'|geo-util-lib| |'version'|1.0-SNAPSHOT| |'package'|com.example| |'name'|Geolocation|

    As always, be sure to modify the POM file with the appropriate Admin and Developer credentials.

  3. Even before creating the Map component, build and deploy the library so it will show up in Innovation Studio.

    projects\geo-util-lib> mvn clean install -Pdeploy

Writing the AngularJS module and directives for the component

By now you are comfortable with importing your Maven project into Eclipse and manipulating the code.

As before, you can save time by getting the sample code from the latest archive from the same place as the previous Lesson: the Path 3 - Create Reusable View Components folder but specifically the geo-util-lib subfolder. This can be copied into your project created from the archetype as we have done in previous lessons, but this time copy the entire geo-util-lib folder out of the sample archive and paste it into your projects area where geo-util-lib was created by the archetype. This is necessary because one of the files at the top level of the project - bundle.conf.js - needs to be updated from the sample code. Allow the sample source files to be merged into the project just created by the archetype.

First find the scripts\view_components folder. The archetype generates it with a readme.txt file in it.

<projects>\geo-util-lib\bundle\src\main\webapp\scripts\view-components

We have already seen the basic structure of a view component from the previous Module but we will point out some differences here.

  1. This component requires some additional libraries to support Google Maps. You will see these placed into the lib folder at the same level as scripts.

  2. Note that when developing re-usable JavaScript code, it is important to give instructions to consuming development projects about how to handle and/or avoid conflicts with other uses of the same libraries. Note that even if you package the 3rd party code within the view-component folder itself, it can still conflict with other instances of the library which use the same package names.

    images/docs/download/attachments/668422865/worddav9d20cb2b4c9ff24f278453bcdf684606.png

    In order to "compile" these libraries along with your component, a developer must change the configuration file used by the Innovation SDK Javascript compiler. This has already been done for you in the sample source for geo-util-lib as follows.

    In the file

    <projects>\geo-util-lib\bundle\bundle.conf.json

    note the library references to the 3rd-party code in the app section.

    . . .
    "app": {
      "scripts": [
         "lib/angular-ui-event-1.0.0/event",
          "lib/angular-ui-map-0.5.0/ui-map",
           "scripts/**/*.global",
           . . .
    
  3. Properties in Config Take a look at the config Javascript file for the component. Note again the namespacing conventions so important to avoid collisions with other libraries. There is an "address" property defined in the google-map.config.js. This defines how the address used by the map appears at design-time in View Designer, and is used at runtime to render the correct location on the map. This will almost always be dynamically resolved by using an expression for the address value (such as the location of a record in the currently selected row of a grid, of the value of a field in a record editor). Therefore, enableExpressionEvaluation is set to true.

    By the way, a common pattern here is to hide the textual representation of the address but show the address visually via this component.

    We also create an apiKey property. Note that while this is optional, it is the responsibility of the developer to make sure the default key specified in the code is enabled by Google. At any point this key may stop working, in which case the code must be changed, or the user of this component can pass in a valid key as a component property (they will have to apply to Google for one).

    (function () {
        'use strict';
        angular.module('com.example.geo-util-lib.view-components.google-map').config(function (rxViewComponentProvider) {
            rxViewComponentProvider.registerComponent([
                {
                    name: 'Google Map',
                  group: 'Geolocation Components',
                    icon: 'mapmarker',
                    type: 'com-example-geo-util-lib-google-map',
                    designType: 'com-example-geo-util-lib-google-map-design',
                 bundleId: 'com.example.geo-util-lib',               
                    propertiesByName: [
                        {
                            name: 'address',
                            type: 'string',
                            isConfig: true,
                            isProperty: true,
                            enableExpressionEvaluation: true
                        },
                        {
                            name: 'apiKey',
                            type: 'string',
                            isConfig: true,
                            isProperty: true,
                            enableExpressionEvaluation: true
                        },
                        {
                            name: 'size',
                            type: 'integer',
                            isConfig: true,
                            isProperty: true
                        }
                    ]
                }
            ]);
        });
    })();
    
  4. Define the Runtime Directive This is the heart of the component, and the init function shows exactly how the address property is used with the Google Map API.

    var init = function () {
          _config = $scope.rxConfiguration.propertiesByName;
          var address = _config.address;
          // Google Map
          var mapOptions = {
                zoom: 8,
                center: {lat: -34.397, lng: 150.644},
                mapTypeId: google.maps.MapTypeId.ROADMAP,
                scrollwheel: false
          };
          var map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
          var geocoder = new google.maps.Geocoder();
    
          if (address) {
                geocodeAddress(geocoder, map, address);
          }
     };
    

    The dynamic behavior is controlled by the callback $watch function. Whenever the address property changes, the map is reinitialized. This is the key to having smart components that work together without having direct dependencies on each other.

    $scope.$watch('rxConfiguration.propertiesByName.address', function() {
        if (window.google) {
           init();
        } else {
           _config = $scope.rxConfiguration.propertiesByName;
         var key = _config.apiKey;
          if (key == null) { 
                // This is the default API key to use, but will not work well in production.
               key = 'AIzaSyA_7yQeLqT_tn8Ln8IixcYhjuHhDbg7o1I'; 
          }
          var url = 'https://maps.googleapis.com/maps/api/js?v=3.exp&key=' + key;
            var size = _config.size + "px";
          if (size == null) {
                size = 300;
            }
          $.getScript(url, function () {
             document.getElementById('map_canvas').style.height = size;
             document.getElementById('map_canvas').style.width = size;
              init();
            });
        }
    });
    
  5. Publish the extension. This is done by adding a dependency to the directive in the bundle's module file. NOTE: as mentioned in the earlier section, this step is different for view components in library bundles as opposed to application bundles. In this case, we do NOT use ext.module.js to publish the extension and this file should not be used in the bundle.

    Instead, the library's own module declaration, generated by the archetype, must be modified to have a reference to the google-map component.

    projects\geo-util-lib\bundle\src\main\webapp\scripts\com.example.geo-util-lib.module.js

    Add the component as a module dependency.

    (function () {
        'use strict';
        angular.module('com.example.geo-util-lib', [
            'com.example.geo-util-lib.view-components.google-map'
        ]);
    })();
    

Consuming the new component in an application

Let's show how this Map component can be used in the Work Order library.

  1. Make sure the geo-util-lib library has been deployed to the server. The component should show up in the palette of Innovation Studio in View Designer for ANY application.

  2. Add a field called Location to the Work Order record definition

    1. Add the field in the Record Designer
    2. Populate the field using the Edit Data feature, creating some sample addresses. You may have to make the new field visible in the options menu on the far right of the Edit Data grid. Note: if you want to take it further, you could populate this field by creating a Named List based on the Foundation's Location record definition. NOTE: if you use a Named List that uses the ID field for the actual value of the location, then this value will not work with the Google Map. There are two ways to resolve this: either extend the Google Map code to handle an ID and configurable field of a record by looking up the address value itself, or, add a hidden field onto the record definition and put a rule and process in place to do a reverse lookup of the actual address whenever the record is created or changed. Then include this field as a hidden column in the grid. Both of these are "exercises left to the reader".

      images/docs/download/attachments/668422865/worddave446e8f18a048fa8f21a29129031e539.png

  3. Create a new View, or, update the List of Work Order view created in Module 1, to include the Google Map.

    1. Add the Map component
    2. Make sure the Grid as configured for "Single Selection
    3. Have the Location field added but mark it as not visible by default.

      images/docs/download/attachments/668422865/worddav718c28c62ecf414e4971f98edfd3548b.png

    4. On the right column, drag in the Google Map component.

      1. Set the Address to get the Location field from the Selected Row of the Grid.
      2. Set the API key if desired (or try to use the default
      3. Set the size if desired.

        images/docs/download/attachments/668422865/worddava23a6a102b8efc9cd697dc71f9468e8b.png

  4. When the View is previewed, there should be a nice effect of the map updating to show the location of each selected Work Order.

    images/docs/download/attachments/668422865/worddav74c4545222481fd71fe715c5bed87242.png

Summary

In this module, we covered a few key topics - how to extend the UI of the client using Javascript and AngularJS. This also shows an example of using third-party Javascript libraries to leverage that functionality. We also did it in a way that shows how library deployment packages can lead to creation of reusable code that can be distributed for use in many projects, even if they just contain useful UI elements.

Conclusion - Module 3

We covered a lot of ground on the UI front in this module, from building reusable view components, to creating custom JavaScript components, to using third-party UI components. We also introduced the topic of localization, to guide you to develop best practices of building internationalized applications.