Using Angular in the backoffice - Some useful tips

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

It has been little over a year now that Umbraco 7 has been released. With the release of v7, the way we extend the backoffice has been moved from the serverside (Webcontrols, usercontrols, ...) to the frontend (Angular controllers and views).

If you haven't tried out Angular yet, now is a good time to add it to your developer toolbelt. 

In this post I will try to explain how to use the components already present in Umbraco to make your life easier when creating property editors or custom sections and trees with Angular.

Notifications

Sometimes you want to display additional information to the user using the built in notifications. You can do that by using the notifactionsService.

This service can be used in your controller by injecting notificationsService as a dependency in your controller.

Out of the box you can use four types of notifications:

  • success
  • warning
  • info
  • error

You can display a notification like this:

angular.module('umbraco').controller('MyController',
    function ($scope, notificationsService) {
        notificationsService.success("Success", "Speaker has been deleted");          
    });

Displaying a success notification

But you can create your own notification types. You can use the add method of the service for this. 

angular.module('umbraco').controller('MyController',
    function ($scope, notificationsService) {
        notificationsService.add({
						headline : 'Custom notification',
						message : 'My custom notification message',
						url: 'http://www.colours.nl',
						sticky : true,
						type : 'custom'
					});         
    });

Display a custom notification

Of course this notification needs to have some (ugly) styling applied. You can load a CSS file by adding it to your package manifest, or by using the assetsService.

The CSS will look like this : 

.alert-custom{
	background-color: purple;
	color: pink;
	border: 2px dotted green !important;
}

.alert-custom a{
	
	color: pink;
	
}

CSS for styling custom notification

But you can also have your own views displayed by the notificationsService. In the core you can see this with the unsaved changes dialog that pops up when you navigate to another item without saving first.

To show how you can use this yourself, I created a simple property editor that shows a dialog with a custom view when you leave a textbox and the contents is longer than 25 characters. The custom dialog will ask you if you want to trim your text and shows an example of the trimmed text.

 The property editor view :

<div ng-controller="NotificationEditorController">
	<input ng-model="model.value" ng-blur="showNotification()" type="text" class="umb-editor umb-textstring textstring" />
</div>

Property editor view

 The property editor controller :

// add ng-blur directive because this is missing in Angular 1.1.5
angular.module('umbraco.directives').directive('ngBlur', function() {
    return function (scope, elem, attrs) {
        elem.bind('blur', function () {
            scope.$apply(attrs.ngBlur);
        });
    };
});

angular.module('umbraco')
    .controller('NotificationEditorController',
	function($scope, notificationsService) {
	
		// function to trim text to a length of 25
		$scope.TrimText = function() {
			$scope.model.value = $scope.model.value.substring(0,25);
			};
	
		// function to show custom notification
		$scope.showNotification = function() {
			if($scope.model.value.length > 25) {
				notificationsService.add({ 
					// the path of our custom notification view
					view: "/App_Plugins/CustomNotification/notification.html", 
					// arguments object we want to pass to our custom notification
					args: { 
						value : $scope.model.value, 
						callback : $scope.TrimText 
						} 
					});
				}
			};
	});	

Property editor controller

As you can see in the above example, we use the add method of the notificationsService to render our own view by setting the view property. We also pass an args object which contains our value and a callback function that we are going to call from our notification. You can decide what the args object consists of.

The notification view :

<div ng-controller="NotificationController">
	<h4>Your Text is too long</h4>
	<p>This can have unwanted visual bugs on your page. Do you want to trim the text ?</p>
	<p>Trimmed text : {{trimmedtext}}</p>
	<button class="btn btn-warning" ng-click="cancel(notification)">No</button>
	<button class="btn" ng-click="trim(notification)">Yes</button>
</div>

Custom notification view

The notification controller :

angular.module('umbraco')
    .controller('NotificationController',
	function($scope, notificationsService) {
		// the notification is set on scope by umbraco, so we can access our args object passed in
		$scope.trimmedtext = $scope.notification.args.value.substring(0,25);
	
		$scope.trim = function(not){
			// call our callback function set on the args object in our property editor controller
			not.args.callback();				
			notificationsService.remove(not);
		};

		$scope.cancel = function(not){
			notificationsService.remove(not);
		};
	});	

Custom notification controller

