jQuery SVG, why can't I addClass?
Asked Answered
Z

15

304

I am using jQuery SVG. I can't add or remove a class to an object. Anyone know my mistake?

The SVG:

<rect class="jimmy" id="p5" x="200" y="200" width="100" height="100" />

The jQuery that won't add the class:

$(".jimmy").click(function() {
    $(this).addClass("clicked");
});

I know the SVG and jQuery are working together fine because I can target the object and fire an alert when it's clicked:

$(".jimmy").click(function() {
    alert('Handler for .click() called.');
});
Zelikow answered 26/12, 2011 at 19:38 Comment(6)
Good article here: toddmotto.com/…Eckenrode
So the actual question of WHY can't I use the jQuery *class functions remains unanswered... If I can access the properties of SVG elements via jQuery attr function, why can't the *class functions also do this? jQuery FAIL?!?Flute
Seriously, does anyone know WHY??Bootlick
Tiny library that adds this functionality for SVG files: github.com/toddmotto/lunarTzong
The "why" is because jQuery uses the "className" property, which is a string property for HTML elements, but is an SVG Animated String for SVG elements. Which can't be assigned to like for HTML elements. Thus, the jQuery code blows up trying to assign the value.Devisable
As per https://mcmap.net/q/99554/-jquery-svg-why-can-39-t-i-addclass the correct answer is: upgrade to one of jQuery 1.12.0, 2.2.0, or 3.0; or a later version of these.Letter
W
438

Edit 2016: read the next two answers.

  • JQuery 3 fixes the underlying issue
  • Vanilla JS: element.classList.add('newclass') works in modern browsers

JQuery (less than 3) can't add a class to an SVG.

.attr() works with SVG, so if you want to depend on jQuery:

// Instead of .addClass("newclass")
$("#item").attr("class", "oldclass newclass");
// Instead of .removeClass("newclass")
$("#item").attr("class", "oldclass");

And if you don't want to depend on jQuery:

