Interactive SVG with AngularJS – Part 2

When developing mobile web applications with responsive design, SVG are a viable solution for flexible images.
AngularJS in turn enables the wrapping of complex UI logic into custom HTML directives, resulting in clean and maintainable modules.
The combination of these technologies provides a good basis for interactive control and status elements. It is suitable both for building highly complex custom controls, as well as covering simple use cases in a generic manner.

Part 1 of this article explores several methods of employing SVG as flexible images in a cross-browser compatible manner.

Part 2 describes the use of AngularJS to construct custom control and status elements by manipulating SVG images.

SVG Manipulation with AngularJS

When developing mobile web applications with responsive design, SVG are a viable solution for flexible images. They can scale to whatever dimensions a given display device requires, while their comparatively small file size helps to improve application loading time. SVG also support both styling via CSS and manipulation through the DOM API. This makes them particularly well suited for interactive control and status elements, enabling a single SVG to represent all phases of a hover/press/release or ok/warning/alert cycle.

However, such an SVG image requires a bit of additional preparation. For convenient DOM access, the image should

  • contain no unnecessary groupings, duplicate shapes, or similar obstructive artifacts
  • reflect the intended logical status/control structure as closely as possible in its internal document structure
  • provide a distinct XML id attribute for each of its logical parts.

In practice, the latter can be achieved easily in customary design software by assigning a name to the respective graphics primitive, or putting it on a named layer. When exporting the design to SVG, the name typically becomes an id attribute, or at least part of the attribute value.

JavaScript

The following example shows a simple “traffic light” status indicator. The SVG contains a filled circle as its sole logical part, with an id attribute for convenient DOM access:

<?xml version="1.0" encoding="UTF-8">
<?xml-stylesheet href="status.css" type="text/css">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 64 64">
  <circle id="status" fill="#888" cx="32" cy="32" r="32">
</svg>

Its external stylesheet defines a number of CSS classes, each representing one of the possible indicator states:

.normal  { fill: green }
.warning { fill: yellow }
.alert   { fill: red }

In order to use the SVG in a web application, the HTML document must link it via an object element. As discussed in part 1 of this article, this is the only sure method of making the SVG DOM accessible. To simplify matters, the example uses a button to actually trigger the status change:

< object id="mysvg" type="image/svg+xml" data="status.svg">
</object>

<input type="button" value="alert" onclick="setStatus('alert')"/>

Finally, the HTML document needs to embed or link some JavaScript containing the respective control logic. The implementation simply looks up the status element and sets an appropriate CSS class.

var oldStatus = "normal";
function setStatus(newStatus) {
  var statusElm = document.getElementById("mysvg")
    .getSVGDocument().getElementById("status");
  statusElm.classList.remove(oldStatus);
  statusElm.classList.add(newStatus);
  oldStatus = newStatus;
}

This approach certainly works for a simple example, but quickly gets out of hand when the SVG contains multiple functional parts, or requires sophisticated control logic. The situation is even worse when the web application contains multiple instances of the same interactive element. With increasing complexity, it becomes more and more difficult to keep track of all the right id attributes and callback function names. A single typo, and the whole construct falls apart.

AngularJS

Enter AngularJS. The framework aims at simplifying MVC-based web application development through a number of powerful features, such as dependency injection, automatic two-way data binding, and custom HTML directives. This makes it very easy to wrap complex UI manipulation and control logic into manageable components.

Consider the previous traffic light example, AngularJS style. The custom directive <my-status> wraps the correct SVG linking and its corresponding control logic, hiding all the intricate details. The single scope variable currentStatus connects status element and trigger button, a typical example of Angular’s declarative loose coupling.

<my-status watch="currentStatus"></my-status>
<input type="button" value="alert" ng-click="currentStatus='alert'"/>

The minimal definition of the custom directive consists of two essential parts. The first is a template, which replaces the <my-status> element at run time. The second is a link function, which prepares the control logic and wires it to a $watch condition listener.

myApp.directive("myStatus", function() {
  return {
    restrict: "E",
    replace: true,
    template: "< object type='image/svg+xml'
                 data='status.svg'></object>",
    link: function(scope, element, attrs) {
      var statusChanged = function(newValue, oldValue) {
        var statusElm = angular.element(element[0]
          .getSVGDocument().getElementById("status"));
        statusElm.removeClass(oldValue);
        statusElm.addClass(newValue);
      };
      scope.$watch(attrs.watch, statusChanged);
    }
  }
});

By default, AngularJS uses its own jqLite API for DOM manipulation. Unfortunately, it supports neither the getSVGDocument() nor getElementById() method. Thus the statusChanged() function first unwraps the element reference, then performs SVG access via the raw DOM API, and finally wraps the result with jqLite again for convenient browser-agnostic handling.

When running this minimal implementation, the browser may occasionally log an error during the SVG element lookup. This happens because browsers attempt to speed up page loading by asynchronously fetching and caching linked resources. If the browser has not finished loading the SVG document at the time Angular executes the link function, getSVGDocument() yields null and getElementById() fails. To rectify the situation, the original body of the link function can be wrapped into an init function, which either executes immediately or gets delayed as necessary.

