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.
angular doesn't pay attentention to namespaces when creating elements (though elements seem to be cloned with namepsace of a parent), so new directives inside <svg>
won't work without a custom compile function.
Something like the following can get around the issue by doing the creation of the dom elements itself. For this to work, you need to have xmlns="http://www.w3.org/2000/svg"
as an attribute on the root element of template
and your template must be valid XML (having only one root element).
directiveGenerator = function($compile) {
var ret, template;
template = "<g xmlns=\"http://www.w3.org/2000/svg\"> +
"<rect top=\"20\" left=\"20\" height=\"10\" width=\"10\" style=\"fill: #ff00ff\" />" +
"</g>";
ret = {
restrict: 'E',
compile: function(elm, attrs, transclude) {
var templateElms;
templateElms = (new DOMParser).parseFromString(template, 'application/xml');
elm.replaceWith(templateElms.documentElement);
return function(scope, elm, attrs) {
// Your link function
};
}
};
return ret;
};
angular.module('svg.testrect', []).directive('testrec', directiveGenerator);
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
};
});
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?
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.