Philip Hendry's Blog

AngularJS Directive Techniques Learnt from a TinyMCE Directive

Jun 2, 2014 • AngularJS • 3 min read

I’ve been looking at a TinyMCE directive that made me realise there are a number of techniques used there that would be useful to document. The directive is a great example of producing clean interfaces between AngularJS and other controls.

So, in no particular order, here are the techniques:

Configuration Options as a JSON Object

There are a number of techniques for passing options to the directive including adding attributes to the HTML element the directive is declared on. However, this particular technique wants to pass a JSON object through to the directive therefore it’s passed as the parameter to the directive:

<textarea ui-tinymce="theoptions" />

These options can be set in a controller:

function aController($scope) {
    $scope.theoptions = {
        menubar: false,
        statusbar: false
    };
}

and then accessed from within the directive:

if (attrs.uiTinymce) {
    expression = scope.$eval(attrs.uiTinymce);
} else {
    expression = {};
}

Configuration Option Overrides

Taking that technique above further, the settings themselves can be defined in multiple areas and, using angular.extend(), merged together. The options are:

angular.module('ui.tinymce', [])
       .value('uiTinymceConfig', {
            menubar: false,
            statusbar: false
       });

Finally the options are merged and passed to TinyMCE:

angular.extend(options, uiTinymceConfig, expression);
setTimeout(function() { 
    tinymce.init(options); 
});

Automatically Generating Ids

This is the first time I’d seen this technique - TinyMCE was used being asked to configure itself against textarea elements with specific IDs. However, if the element with the directive does not have one, one will be generated:

if (!attrs.id) {
    attrs.$set('id', 'uiTinymce' + generatedIds++);
}

Updating the View from a TinyMCE Callback

This technique defines a function that is called from the TinyMCE callbacks:

updateView = function() {
    ngModel.$setViewValue(elm.val());
    if (!scope.$root.$$phase) {
        scope.$apply();
    }
}

It’s using &dollar;setViewValue to write the value from TinyMCE back on to the model. It’s drummed in to you that calling &dollar;apply() is necessary after updating the model but what I’ve not seen much of is the call to scope.$root.$$phase that checks that we aren’t already in the digest loop. Given AngularJS is going to keep processing the digest loop until everything is gone the model update will be picked up.

Hooking into the View Update

Given this directive takes over the rendering of content it must hook into the render request from AngularJS. &dollar;render does exactly that and in ui-tinymce it’s used to copy the data from the model into the TinyMCE control:

ngModel.$render = function() {
    if (!tinyInstance) {
        tinyInstance = tinymce.get(attrs.id);
    }
        if (tinyInstance) {
            tinyInstance.setContent(ngModel.$viewValue || '');
    }
};

Destroying Content

Finally the normal request to the scopes &dollar;on method to respond to the request to &dollar;destroy the control:

scope.$on('$destroy', function() {
    if (!tinyInstance) { tinyInstance = tinymce.get(attrs.id); }
    if (tinyInstance) {
        tinyInstance.remove();
        tinyInstance = null;
    }
});
Post by: Philip Hendry