2

Question

I'd like to know the best way I can wrap the jQuery function while retaining all functionality. Essentially I want to call $('#someId') but have it operate as $('#' + id + 'someId') by wrapping the function, modifying the arguments, and passing it through to the original jQuery function.

Motivation

I have a section of JS that will reuse the same variable winId which is concatenated and passed to jQuery. Instead of writing

$('#' + winId + 'someId').html();
$('#' + winId + 'someOtherId').css();
...
$('#' + winId + 'someThirdId').text();

throughout the whole file, I want to wrap the jQuery function so I can just call

$('#someId').html();
$('#someOtherId').css();
...
$('#someThirdId').text();

and and have winId added in before passing through to $.


My attempt

Here's what I'm thinking as a wrapper:

(function() {
    var fn = $;

    return $ = function() {
       for ( var i = 0; i < arguments.length; i++ ) {
          if ( typeof arguments[i] == 'string') {
             arguments[i] = /* code to add in winId, omitted */
          }
       }
       return fn.apply( this, arguments );
    }
})();

This works great, except that obviously none of the methods like $.ajax are available:

Uncaught TypeError: Object function () {
    for ( var i = 0; i < arguments.length; i++ ) {
       if ( typeof arguments[i] == 'string' ) {
          arguments[i] = /* code to add in winId, omitted */
       }
    }
    return fn.apply( this, arguments );
} has no method 'ajax' 

Note: I know I could copy the object over using jQuery.extend($, jQuery), but I'm interested in a more elegant solution than that if possible.

Tepken Vannkorn
  • 9,648
  • 14
  • 61
  • 86
