Including SVG template in Angularjs directive

2019-01-07 15:51发布

问题:

<div ng-app="testrectApp">
<svg>
    <rect height="10" width="10" style="fill: #00ff00" />
    <testrect />
</svg>
</div>

And this is my directive

module.directive('testrect', function() {
    return {
        restrict: 'E',
        template: '<rect top="20" left="20" height="10" width="10" style="fill: #ff00ff" />',
        replace: true
    };
});

But this is what the element ends up looking like in the browser. The fill twice is not a typo.

<rect top="20" left="20" height="10" width="10" style="fill: #ff00ff;fill: #ff00ff"></rect>

Here's a jsfiddle http://jsfiddle.net/RG2CF/

Is what I'm trying to do not possible or am I doing something wrong?

Thanks

EDIT: I should add that the issue may have to do with the fact that the template svg rect is namespaced to xhtml instead of svg, but I'm unsure of how I can force this or namespace it back to svg, if that is actually the 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.



回答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
  };
});


回答3:

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?



回答4:

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.



回答5:

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);