Using HTML Templates in AngularJS Widgets

When using AngularJS (or some other third parties javascript frameworks), in some cases, an HTML widget may require some static code injected from the Backoffice Framework. To achieve it, HTML templates may be used.

Context

This document shows how to use simple templates to solve typical problems that may occur when writing a widget with third party javascript frameworks. This document does not describe all the possibilities that the Apache VelocityInformation published on non-SAP site supports, yet the most common use cases are presented.

The steps mentioned in this tutorial build upon the configuration from the Extending an AngularJS Widget.

Resolving Identities

Context

The widget that was created in the Extending an AngularJS Widget was supposed to display what comes through the socket, but in the end it was capable of displaying more information. For the sake of simplicity, let's hide the extra information after the first message is received.

Procedure

  1. Define a new class (i.e. spare) and assign it to all DOM elements that should be hidden in the angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.html file:
    <widget ng-controller="SampleAngularController">
        
    	<h1 class="caption spare" cng-local="label.title.hello"></h1>
        
    	<h3 class="caption spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! {{adapter.cng().local('label.title.sockets')}}</h3>
        
    	<!-- ... -->
    
    	<div class="spare>    <h3 class="caption" cng-local="label.title.model"></h3>
    
    		<!-- ... -->
    	
    		<cng-event type="model" handler="modelChanged(model, property)" />
    
            <hr/>
        </div>
    
    	<cng-event type="ready" handler="init()" />
    
    </widget>
  2. Add some JavaScript in order to hide the elements that are of the spare class, just after a new message has been received. Amend the JavaScript file angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.js as follows:
    SampleAngularController = function($scope) {
        
    	$scope.socketInput = function(socketId, data) {
            
    		// ...
    
    		jQuery(".spare").hide();
    
        };
    
    	// ...
    
    };
    
    // ...
    Source Files at This Stage

    See full content of angularwidget.html.

    <widget ng-controller="SampleAngularController">
     
        <h1 class="caption spare" cng-local="label.title.hello"></h1>
        <h3 class="caption spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! {{adapter.cng().local('label.title.sockets')}}</h3>
        <div ng-if="!input" cng-global="label.noinput"></div>
        <table ng-if="input">
            <tr>
                <td class="header" cng-local="label.socket.input"></td>
                <td class="header" cng-local="label.socket.output"></td>
            </tr>
            <tr>
                <td>
                    <table>
                        <tr ng-repeat="(key, value) in input" ng-class="{true:'even', false:'odd'}[$even]">
                            <td class="label">{{key}}</td>
                            <td class="value">{{value}}</td>
                        </tr>
                    </table>
                </td>
                <td>
                    <table>
                        <tr ng-repeat="(key, value) in output" ng-class="{true:'even', false:'odd'}[$even]">
                            <td class="label">{{key}}</td>
                            <td class="value">{{value}}</td>
                        </tr>
                    </table>
                </td>
            </tr>    
        </table>
        <cng-event type="socket" id="input" handler="socketInput(socket, data)" />
        <hr/>
     
        <div class="spare>        <h3 class="caption" cng-local="label.title.model"></h3>
            <div cng-local="label.model.registered"></div>
            <table>
                <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
                    <td class="label">{{key}}</td>
                    <td class="value">{{value}}</td>
                    <td ng-if="(settings && settings.enableModelEditing) || sessionInfo.admin" ng-click="removeModelChange(key)"><a href="#">X</a></td>
                </tr>
            </table>
            <div ng-if="(settings && settings.enableModelEditing) || sessionInfo.admin">
            <div cng-local="label.model.instruction"></div>
            <table>
                <tr class="even">
                    <td class="label" cng-local="label.model.property"></td>
                    <td class="value"><input type="text" ng-model="property"></td>
                </tr>
                <tr class="odd">
                    <td class="label" cng-local="label.model.value"></td>
                    <td id="value" class="value">{{input[property]}}</td>
                </tr>
                <tr class="even">
                    <td class="label" cng-local="label.model.change"></td>
                    <td class="value"><input type="text" ng-model="newValue"></td>
                </tr>
                <tr class="odd">
                    <td colspan="2" class="commit"><button type="button" ng-click="commitModelChanges(property, newValue)" cng-local="label.model.commit"/></td>
                </tr>
            </table>
            </div>
         
            <cng-event type="model" handler="modelChanged(model, property)" />
     
            <hr/>
        </div>
     
        <cng-event type="ready" handler="init()" />
     
      </widget>

    See full content of angularwidget.js.

    SampleAngularController = function($scope) {
     
        $scope.socketInput = function(socketId, data) {
            showMessage(LEVEL_INFO, $scope.adapter.cng().local("message.socket"));
     
            $scope.input = jQuery.extend(true, {}, data);
            var changes = $scope.model.changes;
            if (changes) {
                for(var key in changes) {
                    if (typeof(data[key]) != "undefined") {
                        data[key] = changes[key];
                    }
                }
            }
            $scope.output = data;
            $scope.adapter.cng().socketEvent("output", data);
     
            if (!$scope.model.statistics) {
                $scope.model.statistics = { $class: "example.cng.angular.model.SearchStatistics"}; // Notice! We explicitly point a Java class
                $scope.model.statistics.stats = {};
            }
            if (data.typeCode) {
                var stats = $scope.model.statistics.stats[data.typeCode];
                if (!stats) {
                    stats = {};
                    stats.typeCode = data.typeCode;
                    stats.count = 0;
                    $scope.model.statistics.stats[data.typeCode] = stats;
                }
                stats.count += 1;
     
                $scope.adapter.cng().model("statistics", $scope.model.statistics);
                $scope.adapter.cng().socketEvent("stats", $scope.model.statistics);
            }
            jQuery(".spare").hide();
        };
      
    $scope.modelChanged = function(model, property) {        showMessage(LEVEL_WARNING, $scope.adapter.cng().local("message.model") + ": " + property);
      
            $scope.property = null;
            $scope.newValue = null;
            $scope.model = model;
        };
      
        $scope.commitModelChanges = function(key, value) {
            var changes = $scope.model.changes;
            if (!changes) {
                changes = {};
            }
            changes[key] = value;
      
            $scope.adapter.cng().model('changes', changes);
        };
      
        $scope.removeModelChange = function(key) {
            var changes = $scope.model.changes;
            delete changes[key];
      
            $scope.adapter.cng().model('changes', changes);
        };
      
        $scope.init = function() {
            if (!$scope.model) {
                $scope.model = {};
            }
            $scope.statistics = $scope.adapter.cng().model("statistics");
      
            var settings = $scope.settings["initialChanges"];
            if (settings && !$scope.model.changes) {
                var changes = JSON.parse(settings);
                $scope.model.changes = changes;
                $scope.adapter.cng().model("changes", changes);
            }
        };
      
    };
      
    AngularCNG.init("angularwidget", SampleAngularController); 
  3. Define the problem.

    It is quite unfortunate in this case that there is a possibility for a widget to be used several times in a single perspective. In great simplification, it causes the same code being injected in several places and therefore any DOM segregation/identification may turn to be much more complicated or even impossible. Furthermore, it might cause the identities duplication.

    In the case of our widget, the code multiplication would cause incorrect widget behavior: when any of widgets in a perspective received a massage, all of the instances would hide the spare contents.

  4. Resolve the problem.

    To solve the above problem, use the widget templates. The Backoffice Framework provides a widget container identity which is unique in the whole perspective. This way, we are able to make the identities inside also unique.

    Amend the view file by adding a small template placeholder ${id} in each identity definition and make relevant references to them.

    1. Amend the angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.html as follows:
      <widget ng-controller="SampleAngularController">
          
      	<h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>
          
      	<h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! {{adapter.cng().local('label.title.sockets')}}</h3>
          
      	<!-- ... -->
      
      	<div class="${id}_spare>
      
      		<!-- ... -->
      
          </div>
      
      	<!-- ... -->
      
      </widget>
    2. Amend the angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.js file as follows:
      SampleAngularController = function($scope) {
      
          $scope.socketInput = function(socketId, data) {
              
      		// ...
      
              jQuery("." + $scope.adapter.cng().id() + "_spare").hide();
          };
  5. Make clear for the Backoffice Framework that the view we just defined is a template, and not an HTML itself:
    1. Redeploy the application, start the server and log in to the Backoffice.
    2. Switch to the Application Orchestrator mode (by pressing F4 on Windows or fn + F4 on Unix system).
    3. Open the settings of the Angular Sample Widget and we add a setting to the widget: isTemplate (type java.lang.Bolean) and set it to true:
    4. To make this configuration the default one, add the setting to the definition.xml of the widget:
      <widget-definition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                         xsi:noNamespaceSchemaLocation="http://www.hybris.com/schema/cockpitng/widget-definition.xsd"
                         id="example.cng.jscng.widgets.jswidget">
      
          <!-- ... -->
      
          <settings>
      
              <!-- ... -->
      
              <setting key="isTemplate" type="java.lang.Boolean" default-value="true"/>
          </settings>
      
      </widget-definition>

Source Files at This Stage - Identities

You can check the source files at this stage.

See full content of angularwidget.html

<widget ng-controller="SampleAngularController">
 
    <h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>
    
    <h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! {{adapter.cng().local('label.title.sockets')}}</h3>
    <div ng-if="!input" cng-global="label.noinput"></div>
    <table ng-if="input">
        <tr>
            <td class="header" cng-local="label.socket.input"></td>
            <td class="header" cng-local="label.socket.output"></td>
        </tr>
        <tr>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in input" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in output" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
        </tr>    
    </table>
    <cng-event type="socket" id="input" handler="socketInput(socket, data)" />
    <hr/>
 
    <div class="${id}_spare>
        <h3 class="caption" cng-local="label.title.model"></h3>
        <div cng-local="label.model.registered"></div>
        <table>
            <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
                <td class="label">{{key}}</td>
                <td class="value">{{value}}</td>
                <td ng-if="(settings && settings.enableModelEditing) || sessionInfo.admin" ng-click="removeModelChange(key)"><a href="#">X</a></td>
            </tr>
        </table>
        <div ng-if="(settings && settings.enableModelEditing) || sessionInfo.admin">
        <div cng-local="label.model.instruction"></div>
        <table>
            <tr class="even">
                <td class="label" cng-local="label.model.property"></td>
                <td class="value"><input type="text" ng-model="property"></td>
            </tr>
            <tr class="odd">
                <td class="label" cng-local="label.model.value"></td>
                <td id="value" class="value">{{input[property]}}</td>
            </tr>
            <tr class="even">
                <td class="label" cng-local="label.model.change"></td>
                <td class="value"><input type="text" ng-model="newValue"></td>
            </tr>
            <tr class="odd">
                <td colspan="2" class="commit"><button type="button" ng-click="commitModelChanges(property, newValue)" cng-local="label.model.commit"/></td>
            </tr>
        </table>
        </div>
     
        <cng-event type="model" handler="modelChanged(model, property)" />
 
        <hr/>
    </div>
 
    <cng-event type="ready" handler="init()" />
 
  </widget>

See full content of angularwidget.js

SampleAngularController = function($scope) {
 
    $scope.socketInput = function(socketId, data) {
        showMessage(LEVEL_INFO, $scope.adapter.cng().local("message.socket"));
 
        $scope.input = jQuery.extend(true, {}, data);
        var changes = $scope.model.changes;
        if (changes) {
            for(var key in changes) {
                if (typeof(data[key]) != "undefined") {
                    data[key] = changes[key];
                }
            }
        }
        $scope.output = data;
        $scope.adapter.cng().socketEvent("output", data);
 
        if (!$scope.model.statistics) {
            $scope.model.statistics = { $class: "example.cng.angular.model.SearchStatistics"}; // Notice! We explicitly point a Java class
            $scope.model.statistics.stats = {};
        }
        if (data.typeCode) {
            var stats = $scope.model.statistics.stats[data.typeCode];
            if (!stats) {
                stats = {};
                stats.typeCode = data.typeCode;
                stats.count = 0;
                $scope.model.statistics.stats[data.typeCode] = stats;
            }
            stats.count += 1;
 
            $scope.adapter.cng().model("statistics", $scope.model.statistics);
            $scope.adapter.cng().socketEvent("stats", $scope.model.statistics);
        }
        
        jQuery("." + $scope.adapter.cng().id() + "_spare").hide(); 
    }; 
 
    $scope.modelChanged = function(model) {
        showMessage(LEVEL_WARNING, $scope.adapter.cng().local("message.model"));
 
        $scope.property = null;
        $scope.model = model;
    };
 
    $scope.commitModelChanges = function(key, value) {
        var changes = $scope.model.changes;
        if (!changes) {
            changes = {};
        }
        changes[key] = value;
 
        $scope.adapter.cng().model('changes', changes);
    };
 
    $scope.removeModelChange = function(key) {
        var changes = $scope.model.changes;
        delete changes[key];
 
        $scope.adapter.cng().model('changes', changes);
    };
 
    $scope.init = function() {
        if (!$scope.model) {
            $scope.model = {};
        }
        $scope.statistics = $scope.adapter.cng().model("statistics");
        
        var settings = $scope.settings["initialChanges"];
        if (settings && !$scope.model.changes) {
            var changes = JSON.parse(settings);
            $scope.model.changes = changes;
            $scope.helper.cockpit().model("changes", changes);
        }    
    };
 
};
 
AngularCNG.init("angularwidget", SampleAngularController);

Implementing HTML Templates in Other Backoffice Framework Functionalities

You can implement HTML templates in other Backoffice Framework functionalities.

Using HTML Templates for Localization

In some cases, it may be easier to use template placeholders for localized labels.

Procedure

To illustrate how it works, one of the captions has been changed (global labels may be used in similar way):

The angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.html file:

<widget ng-controller="SampleAngularController">
    
	<h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>
    
	<h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! $labels.local.label.title.sockets</h3>
    
	<!-- ... -->

	<div class="${id}_spare>

		<!-- ... -->

    </div>

	<!-- ... -->

</widget>

Source Files at This Stage - Localization

You can check the source files at this stage.

See full content of angularwidget.html

<widget ng-controller="SampleAngularController">
 
    <h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>
    
    <h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! $labels.local.label.title.sockets</h3>
    <div ng-if="!input" cng-global="label.noinput"></div>
    <table ng-if="input">
        <tr>
            <td class="header" cng-local="label.socket.input"></td>
            <td class="header" cng-local="label.socket.output"></td>
        </tr>
        <tr>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in input" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in output" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
        </tr>    
    </table>
    <cng-event type="socket" id="input" handler="socketInput(socket, data)" />
    <hr/>
 
    <div class="${id}_spare>
        <h3 class="caption" cng-local="label.title.model"></h3>
        <div cng-local="label.model.registered"></div>
        <table>
            <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
                <td class="label">{{key}}</td>
                <td class="value">{{value}}</td>
                <td ng-if="(settings && settings.enableModelEditing) || sessionInfo.admin" ng-click="removeModelChange(key)"><a href="#">X</a></td>
            </tr>
        </table>
        <div ng-if="(settings && settings.enableModelEditing) || sessionInfo.admin">
        <div cng-local="label.model.instruction"></div>
        <table>
            <tr class="even">
                <td class="label" cng-local="label.model.property"></td>
                <td class="value"><input type="text" ng-model="property"></td>
            </tr>
            <tr class="odd">
                <td class="label" cng-local="label.model.value"></td>
                <td id="value" class="value">{{input[property]}}</td>
            </tr>
            <tr class="even">
                <td class="label" cng-local="label.model.change"></td>
                <td class="value"><input type="text" ng-model="newValue"></td>
            </tr>
            <tr class="odd">
                <td colspan="2" class="commit"><button type="button" ng-click="commitModelChanges(property, newValue)" cng-local="label.model.commit"/></td>
            </tr>
        </table>
        </div>
     
        <cng-event type="model" handler="modelChanged(model, property)" />
 
        <hr/>
    </div>
 
    <cng-event type="ready" handler="init()" />
 
  </widget>

Enabling HTML Templates for Configuration

Context

As decided earlier, some of the widget instances should not have the option to configure changes at runtime. You have implemented that by adding the setting enableModelEditing in the definition.xml file and using it in ng-if directive. It already works, yet a form is still sent to a web browser (though not displayed). Widget configuration does not change during its life, so actually, the form might be omitted during the template processing and thereby not be sent to the web browser.

Procedure

Append that in the angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.html view file of our widget:
<widget>
    
	<!-- ... -->

	<div class="${id}_spare">
		
		<!-- ... -->

		<table>
    	    <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
            
				<!-- ... -->

        	    #if ( $settings.enableModelEditing )
        	    <td ng-click="removeModelChange(key)"><a href="#">X</a></td>
        	    #end
        	</tr>
    	</table>
		
		#if ( $settings.enableModelEditing )
		<div cng-local="label.model.instruction"></div>
		<table>
        
			<!-- ... -->

		</table>
		#end

		<!-- ... -->

	</div>

	<!-- ... -->

</widget>

Source Files at This Stage - Configuration

You can check the source files at this stage.

See full content of angularwidget.html

<widget ng-controller="SampleAngularController">
 
    <h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>
    
    <h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! $labels.local.label.title.sockets</h3>
    <div ng-if="!input" cng-global="label.noinput"></div>
    <table ng-if="input">
        <tr>
            <td class="header" cng-local="label.socket.input"></td>
            <td class="header" cng-local="label.socket.output"></td>
        </tr>
        <tr>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in input" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in output" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
        </tr>    
    </table>
    <cng-event type="socket" id="input" handler="socketInput(socket, data)" />
    <hr/>
 
    <div class="${id}_spare>
        <h3 class="caption" cng-local="label.title.model"></h3>
        <div cng-local="label.model.registered"></div>
        <table>
            <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
                <td class="label">{{key}}</td>
                <td class="value">{{value}}</td>
                #if ( $settings.enableModelEditing )
                <td ng-click="removeModelChange(key)"><a href="#">X</a></td>
                #end
            </tr>
        </table>
        #if ( $settings.enableModelEditing )
        <div cng-local="label.model.instruction"></div>
        <table>
            <tr class="even">
                <td class="label" cng-local="label.model.property"></td>
                <td class="value"><input type="text" ng-model="property"></td>
            </tr>
            <tr class="odd">
                <td class="label" cng-local="label.model.value"></td>
                <td id="value" class="value">{{input[property]}}</td>
            </tr>
            <tr class="even">
                <td class="label" cng-local="label.model.change"></td>
                <td class="value"><input type="text" ng-model="newValue"></td>
            </tr>
            <tr class="odd">
                <td colspan="2" class="commit"><button type="button" ng-click="commitModelChanges(property, newValue)" cng-local="label.model.commit"/></td>
            </tr>
        </table>
        #end
        </div>
     
        <cng-event type="model" handler="modelChanged(model, property)" />
 
        <hr/>
    </div>
 
    <cng-event type="ready" handler="init()" />
 
  </widget>

Enabling HTML Templates for Session Information

Procedure

You would like the changes editor to be shown only if a user has the administration privileges. Amend the angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.html file as follows:
<widget>
    
	<!-- ... -->

	<div class="${id}_spare">
		
		<!-- ... -->

		<table>
    	    <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
            
				<!-- ... -->

        	    #if ( $settings.enableModelEditing || $sessionInfo.admin )
					
					<!-- ... -->

				#end
        	</tr>
    	</table>
		
		#if ( $settings.enableModelEditing || $sessionInfo.admin)
        
			<!-- ... -->

		#end

		<!-- ... -->

	</div>

	<!-- ... -->

</widget>

As a result, we get a fully working HTML template widget written using AngularJS.

Session info

Source Files at This Stage - Session Info

You can check the source files at this stage.

See full content of angularwidget.html

<widget ng-controller="SampleAngularController">
 
    <h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>
    
    <h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! $labels.local.label.title.sockets</h3>
    <div ng-if="!input" cng-global="label.noinput"></div>
    <table ng-if="input">
        <tr>
            <td class="header" cng-local="label.socket.input"></td>
            <td class="header" cng-local="label.socket.output"></td>
        </tr>
        <tr>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in input" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in output" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
        </tr>    
    </table>
    <cng-event type="socket" id="input" handler="socketInput(socket, data)" />
    <hr/>
 
    <div class="${id}_spare>
        <h3 class="caption" cng-local="label.title.model"></h3>
        <div cng-local="label.model.registered"></div>
        <table>
            <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
                <td class="label">{{key}}</td>
                <td class="value">{{value}}</td>
                #if ( $settings.enableModelEditing || $sessionInfo.admin )
                <td ng-click="removeModelChange(key)"><a href="#">X</a></td>
                #end
            </tr>
        </table>
        #if ( $settings.enableModelEditing || $sessionInfo.admin)
        <div cng-local="label.model.instruction"></div>
        <table>
            <tr class="even">
                <td class="label" cng-local="label.model.property"></td>
                <td class="value"><input type="text" ng-model="property"></td>
            </tr>
            <tr class="odd">
                <td class="label" cng-local="label.model.value"></td>
                <td id="value" class="value">{{input[property]}}</td>
            </tr>
            <tr class="even">
                <td class="label" cng-local="label.model.change"></td>
                <td class="value"><input type="text" ng-model="newValue"></td>
            </tr>
            <tr class="odd">
                <td colspan="2" class="commit"><button type="button" ng-click="commitModelChanges(property, newValue)" cng-local="label.model.commit"/></td>
            </tr>
        </table>
        #end
        </div>
     
        <cng-event type="model" handler="modelChanged(model, property)" />
 
        <hr/>
    </div>
 
    <cng-event type="ready" handler="init()" />
 
  </widget>

Sources

You can check the source files at this stage.

angularCNG/backoffice/resources/widgets/angularwidget/definition.xml

<widget-definition xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:noNamespaceSchemaLocation="http://www.hybris.com/schema/cockpitng/widget-definition.xsd"
                   id="example.cng.angular.widgets.angularwidget">
    <name>Angular Sample Widget</name>
    <description>Sample widget using AngularJS</description>
    <defaultTitle>AngularJS Sample Widget</defaultTitle>
    <author>hybris</author>
    <version>1.0</version>
    <view src="angularwidget.html" />
    <keywords>
        <keyword>AngularJS</keyword>
    </keywords>
    <sockets>
        <input id="input" type="java.lang.Object" />
        <output id="output" type="&lt;T&gt;" />
        <output id="stats" type="example.cng.angular.model.SearchStatistics" />
    </sockets>
    <settings>
        <setting key="enableModelEditing" type="java.lang.Boolean" default-value="false"/>
        <setting key="initialChanges" type="java.lang.String" default-value="{ &quot;globalOperator&quot; : &quot;AND&quot; }"/>
        <setting key="isTemplate" type="java.lang.Boolean" default-value="true"/>
    </settings>
</widget-definition>

angularCNG/backoffice/resources/cng/js/js-libraries.xml

<libraries xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:noNamespaceSchemaLocation="http://www.hybris.com/schema/cockpitng/libraries.xsd">

    <library>
        <name>angular</name>
    </library>

    <library protocol="RESOURCE" url="widgets-angular.js" /> <!-- this is a local library for extension-only use -->

    <library protocol="CLASSPATH" url="alerts.js" /> <!-- this is a dependency that can be defined by any other extension -->

</libraries>

angularCNG/resources/cockpitng/cng/alerts.js

const LEVEL_ERROR = "ERROR";
const LEVEL_WARNING = "Warning";
const LEVEL_INFO = null;

showMessage = function (level, message) {
    if (level === undefined || level == null) {
        alert(message);
    } else {
        alert(level + ": " + message);
    }
};

angularCNG/backoffice/resources/cng/js/widgets-angular.js

AngularCNG.init = function(widgetName, controller, services) {

    return new AngularCNG( "widgets." + widgetName ).controller(controller, services);

};

angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.html

<widget ng-controller="SampleAngularController">

    <h1 class="caption ${id}_spare" cng-local="label.title.hello"></h1>

    <h3 class="caption ${id}_spare">{{sessionInfo.customAttributes['name']}} ({{sessionInfo.currentUser}})! $labels.local.label.title.sockets</h3>
    <div ng-if="!input" cng-global="label.noinput"></div>
    <table ng-if="input">
        <tr>
            <td class="header" cng-local="label.socket.input"></td>
            <td class="header" cng-local="label.socket.output"></td>
        </tr>
        <tr>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in input" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
            <td>
                <table>
                    <tr ng-repeat="(key, value) in output" ng-class="{true:'even', false:'odd'}[$even]">
                        <td class="label">{{key}}</td>
                        <td class="value">{{value}}</td>
                    </tr>
                </table>
            </td>
        </tr>
    </table>

    <cng-event type="socket" id="input" handler="socketInput(socket, data)" />

    <hr class="${id}_spare"/>

    <div class="${id}_spare">
        <h3 class="caption" cng-local="label.title.model"></h3>
        <div cng-local="label.model.registered"></div>
        <table>
            <tr ng-repeat="(key, value) in model.changes" ng-class="{true:'even', false:'odd'}[$even]">
                <td class="label">{{key}}</td>
                <td class="value">{{value}}</td>
                #if ( $settings.enableModelEditing || $sessionInfo.admin )
                <td ng-click="removeModelChange(key)"><a href="#">X</a></td>
                #end
            </tr>
        </table>

        #if ( $settings.enableModelEditing || $sessionInfo.admin )
        <div cng-local="label.model.instruction"></div>
        <table>
            <tr class="even">
                <td class="label" cng-local="label.model.property"></td>
                <td class="value"><input type="text" ng-model="property"></td>
            </tr>
            <tr class="odd">
                <td class="label" cng-local="label.model.value"></td>
                <td id="value" class="value">{{input[property]}}</td>
            </tr>
            <tr class="even">
                <td class="label" cng-local="label.model.change"></td>
                <td class="value"><input type="text" ng-model="newValue"></td>
            </tr>
            <tr class="odd">
                <td colspan="2" class="commit"><button type="button" ng-click="commitModelChanges(property, newValue)" cng-local="label.model.commit"/></td>
            </tr>
        </table>
        #end

        <cng-event type="model" handler="modelChanged(model, property)" />

        <hr/>
    </div>

    <cng-event type="ready" handler="init()" />

</widget>

angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.js

SampleAngularController = function($scope) {

    $scope.socketInput = function(socketId, data) {
        showMessage(LEVEL_INFO,  $scope.adapter.cng().local("message.socket"));

        $scope.input = jQuery.extend(true, {}, data);
        var changes = $scope.model.changes;
        if (changes) {
            for(var key in changes) {
                if (typeof(data[key]) != "undefined") {
                    data[key] = changes[key];
                }
            }
        }
        $scope.output = data;
        $scope.adapter.cng().socketEvent("output", data);

        if (!$scope.model.statistics) {
            $scope.model.statistics = { $class: "example.cng.angular.model.SearchStatistics"}; // Notice! We explicitly point a Java class
            $scope.model.statistics.stats = {};
        }
        if (data.typeCode) {
            var stats = $scope.model.statistics.stats[data.typeCode];
            if (!stats) {
                stats = {};
                stats.typeCode = data.typeCode;
                stats.count = 0;
                $scope.model.statistics.stats[data.typeCode] = stats;
            }
            stats.count += 1;

            $scope.adapter.cng().model("statistics", $scope.model.statistics);
            $scope.adapter.cng().socketEvent("stats", $scope.model.statistics);
        }

        jQuery("." + $scope.adapter.cng().id() + "_spare").hide();
    };

    $scope.modelChanged = function(model, property) {
        showMessage(LEVEL_WARNING, $scope.adapter.cng().local("message.model") + ": " + property);

        $scope.property = null;
        $scope.newValue = null;
        $scope.model = model;
    };

    $scope.commitModelChanges = function(key, value) {
        var changes = $scope.model.changes;
        if (!changes) {
            changes = {};
        }
        changes[key] = value;

        $scope.adapter.cng().model('changes', changes);
    };

    $scope.removeModelChange = function(key) {
        var changes = $scope.model.changes;
        delete changes[key];

        $scope.adapter.cng().model('changes', changes);
    };

    $scope.init = function() {
        if (!$scope.model) {
            $scope.model = {};
        }
        $scope.statistics = $scope.adapter.cng().model("statistics");

        var settings = $scope.settings["initialChanges"];
        if (settings && !$scope.model.changes) {
            var changes = JSON.parse(settings);
            $scope.model.changes = changes;
            $scope.adapter.cng().model("changes", changes);
        }
    };

};

AngularCNG.init("angularwidget", SampleAngularController);

angularCNG/backoffice/resources/widgets/angularwidget/labels/labels.properties

label.title.sockets = Below, you may see what has been received through an input socket
label.title.hello = Hello! This example shows how to write widgets using 3rd party js libraries
message.socket = New socket message received

label.socket.input=Input
label.socket.output=Output

label.title.model = Here you may see, how a widget integrates with model
label.model.instruction = Type property name, to change its value
label.model.commit = Commit change
label.model.property = Property
label.model.value = Current value
label.model.change = New value
label.model.registered = Registered changes
message.model = Model changed

angularCNG/resources/angularCNG-backoffice-labels/labels_fr.properties

label.noinput = Rien ne s'est passé, j'attends le message...

angularCNG/backoffice/resources/widgets/angularwidget/labels/labels_fr.properties

label.title.sockets=Au-dessous, vous pouvez voir ce qui a été reçu via prise d'entrée.
label.title.hello=Salut! Cet exemple montre comment écrire widget en utilisant des librairies externes.
message.socket=Message réçu via prise d'entrée.

angularCNG/backoffice/resources/widgets/angularwidget/angularwidget.css

.yw-labelstyle
{
    color: red;
}

.caption {
    font-weight: bold;
    color: #13275a;
}

table {
    border: none;
    margin: 0;
}

.caption {
    font-weight: bold;
    color: #13275a;
}

table .header {
    text-align: center;
    font-weight: bold;
    background-color: #13275a;
    color: #c2e1ef;
}

table .label {
    text-align: right;
}

table .value {
    text-align: left;
}

table .even {
    background-color: #c4c4c4;
}

table .odd {
    background-color: #6699cc;
}

table .commit {
    text-align: center;
}

widget a {
    text-decoration: none;
    color: #303030;
}

widget a:hover {
    text-decoration: underline;
    color: #303030;
}

angularCNG/resources/angularCNG-backoffice-spring.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="angularCNGLabelLocator" class="com.hybris.cockpitng.util.labels.ResourcesLabelLocator" scope="singleton" init-method="init" lazy-init="false">
        <property name="location" value="/angularCNG-backoffice-labels/"/>
    	<property name="name" value="labels"/>
    </bean>

    <bean id="typeSearchStatisticsObjectMapperFactory" class="example.cng.angular.json.TypeSearchStatisticsMapperConfiguration" />

    <bean parent="abstractJSONMapperExtender" lazy-init="false" init-method="extend" destroy-method="clean">
        <property name="additionalValues">
            <list merge="true" value-type="com.hybris.cockpitng.json.ObjectMapperConfiguration">
                <ref bean="typeSearchStatisticsObjectMapperFactory" />
            </list>
        </property>
    </bean>

    <bean id="userNameSessionDecorator" class="example.cng.angular.session.UserNameSessionDecorator">
        <property name="userService" ref="userService" />
    </bean>

    <bean parent="abstractJsWidgetSessionInfoCollectorExtender" lazy-init="false" init-method="extend" destroy-method="clean">
        <property name="additionalValues">
            <list merge="true" value-type="com.hybris.cockpitng.util.js.JsWidgetSessionInfoDecorator">
                <ref bean="userNameSessionDecorator" />
            </list>
        </property>
    </bean>

</beans>