link: function(scope, element, attrs) {
  var init = function() {
    var statusChanged = function(newValue, oldValue) {
      ...
    };
    scope.$watch(attrs.watch, statusChanged);
  };
  if (element[0].getSVGDocument()) {
    init();
  } else {
    element.on("load", init);
  }
}

This example directive implements a pure status element. However, a control directive would work in almost the same fashion. In order to use the filled circle in the reference SVG as a button, the init function could be changed to:

var init = function() {
  var statusElm = angular.element(element[0]
    .getSVGDocument().getElementById("status"));
  statusElm.on("click", function(event) {
    scope.$apply(function() {
      scope.currentStatus='alert';
    });
  });
};

Note that the scope modification occurs inside an event handler within the SVG DOM. The code has to use $apply, so Angular will notice the change and update its data bindings accordingly.

Generalization

The solution discussed so far will work for SVG-based status and control elements of arbitrary complexity. For simple cases however, it has the distinct disadvantage that each custom element requires its own directive – all virtually identical in function, just with different SVG resources and id attributes. In such cases, it would be far more elegant to define generic directives, leveraging Angular’s data binding and builtin conditionals. For example:

<svg-control href="{{mysvg}}">
  <svg-toggle href="#status" clazz="normal"
    ng-if="currentStatus==='cool'"></svg-toggle>
  <svg-toggle href="#status" clazz="warn"
    ng-if="currentStatus==='heating'"></svg-toggle>
  <svg-toggle href="#status" clazz="alert"
    ng-if="currentStatus==='hot'"></svg-toggle>
  <svg-handle href="#status"
    click="currentStatus=''"></svg-handle>
</svg-control>

The idea is for the outer <svg-control> directive to manage SVG linking, as in the previous monolithic example. However, it should be more flexible, accepting an Angular interpolation as well as an explicit URL as image resource. The inner directives then operate on named parts of the SVG image. Each <svg-toggle> applies a given CSS class to its SVG element; in combination with Angular’s ng-if conditional directive this provides the status indicator logic. <svg-handle> in turn hooks up a click handler to its SVG element for simple control logic.

The implementation of <svg-control> again works with a replacement template, which links the SVG via an object element. However, it is not possible to simply place an Angular interpolation into the data attribute. Browsers typically are quite aggressive when loading external resources, and will spring into action before Angular can resolve the interpolation value in the template. The special ng-attr- prefix compensates for this behavior. Consequently, a basic implementation might be:

myApp.directive("svgControl", function() {
  return {
    restrict: "E",
    replace: true,
    scope: { href: '@' },
    template: "< object type='image/svg+xml'
                 ng-attr-data='{{href}}'></object>"
  }
});

Unfortunately, this approach has two distinct drawbacks: First, it establishes an isolate scope, which is a problem later when the <svg-handle> directive needs to apply its handler expression. Second, browsers can be picky regarding modifications of link attributes like data; there is no guarantee that this actually will trigger a reload. A safer method is to use Angular’s ng-hide class for this purpose: The implementation initially hides the template, then explicitly changes the data attribute, and finally shows the resulting element.

myApp.directive("svgControl", function() {
  return {
    restrict: "E",
    replace: true,
    template: "< object type='image/svg+xml'
                 class='ng-hide'></object>",
     link: function(scope, element, attrs) {
      element.attr("data", attrs.href);
      element.removeClass("ng-hide"); // force reload
    }
  }
});

This approach loads the SVG image as expected. However, the template replaces the entire <svg-control> element, including its inner directives! Normally, this could be fixed with Angular’s transclude mechanism. But on an object element, the browser replaces all children with the embedded SVG DOM, so any transcluded directives would be lost again.
The only way to preserve them is a side by side placement. This in turn necessitates an extra <div> as a wrapper, since AngularJS restricts its templates to a single root element. Finally, the object element requires a width and height of 100%, so any styled size of the <svg-control> element affects the SVG viewport as well.
The new template changes the link function, which now must descend the hierarchy to find the object element for manipulation:

myApp.directive("svgControl", function() {
  return {
    restrict: "E",
    replace: true,
    transclude: true,
    template:
      "<div> \
        < object type='image/svg+xml' class='ng-hide' \
          height='100%' width='100%'></object> \
        <div ng-transclude></div> \
      </div>",
    link: function(scope, element, attrs, ctrl) {
      var obj = element.children().eq(0);
      obj.attr("data", attrs.href);
      obj.removeClass("ng-hide");
    }
  }
});

Controller

Having prepared the internal DOM structure, <svg-control> also needs some means of communication between the outer and inner directives. As usual in Angular, a controller provides the necessary API. It basically contains the two pieces of functionality already discussed in the monolithic example:
The resolve() function looks up a named part in the SVG, delivering it in a jqLite wrapper for convenient access.
The init() function handles linking code, either executing it immediately or deferring it until the SVG finished loading.
Finally, the internal ready() function does the necessary groundwork; it sets up the SVG reference for id lookup, then executes any deferred linking code.

