Using SVG with CSS3 and HTML5 — Supplementary Material

Example code and online extras for the O'Reilly Media book by Amelia Bellamy-Royds, Kurt Cagle, and Dudley Storey.

Importing SVG Assets, with AJAX

SVG was designed to support cross-references to other SVG files containing graphical assets—icons, filters, patterns, and so on—that you reuse in other graphics. However, browsers have been slow to implement support.

Furthermore, the SVG spec was written before CORS (cross-origin permissions) became an accepted part of web development. Even where browsers support cross-file references, they won’t usually support cross-origin references. (For more on CORS, see “Understanding CORS and SVG”.)

There are a few ready-made JavaScript libraries for importing SVG icons cross-referenced from <use> elements. We mention two in the book:

If either of these libraries meet your needs, you can skip the rest of this article.

But maybe you don’t want to use a library. Or maybe you need to work around one of the other browser bugs with externally-referenced content (such as no <style> support).

Whatever the reason, maybe you’re interested in coding up your own XMLHttpRequest for fetching your external SVG assets. In which case: read on.

Loading part of your website's data with JavaScript requests is known as AJAX, for Asynchronous JavaScript and XML. These days, you're as likely to load JSON data files as XML files, but the names AJAX and XMLHttpRequest live on. And because the methods were originally designed for working with XML data, they are naturally suited to loading SVG XML files.

Note

If you’re used to writing ES6+ JavaScript code—and in particular, using Promise chains—you might prefer to use the newer Fetch API instead of XMLHttpRequest. However, you’ll need an additional polyfill for older browsers (including most of the browsers that don’t support cross-file <use>).

The SVG-specific aspects of what to do with the file after you’ve fetched it would remain the same.

A full discussion of XMLHttpRequest belongs in a book on JavaScript and the DOM (or you could start on MDN), but we’re not going to use every feature. The following key points should help you use it to dynamically access an external SVG file:

Example 10-X1 presents a script to import SVG files used in <use>:

It can be used equally well in HTML as well as SVG files, but see the discussion after the code for important limitations.

Example 10-X1. Copying external SVG content into a document for reuse
window.addEventListener("load", function() {                 1
    var svgNS = "http://www.w3.org/2000/svg",
        xlinkNS = "http://www.w3.org/1999/xlink";

    var svg = document.getElementsByTagName("svg")[0];       2

    var extUse = document.querySelectorAll(
                    "use:not([*|href^='#'])"                 3
    );
    var extFiles = {};                                       4
    for (var i=0, n=extUse.length; i<n; i++) {
        var use = extUse[i];
        var ref = use.getAttributeNS(xlinkNS,"href")
                      .split("#");                           5

        var file = ref[0];
        if (!extFiles[file]){
            extFiles[file] = [];
        }
        extFiles[file].push({element:use, target:ref[1] });  6
    }

    for (var fileURL in extFiles) {
        var request = new XMLHttpRequest();
        request.addEventListener("load",
            createResponseHandler(request,
                    extFiles[fileURL])                       7
        );
        request.overrideMimeType("image/svg+xml");
        request.open("GET", fileURL);
        request.send();
    }

    function createResponseHandler(request, elementList) {
        return function() {                                  8
            var file = request.responseXML;

            var defs = document.createElementNS(svgNS, "defs");
            defs.appendChild(file.documentElement);
            svg.appendChild(defs);                           9

            for (var i=0, n=elementList.length; i<n; i++) {
                var o = elementList[i];
                o.element.setAttributeNS(xlinkNS, "href",
                        "#" + o.target );                    10
            }
        }
    }
});
1

The script runs after the main document loads, so that all the <use> elements have been processed.

2

The imported SVG content will be inserted as <defs> sections within the first <svg> element in the main file. For an SVG file, this will be the root element, but for an HTML file it may be an inline icon; since we’re only adding definitions, it doesn’t really matter which <svg> we add it to.

3

The complicated CSS selector "use:not([*|href^='#'])" finds all <use> elements that do not have local cross-reference values; in other words, they do point to external files. The [attribute^=string] structure selects elements where the value of that attribute starts with that string. The # character will be the start of the xlink:href attribute value for all local URL references. As discussed in Chapter 3, the [*|attribute] structure selects attributes of that name (href in this case) regardless of the XML namespace.

4

The script allows for the possibility that there may be more than one external SVG file; the different file URL strings will be stored as the property names in a JavaScript object.

5

For each <use> element with an external reference, the reference URL is split into the file URL and the target fragment.

6

For each unique file URL, an array is generated to hold the individual <use> elements that referenced it. For convenience, the use element is paired up with the target fragment part of the reference string; this saves us having to split the string a second time.

7

After processing all the <use> elements, an XMLHttpRequest is created for each file URL. Individual response-handler functions are created for each file, that will have access to the list of elements that reference that file. Note that the createResponseHandler() function itself is not the event listener; it is called immediately with the given parameters, and returns the actual listener function.

8

The anonymous function returned by createResponseHandler will have access to a preserved copy of the parameters, with their values at the time the function was created. In contrast, the original variables in the main function would have changed by the time the response handler was called.

9

After the file has loaded, and the listener runs, the entire document tree from the downloaded file is inserted into a new <defs> element. That definition block is inserted at the end of the first SVG in the main document.

10

The references to the external file are then replaced with local references to the same target IDs in the current, compound document.

The script in Example 10-X1 always imports the external files. This may seem unnecessary for the card suit web page from Example 10-2, since that web page displayed fine in many browsers. However, there is no reliable way to test whether or not the graphics displayed correctly, with styles from the external file. If the browser has already downloaded the external file because of the <use> references, the script should run fairly quickly.

That said, there are a number of possible complications to keep in mind when merging files like this:

Again, all these limitations are reasons to use a tested library instead of creating your own. But it’s also a good idea to know how it works, in case you need to adapt it.

One reason you might want to adapt the script is to download other external SVG assets: filters, masks, clip paths, gradients, and patterns. (At the time of writing, even new versions of Chrome only allow same-file references for most of these.) However, because these style effects can be applied with CSS, you can’t simply identify the relevant elements with an attribute selector.

Instead, you’ll probable want to provide the list of asset URLs directly. For example, you could include an array of URLs in a JavaScript object that your script can access, or you could use data-* attributes in your markup. The alternatives are to search every SVG element’s computed styles for external references (slow, and might overlook dynamic styles) or to parse all the CSS yourself (slow, and a lot of extra code).

The downside: since you’re not keeping track of individual references, you can’t easily update them. The references need to be written assuming that the AJAX import has already happened, and your assets are in the local document. The downside is that you’re not taking advantage of the few browsers (Firefox and MS Edge) that do support cross-file references for these assets.

One final option is to skip the AJAX altogether, and use server-side templating to inject your shared SVG markup, before your web page gets sent to the browser. If you’re already using a templating system to dynamically create your files on the server, this can save a lot of headache and performance impacts on the browser. If you have a lot of complex SVG graphics that are used on every page, you’ll need to consider the impact on transferred file size. But for most sites, the difference is small and can be minimized with proper file compression.