var element = document.getElementById("item");
// Instead of .addClass("newclass")
element.setAttribute("class", "oldclass newclass");
// Instead of .removeClass("newclass")
element.setAttribute("class", "oldclass");
Welltimed answered 21/4, 2012 at 9:27 Comment(13)
In support to forresto suggestion - discusscode.blogspot.in/2012/08/…Limicolous
perfect. this should be the answer. though the REAL answer is for jquery to include this by default, at least as a setting or something.Confetti
.attr("class", "oldclass") (or the more cumbersome .setAttribute("class", "oldclass")) is really not equivalent to removing newclass!Plaudit
Great answer ( and v.nice technique ) ... but would it be an idea to edit the question so that it starts by stating that jquery can't add a class to an SVG ( if that's the case ... as it seems )?Buy
The non-jQuery version won't work on old IE (certainly 7 and earlier, can't remember about 8) because setAttribute() is broken.Kokura
Sagar Gala posted much better solution. Manually setting class isn't the same as jquery add/removeClass function.Alliance
The OP asked "why", not "how". so this answer did not answer the question at all.Erogenous
Does anyone know how to preserve the existing classes because they are getting wiped when I add the attribute class?Isentropic
@FrazerFindlater oldclass in the example is for that $("#item").attr("class", "oldclass newclass");Welltimed
Just an addendum: I tried and JQuery 3 did not fix the issue.Shulamite
Regarding your 2016 classList edit - please read the comment in Tomas Mikula's answer below. IE does not implement classList in SVG elements.Fillian
a small workaround i ended up with : instead of $("#item").addClass("newClass") I'm writing $("#item").attr("class", $(#item).attr('class').replace('newClass','')+' newClass'); and for $("#item").removeClass("oldClass"); I'm writing $("#item").attr("class", $(#item).attr('class').replace('oldClass','')); in that way, you don't have duplicata of classGoolsby
Thanks! Used this to add class to leaflet SVG markers: L.circle([lat, lng], radius)._path.classList.add("circlemarkerclassname")Goddord
C
76

There is element.classList in the DOM API that works for both HTML and SVG elements. No need for jQuery SVG plugin or even jQuery.

$(".jimmy").click(function() {
    this.classList.add("clicked");
});
Cowfish answered 4/4, 2013 at 21:6 Comment(10)
I tested this, and .classList appears to be undefined for svg sub-elements, at least in Chrome.Livi
Works for me in Chrome. I have SVG embedded in HTML, so my document structure looks like this: <html><svg><circle class="jimmy" ...></svg></html>Cowfish
I used this to add class in a <g> SVG element, works fine in Chrome.Laufer
IE does not implement .classList and API on SVG Elements. See caniuse.com/#feat=classlist and the "known issues" list. That mentions WebKit browsers also.Nailhead
Seems like a pretty fragile approach.Elurd
@Nailhead There's no mention of that in the "known issues" list.Tzong
And as time goes on, this answer becomes even more correct (and the best answer)Truth
@Chuck : in the Caniuse notes keyed to IE10,11: "Partial support refers to not working on SVG elements."Gyrostatics
It does work in (all modern) Chrome, but not in IE for svg. 10+ IE does support classList, but it doesn't modify svg elements. See CanIUseThisColvert
There is a polyfill for older browsers here developer.mozilla.org/en-US/docs/Web/API/Element/classListVerse
Z
69

jQuery 3 does not have this problem

One of the changes listed on the jQuery 3.0 revisions is:

add SVG class manipulation (#2199, 20aaed3)

One solution for this issue would be to upgrade to jQuery 3. It works great:

var flip = true;
setInterval(function() {
    // Could use toggleClass, but demonstrating addClass.
    if (flip) {
        $('svg circle').addClass('green');
    }
    else {
        $('svg circle').removeClass('green');
    }
    flip = !flip;
}, 1000);
svg circle {
    fill: red;
    stroke: black;
    stroke-width: 5;
}
svg circle.green {
    fill: green;
}
<script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>

<svg>
    <circle cx="50" cy="50" r="25" />
</svg>

The Problem:

The reason the jQuery class manipulation functions do not work with the SVG elements is because jQuery versions prior to 3.0 use the className property for these functions.

Excerpt from jQuery attributes/classes.js:

cur = elem.nodeType === 1 && ( elem.className ?
    ( " " + elem.className + " " ).replace( rclass, " " ) :
    " "
);

This behaves as expected for HTML elements, but for SVG elements className is a little different. For an SVG element, className is not a string, but an instance of SVGAnimatedString.

Consider the following code:

var test_div = document.getElementById('test-div');
var test_svg = document.getElementById('test-svg');
console.log(test_div.className);
console.log(test_svg.className);
#test-div {
    width: 200px;
    height: 50px;
    background: blue;
}
<div class="test div" id="test-div"></div>

<svg width="200" height="50" viewBox="0 0 200 50">
  <rect width="200" height="50" fill="green" class="test svg" id="test-svg" />
</svg>

If you run this code you will see something like the following in your developer console.

test div
SVGAnimatedString { baseVal="test svg",  animVal="test svg"}

If we were to cast that SVGAnimatedString object to a string as jQuery does, we would have [object SVGAnimatedString], which is where jQuery fails.

How the jQuery SVG plugin handles this:

The jQuery SVG plugin works around this by patching the relevant functions to add SVG support.

Excerpt from jQuery SVG jquery.svgdom.js:

function getClassNames(elem) {
    return (!$.svg.isSVGElem(elem) ? elem.className :
        (elem.className ? elem.className.baseVal : elem.getAttribute('class'))) || '';
}

This function will detect if an element is an SVG element, and if it is it will use the baseVal property of the SVGAnimatedString object if available, before falling back on the class attribute.

jQuery's historical stance on the issue:

jQuery currently lists this issue on their Won’t Fix page. Here is the relevant parts.

SVG/VML or Namespaced Elements Bugs

jQuery is primarily a library for the HTML DOM, so most problems related to SVG/VML documents or namespaced elements are out of scope. We do try to address problems that "bleed through" to HTML documents, such as events that bubble out of SVG.

Evidently jQuery considered full SVG support outside the scope of the jQuery core, and better suited for plugins.

Zaporozhye answered 8/4, 2015 at 21:32 Comment(0)
S
37

If you have dynamic classes or don't know what classes could be already applied then this method I believe is the best approach:

// addClass
$('path').attr('class', function(index, classNames) {
    return classNames + ' class-name';
});

// removeClass
$('path').attr('class', function(index, classNames) {
    return classNames.replace('class-name', '');
});
Supplant answered 26/11, 2013 at 15:31 Comment(2)
This was a livesaver for me as I needed to change a lot of SVG-elements to get things done. +999 from me :)Camarilla
It's hard to believe that jQuery would still have their heels dug in on this, even after 2 years and the release of 2.x.x. Your fix, nav, saved the day for me. The rest of these fixes were unnecessarily complex. Thanks!Slowmoving
B
18

Based on above answers I created the following API

/*
 * .addClassSVG(className)
 * Adds the specified class(es) to each of the set of matched SVG elements.
 */
$.fn.addClassSVG = function(className){
    $(this).attr('class', function(index, existingClassNames) {
        return ((existingClassNames !== undefined) ? (existingClassNames + ' ') : '') + className;
    });
    return this;
};

/*
 * .removeClassSVG(className)
 * Removes the specified class to each of the set of matched SVG elements.
 */
$.fn.removeClassSVG = function(className){
    $(this).attr('class', function(index, existingClassNames) {
        var re = new RegExp('\\b' + className + '\\b', 'g');
        return existingClassNames.replace(re, '');
    });
    return this;
};
Beers answered 12/6, 2014 at 22:28 Comment(4)
This is helpful, but the algorithms have issues: 1. If there is no existing class string, you get 'undedfined className' upon adding a class. 2. If the class string contains a class that is the superset of the regular expression, you leave behind junk in the class string. For example, if you try to remove the class 'dog' and the class string contains 'dogfood', you'll be left with 'food' in the class string. Here are some fixes: jsfiddle.net/hellobrett/c2c2zftoSemipostal
This is not a perfect solution. It replace all words containing class you want to remove.Octangular
the post has been editing addressing both concerns above.Ploss
You may want to add ` *` at the beginning of the regexp to avoid leaving whitespaces behind when removing the classPloss
D
13

After loading jquery.svg.js you must load this file: http://keith-wood.name/js/jquery.svgdom.js.

Source: http://keith-wood.name/svg.html#dom

Working example: http://jsfiddle.net/74RbC/99/

Decontrol answered 27/12, 2011 at 0:18 Comment(1)
Added it - still doesn't seem to work. I've been looking through the documentation on jquery SVG's site, but there is no clear explanation of what you need to do to make use of its extra functionality.Zelikow
P
8

Just add the missing prototype constructor to all SVG nodes:

SVGElement.prototype.hasClass = function (className) {
  return new RegExp('(\\s|^)' + className + '(\\s|$)').test(this.getAttribute('class'));
};

SVGElement.prototype.addClass = function (className) { 
  if (!this.hasClass(className)) {
    this.setAttribute('class', this.getAttribute('class') + ' ' + className);
  }
};

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

You can then use it this way without requiring jQuery:

this.addClass('clicked');

this.removeClass('clicked');

All credit goes to Todd Moto.

Potiche answered 26/12, 2011 at 19:38 Comment(1)
ie11 chokes on this code. this polyfill was a better route: github.com/eligrey/classList.jsPangermanism
U
5

jQuery does not support the classes of SVG elements. You can get the element directly $(el).get(0) and use classList and add / remove. There is a trick with this too in that the topmost SVG element is actually a normal DOM object and can be used like every other element in jQuery. In my project I created this to take care of what I needed but the documentation provided on the Mozilla Developer Network has a shim that can be used as an alternative.

example

function addRemoveClass(jqEl, className, addOrRemove) 
{
  var classAttr = jqEl.attr('class');
  if (!addOrRemove) {
    classAttr = classAttr.replace(new RegExp('\\s?' + className), '');
    jqEl.attr('class', classAttr);
  } else {
    classAttr = classAttr + (classAttr.length === 0 ? '' : ' ') + className;
    jqEl.attr('class', classAttr);
  }
}

An alternative all tougher is to use D3.js as your selector engine instead. My projects have charts that are built with it so it's also in my app scope. D3 correctly modifies the class attributes of vanilla DOM elements and SVG elements. Though adding D3 for just this case would likely be overkill.

d3.select(el).classed('myclass', true);
Uncommonly answered 23/7, 2013 at 15:51 Comment(1)
Thanks for that d3 bit... I actually already had d3 on the page so just decided to use that. Very useful :)Matron
G
5