As you can see here there is a notification object set on the scope. This is handled by Umbraco. The notification object contains the args object that we passed to the view in our property editor controller. When the user clicks the yes button we call our callback function from our property editor controller so it gets executed on the scope of our property editor.

Dialogs

Umbraco comes out of the box with a set of dialogs you can use in your editors. For example mediaPicker, contentPicker, memberPicker, ... But you can also show your own views in a dialog. 

To use the dialogs you need to inject dialogService as a dependency on your controller.

As an example I made a property editor for managing contact details. It allows you to select an image for the contact person and edit details like name, email and phone number. 

<div ng-controller="ContactEditor.Controller">
	<div>
		
		<div ng-show="thumbnail != ''">
			<umb-image-thumbnail 
				width="{{width}}"
				height="{{height}}"
				src="thumbnail"			
				max-size="100"	
				/>
		</div>
		<div ng-show="model.value.image == undefined || model.value.image == ''">
			<i class="icon icon-add blue"></i>
		        <a href="#" ng-click="pickImage()" prevent-default="">
		            <localize key="general_add" class="ng-isolate-scope ng-scope">Add</localize>
		        </a>
		</div>
		<div ng-show="model.value.image">			
				<i class="icon icon-delete red"></i>
		        <a href="#" ng-click="removeImage()" prevent-default="">
		            <localize key="general_delete" class="ng-isolate-scope ng-scope">Remove</localize>
		        </a>
		</div>
	</div>
	<div>

		<div>
			<div>
				Name : {{model.value.details.name}}
			</div>
			<div>
				Email : {{model.value.details.email}}
			</div>
			<div>
				Phone : {{model.value.details.phonenumber}}
			</div>
			<i class="icon icon-edit blue"></i>
		        <a href="#" ng-click="openEditDialog()" prevent-default="">
		           Edit
		        </a>
		</div>
	</div>
	
</div>

Property editor view

In the view I make use of the umb-image-thumbnail directive. This allows you to show a resized or cropped image in the backend. In my case I want to show an image with max width or height of 100px.

Another directive I'm using is the localize directive, which takes a key and retrevies a localized text from the Umbraco language XML files. The key has to be in the format area_keyalias from the language file.

angular.module('umbraco')
    .controller('ContactEditor.Controller',
	function($scope, dialogService, entityResource) {
		
		// if model value is empty, create empty scope object
		if($scope.model.value == ''){
			$scope.model.value = {};
			$scope.model.value.image = '';
			$scope.model.value.details = {
				email : '',
    			name : '',
    			phonenumber : ''
    			};	
    		$scope.thumbnail = '';	
    		$scope.width = 0;
    		$scope.height = 0;
			}
		else {
			// if existing scope has no image, create empty object
			if($scope.model.value.image == ''){
				$scope.thumbnail = '';	
	    		$scope.width = 0;
	    		$scope.height = 0;
			}
			else {
				// if we have a image id, call information about media item using media resource
				entityResource.getById($scope.model.value.image, "Media").then(function (ent) {
					console.log(ent);
					$scope.thumbnail = ent.metaData.umbracoFile.Value;
					$scope.width = ent.metaData.umbracoWidth.Value;
					$scope.height = ent.metaData.umbracoHeight.Value;					
				});
			}
		}

		$scope.pickImage = function() {
			// open the build in mediapicker
			dialogService.mediaPicker({
				multiPicker : false, // only allow one image to be picked
				// function that is called when the dialog is closed. 
				// Selected item(s) will be passed in by the data object
				callback : function(data) { 					
					$scope.model.value.image = data.id;
					$scope.thumbnail = data.thumbnail;
					$scope.width = data.originalWidth;
					$scope.height = data.originalHeight;
					
				}
			});
		};

		$scope.openEditDialog = function () {
			// open a custom dialog
            dialogService.open({
            	// set the location of the view
                template: "/App_Plugins/ContactEditor/dialog.html",
                // pass in data used in dialog
                dialogData: $scope.model.value.details,
                // function called when dialog is closed
                callback: function (value) {
                    if (value != null && value != '') {                      
                        $scope.model.value.details.name = value.name;   
                        $scope.model.value.details.email = value.email;                    
                        $scope.model.value.details.phonenumber = value.phonenumber;
                    }
                }
            });
        };

		$scope.removeImage = function() {
			$scope.thumbnail = '';
			$scope.model.value.image = '';			
		};		
	});

