Update: I’ve turned this into a small JavaScript module named Lunar and pushed to GitHub, please use that code instead as it doesn’t extend native DOM APIs and also has accompanying unit tests :)
I encountered how painful traversing inline SVG can be when working on a recent project, simple DOM APIs such as adding, removing and toggling classes just aren’t there, or supported by tools such as jQuery (yes, I even tried jQuery).
Inline SVG is SVG in the DOM, rendered from its XML. Here’s a quick look at example inline SVG which would sit anywhere in the DOM:
Table of contents
<svg id="svg" xmlns="https://www.w3.org/2000/svg" version="1.1" height="190">
<circle cx="100" cy="50" r="40" fill="red" />
</svg>
The svg
element acts as a wrapper to the XML inside, whilst defining a few things such as height, width, namespace and version. You’ll notice I’ve added an id
attribute, with the value of svg
. Current DOM APIs make this seamlessly easy to target:
// grabs
var mySVG = document.querySelector('#svg');
Problem: DOM stuff
But the problem begins when trying to do the ‘usual’ DOM stuff, adding a class, or removing a class. You’d think it would be pretty simple, but even using jQuery’s APIs don’t allow it to work either, so I wrote my own and I’m pretty pleased with its compactness. The trick is to set the attribute again, you can’t keep adding/removing classes using the .className method. The getAttribute method is what I’ve used to grab the class attribute’s value, and then the idea behind it is to grab that attribute, manipulate it and then set it back again.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Let’s say I have a function, I need to add a class to an SVG circle onclick:
// grabs
var mySVG = document.querySelector('#svg circle');
mySVG.setAttribute('class', 'myClass');
… will give us:
<svg id="svg" xmlns="https://www.w3.org/2000/svg" version="1.1" height="190">
<circle cx="100" cy="50" r="40" fill="red" class="myClass" />
</svg>
I have to think as the ‘class’ attribute as totally made up and that className doesn’t exist. And manipulating this is where the magic happens.
hasClass API
As with all the APIs, I’m hanging these off the SVGElement’s prototype constructor so all SVG nodes inherit the methods. With hasClass, I’m extending the native Object with a function. This method allows simple declaration of the APIs. Inside the hasClass function, I’m creating a new Regular Expression, which gets dynamically created through its className parameter and immediately tested against its attribute value. JavaScript’s .test() returns a boolean (true/false), a simple way to test a class presence.
SVGElement.prototype.hasClass = function (className) {
return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.getAttribute('class'));
};
addClass API
Adding a class is simple, just set the attribute. Here I simply make a check using the hasClass API, and if it doesn’t exist I add it. There’s no point adding it again if it does exist. If it doesn’t exist, I set the attribute class with the current class value, plus my new class name, super simple.
SVGElement.prototype.addClass = function (className) {
if (!this.hasClass(className)) {
this.setAttribute('class', this.getAttribute('class') + ' ' + className);
}
};
removeClass API
Removing a class was the most fun, there’s also the issue of keeping spaces intact, for instances I had to work out how to remove a class and keep the appropriate spaces around that class name. You can see I create a new class here called removedClass, where I get the current value, then replace the passed in className using a dynamically created RegExp again. This RegExp has some coolness added to it, you’ll see I declare ‘g’ at the end of the RegExp declaration, this means global, and will replace all instances of the class, for example if it was declared more than once throughout the class value. I then make a safety check to ensure the class is there, and set the attribute back on the element.
You’ll also see I used a second parameter as well in the replace method, which says ‘$2’. This is a nifty little trick, which refers to the capture group in the RegExp. Capture groups are denoted by circular brackets, my example at the end of the RegExp says _’(\s | $)’, this denotes a capture group, and then looks for whitespace after the className, OR it’s at the end of the string, which is what the _$ means. I can then remove the className and leave whatever was in the capture group, either a space or nothing, which keeps the class value tidy. |
SVGElement.prototype.removeClass = function (className) {
var removedClass = this.getAttribute('class').replace(new RegExp('(\\s|^)' + className + '(\\s|$)', 'g'), '$2');
if (this.hasClass(className)) {
this.setAttribute('class', removedClass);
}
};
toggleClass API
Toggling from hereon is super simple, I’ll check if the element has the class, and based on that I’ll either add or remove the class using the above APIs.
SVGElement.prototype.toggleClass = function (className) {
if (this.hasClass(className)) {
this.removeClass(className);
} else {
this.addClass(className);
}
};
Usage
Usage is simple, and simple API style:
// Grab my Node
var mySVG = document.querySelector('#svg circle');
// hasClass
mySVG.hasClass('zzz');
// addClass
mySVG.addClass('zzz');
// removeClass
mySVG.removeClass('zzz');
// toggleClass
mySVG.toggleClass('zzz');
Thank you for reading!