jQuery 2.2 supports SVG class manipulation

The jQuery 2.2 and 1.12 Released post includes the following quote:

While jQuery is a HTML library, we agreed that class support for SVG elements could be useful. Users will now be able to call the .addClass(), .removeClass(), .toggleClass(), and .hasClass() methods on SVG. jQuery now changes the class attribute rather than the className property. This also makes the class methods usable in general XML documents. Keep in mind that many other things will not work with SVG, and we still recommend using a library dedicated to SVG if you need anything beyond class manipulation.

Example using jQuery 2.2.0

It tests:

  • .addClass()
  • .removeClass()
  • .hasClass()

If you click on that small square, it will change its color because the class attribute is added / removed.

$("#x").click(function() {
    if ( $(this).hasClass("clicked") ) {
        $(this).removeClass("clicked");
    } else {
        $(this).addClass("clicked");
    }
});
.clicked {
    fill: red !important;  
}
<html>

<head>
    <script src="https://code.jquery.com/jquery-2.2.0.js"></script>
</head>

<body>
    <svg width="80" height="80">
        <rect id="x" width="80" height="80" style="fill:rgb(0,0,255)" />
    </svg>
</body>

</html>
Gourd answered 9/1, 2016 at 19:35 Comment(0)
T
4

I use Snap.svg to add a class to and SVG.

