Monday, April 01, 2013

jQuery Plugin Include Protection

I have a few blog posts that show how to use jQuery plugins on PeopleSoft homepages. When designing those pagelets in Pagelet Wizard, it is important that your XSL/HTML include jQuery and any necessary plugins within your pagelet's HTML/XSL. This is how my Slideshow and Accordion Navigation templates work. Including jQuery and required plugins in each pagelet, however, means that a homepage using these pagelets will have multiple instances of jQuery. jQuery is designed to load once, with plugin scripts loaded as needed. Since each pagelet has its own pointer to jQuery, as each pagelet loads, the browser tries to reload jQuery, redefining the jQuery and $ global variables and resetting the collection of previously loaded plugins. The end result is that a homepage with multiple jQuery based pagelets will only have one working pagelet. The rest will have been invalidated by the last pagelet to load jQuery.

The jQuery documentation discourages the presence of multiple instances of jQuery within the same page. The theoretical concept is that each page should load jQuery once, and sites should be written to include only one jQuery script tag. The nature of homepages with their independently managed fragments doesn't allow for this. The way I work around this is to wrap the jQuery JavaScript library in something akin to the C-style header #ifndef include guards.

After downloading the jQuery JavaScript library, I wrap the contents of the file in a conditional block that looks something like this:

if(!window.jQuery) {
/* downloaded, compressed/minified jQuery content goes here */
}

I make the same change to jQuery plugins because they often include their own setup and usage data, but, of course, testing for a different variable. Here is my jQuery UI processing protection:

if(!window.jQuery.ui) {
/* downloaded, compressed/minified jQuery UI content goes here */
}

This minor change to the jQuery JavaScript library and plugin files keeps the browser from re-interpreting these JavaScript libraries. The browser interprets these file once, and then fails the conditional for each subsequent script tag that points to that particular library. This allows plugins to load as needed and all plugin setup and usage data to persist across multiple pagelets.

Of course, the best solution would be to just have each JavaScript file referenced once. Since that isn't practical on a homepage, this solution at least ensures the files are only processed once.

24 comments:

George Varghese said...

Hi Jim-

I have your book and am using it to get a better understanding of coding using JQuery and Javascript. I have two grids on a time entry page and am trying to align them one below the other exactly column to column and am thinking I need to use something like jquery to achieve this because when PS renders the page, no matter how much I align the grids in app designer they are always off. Any ideas on how I should achieve this ? I got as far as finding the elements in firebug but cannot get the position to change.
Thanks.

George

Jim Marion said...

@George, lining them up column by column will be difficult. As you saw, the tr elements have a unique ID, but the cell (td) elements do not. It is certainly possible to set both to have the same column widths using jQuery. One way to do this would be to iterate over the td's in row 1 of the first table, and compare the widths with the second table. For each column, set the width of both to the greater of the two.

Jim Marion said...

@George, here is an example of iterating over row 1 in the user profile roles and then printing the width of each cell in that row:

$("#trPSROLEUSER_VW\\$0_row1").find("td").each(function() {
console.log($(this).width());
});

When using jQuery selectors, always start with something very specific with browser native support, like an ID selector, and then use find on the subset to reduce the selected elements. jQuery resolves selectors right to left, so searching for the selector "#trPSROLEUSER_VW\\$0_row1 td" is accurate, but not as efficient because the sizzle engine will iterate over all td's, reducing the set to just the ones that are children of #trPSROLEUSER_VW$0_row1.

Also notice the \\ before the $. $ has a special meaning in selectors, so you have to escape it in jQuery selectors.

lakshminarayana said...

Hi Jim,

How are you? This is lakshminarayana. I need your help on pagiantion on peoplesoft page (below the grid)


Current Functionality : Page has a grid and set the occurs level as 5. If they click on the "Next" Icon then next set of 5 rows will be disaplyed.

