After becoming a convert (and a somewhat quiet evangelist) to jQuery, I wrote a series of introductory articles for the InformIT.com web site. The first one, Introduction to jQuery, covers the basic concepts, including selectors, chaining, and the page ready event. It also contains a comparison between a “traditional” JavaScript code and jQuery code.
Refactoring a menu: Remove unnecessary IDs
If you’re new to this series, read the “Refactoring a simple menu” and “Remove inline CSS” first.
The original HTML code for the dropdown menu (see below) was full of IDs that were used as a DOM navigation tool (I’ve used getElementById instead of traversing the DOM tree). When moving from a simple wrapper JavaScript library (I was using the X library) to jQuery, it makes sense to use its powerful traversal functions to replace the ID-based navigation.
<div class="rowMenu" id="IDAHYJQB">
<p><a href="/climbing/myClimbs/myClimbs.asp">First page</a></p>
</div>
<div class="rowMenu" id="IDAKYJQB">
<script>menuRegister('IDAKYJQB')</script>
<p id="IDAKYJQB_main"><a href="javascript:menuClick('IDAKYJQB')">Add ...</a></p>
<div id="IDAKYJQB_sub">
<p><a onclick="menuSelect('IDAKYJQB')"
href="/climbing/myClimbs/myClimbs_add.asp">New entry </a></p>
<p><a onclick="menuSelect('IDAKYJQB')"
href="/climbing/myClimbs/myClimbs_editWall.asp?a=add">Edit</a></p>
… more …
</div>
</div>The HTML code is interspersed with inline calls to three JavaScript functions:
- menuRegister registers the ID of a drop-down menu
- menuClick opens a drop-down menu
- menuSelect hides the drop-down menu and
Today we’ll replace these functions with simpler jQuery equivalents while getting rid of element IDs.
The menuRegister function will not be needed once we fix the DOM traversal problem. The menuClick and menuSelect functions will be merged into a single menuClickEvent function that will identify whether it works on a main menu entry or an entry of a drop-down menu. The this variable will be passed to the menuClickEvent function to avoid any ID-based references.
The menuClickEvent function has to perform a few setup tasks:
- Get the DOM element that was clicked, using either the function parameter or (for future use) the value of the this variable.
- Get the parent paragraph.
- Decide whether it’s operating on a top-row entry (the parent paragraph has a DIV sibling) or not.
Using jQuery makes the job extremely simple:
function menuClickEvent(thisParam) {
var a = thisParam || this;
var p = $(a).parents('P:first');
var d = p.siblings('DIV');
var toprow = d.length != 0;
… rest of the code …
}If the menuClickEvent function works on an element of a drop-down menu, it should hide the drop-down menu and return true (indicating that the event handler should perform its default task), otherwise it should toggle the state of the down class of the current paragraph and the visibility of the sibling DIV. The jQuery-based code is much shorter, more elegant and way more readable than the original.
function menuClickEvent(thisParam) {
var a = thisParam || this;
var p = $(a).parents('P:first');
var d = p.siblings('DIV');
var toprow = d.length != 0;
if (toprow) {
p.toggleClass('down');
d.toggle();
return false;
} else {
p.parents('DIV').hide().siblings('P').removeClass('down');
return true;
}
}With the new jQuery-based implementation of the drop-down menu, we can get rid of all element IDs, resulting in much shorter HTML code:
<div class="rowMenu">
<p><a href="/climbing/myClimbs/myClimbs.asp">First page</a></p>
</div>
<div class="rowMenu">
<p><a href="#" onclick="menuClickEvent(this)">Add ...</a></p>
<div>
<p><a onclick="menuClickEvent(this)"
href="/climbing/myClimbs/myClimbs_add.asp">New entry </a></p>
<p><a onclick="menuClickEvent(this)"
href="/climbing/myClimbs/myClimbs_editWall.asp?a=add">Edit</a></p>
… more …
</div>
</div>Next task: remove inline JavaScript calls
Opinion: Is the Firefox development model really better than Microsoft’s?
Everyone loves bashing Microsoft and preaching the vast superiority of Firefox over IE, particularly its near-perfect implementation of W3C standards. Unfortunately, that might be true as long as you’re doing what the most vocal advocates (the CSS folks) are doing, if you happen to rely on some other “supported” standard, you might find yourself in deep ****.
As my regular readers know, I happened to make a decision to use client-side XSLT a while ago. From the technology standpoint, it was a perfect solution … until Firefox 3 came out. It has so many XSLT-related problems that it’s almost impossible to get the right combination of relevant parameters to have the same XSLT stylesheet working on Firefox, IE, Chrome and the web server (for non-XSLT-capable clients).
OK, one would understand that every major software project has bugs. But it’s hard to understand that so many of the XSLT bugs are untouched after several years. For example: generic ticket describing XSLT result document problems (4 years), HTML-DOM initialization issues (5 years), Firefox crashing when combining XSLT with document.write (6 years), Firebug not working on XSLT-generated pages (1,5 year).
I’ve heard all about limited resources and priorities in my “previous life”, but let’s face the reality: once the motivation (let’s beat IE) wears off and the platform accumulates years of old sins and bad decisions, the development model doesn’t matter. Firefox is becoming no better than IE.
Firefox XSLT errors
When I decided to use server-side XML output and client-side XSLT transformations a while ago, Firefox 2 was the ideal development platform – reliable, well implemented browser with excellent debugging capabilities (Firebug). Since then, the XSLT ignorami contributing code to Firefox have managed to break so many things I simply have to document everything I know to be broken in one place:
- You have to use <xsl:output method=’html’>, and the root element of the transformed XML tree must be html (not HTML), otherwise the DOM is not properly initialized. DOCTYPE does not matter.
- Even though the DOCTYPE parameters in the xsl:output indicate transitional HTML, the attributes are case-sensitive (for example, onClick does not work).
- DOM initialization on Unix differs from Windows (document.body is missing).
- In some versions, numerous properties of the document object are missing.
- Output escaping cannot be disabled (this is not a bug, but a very particular feature implementation).
On top of everything else, Firebug does not work.
It breaks my heart, but I have to admit that with the rollout of Firefox 3 Internet Explorer 7 does a better job of handling client-side XSLT than Firefox.
Blogger backup works (finally)
Firebug does not work on XSLT-translated pages
If you want to use Firebug with Firefox 3 on pages using client-side XSLT transformation, you’re out of luck. Firebug simply does not work; the only way to a half-working environment is the following:
- Restart the browser
- Open an XML page with the xml-stylesheet directive.
- Open a new tab and load a regular page into it.
- Switch back to the previous tab. Firebug works within the context of the currently loaded page.
What’s really sad is that this has been reported by one of the Firebug developers almost two years ago … and nobody found it important enough to change the bug status from NEW to anything else.
The Blogger software "quality"
- Google has added embedded comment form to Blogger posts. This form works flawlessly with all versions of Firefox on Windows/Linux platforms and fails consistently with IE7 (you have to submit the comment twice). The bug appears in all of my blogs, regardless of when they've been created and what template they use.
- Google has introduced post ratings months ago. They've immediately got customer feedback that the web pages hang when viewed with Firefox. Five minute long investigation reveals the cause to anyone who's willing to figure it out: Sometimes the AJAX requests they're using take a long time to complete and obviously they're using synchronous XMLHTTPRequest calls in Firefox, blocking the browser.
In both cases, the bugs have been present for months, numerous users have complained (loudly) and nothing has been fixed. Will this affect Google? Probably not. Will I recommend their business applications to my customers? Definitely not.
Just in case you might wonder why I'm yammering in my blog and not complaining to Google: Once they've explicitely asked for our feedback on a new Blogger feature. I've submitted my bug report and never got as much as an acknowledgement ... and the bug that's been bothering me is still there.
Chrome does not hide an element that is not attached to the document
In IE, FF and Opera, the following code hides a DIV element, starts loading the content in it and appends the hidden DIV to the body:
$("<div id='loginDialog'>").hide().load(url).appendTo("body");In Chrome, the element is created, content is loaded into it, it appears at the end of the document, but it’s not hidden. To make the same code work in Chrome, you have to switch the order of operations: append first, hide later:
$("<div id='loginDialog'>").appendTo("body").load(url).hide();jQuery interprets NULL parameter to val() call as no parameter
I tried to be smart (no, I was not obfuscating my code) and squeeze the following algorithm into one line of code:
- Set the field email to the value of cookie user
- If the cookie user is not present, focus the email field, otherwise focus the pwd field.
The one-liner I (almost) got working is:
if ($("#email").focus().val($.cookies.get("user")).val()) $("#pwd").focus();The description of how this beast works is left as an exercise for the reader
However, the $.cookies.get function returns NULL if the cookie is not set and the first val call interprets the NULL value as no parameter, returning the current value of the field, not the chainable jQuery object.
Too bad, I had to write two lines of code:
$("#email").focus().val($.cookies.get("user"));
if($("#email").val()) $("#pwd").focus();Convert Atom-formatted DateTime
The following function converts ATOM (RFC4287) formatted Date/Time string into a VBScript Date variable. You’ll find it useful if you want to process Atom feeds with VBScript/ASP.
Function AtomDateTime(S)
Dim CompArray,ValueArray,DateVal,TimeString,TimeVal,ZoneSign,ZoneString,I
CompArray = Split(S,"T")
If UBound(CompArray) <> 1 Then _
Err.raise vbObjectError,"AtomDateTime","No 'T' in Atom date/time"
ValueArray = Split(CompArray(0),"-")
If UBound(ValueArray) <> 2 Then _
Err.raise vbObjectError,"AtomDateTime","Atom date part is not a date"
DateVal = DateSerial(CInt(ValueArray(0)),CInt(ValueArray(1)),CInt(ValueArray(2)))
TimeString = CompArray(1) : I = InStr(TimeString,"Z")
If I > 0 Then
TimeString = Left(TimeString,I-1)
TimeVal = GetAtomTime(TimeString)
Else
I = InStr(TimeString,"-")
If I > 0 Then
ZoneSign = 1
Else
ZoneSign = -1 : I = InStr(TimeString,"+")
End If
If I = 0 Then _
Err.raise vbObjectError,"AtomDateTime","cannot recognize timezone separator"
ZoneString = Mid(TimeString,I+1,Len(TimeString))
TimeString = Left(TimeString,I-1)
TimeVal = GetAtomTime(TimeString) + ZoneSign * GetAtomTime(ZoneString)
End If
AtomDateTime = DateVal + TimeVal
End Function The DateTime is converted into GMT time value. You might need to add/subtract your local timezone offset if you want to get local time.
The GetAtomTime function converts the time portion of the DateTime field:
Function GetAtomTime(TS) Dim I : I = InStr(TS,".") : If I > 0 Then TS = Left(TS,I-1) GetAtomTime = Timevalue(TS) End Function
Login to Google Data API
Google Data API has numerous client-side libraries, but not an ASP/VBScript one. Here’s the first routine you need: it authenticates you with the Google API and returns a handle you can use in subsequent calls.
The HTTPCheck is a function that checks the status code before returning to the caller. You could replace if with XRQ.status < 300. You'll find the EncodeFormData function in a previous post.
Dim XRQ ' global XMLHttpRequest object
Function GoogleLogin(UN,Pwd,Svc)
Dim RQData,I,RT
GoogleLogin = Null
RQData = EncodeFormData(Array("Email","Passwd","service","source"), _
Array(UN,Pwd,Svc,"VBScript-BloggerLibrary-0.1"))
If Not IsObject(XRQ) Then Set XRQ = CreateObject("Microsoft.XMLHTTP")
XRQ.open "POST","https://www.google.com/accounts/ClientLogin",false
XRQ.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
XRQ.send RQData
If HTTPCheck(XRQ,"Google login response", _
"https://www.google.com/accounts/ClientLogin") Then
RT = XRQ.responseText
I = InStr(RT,"Auth=")
If I = 0 Then wr "Google response has no AUTH data" : Exit Function
RT = Mid(RT,I+5,Len(RT))
I = InStr(RT,Chr(10)) : If I > 0 Then RT = Left(RT,I-1)
GoogleLogin = RT
End If
End FunctionThe inconsistent consistencies are driving me crazy
Here’s a simple question: what’s wrong with this code?
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns:xf="http://www.example.com/common/xslforms"> ... <input type="button" value="X" id="tb_0" onClick="callX(0)" />
Among other things, this is not valid XHTML (as claimed in DOCTYPE) because the onClick attribute should be spelled in all-lowercase (onclick).
Now the trick question: why am I so annoyed by this code? Because it works in IE7, FF2 and FF3 if the server serves HTML to the client and fails only in FF3 if the client performs XSLT transformation using a stylesheet similar to this one:
<?xml version="1.0" encoding="utf-8" ?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" encoding="utf-8" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/> <xsl:template match="/"> <html xmlns:xf="http://www.plezanje.net/common/xslforms"> … <input type="button" value="X" id="tb_0" onClick="callX(0)" /> … </xsl:template>
The ever-more-crazy implementation of XSLT in Firefox will persuade me to:
- Stop using client-side XSLT transformations or
- Stop supporting Firefox and recommend everyone to use IE or Chrome.
Generate HTML form data in VBscript
Sometimes you have to post form data in an AJAX call started from server-side ASP script (or WSH script). The following VBScript functions will return well-formed form data that you can use with the application/x-www-form-urlencoded MIME type:
Function URLEncoded(V) ' … private
Dim CC,I,Enc
Enc = ""
For I = 1 To Len(V)
CC = AscW(Mid(V,I,1))
If (CC >= 48 And CC < 58) Or (CC >= 64 And CC < 90) Or (CC >= 97 And CC < 124) Or CC = 95 Or CC = 45 Or CC = 46 Then
Enc = Enc & Mid(V,I,1)
Else
Enc = Enc & "%" & Hex(CC)
End If
Next
URLEncoded = Enc
End Function
Function CreateFormField(F,V) ' … private
CreateFormField = F & "=" & URLEncoded(CStr(V))
End Function
'
' EncodeFormData: Return form data for a POST request
'
' Input: N – array of form field names
' V – array of form field values
'
' Return: encoded form data
'
Function EncodeFormData(N,V)
Dim I
For I = LBound(N) To UBound(N)
If I <> LBound(N) Then EncodeFormData = EncodeFormData & "&"
EncodeFormData = EncodeFormData & CreateFormField(N(I),V(I))
Next
End FunctionHow crazy can it get: IE does not set the Referer field if the page was built via client-side XSLT
OMG: jQuery tag selectors are case sensitive
… if you use Firefox and client-side XSLT transformations. This is the scenario:
- The server generates an XML document
- The document is transformed using an XSLT stylesheet, either on the server or in the browser.
- jQuery is the client-side library.
If you use tags in jQuery selectors, for example $(".pulldownMenu P A").click(handler), the tag names are case insensitive if you use IE or server-side transformation with Firefox but become case sensitive if Firefox performs client-side transformation regardless of xsl:output settings. To ensure your jQuery code works in all cases, write exactly the same tag names in jQuery selectors and XSLT stylesheet.