var jimmy = Snap(" .jimmy ")

jimmy.addClass("exampleClass");

http://snapsvg.io/docs/#Element.addClass

Thorbert answered 7/12, 2015 at 21:16 Comment(0)
K
3

Here is my rather inelegant but working code that deals with the following issues (without any dependencies):

  • classList not existing on <svg> elements in IE
  • className not representing the class attribute on <svg> elements in IE
  • Old IE's broken getAttribute() and setAttribute() implementations

It uses classList where possible.

Code:

var classNameContainsClass = function(fullClassName, className) {
    return !!fullClassName &&
           new RegExp("(?:^|\\s)" + className + "(?:\\s|$)").test(fullClassName);
};

var hasClass = function(el, className) {
    if (el.nodeType !== 1) {
        return false;
    }
    if (typeof el.classList == "object") {
        return (el.nodeType == 1) && el.classList.contains(className);
    } else {
        var classNameSupported = (typeof el.className == "string");
        var elClass = classNameSupported ? el.className : el.getAttribute("class");
        return classNameContainsClass(elClass, className);
    }
};

var addClass = function(el, className) {
    if (el.nodeType !== 1) {
        return;
    }
    if (typeof el.classList == "object") {
        el.classList.add(className);
    } else {
        var classNameSupported = (typeof el.className == "string");
        var elClass = classNameSupported ?
            el.className : el.getAttribute("class");
        if (elClass) {
            if (!classNameContainsClass(elClass, className)) {
                elClass += " " + className;
            }
        } else {
            elClass = className;
        }
        if (classNameSupported) {
            el.className = elClass;
        } else {
            el.setAttribute("class", elClass);
        }
    }
};