New Functionality: Pagination need to impliment.For that I analysed the "Next" Icon functionality of the grid property. It is calling the below logic.
--------------------
class='PSHYPERLINK' name='MY_PAGE_FIELD_NAME$hdown$0' id=MY_PAGE_FIELD_NAME$hdown$0' tabindex='29' href="javascript:submitAction_win0(document.win0,'MY_PAGE_FIELD_NAME$hdown$0')
--------------------

When page loads by default 1st 5 set of rows are disaplying. Then if user clicks on the 5 button(Pagination button) then i will check the difference between previous and currnet pagination buttons and that many times i will trigger the above code.

For me i am not getting how to trigger the above code.

So please help me to trigger the above code when we click on the pagiantion button.

For pagination buttons i am taking peoplesoft push buttons.

Thanks for your support.

Lakshminarayana

Jim Marion said...

@Lakshminarayana, the submitAction call in the button click is a JavaScript form post. There are two ways to execute this code. The first is using jQuery selectors and .click() to trigger the original button's click. The second is to call the exact same code from your event handler. If you hide the navigation buttons within the grid's or scroll's properties, I'm not sure if the JavaScript will still work. This will perform an Ajax form post and reload the grid based on the Ajax response.

I also thought there were PeopleCode methods or functions for switching pages in a scroll. Worst case, I thought you could set the active row using PeopleCode in a FieldChange event.

James La Brash said...

Hi Jim,

