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:
-
You create an
XMLHttpRequest
object using the commandnew XMLHttpRequest()
. -
The object’s
open()
method allows you to define the request you are going to make. There are two required parameters to theopen()
method:-
the name of the HTTP method to use; this is
"GET"
for normal file access; -
the URL of the information you wish to retrieve.
-
-
The object’s
send()
method processes the request, sending it to the web server. ForGET
requests, there are no parameters. -
By default, the request is processed asynchronously, meaning that the rest of the script continues to run while waiting for the file to be loaded. (Synchronous requests are only allowed under limited circumstances, since they block all other scripts in the thread.)
-
In order to actually do something with the file you’re requesting, you must set a listener method to run after the request returns. The listener is set on the request object using the familiar
addEventListener()
method. There are five events you can listen for:-
"progress"
, for updates on the request and file download status; -
"load"
, for the successful download of the entire file; -
"error"
, for any error in requesting or downloading the file; -
"abort"
, for any event that cancels download; and, -
"loadend"
, for any of the load, error, or abort situations.
-
-
Add the event listeners, and set other attributes, before sending the request.
-
Once a file has been successfully loaded, it can be accessed—depending on the file format—as the
response
,responseText
, orresponseXML
property of the request object. For loading SVG files, we wantresponseXML
, which returns a fully-parsed XML or HTML document (DOM tree). -
The browser will usually figure out the response file type automatically, based on information from the web server; however, you can ensure that the SVG is parsed correctly by calling the
overrideMimeType()
method (with the value"image/svg+xml"
) prior to sending the request.
Example 10-X1 presents a script to import SVG files used in <use>
:
-
identify all external file references in
<use>
elements in the current document, -
access the files with
XMLHttpRequest
, -
copy the file’s contents into
<defs>
definition blocks in the current document, -
update all the external references to point to the embedded definitions.
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
(
)
{
var
svgNS
=
"http://www.w3.org/2000/svg"
,
xlinkNS
=
"http://www.w3.org/1999/xlink"
;
var
svg
=
document
.
getElementsByTagName
(
"svg"
)
[
0
]
;
var
extUse
=
document
.
querySelectorAll
(
"use:not([*|href^='#'])"
)
;
var
extFiles
=
{
}
;
for
(
var
i
=
0
,
n
=
extUse
.
length
;
i
<
n
;
i
++
)
{
var
use
=
extUse
[
i
]
;
var
ref
=
use
.
getAttributeNS
(
xlinkNS
,
"href"
)
.
split
(
"#"
)
;
var
file
=
ref
[
0
]
;
if
(
!
extFiles
[
file
]
)
{
extFiles
[
file
]
=
[
]
;
}
extFiles
[
file
]
.
push
(
{
element
:
use
,
target
:
ref
[
1
]
}
)
;
}
for
(
var
fileURL
in
extFiles
)
{
var
request
=
new
XMLHttpRequest
(
)
;
request
.
addEventListener
(
"load"
,
createResponseHandler
(
request
,
extFiles
[
fileURL
]
)
)
;
request
.
overrideMimeType
(
"image/svg+xml"
)
;
request
.
open
(
"GET"
,
fileURL
)
;
request
.
send
(
)
;
}
function
createResponseHandler
(
request
,
elementList
)
{
return
function
(
)
{
var
file
=
request
.
responseXML
;
var
defs
=
document
.
createElementNS
(
svgNS
,
"defs"
)
;
defs
.
appendChild
(
file
.
documentElement
)
;
svg
.
appendChild
(
defs
)
;
for
(
var
i
=
0
,
n
=
elementList
.
length
;
i
<
n
;
i
++
)
{
var
o
=
elementList
[
i
]
;
o
.
element
.
setAttributeNS
(
xlinkNS
,
"href"
,
"#"
+
o
.
target
)
;
}
}
}
}
)
;
-
The script runs after the main document loads, so that all the
<use>
elements have been processed. -
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. -
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 thexlink: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. -
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.
-
For each
<use>
element with an external reference, the reference URL is split into the file URL and the target fragment. -
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. -
After processing all the
<use>
elements, anXMLHttpRequest
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 thecreateResponseHandler()
function itself is not the event listener; it is called immediately with the given parameters, and returns the actual listener function. -
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. -
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. -
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:
-
Importing files injects scripts and other active content into your document. Don’t use this script unless you are 100% confident about the security of the document you are using it in, and the documents you are importing.
-
This particular script also assumes that same-file references will always use
#
-only URLs. If they instead use the full URL, you would end up with circular loops: an SVG importing a copy of itself into the file, including its script, which then runs again, which imports a new copy, which…goes on until the browser tab crashes. -
All of your
id
values, across all files, need to be unique. Otherwise, your references will be mis-directed. You could modifyid
values as you import them to make them unique, but then you would need to find and replace all references to the originalid
. -
If your icon file uses a
<style>
block, those styles will now apply to the entire document you copy it into. Either use style rules with very specific class names, or ensure all styling in the external file is done using presentation attributes or inline style attributes. -
If your icon file uses an external stylesheet embedded with an
<?xml-stylesheet ?>
instruction, the code in Example 10-X1 will not copy it. -
Relative links to any other external resources may also be disrupted, as the base URL for the elements would change.
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.