16

I have some code that retrieves a scripted svg image from a server via Ajax. I can get the image text back into the browser, but I can't find a way to insert it into the DOM that will actually display it. Can anyone help with this? The svg looks likes this:

<svg id="chart" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<script type="application/ecmascript">
<![CDATA[
...lots of code, changes on each Ajax request
//]]>
</script>
<script type="application/ecmascript" xlink:href="js-on-server-1.js"/>
<script type="application/ecmascript" xlink:href="js-on-server-2.js"/>
</svg>

I've tried various things. If I do this:

// xmlhttp.onreadystatechange:
addImage(xmlhttp.responseXML, "somewhere");
...
function addImage(txt, dst_id) {
   var scr = document.createElement("div");

   if("textContent" in scr)
      scr.textContent = txt;  // everybody else
   else
      scr.text = txt;         // IE

   document.getElementById(dst_id).appendChild(scr);
}

Then Opera and Chrome do nothing, and F/F complains "[object XMLDocument]". If I change 'responseXML' to 'responseText', then Opera/Chrome correctly display the entire svg text (not image) in the right place, and F/F still gives the same warning. I've also tried assigning the response to an innerHTML, but that does nothing. Any ideas? Thanks.

EDIT

In response to Phrogz'z answer below - I've added two simple svg files. The first is a 'standard' simple svg, displaying a circle. The second is a scripted svg, displaying a rectangle. You should be able to view both directly in any browser, except IE8-. If I edit Phrogz'z code to use the circle file (replace 'stirling4.svg' with the name of this file), then it works, but if I want the scripted rectangle instead, it doesn't. Tested on F/F, Opera, Chromium, but doesn't work anyway on (my) Chromium.

File 1, circle:

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
</svg>

File 2, rectangle:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)">
<script type="application/ecmascript">
<![CDATA[
var svgDocument;
var svgns = "http://www.w3.org/2000/svg";
function init(evt) {
  if(window.svgDocument == null)
    svgDocument = evt.target.ownerDocument;
   var lbox = svgDocument.createElementNS(svgns, "rect");
   lbox.setAttributeNS(null, "x",                10);
   lbox.setAttributeNS(null, "y",                10);
   lbox.setAttributeNS(null, "width",            30);
   lbox.setAttributeNS(null, "height",           30);
   lbox.setAttributeNS(null, "stroke",           "#8080ff");
   lbox.setAttributeNS(null, "stroke-width",     2);
   lbox.setAttributeNS(null, "fill-opacity",     0);
   lbox.setAttributeNS(null, "stroke-opacity",   1);
   lbox.setAttributeNS(null, "stroke-dasharray", 0);
   svgDocument.documentElement.appendChild(lbox);
}
//]]>
</script>
</svg>

Presumably the answer is to get the script into the header??

EML
  • 9,619
  • 6
  • 46
  • 78
  • Try: `addImage(xmlhttp.response, "somewhere");` ? Which content-type of your html document: _text/html_, _application/xhtml+xml_ ? – Andrew D. Nov 02 '11 at 13:42
  • What is the doctype and attributes of the documentElement you are trying to insert the SVG into? – stephband Nov 02 '11 at 13:44
  • (1) The test page originally had no doc or content types. I've now added DOCTYPEs for XHTML 1.0 Transitional, and XHTML 1.1, with no change. (2) I also tried explicit text/html, and application/xhtml+xml, with no change. (3) I've just tried 'xmlhttp.response': no change from F/F, but Chrome&Opera now say output 'undefined' rather than the svg text. (4) I'm inserting the SVG into an empty span: . Thanks. – EML Nov 02 '11 at 14:20
  • I'm starting to think that I need to read this as XML (responseXML), importNode, and then appendChild. I'll give this a go in a while... – EML Nov 02 '11 at 15:43
  • Here's a simple example that [fetches and embeds SVG in SVG](http://phrogz.net/SVG/fetch_fragment.svg). I'll create an SVG into HTML sample for you shortly. – Phrogz Nov 02 '11 at 17:02

1 Answers1

26

In general, the problem is twofold threefold:

  1. HTML is not XHTML, and support for SVG in HTML is shoddy and poorly-defined as of this writing. The solution is to use a real XHTML document where SVG-namespaced elements are actually treated as SVG.

  2. The responseXML is in another DOM document, and you can't normally just move nodes from one document to another. You are supposed to use document.importNode to import a node from one document to another.

  3. Loading an SVG file with onload event handlers will not have those handlers invoked by either creating the node or appending it to the document. Code inside the script block will be run, however, so you need to rewrite your scripts in a manner that works standalone and also with the dynamic loading.


Here's a simple example that works in Chrome, Safari, and Firefox...but not IE9:

var xhr = new XMLHttpRequest;
xhr.open('get','stirling4.svg',true);
xhr.onreadystatechange = function(){
  if (xhr.readyState != 4) return;
  var svg = xhr.responseXML.documentElement;
  svg = document.importNode(svg,true); // surprisingly optional in these browsers
  document.body.appendChild(svg);
};
xhr.send();

See it in action here: http://phrogz.net/SVG/import_svg.xhtml


Unfortunately IE9 does not properly support document.importNode. To work around this, we write our own cloneToDoc function that creates an equivalent structure for any given node by recursively crawling the hierarchy. Here's a full working example:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head> 
  <meta http-equiv="content-type" content="application/xhtml+xml;charset=utf-8"/>
  <title>Fetch and Include SVG in XHTML</title>
  <script type="text/ecmascript"><![CDATA[
    setTimeout(function(){
      var xhr = new XMLHttpRequest;
      xhr.open('get','stirling4.svg',true);
      xhr.onreadystatechange = function(){
        if (xhr.readyState != 4) return;
        var svg = cloneToDoc(xhr.responseXML.documentElement);
        document.body.appendChild(svg);
      };
      xhr.send();
    },1000);
    function cloneToDoc(node,doc){
      if (!doc) doc=document;
      var clone = doc.createElementNS(node.namespaceURI,node.nodeName);
      for (var i=0,len=node.attributes.length;i<len;++i){
        var a = node.attributes[i];
        if (/^xmlns\b/.test(a.nodeName)) continue; // IE can't create these
        clone.setAttributeNS(a.namespaceURI,a.nodeName,a.nodeValue);
      }
      for (var i=0,len=node.childNodes.length;i<len;++i){
        var c = node.childNodes[i];
        clone.insertBefore(
          c.nodeType==1 ? cloneToDoc(c,doc) : doc.createTextNode(c.nodeValue),
          null
        ); }
      return clone;
    }
  ]]></script>
</head><body></body></html>

See it in action here: http://phrogz.net/SVG/import_svg_ie9.xhtml


Edit 2: As suspected, the problem is that the onload event does not fire when dynamically adding script. Here's a paired solution that works:

  1. Rewrite your script to remove the onload event handler. Instead, trust that document exists.
  2. Rewrite your script to ask for a global svgRoot; if it doesn't exist, use document.documentElement.
  3. When fetching the SVG set a global svgRoot to the new svg element after you import it into the document.

Here's the code in action:

And, in case my site is down, here is the code for posterity:

script-created.svg

<svg xmlns="http://www.w3.org/2000/svg">
  <script type="text/javascript"><![CDATA[
    function createOn( root, name, a ){
      var el = document.createElementNS(svgNS,name);
      for (var n in a) if (a.hasOwnProperty(n)) el.setAttribute(n,a[n]);
      return root.appendChild(el);
    }
    // Trust someone else for the root, in case we're being
    // imported into another document
    if (!window.svgRoot) svgRoot=document.documentElement;
    var svgNS = svgRoot.namespaceURI;
    createOn(svgRoot,'rect',{
      x:10, y:10, width:30, height:30,
      stroke:'#8080ff', "stroke-width":5,
      fill:"none"
    });
  ]]></script>
</svg>

import_svg_with_script.xhtml

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head> 
  <meta http-equiv="content-type"
        content="application/xhtml+xml;charset=utf-8" />
  <title>Fetch and Include Scripted SVG in XHTML</title>
  <script type="text/ecmascript"><![CDATA[
    setTimeout(function(){
      var xhr = new XMLHttpRequest;
      xhr.open('get','script-created.svg',true);
      xhr.onreadystatechange = function(){
        if (xhr.readyState != 4) return;
        var svg = xhr.responseXML.documentElement;
        svg = cloneToDoc(svg);
        window.svgRoot = svg; // For reference by scripts
        document.body.appendChild(svg);
        delete window.svgRoot;
      };
      xhr.send();
    },1000);
    function cloneToDoc(node,doc){
      if (!doc) doc=document;
      var clone = doc.createElementNS(node.namespaceURI,node.nodeName);
      for (var i=0,len=node.attributes.length;i<len;++i){
        var a = node.attributes[i];
        if (/^xmlns\b/.test(a.nodeName)) continue; // IE can't create these
        clone.setAttributeNS(a.namespaceURI,a.nodeName,a.nodeValue);
      }
      for (var i=0,len=node.childNodes.length;i<len;++i){
        var c = node.childNodes[i];
        clone.insertBefore(
          c.nodeType==1 ? cloneToDoc(c,doc) : doc.createTextNode(c.nodeValue),
          null
        )
      }
      return clone;
    }
  ]]></script>
</head><body></body></html>
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Hi Phrogz - unfortunately, I think there's a couple of issues here. I can't get this to run on Chromium (8.0.552.237 (70801) Ubuntu 10.10) - are you sure it's Ok on Chrome? The next issue is trickier - it doesn't work for scripted svg. I've added two examples in my original post to replace your 'stirling4.svg'. Thanks. – EML Nov 02 '11 at 21:25
  • @user785194 Yes, both examples work perfectly in Chrome v15. I'll investigate loading an SVG that creates its content in script directly. I'm assuming the problem is that you're using the `onload` attribute to trigger your script, and this is not firing. – Phrogz Nov 02 '11 at 21:48
  • Also, when you say "I can't get this to run on Chromium" it would be far better for you to include details. What happens? Do you see any errors in the Console? – Phrogz Nov 02 '11 at 22:23
  • @user785194 I've edited my answer with a working solution (tested only on Safari v5 and Firefox v6, but should work everywhere). – Phrogz Nov 03 '11 at 04:32
  • Great! Nearly there. I'm getting nasty problems with more complex svg files, which I can fix by taking out your 'delete.window.svgRoot'. Can you tell me what's going on here? – EML Nov 03 '11 at 15:41
  • `delete window.svgRoot` (note only one period) is just cleanup, removing the global `svgRoot` property from the window after appending the file. If you have delayed scripts, perhaps you want to leave that in. – Phrogz Nov 03 '11 at 16:25
  • Yes, typo on my part. Commenting out the (correct) line fixes various issues with more complex svg files. One obvious problem in my case was that svgNS is wrong (and svgRoot.namespaceURI if checked in the debugger). It's set to "http://www.w3.org/1999/xhtml" instead of "http://www.w3.org/2000/svg". This is a complex svg which displays correctly without errors when stand-alone, but fails with stupid errors (nodes aren't actually nodes) when loaded with this code. I get the same behaviour with F/F and Opera, and the file loads and displays Ok in both browsers with the line out. – EML Nov 03 '11 at 17:15
  • Hi @Phrogz, I was running into a similar issue, trying to append an SVG doc loaded via Ajax, using the 'pre-prepped' responseXML, not aware of the whole "responseXML is in a different document and you can't worry-free add a node from one doc to another" issue. Your solution worked for me, but then I found I can forego the responseXML and simply create a new document by parsing the responseText in a newly created DOMParser. It seems that at this point, the newly created svg document is part of my main document, and I don't have any issues appending like before. Does this sound legit? Thanks! – dontGoPlastic Mar 30 '12 at 15:13
  • 2
    @Phrogz Is there any chance we can get an update on this answer as it is relatively high viewed and seems outdated? – Zach Saucier Aug 02 '14 at 16:35
  • 1
    @ZachSaucier Just because it's old doesn't mean it's outdated. :) What new information would you have included? Note that Stack Overflow is wiki-like, and encourages users to improve an existing answer. – Phrogz Aug 03 '14 at 02:36
  • @Phrogz I was simply curious if there have been any updates since the last update. I'm not familiar enough with the content to know if it's outdated or not. If there's nothing to add then it's great! – Zach Saucier Aug 03 '14 at 02:45
  • @Phrogz It should be mentioned that the `cloneToDoc` workaround is needed for Firefox as well and not only for IE. It took me long to find that out. – Scindix Dec 21 '16 at 15:50