Long-time reader, first-time commenter (though I believe we've been on at least one or two conference calls / e-mail threads together, through Matt).

I wanted to share the basics of the code we use to overcome this same problem within InFlight.

SharePoint's Web Parts on a container page are very much akin to PS pagelets -- autonomous and (for all intents and purposes) unaware of each other when they're rendered.

So, like you, we aim to load jQuery and the plugins once and only once.

Here's the code that would go inside each pagelet: (tags are in [] instead of <>)
[script type='text/javascript']
if (typeof INFLIGHT_LOADER_EXISTS == 'undefined') {
document.write('[script src="/InFlight/Common/js/InFlightAssetLoader.min.js"][\/script]');
}
[/script]
[script type='text/javascript']
InFlightLoadFile('/path/to/resource.1.js'); // e.g. plugin #1
.
.
.
InFlightLoadFile('/path/to/resource.N.js');
[/script]


Here's the simplified edition of the InFlightAssetLoader.js... It can be modified to support whatever flavor of lazy loading you like (though document.write has proven consistently successful for us). Also, the real script does things like allowing you to write into different spots within the DOM, but I wanted to keep this simple. The basic structure is here:

var INFLIGHT_LOADER_EXISTS = true; // note that in a real implemetation, variables like this should be namespaced, not global
var InFlightLoadedFiles = [];
var JQUERY_FILE = "/InFlight/Common/js/jquery.min.js";
var INFLIGHT_CSS = "/InFlight/Common/css/InFlightHelperStyles.css";
InFlightLoadFile(JQUERY_FILE);
InFlightLoadFile(INFLIGHT_CSS);
function InFlightLoadFile(filePath, args) {
// Isolate the file name and extension
var fileName = filePath.replace(/^.*(\\|\/|\:)/, '');

if (InFlightLoadedFiles[fileName] == null) {
// Detect if we're trying to add jQuery itself and
// look to see if the jQuery() function already exists
var REjQ = /jquery(?:\-(\d|\.)*)?(?:\.min)?\.js/;
var skipLoad = false;
if (REjQ.test(fileName)) {
skipLoad = (typeof jQuery != 'undefined');
}

if (!skipLoad) {
// We haven't loaded this script yet, so load it
var REext = /(?:\.([^.]+))?$/;
var ext = REext.exec(fileName)[1];
var isCSS = (ext === 'css');
if (isCSS) {
document.write("[link rel='stylesheet' type='text/css' href ='" + filePath + "' /]");
} else {
document.write("[script type='text/javascript' src ='" + filePath + "'][\/script]");
}
InFlightLoadedFiles[fileName] = true;
}
}
}

Jim Marion said...

@James, thank you for sharing. This is a very good approach because it keeps the browser from attempting to read all that JavaScript multiple times. For sites using Pagelets with a PeopleToos 8.49 Portal and earlier, your recommendation is better than mine. The reason I qualify the PeopleTools release is because later versions of PeopleTools (8.50 or 8.51... can't remember which) switched from delivering a server-side assembled homepage to a client-side assembled homepage. The 8.5+ homepage contains pointers to pagelets and uses Ajax to parallel load pagelet content after the homepage loads. document.write is a great strategy for loading JavaScripts when a page is loading because of its blocking nature. I recommend this approach in my PeopleTools Tips book. After a page loads, however, document.write will actually replace the page. The way PeopleTools 8.5+ handles this is to check for pagelets using document.write, and then force a full homepage non-Ajax reload.

The document.write strategy with a PeopleSoft portal is great for 8.49 and earlier, but may cause performance problems with later versions of PeopleTools because it causes the homepage to stop processing and reload. You can see this in action by creating a simple HTML pagelet that has the following script:

[script type="text/javascript]
document.write("Hello World!");
[/script]

When a PeopleTools 8.50 (or was it 8.51...) portal loads this pagelet, as soon as it hits document.write, the whole homepage will reload in "classic" mode without Ajax.

I discovered this "feature" after a PeopleTools upgrade. I had some user homepages that would "flicker" (load part way, then reload) and I wasn't sure why. I had pagelets that used document.write, which was causing a full homepage refresh. This behavior makes it so document.write continues to work, it just doesn't perform as well.

How does SharePoint handle this? Does it serve a fully loaded homepage with all WebParts and WebPart content or does it use Ajax to fetch WebPart content in parallel/asynchronously after the homepage loads?

Jim Marion said...

@James, I was just looking at my collaborate schedule for tomorrow and I see you have a session in the morning. I look forward to seeing you there!

Randy Roberts said...

I did buy your book last week, great information. I have a question. In our Interaction Hub (9.1, 8.52.07) we have content from HCM and Financials and it's working well. We have a go-live coming up for a large group of self service users. When the content is rendered there is a return link that we would like to remove. Can you point me to what might drive that return link and how to remove it from the the HCM and Financials content? Thanks in advance.

Jim Marion said...

@Randy, what type of content? Is it query content?

Justin Cvancara said...

Hi Jim-

Trying to apply your fix and for the life of me can't get it to work. Still pretty green with javascript and jQuery. Here's the java calls that I'm using:

script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">/script>
script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js">/script>
script type="text/javascript">
$(document).ready(function() {
$("#demo_myHrLinks").accordion({
heightStyle: "content",
active: false,
collapsible: true,
header: 'h3',
disabled: false,
animated: 'bounceslide' });
});
/script>
I currently have this in the head. When I try to wrap your if statement, I see the statement in my pagelet. Help as to where to place it?

Thanks
Justin

Jim Marion said...

@Justin, this post applies to the jQuery and jQuery UI files directly, not to the HTML that inserts the HTML. From what you posted in the comments, your jQuery is actually hosted by the Google Ajax API's/CDN, so you can't modify the files to take advantage of what I mentioned in this post. To apply "include protection," download the jquery files, modify them according to this post, place them in a location where your browser can access them (your PeopleSoft web server, etc), and then update your HTML to point to your versions of the JavaScript libraries.

FoggyRider said...

Jim,

Just wondering if you have you tried to get Raphael.js to work in a PS environment. I tried to created some dashboard using Raphael but nothing returned. I'm sure the Raphael library was loaded, I could see the code in Firebug, but just got no results back.

Thanks Gary

Jim Marion said...

@Gary, no, I haven't looked into Raphael.

Chris Couture said...

Hi Jim,

Any reason not to put the JS path in the header? I do it regularly as I'm usually doing alternate templates for homepage to get the look I want. In cases where only an HTML override is used I call JS from a directory on the web server. If I'm doing a WEBLIB-based template I can get JS from the database (as an HtML definition).

Another approach is to put a "hidden" pagelet on the page, where the pagelet contains a URL or source code. I think it's rather hack-ish but it works in a pinch.

Gary - though I've not done Raphael work directly I have seen it used in PS in production. It is definitely do able.

Chris

Jim Marion said...

@Chris, I have jQuery in my header as well. The problem with this approach is that it doesn't help with Pagelet Wizard. Yes, if I put jQuery in the header, then Pagelet Wizard pagelets on the homepage will be able to use it, but Pagelet Wizard won't use it in the Pagelet Wizard itself. This is because of the separation of the header and component pages through frames and is why I put jQuery in my pagelet HTML and XSL as well.

Gary Noar said...

@Chris, Thanks I was able to get Raphael JS working in PS environment. Issue was with my function being called by window.onLoad(). I called the function directly from the pagelet and it worked.

Gary

Chris Couture said...

I've done CSS injection into the component iFrame using JS to add another Link element to the head section. What might one need to consider in trying a similar approach with a script element? Needs a path, like to jquery on a server, or something else. Not quite the same as CSS, since style sheets are cached and we can get the URL via PeopleCode.

There are always little nuances to these nifty tricks. Just because one can doesn't mean one should, eh?

Cheers,

Chris

Jim Marion said...

@Chris, right, just because you can, doesn't mean you should. In Part II of my PeopleTools Tips book I show how to insert scripts into pages. It works the same in iframes (as long as they don't violate the domain of origin policy). Here is the JavaScript:

apt.files.importScript = function(parms) {
var s = document.createElement("script");
s.type = "text/javascript";
s.src = parms.url;
s.id = parms.id;
s.defer = (parms.defer) ? parms.defer : false;
document.getElementsByTagName("head")[0].appendChild(s);
}

Just replace the document references with the document inside the iframe.

Daniel Kibler said...

Jim

We are upgrading our PeopleTools from 8.49 to 8.53. I wrote a lot of jQuery JavaScript to improve the user experience in our self-service modules running in 8.49. I've moved on and another team is dealing with the upgrade. They are having some problems with the JavaScript in 8.53. I'm wondering if you are familiar with any conflicts between jQuery and the delivered 8.53 JavaScript?

One person on that team has theorized that PeopleSoft is now using prototype.js and that there are known conflicts with jQuery. I've seen nothing to indicates that prototype.js is being used.

I'm tied up with other work, so don't know the details of the problems, but may have to dig in myself at some point to see what is going on.

Thanks for any help.
Cheers
Dan

Jim Marion said...

@Dan, as always, it is good to hear from you. No, PeopleSoft is not using Prototype. In fact, 8.53 now ships with jQuery and jQuery UI. It is an older version and it isn't used by regular transaction pages (yet?), but it is now delivered as HTML definitions.

I would need more information, but just FYI, I use jQuery with 8.53 with no issues. When I switched from 8.49 to 8.50, I had to work through the fact that $(document).ready doesn't fire on transaction pages because the content is ajax'd into the browser (It didn't work on 8.50 anyway).

Daniel Kibler said...

Thanks for your insight Jim. I will recommend looking into the $(document).ready problem. One of the errors I've seen could be the result of that.

Dan

Nagaraj Nagavarpu said...

Hi Jim,

I am creating a context menu in all PeopleSoft pages using Jquery. I have been successfull when I test the pages in Firefox or Chrome browsers. However in Internet Explorer (8,9,10) version i keep getting error "SCRIPT438: Object doesn't support property or method 'defineProperty' ". Because of this error my other custom javascript files errors out probably. I have tried all Jquery version but have not been successful. The only version were I am able to get my context menu working in IE is 1.8.2 but again here too the context menu does not show up as it was showing up in the Firfox or Chrome. Do you have any idea on how to resolve this.

Thanks
Nagaraj

Jim Marion said...

@Nagaraj, I am not sure what is causing this. Is defineProperty part of your code or is it from a jQuery plugin? Perhaps you can place some debug code around it? Are you using the IE Developer Toolbar?