Rick Hanlon II
  • 20,549
  • 7
  • 47
  • 53
  • Just don't name the wrapper function `$`? – Bergi Sep 20 '13 at 02:40
  • As a last resort, you can do this by [exchanging `$.fn.init`](http://stackoverflow.com/q/11146580/1048572) instead. – Bergi Sep 20 '13 at 02:44
  • @Bergi I could name the wrapper something other than `$`, but that's not the solution I'm looking for. However, I think wrapping `$.fn.init` is the a great solution, and I'd be grateful if you would post and answer so I could accept it. – Rick Hanlon II Sep 20 '13 at 02:54
  • 1
    Sounds like an x-y problem to me – zzzzBov Sep 20 '13 at 02:58
  • I disagree that it's a x-y problem because my question is well-defined. My "motivation" is provided only as a single example use case; the question is applicable and valuable in general. – Rick Hanlon II Sep 20 '13 at 03:13
  • @RickHanlonII, Make sure that you take the lastest version of the code in my answer. The last implementation had a substantial bug since I was applying the constructor on the `prototype`. – plalx Sep 20 '13 at 15:11

5 Answers5

4

Here's a different implementation:

DEMO

(jQuery.fn.init = (function (init) {

    return function (selector) {
        if (typeof selector === 'string' && selector[0] === '#') {
            arguments[0] = selector.replace('#', '#prefix_');
        }

        return init.apply(this, arguments);
    };

})(jQuery.fn.init)).prototype = jQuery.fn;


$(function () {
    console.log($('#test').length);
    console.log($.ajax);
});

EDIT: Followup question: How can I apply this only within a closure? For example, within an object.

Perhaps with functions that allows to add named decorators and remove them, something like:

HTML

<div id="prefix_test"></div>

JS

var decJQ = (function (decJQ, $) {
    var decorators = {},
        init = $.fn.init;

    ($.fn.init = function () {

        for (var k in decorators) {
            if (decorators.hasOwnProperty(k)) {
                arguments = decorators[k].apply(this, arguments);
            }
        }

        return init.apply(this, arguments);
    }).prototype = $.fn;

    return $.extend(decJQ, {
        decorate: function (name, fn) {
            decorators[name] = fn;
        },
        undecorate: function (name) {
            delete decorators[name];
        }
    });

})(window.decJQ || {}, jQuery);

decJQ.decorate('idPrefix', function (selector) {
    if (typeof selector === 'string' && selector[0] === '#') {
        arguments[0] = selector.replace('#', '#prefix_');
    }

    return arguments;
});

$(function () {
    console.log($('#test').length); //1

    decJQ.undecorate('idPrefix');

    console.log($('#test').length); //0
});

EDIT 2: You could also go for something extremely simple, such as:

(function ($) {
    //use $ which has been wrapped
})(function () {
    //do some manipulations
    return jQuery.apply(this, arguments);
});
plalx
  • 42,889
  • 6
  • 74
  • 90
  • This is exactly what I needed. I don't know why I didn't think to just override the jQuery.prototype init function. – Rick Hanlon II Sep 20 '13 at 13:33
  • Why don't you do `init.apply(this, arguments)`? – Bergi Sep 20 '13 at 14:01
  • Followup question: How can I apply this only within a closure? For example, within an object. – Rick Hanlon II Sep 20 '13 at 14:41
  • @RickHanlonII: You cannot, this is a global action. That's why I suggested to use a simple wrapper function with a different name, which can be locally scoped. Maybe even shadow the `$` identifier, but then you will need to use `jQuery` for the namespace. – Bergi Sep 20 '13 at 14:44
  • @Bergi, Yeah that was a big issue, fixed. – plalx Sep 20 '13 at 15:08
  • @Bergi, Do you have a better idea on how the decorators could be implemented? I am not sure that I like the fact they have to return `arguments`. Also, how would you package this? – plalx Sep 20 '13 at 15:58
  • @plalx: Returning any arrays to become arguments is fine I think. Maybe pass them a real array explicitly to emphasize the architecture, instead of applying it. I like your packaging with the module, but I'm still unsure whether overwriting `$.fn.init` might cause internal issues when jQuery constructs new objects. – Bergi Sep 21 '13 at 17:07
2

Following the suggestion by Bergi and the post he links to here, this is one way to go:

$.fn.extend({
    initCore: $.fn.init,
    init: function (selector, context, rootjQuery) {
        if (typeof selector === 'string' && selector[0] === '#') {
            selector = selector.replace('#', '#' + winId);
        }
        return $.fn.initCore(selector, context, rootjQuery);
    }
});

$.fn.init.prototype = $.fn;

I've tested $('#foo') will find a div that has a winId prefixed to the id value, like this <div id="1foo"></div>.

For example: http://jsfiddle.net/MfdJS/1/

Community
  • 1
  • 1
Marty Cortez
  • 2,325
  • 1
  • 17
  • 22
  • This is good, but I accepted @plalx because from the jQuery docs: "the jQuery.fn.extend() method extends the jQuery prototype ($.fn) object to provide *new methods* that can be chained to the jQuery() function." – Rick Hanlon II Sep 20 '13 at 13:42
  • What you may not understand is that `.init()` is from the `jQuery.prototype` chain. Martin overwrote the original `jQuery.fn.init` function with extend. Plalx does the same thing, just without `.extend()` – Joe Simmons Sep 20 '13 at 15:22
  • I understand where .init() comes from. I just don't like using .extend() to *wrap* an method when the jQuery spec says it is to provide *new methods.* It's a nuanced point, but the answers were essentially the same so that nuance tipped it for me. – Rick Hanlon II Sep 20 '13 at 15:34
  • @JoeSimmons, However it adds `initCore` as garbage to the `prototype`. – plalx Sep 20 '13 at 15:34
  • @plalx: I know. It's not the best solution, but it accomplishes the same goal as yours. – Joe Simmons Sep 20 '13 at 15:38
  • @JoeSimmons I agree, however I was responding to "this one's a winner" and you just agreed that it actually isin't =P – plalx Sep 20 '13 at 17:49
  • I should have worded that better. I like Martin's method more solely because he uses extend, but it's not perfect because it saves the old ref in `initCore` – Joe Simmons Sep 20 '13 at 19:43
1

Add class="winID" to your elements.

Use $(".winID").find('#someId").css(...) to access CSS attributes of specific element.

Use $(".winID").css(...) to access CSS attribues to all winID tagged elements.

-1

ok well i just tested

$('.con'+'tainer')

and

$('d'+'iv');

and

var s = 't';
$('.con'+s+'ainer');

and the console is returning the correct values

i belive that you are calling a function jQuery() with a string parameter, so as long as you use the normal syntax for building/appending/constructing a string with the plus signs, i think you're golden. im glad you asked this question because now i know too

neaumusic
  • 10,027
  • 9
  • 55
  • 83
  • sorry, i see that you know how to do this already, and i guess i dont understand the question. if i was you i would construct the strings like var someID = stringBase + stringParameters; so you can call jQuery like $(someID) – neaumusic Sep 20 '13 at 04:16
  • You completely misunderstood the question. – Joe Simmons Sep 20 '13 at 05:43
  • I'm actually trying to abstract the repetitive concatenation away so that I only have to do it once but it applies to all calls in scope. – Rick Hanlon II Sep 20 '13 at 13:26
-2

That's a pretty strange thing to do. Why don't you just create a CSS selector string for winId and save it as a variable?

var foo = '#' + winId;

Now you can do:

$(foo + ', #bar').html("add some content"); 

What you're proposing to do will leave any programmer working on this project -- including you six months from now -- completely flummoxed when they use $('#bar') and it's actually selecting #foo and #bar.

Neil Girardi
  • 4,533
  • 1
  • 28
  • 45