Tuesday, June 25, 2013

Using the PeopleTools 8.53 Version of jQuery "Safely"

Now that PeopleTools 8.53 includes jQuery, the question is, "how do I use it?" Probably the easiest way is to add something like this to your Pagelet or page where you want to use jQuery (see New PeopleTools 8.53 Branding Tools):

<script type="text/javascript" src="/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS"></script>

Some of you may have tried this. It works great if you have just one item that uses jQuery. If you add more (for example, if you add multiple pagelets) where some use plain jQuery, some use jQuery UI, and others use jQuery Cycle, you will see that each new inclusion overwrites your plugin list, rendering your new pagelet unusable. What about include protection? This concept is still relevant, but I would hate to manage it as a modification if there was another way. Here is what I came up with for 8.53:

<script type="text/javascript" src="/psc/ps/EMPLOYEE/EMPL/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS"></script>
<script type="text/javascript">
  // special variable to store "our" single jQuery instance
  if(!window.psjq$) {
    window.psjq$ = window.$ = window.jQuery;
  } else {
    // ignore recent import and use the global single instance
    window.jQuery = window.$ = psjq$;
  }
</script>

If you look at the PT_JQUERY_1_6_2_JS HTML definition in app designer, you will see that PeopleTools uses jQuery.noConflict() to free up $, but not jQuery. Using noConflict to move $ into a new variable named ptjq162 is appropriate, but doesn't help with plugins. Properly coded plugins use the global variable window.jQuery. Looking through jQuery UI, cycle, and many of the other plugins I use, they all use the global jQuery variable, with no mechanism for replacing it. As you can see here, my solution is to store jQuery in the custom global variable psjq$, and then override jQuery on each import of jQuery. This way all plugin scripts loaded after my script will always use the original, single instance jQuery copy of psjq$. Note: I tried using ptjq162.noConflict(true) to manage a single instance, but didn't get it working. My approach just seemed easier for me to understand, and, well, it just worked.

I don't have much "burn in" time with this, so I'm open to suggestions. One key difference between this approach and the Include Protection approach is this approach processes jQuery for each jQuery include. It drops it after processing it, but you still take the performance hit (you may not notice it, but it is still there). The include Protection approach only processes jQuery once.

Note: My script above assumes $ is for jQuery. The point of jQuery's noConflict is compatibility with other libraries that use $. If you find yourself in this situation, just remove $ from the assignments above. For compatibility reasons, you should probably never use $ except in a closure, but...

Warning: PT_JQUERY_1_6_2_JS is NOT safe for use with multiple versions of jQuery. The noConflict call at the end of the script is compatible with $, so if you have another, more recent version of jQuery, $ will still point to your newer version. The jQuery variable, however, will only point to the last version parsed (or the last assignment, as shown above). $ is nice, but it is really the jQuery variable that matters. To be compatible with other versions of jQuery, the HTML definition would have to use jQuery.noConflict(true). The jQuery docs don't recommend having two versions on the same page, but it is important to note.

19 comments:

Eva Lona said...

Where can i find Peoplesoft Solutions for different problems?

Jim Marion said...

@Eva support.oracle.com.

Unknown said...

Excellent! I used this technique with the XSL for accordion navigation collections. Brought in the images/JQUERY/CSS with this method. Any idea how to remove the Site which is the last bit of hardcoding?

Unknown said...

Excellent! I used this technique with the XSL for accordion navigation collections. Brought in the images/JQUERY/CSS with this method. Any idea how to remove the Site which is the last bit of hardcoding?

Jim Marion said...

I don't have a "good" way to remove the site name from URL's. I posted some JavaScript that shows how to identify the site here. You could construct your URL with this information. The problem with this approach is then injecting the JavaScript, CSS, etc with the constructed URL. You don't want to use document.write because that messes up the Ajax loaded homepage. You could use the document.createElement method to create the script and link tags, but that often happens after the content loads, so it might be visually unattractive. The best approach still seems to be a custom Meta-HTML transformer to generate the URL's server side.

Unknown said...

Since this was a navigation transform I was trying to use the URL within the collection. I thought I could get away with...


While this does return just the portion I want I'm having problem pushing this into the href for some reason.

Jim Marion said...

Oh, if this is a transform, yes, you can get stuff out of the href and parse/process the URL. Great idea.

Jim Marion said...

Building a new display format would give you a solid, fail-proof method to generate URL's, but the custom display format with XSL transform is a good idea. The site name is always between the second and third "/", so it would be easy to parse the site portion out of a URL. I would probably do this early and put the value in a high level xsl variable. Then you can reference it throughout your templates.

Kenny Choi said...