var removeClass = (function() {
    function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
        return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
    }

    return function(el, className) {
        if (el.nodeType !== 1) {
            return;
        }
        if (typeof el.classList == "object") {
            el.classList.remove(className);
        } else {
            var classNameSupported = (typeof el.className == "string");
            var elClass = classNameSupported ?
                el.className : el.getAttribute("class");
            elClass = elClass.replace(new RegExp("(^|\\s)" + className + "(\\s|$)"), replacer);
            if (classNameSupported) {
                el.className = elClass;
            } else {
                el.setAttribute("class", elClass);
            }
        }
    }; //added semicolon here
})();

Example usage:

var el = document.getElementById("someId");
if (hasClass(el, "someClass")) {
    removeClass(el, "someClass");
}
addClass(el, "someOtherClass");
Kokura answered 11/2, 2015 at 11:55 Comment(4)
Did you test it using IE7? Because LTE IE7 requires strAttributeName to be "className" when setting, getting, or removing a CLASS attribute.Ethiopic
@Knu: It definitely works in IE 6 and 7 for regular HTML elements because in those browsers it uses the className property rather than trying to set an attribute. The reason that "LTE IE7 requires strAttributeName to be "className" when setting, getting, or removing a CLASS attribute" is that those browsers have a broken implementation of getAttribute(x) and setAttribute(x) that simply get or set the property with name x, so it's easier and more compatible to set the property directly.Kokura
@Knu: OK. I wasn't sure since SVG is not supported in IE 7, so I'm not sure what you'd hope to achieve by including an <svg> element in a page that is designed to work in IE 7. For what it's worth, my code does add a class attribute successfully to <svg> elements in IE 7 since such an element behaves like any other custom element.Kokura
Completely forgot about that thanks for the reminder. Ill try to get my hands on Adobe SVG Viewer to check if it works as intended.Ethiopic
H
1

One workaround could be to addClass to a container of the svg element:

$('.svg-container').addClass('svg-red');
.svg-red svg circle{
    fill: #ED3F32;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="svg-container">
  <svg height="40" width="40">
      <circle cx="20" cy="20" r="20"/>
  </svg>
</div>
Hamhung answered 20/8, 2016 at 14:21 Comment(0)
A
1

I wrote this in my project, and it works... probably;)

$.fn.addSvgClass = function(className) {

    var attr
    this.each(function() {
        attr = $(this).attr('class')
        if(attr.indexOf(className) < 0) {
            $(this).attr('class', attr+' '+className+ ' ')
        }
    })
};    

$.fn.removeSvgClass = function(className) {

    var attr
    this.each(function() {
        attr = $(this).attr('class')
        attr = attr.replace(className , ' ')
        $(this).attr('class' , attr)
    })
};    

examples

$('path').addSvgClass('fillWithOrange')
$('path').removeSvgClass('fillWithOrange')
Ashmead answered 1/9, 2017 at 11:9 Comment(0)
W
0

Inspired by the answers above, especially by Sagar Gala, I've created this API. You may use it if you don't want or can't upgrade your jquery version.

Wintertide answered 11/7, 2016 at 12:12 Comment(0)
A
-1

Or just use old-school DOM methods when JQ has a monkey in the middle somewhere.

var myElement = $('#my_element')[0];
var myElClass = myElement.getAttribute('class').split(/\s+/g);
//splits class into an array based on 1+ white space characters

myElClass.push('new_class');

myElement.setAttribute('class', myElClass.join(' '));

//$(myElement) to return to JQ wrapper-land

Learn the DOM people. Even in 2016's framework-palooza it helps quite regularly. Also, if you ever hear someone compare the DOM to assembly, kick them for me.

Aplanatic answered 19/12, 2016 at 23:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.