controller: function($scope) {
  var svg = null;
  var deferred = [];
  this.init = function(fn) {
    if (svg) {
      fn($scope);
    } else {
      deferred.push(fn);
    }
  };
  this.resolve = function(href) {
    var id = href.replace("#", "");
    var dom = svg.getElementById(id);
    return dom ? angular.element(dom) : null;
  };
  this.ready = function(obj) {
    svg = obj[0].getSVGDocument();
    deferred.forEach(function(fn) {
      fn($scope);
    });
  };
}

The <svg-control> directive uses its link function to bootstrap the controller once the SVG document finished loading. For this purpose, it registers a "load" event handler on the object element, which calls the controller’s ready() function. Note that this registration happens before forcing SVG resource loading, so the event is guaranteed to fire as expected.

link: function(scope, element, attrs, ctrl) {
  var obj = element.children().eq(0);
  obj.on("load", function() { ctrl.ready(obj) });
  obj.attr("data", attrs.href);
  obj.removeClass("ng-hide");
}

Inner Directives

With the controller in place, it is time to implement the inner directives that augment the SVG image with status and control logic.

The implementation of the <svg-toggle> directive looks up the <svg-control> controller by require option. Thus it can use the resolve() function within its core toggle() function, to look up the target SVG element and apply the specified CSS class. The directive also uses the controller’s init() function for deferred initialization.
It calls the toggle() function once when the directive becomes active, and once again via event handler when its local scope is destroyed. This way, the directive is compatible with the DOM manipulation mechanism of Angular’s ng-if and ng-switch conditionals.

myApp.directive("svgToggle", function() {
  return {
    restrict: "E",
    require: "^svgControl",
    link: function(scope, element, attrs, ctrl) {
      var toggle = function() {
        ctrl.resolve(attrs.href).toggleClass(attrs.clazz);
      };
      ctrl.init(function() {
        toggle();
        scope.$on("$destroy", toggle);
      });
    }
  };
});

The implementation of the <svg-handle> directive is slightly more difficult. Unfortunately, it is not possible to simply graft Angular’s builtin event directives like ng-click onto SVG elements. Although the SVG DOM is embedded inside the HTML DOM, Angular does not consider it part of its ng-app template.
Thus <svg-handle> has to register its own "click" handler on the target SVG element. For this purpose, it again looks up the <svg-control> controller by require option, and uses its init() and resolve() functions in the proven manner.

app.directive("svgHandle", function($parse) {
  return {
    restrict: "E",
    require: "^svgControl",
    link: function(scope, element, attrs, ctrl) {
      ctrl.init(function() {
        ctrl.resolve(attrs.href).on("click",
          function(event) { 
            ...
          });
      });
    }
  };
});

Handling the specified click statement requires a bit of extra work. Fortunately, Angular includes the powerful if somewhat arcane $parse service. The directive can use it to compile the statement into an evaluation function, and call it later within the click handler. This function expects a context scope as first argument, and an object containing extra variables as second argument. The implementation uses the latter to emulate the execution environment of ng-click, with the original DOM event in the $event variable.
Providing the correct scope requires some diligence though. Remember that <svg-handle> is a transcluded element, so Angular automatically creates a new scope for it. However, the click statement must be evaluated in the scope of <svg-control> to work as expected. For such purposes the controller’s init() and ready() functions provide the parent scope as an optional argument to their initialization functions. The <svg-handle> directive can use it both as context for the evaluation function, and to notify Angular via $apply, as required for DOM handlers. The resulting link function is:

link: function(scope, element, attrs, ctrl) {
  var fn = $parse(attrs.click);
  ctrl.init(function(parentScope) {
    ctrl.resolve(attrs.href).on("click",
      function(event) {
        parentScope.$apply(function() {
          fn(parentScope, {$event:event});
        });
      });
  });
}

Of course, this solution need not be restricted to mouse clicks. The <svg-handle> directive could check for other control attributes as well. For each specified event type, it can pass an additional linking function to the init() function of its parent controller.

Conclusion

Using SVG and AngularJS, is is easy to enhance mobile web applications with interactive control and status elements. Complex pieces of UI logic become highly configurable custom directives, which can interact with the application environment via Angular’s intuitive data binding. On the other hand, a few generic directives suffice to accommodate the needs of simpler UI elements. The overall result is clean and maintainable code, combined with low resource requirements and a natural affinity for responsive design. What more could you want?

CAVEAT: All code was simplified for presentation purposes. Appropriate error handling and cross-browser compatibility is left as an exercise for the reader. 🙂

5 thoughts on “Interactive SVG with AngularJS – Part 2

  1. For svgHandle:
    require: “^svgInteractive” ?!? or   require: “^svgControl”.
    Thanks in advance.

  2. It should indeed be “^svgControl”, sorry about that, fixed it. On the svg-toggle, “clazz” is correct; it specifies the CSS class to apply when the ng-if conditional matches. Using the HTML standard attribute “class” would apply the CSS class to the svg-toggle element itself, not to the intended target element (“href”). See the directive definition “svgToggle” for details.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s