Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery custom selector, "undefined"

i am trying to make a jQuery ui calendar make ajax calls when a date is clicked on, but i ran into a problem a few days ago. I found a snippet of code that supposedly does this , but as i found out it uses jQuery custom selectors. The code gave me an error so i started digging into the custom selectors to find out more about them. So far i haven't been able to find out why i get this strange behavior.

Here is a picture to hopefully clear things up , i will explain more after it enter image description here

I've typed in the console

$('.ui-datepicker-calendar td a:test(3)')

And as you see i meta2 and stack2 are undefined and one more strange thing , why does index2 return a #document , its supposed to contain the index of the array of elements.

Moreover the element (el2) is not even the right element. Take a look , i call

$('.ui-datepicker-calendar td a:test(3)')

this is supposed to select all the dates from the calendar, and in the first loop , console.log should print out this

<td class=" ui-datepicker-week-end " data-handler="selectDay" data-event="click" data-month="8" data-year="2012"><a class="ui-state-default" href="#">1</a></td>

but instead i get the the first "a" tag in the whole document, in this case its the one for the previous month( as seen in the picture ).

If anyone can shed a little light on this situation , please do. Oh and one more thing i forgout about

meta2 , its supposed to contain this

[
    ':test(argument)', // full selector
    'test',            // only selector
    '',                // quotes used
    'argument'         // parameters
]

and again in my case its undefined...

I will share my javascript code i hope it helps

<script>
    $(function()
    {
        $.expr[":"].test = function(el2,index2,meta2,stack2)
        {
            debugger;
            console.log(el2);
            console.log(index2);
            console.log(meta2);
            console.log(stack2);
        }
    })

    $(function()
    {
        function getJsonDate(year, month)
        {
            $.getJSON('dates.php?year='+year+'&month='+month, function(data)
            {
                var i = 0;
                for (i = 0; i < data.data.length; i++)
                {
                    debugger;
                    var myDay = data.data[i]['d'];
                    $('.ui-datepicker-calendar td a:exactly('+data.data[i]['d']+')')
                    .css({color: '#f00'})
                    .attr('href',data.data[i]['link'])
                    .parent().attr('onclick','');
                }
            });
        }
        $.expr[":"].exactly = function(el, index, meta, stack) 
        {
            debugger;
            console.log(el);
            console.log(index);
            console.log(meta);
            console.log(stack);
            var s = meta[3];
            if (!s) return false;
            return eval("/^" + s + "$/i").test($(el).text());
        };
        $('#datepicker').datepicker(
        {
            inline: true,
            onSelect: function(dateText, inst) 
            {
                Date.prototype.toString = function () {return isNaN (this) ? 'NaN' : [this.getDate(), this.getMonth(), this.getFullYear()].join('/')}
                d = new Date(dateText);
                getJsonDate(d.getFullYear(), d.getMonth()+1);
            },
            onChangeMonthYear: function(year, month, inst) 
            {
                //alert(year);
                //alert(month);
                getJsonDate(year, month);
            }
        });
    });
</script>
like image 435
Jordashiro Avatar asked Sep 03 '12 09:09

Jordashiro


2 Answers

The shortest explanation is "jQuery 1.8.0 has a bug in it, upgrade to 1.8.1 for the fix", but that doesn't quite answer everything.

jQuery 1.8.x has a significantly upgraded "Sizzle" engine, which is what it uses for selectors. The way custom selectors are called has been altered as part of this change, but moreover the way a lot of selectors are processed have been altered. Various assumptions about the order in which rules are processed no longer hold true, etc. It's also significantly faster in various use-cases.

Even when upgrading to 1.8.1, you'll still see things look quite a bit different from the way they did in 1.7.2 (the latest in the pre-1.8.x series) when processing the example you provided. This explains what you're seeing in the selection of "the first <a> element on the page". That is to say: your expectation of how custom selectors work is not how they actually work, and if you allowed the loop to continue (rather than calling 'debugger;' at the first iteration), you'd see that it's actually going through all <a> elements). In short: Sizzle doesn't guarantee what order the various rules are going to be called, only that the result will match all of them.

If you are certain that your custom rule will be less-efficient than other rules (perhaps because you are certain that other rules can severely reduce the number of matched elements), you can force these to run first by selecting them, then calling .find() on just that subset of elements, eg:

$(".ui-datepicker-calendar").find("td a:test(3)");

As for the "stack" being undefined, as Kevin B points out, though the 1.8.1 update restores backwards-compatibility, the API has changed, and it looks like "stack" is simply no-longer passed into the pseudo. This makes sense, really, due to the altered order in which tests may be called. That is: the stack is empty at the time you reach it, because "see if any of the <a> elements match this pseudo-selector" is the first rule that gets processed. Tests should always be self-contained, so the stack wouldn't really be very useful anyway (may only lead to confusion).

So if 1.8.1 restores backwards-compatibility, then what is the forwards-compatible method for creating pseudo-selectors? As you can see in the documentation for Sizzle's pseudo-selectors, the preferred method for creating pseudo-selectors as of jQuery 1.8 is the "createPseudo" method ($.expr.createPseudo), which prefers to use a closure instead of the "meta" argument. So for your particular examples, the "new" ways of doing them would be:

$.expr[":"].test = $.expr.createPseudo(function( tomatch )
{
        return function( el2 )
        {
            debugger;
            console.log(el2); // not much else to see here
        };
})

where "tomatch" is the argument to the :test(...) selector. As you can see, the extra arguments you were looking for are simply no longer necessary in this new syntax. As for something a bit more useful:

$.expr[":"].exactly = $.expr.createPseudo(function( s )
{
    return function(el)
    {
        if(!s) return false;
        return eval("/^" + s + "$/i").test($(el).text());
    };
});

This version of "exactly" should be compatible with 1.8+, and is the preferred* method of doing things.

I think that even with the bump in jQuery version / api, the code you provided still won't do exactly what you want, as the datepicker is liable to be rebuilt on the whim of the plugin. There is still a brief moment when you can tell that the desired elements are indeed highlighted as intended, so the selector itself does appear to be working.

A full example is below, based on the samples which you provided. You can see the differences in behaviour by changing the jQuery version used between 1.7.2, 1.8.0, and 1.8.1. For compatibility across versions, a test for $.expr.createPseudo has been added to the pseudo-selector function assignments. Note that all "debugger;" statements have been commented-out, as having a breakpoint at each iteration across all date links in the datepicker gets rather tedious, and the getJSON call has been mocked to allow the test to be self-contained.

<html>
<head>
    <title>jQuery custom selector, "undefined"</title>
    <!-- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.js"></script> -->
    <!-- <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.js"></script> -->
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.js"></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.23/jquery-ui.js"></script>
    <link rel="stylesheet"
        href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.23/themes/base/jquery-ui.css" />
    <script>
    $(function()
    {
        $.expr[":"].test = $.expr.createPseudo ?
            $.expr.createPseudo(function( tomatch )
            {
                return function( el2 )
                {
//                  debugger;
                    console.log(el2);
                };
            }) :
            function(el2,index2,meta2,stack2)
            {
    //          debugger;
                console.log(el2);
                console.log(index2);
                console.log(meta2);
                console.log(stack2);
            };
    })

    $(function()
    {
        function getJsonDate(year, month)
        {
            //$.getJSON('dates.php?year='+year+'&month='+month, function(data)
            //{
                var data = {data:[
                    {d:1,link:"a"},
                    {d:15,link:"b"},
                    {d:25,link:"c"}
                ]};
                var i = 0;
                for (i = 0; i < data.data.length; i++)
                {
//                  debugger;
                    var myDay = data.data[i]['d'];
                    $('.ui-datepicker-calendar td a:exactly('+data.data[i]['d']+')').
                        css({color: '#f00'}).
                        attr('href',data.data[i]['link']).
                        parent().attr('onclick','');
                }
            //});
        }

        $.expr[":"].exactly = $.expr.createPseudo ?
            $.expr.createPseudo(function( s )
            {
                return function(el)
                {
                    if(!s) return false;
                    return eval("/^" + s + "$/i").test($(el).text());
                };
            }) :
            function(el, index, meta, stack) 
            {
//              debugger;
                console.log(el);
                console.log(index);
                console.log(meta);
                console.log(stack);
                var s = meta[3];
                if (!s) return false;
                return eval("/^" + s + "$/i").test($(el).text());
            };

        $('#datepicker').datepicker(
        {
            inline: true,
            onSelect: function(dateText, inst) 
            {
                Date.prototype.toString = function () {
                    return isNaN (this) ?
                        'NaN' :
                        [this.getDate(), this.getMonth(), this.getFullYear()].join('/')
                }
                d = new Date(dateText);
                getJsonDate(d.getFullYear(), d.getMonth()+1);
            },
            onChangeMonthYear: function(year, month, inst) 
            {
                //alert(year);
                //alert(month);
                getJsonDate(year, month);
                return false;
            }
        });
    });
    </script>
    <script>
    (function($){$(function(){
        $("<input />").
            attr({type:"button", value: "run test selector"}).
            click(function(){
                $(".ui-datepicker-calendar td:test(3) a");

                // Or, if you are certain that your test will be less-efficient than an exclusion based
                // on parents, you could do:
                //  $(".ui-datepicker-calendar").find("td a:test(3)");
            }).
            appendTo("body");
    })}(window.jQuery));
    </script>
</head>
<body>
    <a href="#ignoreThisLink">.</a>
    <a href="#ignoreThisToo">.</a>
    <p>
        (first, click the input box to cause the datepicker to initialise)
    </p>
    <input type="text" id="datepicker" />
</body>
</html>

I hope that helps to shed some light on things.

*I say this is the "preferred" method, but you are still using "eval". This is highly discouraged, as it's slow and can lead to unexpected/surprising/dangerous results. A better way to build regexes based on a string would be

return (new RegExp("^" + s + "$", "i")).test($(el).text());

Though even this has problems, as "s" may contain RegExp special-characters. In this particular case, though, a regular expression is not even needed, and things can be tested much more efficiently via:

return String(s).toUpperCase() === $(el).text().toUpperCase();

You can even save a bit more by rolling the conversion into the closure, giving you the full function of:

$.expr[":"].exactly = $.expr.createPseudo(function( s )
{
        if(!s) return function(){ return false; };
        s = String(s).toUpperCase();

        return function(el)
        {
            return (s === $(el).text().toUpperCase());
        };
});
like image 62
Will Palmer Avatar answered Sep 28 '22 05:09

Will Palmer


The jquery ui datepicker has hooks for this kind of functionality. Instead of trying to target the DOM elements which make up the dates, you should bind to the behaviour of selecting one. jsFiddle

$('#datepicker').datepicker({
    onSelect: function(dateText, inst){
        //awesome ajax stuff based on dateText
    }
});​

edit for comment: if you need to style a particular date then you should target it by applying a custom class before it is drawn. jsFiddle

$('#datepicker').datepicker({
    beforeShowDay: function(date){
        return [true, 'date-' + date.getDate() ];
    }
});
like image 31
Sinetheta Avatar answered Sep 28 '22 07:09

Sinetheta