Hi Jim, I am from Adventist Health System (AHS) and I implemented the DataTables jQuery plug-in to a Pagelet in PS 9.2, it works! However, when i tried to include ".../s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS" in the XSLT, it doesn't work. Instead, the link would actually take me to a minified .js file "PT_JQUERY_1_6_2_JS_MIN_1.js". I can't even find the definition for "PT_JQUERY_1_6_2_JS_MIN_1.js" in App Designer. When I use the Google CDN (jquery 1.6.2 full) as a resource, it works like a charm. Could you please give me some directions regarding this issue? Thanks!

Jim Marion said...

@Kenny, PT_JQUERY_1_6_2_JS should be an HTML definition in any 8.53+ system. If you don't find PT_JQUERY, search for %JQ%.

I am confused by the statement, "Instead, the link would actually take me to a minified .js file PT_JQUERY_1_6_2_JS_MIN_1.js." So when you open the iScript URL in a browser, you see a minified jQuery 1.6.2 file? That is what you are supposed to see. The iScript calls %Response.GetJavaScriptURL and then redirects to the URL. When you have your pagelet open, if you look in Firebug or in Chrome (or Safari) tools, do you see the jQuery JavaScript file in the list of script resources? What if you save in Pagelet wizard and load the pagelet on a homepage?

Jim Marion said...

@kenny, one more thing. The URL in your post had "..." Is that because you left off your site specific information for the comment post or your URL really has ... in it? Just confirming because a relative URL should only have two dot's ".." rather than three.

Kenny Choi said...

Hi Jim! Thanks for your prompt response.

Yes, I can see the jQuery JavaScript file in the list of script resources when I open the iScript URL in Chrome. Here is what I have in the Pagelet XSLT:


***** THIS DOES NOT WORK WITH MY JQUERY PLUGIN (IMPLEMENTED INTO THE PAGELET) *****

script src="/psp/fscsnd/EMPLOYEE/ERP/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS" type="text/javascript" /
script type="text/javascript"
if(!window.psjq$) {
// Special variable to store "our" single jQuery instance
window.psjq$ = window.$ = window.jQuery;
} else {
// Ignore recent import and use the global single instance
window.jQuery = window.$ = psjq$;
}
/script


VS.


***** THIS WORKS WITH MY JQUERY PLUGIN (IMPLEMENTED INTO THE PAGELET) *****

script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.js" type="text/javascript" /


When I save in the Pagelet Wizard (with the above iScript call) and load the Pagelet on a homepage, it only returns a "basic" PS Query without any functionality delivered by the jQuery plug-in. However, the jQuery plug-in works like a charm after replacing the iScript call by the Google Hosted Libraries. So, I would like to know if it is casued by the incompatibility of jQuery 1.6.2.

I try to make the posts as short and precise as possible. If you found that it is too abbreviated, I am more than happy to give more information. In my previous post, I just left off the site specific information by "...". Sorry for confusion caused. :P

I also took off the Tags for "script" this time for posting purpose.

Jim Marion said...

@Kenny, that makes sense. I don't have experience with Data Tables. Your version theory sounds good. For testing purposes, you can tell if jQuery is included by typing the following into the console:

frames["TargetContent"].jQuery.fn.jquery

This will give you the version #. If you get results, then you know you have jQuery included, but the plugin isn't registering.

The other possibility is that jQuery is loading multiple times and one of the loads happens after the plugin, thereby erasing the list of registered plugind (this is actually why I created the work-around in this blog post).

Knowing that it works with the CDN is good, but I have concern over the control aspect and security of a CDN. Basically, you have no control over the CDN and no agreement for liability, security, reliability, etc. If you have a version of jQuery that works, then my preference would be to load that version of jQuery into an HTML definition and change the ID in the iScript URL to match the ID of your new HTML definition. I would also add the if(!window.jQuery) {... } wrapper around the downloaded compressed jQuery (inside the HTML definition, see jQuery Plugin Include Protection) to ensure you only have one copy of jQuery on your homepage. The reason you only want one copy is to keep another pagelet from reloading jQuery and erasing the list of previously loaded plugins.

Kenny Choi said...
This comment has been removed by a blog administrator.
Jim Marion said...

Kenny Choi wrote:

Hi Jim,

Thanks for your reply and valuable suggestions. Today, I found something interesting:

*** WHEN USING < script src="/psp/fscsnd/EMPLOYEE/ERP/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS" type="text/javascript" / > ***

It doesn't work and Chrome console returns:

TableManager::findTables() | content.scripts.c.js:11
> Resource interpreted as Script but transferred with MIME type text/html: "http://my.peoplesoft.server/psp/fscsnd/EMPLOYEE/ERP/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS". | PORTAL_REFRESHPAGE_MIN_233.js:215
Uncaught SyntaxError: Unexpected token < | WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS:1
> Uncaught ReferenceError: jQuery is not defined | jquery.dataTables.js:39
> Uncaught ReferenceError: jQuery is not defined | ColReorder.js:982
> Uncaught ReferenceError: jQuery is not defined | TableTools.js:2475
> Uncaught TypeError: Property '$' of object [object Object] is not a function


