Including SVG template in Angularjs directive

Solution 1:

There is a templateNamespace property you can set to svg:

module.directive('testrect', function() {
    return {
        restrict: 'E',
        templateNamespace: 'svg',
        template: '<rect .../>',
        replace: true
    };
});

Here is a link to the documentation.

Solution 2:

Angular 1.3 Update!

My original answer below was a giant hack at best. What you should really do is update to Angular 1.3 and set the templateNamespace:'svg' property on your directive. Look below at @Josef Pfleger answer for an example: Including SVG template in Angularjs directive


Old Answer

In case anyone comes across this in the future, I was able to create a compile function that works with svg templates. Its a bit ugly right now, so refactoring help is much appreciated.

live example: http://plnkr.co/edit/DN2M3M?p=preview

app.service('svgService', function(){

    var xmlns = "http://www.w3.org/2000/svg";
    var compileNode = function(angularElement){
      var rawElement = angularElement[0];

      //new lines have no localName
      if(!rawElement.localName){
        var text = document.createTextNode(rawElement.wholeText);
        return angular.element(text);
      }
      var replacement = document.createElementNS(xmlns,rawElement.localName);
      var children = angularElement.children();

      angular.forEach(children, function (value) {
        var newChildNode = compileNode(angular.element(value));
        replacement.appendChild(newChildNode[0]);
      });

      if(rawElement.localName === 'text'){
        replacement.textContent = rawElement.innerText;
      }

      var attributes = rawElement.attributes;
      for (var i = 0; i < attributes.length ; i++) {
        replacement.setAttribute(attributes[i].name, attributes[i].value);
      }

      angularElement.replaceWith(replacement);

      return angular.element(replacement);
    };

    this.compile = function(elem, attrs, transclude) {
      compileNode(elem);

      return function postLink(scope, elem,attrs,controller){
//        console.log('link called');
      };
    }
  })

use case:

app.directive('ngRectAndText', function(svgService) {
  return {
    restrict: 'E',
    template: 
    '<g>'
    + '<rect x="140" y="150" width="25" height="25" fill="red"></rect>'
    + '<text x="140" y="150">Hi There</text>'
    + '</g>'
    ,
    replace: true,
    compile: svgService.compile
  };
});

Solution 3:

Using an innerHTML shim for SVG (aka "innerSVG") provides another piece of this puzzle, as an alternative to Jason More's insightful answer. I feel this innerHTML/innerSVG approach is more elegant because it does not require a special compile function like Jason's answer, but it has at least one drawback: it does not work if "replace: true" is set on the directive. (The innerSVG approach is discussed here: Is there some innerHTML replacement in SVG/XML?)

// Inspiration for this derived from a shim known as "innerSVG" which adds
// an "innerHTML" attribute to SVGElement:
Object.defineProperty(SVGElement.prototype, 'innerHTML', {
  get: function() {
    // TBD!
  },
  set: function(markup) {
    // 1. Remove all children
    while (this.firstChild) {
      this.removeChild(this.firstChild);
    }

    // 2. Parse the SVG
    var doc = new DOMParser().parseFromString(
        '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">'
        + markup
        + '</svg>',
        'application/xml'
    );

    // 3. Import the parsed SVG and grab the childNodes
    var contents = this.ownerDocument.importNode(doc.documentElement, true).childNodes;

    // 4. Append the childNodes to this node
    // Note: the following for loop won't work because as items are appended,
    // they are removed from contents (you'd end up with only every other
    // element getting appended and the other half remaining still in the
    // original document tree):
    //for (var i = 0; i < contents.length; i++) {
    //  this.appendChild(contents[i]);
    //}
    while (contents.length) {
      this.appendChild(contents[0]);
    }
  },
  enumerable: false,
  configurable: true
});

Here's a Plunker to try: http://plnkr.co/edit/nEqfbbvpUCOVC1XbTKdQ?p=preview

The reason why this works is: SVG elements live within the XML namespace, http://www.w3.org/2000/svg, and as such, they must either be parsed with DOMParser() or instantiated with createElementNS() (as in Jason's answer), both of which can set the namespace appropriately. It would be nice if this process of using SVG was as easy as HTML, but unfortunately their DOM structures are subtly different which results in the need to supply our own innerHTML shim. Now, if we could also get "replace: true" to work with this, that will be something good!

EDIT: Updated the appendChild() loop to workaround contents changing while loop runs.

Solution 4:

Currently SVG tags don't support dynamic addition of tags, at least in Chrome. It doesn't even display the new rect, nor would it even if you just added it directly with the DOM or JQuery.append(). Have a look at just trying to do it with JQuery

// this doesn't even work right. Slightly different error than yours.
$(function() {
    $('svg').append('<rect top="20" left="20" height="10" width="10" style="fill: #ff00ff" />');
});

My guess is you're going to see some really crazy behavior no matter what you do in this manner. It seems like it's an issue with the browser, and not so much Angular. I'd recommend reporting it to the Chromium project.

EDIT:

It looks like it might work if you add it via the DOM 100% programmatically: How to make Chrome redraw SVG dynamically added content?