Override JQuery Selector to support Dollar Sign ($) in element id

2.7k views Asked by At

I am trying to write a JQuery plugin to support $ in element id, my code is:

DollarSignPlugin.js:

function escapeJQueryElementName(elementName) {
    elementName = elementName.replace(/\\\$/g, "$");

    return elementName.replace(/\$/g, "\\$");
}

var jQueryInit = $.fn.init;

$.fn.init = function(arg1, arg2, rootjQuery){
    arg2 = arg2 || window.document;
    if (arg1 && arg1.replace) {
        var newArg1 = escapeJQueryElementName(arg1);
        return new jQueryInit(newArg1, arg2, rootjQuery);
    }

    return new jQueryInit(arg1, arg2, rootjQuery);
};

It is working great, but i faced one issue with current line:

var $anyselector = $("#");

JQuery throw error "Syntax error, unrecognized expression: #".

This line is from bootstrap when clicking on any tab with href="#".

This error doesn't appear when removing my plugin, also if i copy paste the replace function to directly the jquery file it works fine.

So is there a better way to override the selector or i have some issue with my code, please help?

2

There are 2 answers

1
T.J. Crowder On BEST ANSWER

I would strongly recommend you don't do this (but if you want to, keep reading, I do have a fix below), because:

  1. It means you're using invalid selectors in your code, which is a maintenance issue — at some point, someone doing maintenance won't understand what's going on, or you'll hit an edge condition, etc.

  2. Your current implementation will mess up things like

    $("<div>$12</div>").appendTo(document.body);
    

    (by putting a backslash in front of the dollar sign). Who knows what else it's messing up?

Instead, I'd just have a simple:

function $gid(id) {
    var e = document.getElementById(id);
    if (!e) {
        return $();
    }
    if (e.id === id) {
        return $(e);
    }
    // Fallback processing for old IE that matches name attributes
    return $('[id="' + id.replace(/"/g, '\\"') + '"]').filter(function() {
        return this.id === id;
    }).first();
}

Example:

// Plugin
function $gid(id) {
    var e = document.getElementById(id);
    if (!e) {
        return $();
    }
    if (e.id === id) {
        return $(e);
    }
    // Fallback processing for old IE that matches name attributes
    return $('[id="' + id.replace(/"/g, '\\"') + '"]').filter(function() {
        return this.id === id;
    }).first();
}

// Use
$gid("a$b").css("color", "blue");
$gid("$c").css("color", "green");
<div id="a$b">a$b</div>
<div id="$c">$c</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

However: If you're going to do it, you have to be careful to tread as softly as possible. That means calling the original with exactly the same number of arguments, the same thisvalue, etc.; and making sure that $.fn.init.prototype has the same value after you replace $.fn.init as before.

Doing both of those things seems to solve the problem:

// Plugin
(function () {
    function escapeJQueryElementName(elementName) {
        elementName = elementName.replace(/\\\$/g, "$");
        return elementName.replace(/\$/g, "\\$");
    }

    var slice = Array.prototype.slice;
    var jQueryInit = $.fn.init;

    $.fn.init = function (arg1) {
        var args = slice.call(arguments, 0);
        if (arg1 && arg1.replace) {
            args[0] = escapeJQueryElementName(arg1);
        }
        return jQueryInit.apply(this, args);
    };
    $.fn.init.prototype = $.fn;
})();

// Use
$("#a$b").css("color", "blue"); // Using special version that handles $ without \\
$("#$c").css("color", "green"); // Using special version that handles $ without \\
$("<div>Checking '#' selector, should get 0: " + $("#").length + "</div>").appendTo(document.body);
<div id="a$b">a$b</div>
<div id="$c">$c</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

3
Ja͢ck On

Instead of trying to attack this problem by overriding something that should really only be used internally, a nicer solution is a function that will create a new alias of jQuery with support for escaping selectors.

Btw, keep in mind that escaping should not be done for things that are HTML, e.g. $('<span>give me $$$</span>'), so I've made a crude check for that.

(function(jQuery) {
  jQuery.withSelectorEscaping = function() {
    function escapeJQueryElementName(elementName) {
      elementName = elementName.replace(/\\\$/g, "$");
      return elementName.replace(/\$/g, "\\$");
    }

    return function(selector, context) {
      // avoid doing this for HTML
      if (selector && selector.replace && selector[0] !== '<') {
        selector = escapeJQueryElementName(selector);
      }
      return jQuery(selector, context);
    };
  }
}(jQuery));

// create new alias here
$ = jQuery.withSelectorEscaping();

// use new alias
console.log($('#'));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

This code snippet adds a new function to jQuery itself called jQuery.withEscaping; it returns a function which you can then assign to an alias of your choosing, I picked $ here but you could also do:

$foo = jQuery.withSelectorEscaping();

It leaves the behaviour of jQuery itself alone and only exposes your escaping feature for things that need it.