*** WHEN USING < script src="/cs/fscsnd/cache/PT_JQUERY_1_6_2_JS_MIN_1.js" type="text/javascript" / > ***

It works and Chrome console returns:

TableManager::findTables() | content.scripts.c.js:11
>


I compared two .js files (by http://www.diffnow.com/) and found that: Number of differences: 0. Any thought?

@Kenny, I'm going to do some testing with this. The text/html content type seems odd since the iScript does a redirect, and weblogic should then set the content type based on the redirected file extension. I'm also wondering about that "Unexpected token"... in the resources tab of Chrome, you should be able to see the source of every file downloaded. In the Network tab, you will see the request and response headers. The fact that you are getting a text/html content type response from the iScript makes me think the iScript is returning an HTML response, something like "You are not authorized..."

Kenny Choi said...

Hi Jim! Good morning!!

I would like to throw in some supplementary information. The Network Tab from Chrome JavaScript Console reads:

< script src="/psp/fscsnd/EMPLOYEE/ERP/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS" type="text/javascript" / >

HEADERS
Request URL:http://my.peoplesoft.server:12345/psp/fscsnd/EMPLOYEE/ERP/s/WEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS?ID=PT_JQUERY_1_6_2_JS
Request Method:GET
Status Code:200 OK

Request Headers | view source
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8
Connection:keep-alive
Cookie:[...]
Host:my.peoplesoft.server:12345
Referer:http://my.peoplesoft.server:12345/psp/fscsnd/EMPLOYEE/ERP/h/?tab=DEFAULT
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.69 Safari/537.36

Query String Parameters | view source | view URL encoded
ID:PT_JQUERY_1_6_2_JS

Response Headers | view source
Cache-Control:no-store
Cache-Control:no-cache
Content-Encoding:gzip
Content-Length:9609
Content-Type:text/html; CHARSET=UTF-8
Date:Thu, 17 Oct 2013 13:07:20 GMT
Expires:Thu, 01 Dec 1994 16:00:00 GMT
ignoreportalregisteredurl:1
portalcachecontent:true
portalregisteredurl:http://my.peoplesoft.server:12345/psc/fscsnd/EMPLOYEE/ERP/s/WEBLIB_PT_NAV.ISCRIPT1.FieldFormula.IScript_UniHeader_Frame
portaltopnav:true
pscache-control:role%2cmax-age%3d-1
pscache-excludeparams:c
pscache-handler:psft.pt8.cache.handler.MenuCacheHandler
Set-Cookie:PS_TOKENEXPIRE=17_Oct_2013_13:07:20_GMT; domain=.peoplesoft.server; path=/
usesportalrelativeurl:true
X-Powered-By:Servlet/2.5 JSP/2.1

RESPONSE
< !DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd" >
< html dir="ltr" lang="en" >
< !--
Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
CHROME/30.0.1599.69 ToolsRel:8.53.02 Portal Target URL: http%3A%2F%2Fmy.peoplesoft.server%3A12345%2Fpsc%2Ffscsnd%2FEMPLOYEE%2FERP%2Fs%2FWEBLIB_PTBR.ISCRIPT1.FieldFormula.IScript_GET_JS%3FID%3DPT_JQUERY_1_6_2_JS-->
< head >
< script type="text/javascript" > ...


< script src="/cs/fscsnd/cache/PT_JQUERY_1_6_2_JS_MIN_1.js" type="text/javascript" / >

HEADERS
Request URL:http://my.peoplesoft.server:12345/cs/fscsnd/cache/PT_JQUERY_1_6_2_JS_MIN_1.js
Request Method:GET
Status Code:200 OK (from cache)

RESPONSE
[.js codes : (function( window, undefined ) {
...]

Kenny Choi said...

Hi Jim

How are you today? I would like to get an update if the test was performed (iScript Call vs Cached PT_JQUERY_1_6_2_JS_MIN_1.js)

Thanks!

Regards
Kenny

Jim Marion said...

@Kenny, In a nutshell, the delivered iScript mentioned in this post uses %Response.RedirectURL(%Response.GetJavaScriptURL(HTML.PT_JQUERY_1_6_2_JS)). The redirect will cause the browser to use the version in cache. Using Fiddler you can see the request/response and the redirect headers, but no request for the PT_JQUERY_1_6_2_JS file once it is cached. An alternative would be to write the JavaScript directly to the iScript response and add caching headers to the iScript. This would eliminate the HTTP redirect request, but would miss out on some of the features of the /cs/ servlet.

Steve Fradlin said...

@Kenny, It looks like you are using psp not psc when calling WEBLIB_PTBR