Property editor controller

In the controller we have a function called pickImage. This opens the built in mediaPicker. We pass in an options object telling the picker to disable multiselect and a callback function to execute when the user selects an item in the dialog.

The function openEditDialog opens a dialog showing a view we created. We pass in the path of our view in the template parameter. The data we want to edit gets passed to the view using the dialogData parameter. The callback function is a function we call when the data in the dialog is submitted.

 

<div ng-controller="ContactEditorDialog.Controller">
    <form name="dialogForm" ng-submit="submit(model)">
        <umb-panel>
            <div class="umb-panel-body no-header umb-scrollable with-footer" >
                <umb-pane>
                    <umb-control-group label="Name">
                        <input type="text" name="name" ng-model="model.name" required />
                    </umb-control-group>
                    <umb-control-group label="E-mail">
                        <input type="email" name="email" required ng-model="model.email" />
                    </umb-control-group>
                    <umb-control-group label="Phone Number">
                        <input type="text" name="phone" ng-model="model.phonenumber" />
                    </umb-control-group>
                </umb-pane>
            </div>
            <div class="umb-panel-footer">
                <div class="btn-toolbar umb-btn-toolbar pull-right">
                    <a class="btn btn-link" ng-click="close()">
                        <localize key="cancel" />
                    </a>
                    <button type="submit" class="btn btn-primary" ng-disabled="!dialogForm.$valid">
                        <localize key="buttons_save">Save</localize>
                    </button>
                </div>
            </div>
        </umb-panel>
    </form>
</div>

Dialog view

 

angular.module('umbraco')
    .controller('ContactEditorDialog.Controller',
    function($scope) {
    	$scope.model = {
    		name : $scope.dialogData.name,
    		email : $scope.dialogData.email,
    		phonenumber : $scope.dialogData.phonenumber
    	}    	    	
    });

Dialog controller

The dialogData object on the scope of our controller is the one we set in the editor controller. We can now use this data in our dialog.

Reusing existing datatypes

If you ever tried using reusing existing datatype editors in Umbraco V6 or earlier you know that is almost a impossible mission. 

Luckily in V7 this has become very easy. 

Before I continue, I want you to read this post by Markus Johansson about re-using the Rich Text Editor in a custom section. He explains how the umb-editor directive comes in to play and what the best way is to get this working.

The only problem with the approach from Markus is that you have to find out the config of the editor by taking a deep dive in the Umbraco source code.

I knew this could be done easier because packages like Vorto and Archetype are also using existing datatypes. Looking at the source code of Vorto I saw an API controller was created to get the config from the Umbraco datatype. 

Have a look at the source code of the API controller I created for my talk at the Umbraco UK festival. It allows you to get all the information needed by the umb-editor directive. 

For the UmbUkFest demo I created an angular resource that handles all calls to this API. The resource can be injected as a dependency into your controllers.

Below you find the code needed to use the Richtext editor datatype in your property editor or custom section once you have the API and the angular resource in place.

datatypeResource.getByName('Richtext editor').then(function (result) {
  $scope.rteField = [
    {
      alias: 'rtefield',
      label: 'Text',
      view: result.data.view,
      config: result.data.config,
      value: $scope.model.rtevalue
      }
    ];
});

$scope.$watch('rteField', function () {
  if ($scope.rteField != undefined) {
    $scope.session.description = $scope.rteField[0].value;
  }
}, true);

Calling the datatype resource to get the Rich Text editor datatype configuration

 The first part is where we call the resource to get the config from the API and store the result in a JavaScript Array. 

The second part is a watch on the array so that we are sure our model gets updated, when something in the Richtext editor is changed by the user.

To render this we just need the following HTML in our view:

<div ng-repeat="editor in rteField">
	<umb-editor model="editor"></umb-editor>
</div>

The html needed to render our Richtext editor

Conclusion

So if you haven't tried Angular yet, use the holidays to get to know the basics.

You don't need to be an angular expert to extend the Umbraco Backend. 

And the last tip I have is:

Use the source, Luke

You can find all the angular related code in the Umbraco.Web.UI.Client folder and it is well documented.

Happy ng-holidays !!!

Dave Woestenborghs

Dave is on Twitter as