Access Salesforce REST API with Javascript
So this has been a bit of a holy grail kind of endeavor for me. A way to access Salesforce org data using only javascript. Salesforce has a million ways to get at it’s data an interact with it, but it’s always required some kind of server side technology to create the requests and deal with authentication. Not anymore! Now before I get too far, this is just proof of concept code I’d say. It works, but I only just got it working in the last few hours and have done very minimal testing, but the concepts are solid.
The approach is create a Salesforce Apex REST service that can perform all the basic CRUD operations. You then host that REST service on a salesforce.com site. By using JSONP and callbacks you can hit the service from any javascript program, send requests and get data back. This idea was put forth by Pat Patterson via his awesome post at
I just took the idea an expanded upon it a bit. So first off, here is the code.
/* RestProxy
Author: Daniel Llewellyn (Kenji776@gmail.com, @Kenji776)
Description: This is a rest class that can be made available to client applications via salesforce.com
sites which will allow them to perform basic CRUD operations against the org. Before this, there was no
way I know of for pure javascript applications to 'integrate' with Salesforce. You had to deal with oAuth or
SOAP or whatever. Stuff Javascript can't really do. I needed a way to get data in and out of a site.com hosted site
which only gives you access to javascript. So I wrote this. YOu can perform queries, updates and deletes. Very basic right
now, but could be expanded on as required. Inspired by the awesome post at
http://blogs.developerforce.com/developer-relations/2012/02/quick-tip-public-restful-web-services-on-force-com-sites.html
*/
@RestResource(urlMapping='/respondentPortal/*')
global class respondentPortal
{
//simple wrapper response class. Makes it so all replies via this API have the same basic structure
//including a boolean success flag, a message, any sObjects affected and some debugging params.
global class restResponseWrapper
{
string message = 'run successful';
boolean success = true;
list<string> params = new list<string>();
list<sobject> sObjects = new list<sobject>();
string requestURI = RestContext.request.requestURI;
}
class applicationException extends Exception {}
//can run queries. This one doesn't except an /objectType/Id as the other methods do. This one just takes a query as specified in the
//?query= param of the url. It will return all the objects in the sObjects property of the returned object.
@HttpGet
global static void doGet()
{
restResponseWrapper thisResponse = new restResponseWrapper();
//since all requests to this service will be get, since we are operating using JSONP, we first need to figure out what they actually want to do.
thisResponse.params = getRestParams(thisResponse.requestURI);
string httpVerb = thisResponse.params[1].toLowerCase();
try
{
if(RestContext.request.params.get('authToken') == null)
{
throw new applicationException('No auth token provided. Please provide one with ?authToken= in the request url');
}
long authToken = getAuthToken();
long providedToken = long.valueOf(RestContext.request.params.get('authToken'));
//check the auth token, provide a 20 second range where it is valid (+10 seconds, -10 seconds)
if(providedToken == null || providedToken < authToken - 10000 || providedToken > authToken + 10000)
{
throw new applicationException('Invalid auth token');
}
if(httpVerb == 'get')
{
String query = RestContext.request.params.get('query');
thisResponse.sObjects = runQuery(EncodingUtil.urlDecode(query,'UTF-8'));
}
else if(httpVerb == 'post')
{
thisResponse.sObjects.add(saveSObject(thisResponse.params[2],null,RestContext.request.params));
}
else if(httpVerb == 'put')
{
thisResponse.sObjects.add(saveSObject(thisResponse.params[2],thisResponse.params[3],RestContext.request.params));
}
else if(httpVerb == 'delete')
{
Schema.sObjectType objectDef = Schema.getGlobalDescribe().get(thisResponse.params[2]).getDescribe().getSObjectType();
sObject obj = objectDef.newSobject(thisResponse.params[3]);
database.delete(obj);
}
else
{
thisResponse.success = false;
thisResponse.message = 'method ' + httpVerb + ' not implimented';
}
}
catch(exception e)
{
thisResponse.success = false;
thisResponse.message = e.getMessage();
}
RestContext.response.addHeader('Content-Type', 'text/plain');
RestContext.response.responseBody = formatResponse(thisResponse);
}
//take a query string and run it. Yeah, I know SOQL injection possible here, but whatever. This
//whole thing is inherently insecure. It's like worrying about wet floors on the titanic.
public static list<sobject> runQuery(string queryString)
{
list<sobject> objs = new list<sobject>();
objs = database.query(queryString);
return objs;
}
//returns rest params (things after the main url, separated by /)
//in an easy to use list. The name of the class should be element 0, the
//type of object should be element 1 and the id of the object should be element 2
global static list<string> getRestParams(string url)
{
list<string> returnParams = new list<string>();
integer endOfUrl = url.indexOf('?');
if(endOfUrl == -1)
{
endOfUrl = url.length();
}
//clean up the url, make sure we are only dealing with the section we want to. After the host name, before the ? mark.
//i've seen in come in with the full host name, and also just the relative portion, so we gotta make sure it's exactly the same
//from here on in, or else the array it returns could get all messed up.
if(url.indexOf('/apexrest/') > 0)
{
url = url.substring(url.indexOf('/apexrest/')+10,endOfUrl);
}
else
{
url = url.substring(1,endOfUrl);
}
list<String> URLParams = url.split('/');
for(integer i =0; i < 4; i++)
{
if(i<urlParams.size())
{
returnParams.add(EncodingUtil.urlDecode(urlParams[i], 'UTF-8'));
}
else
{
returnParams.add(' ');
}
}
return returnParams;
}
//so for the record, I really dislike this method. I wish I could just take the pure json data and deserialize it into
//an sobject type defined at runtime, but as far as I know, you cannot do that (at least I couldn't find the syntax for it).
//so instead this iterates over the keys of the sobject known from the global describe, finds matching keys in the JSON data
//and assignes the value from the JSON to the sObject.
public static sObject saveSObject(string objectType, string recordid, Map<String, Object> sObjectData)
{
//create a generic sObject to contain the update values.
sObject updateObj;
//get the describe object for the type of object passed in (its just passed in as a string from the URL)
Schema.sObjectType objectDef = Schema.getGlobalDescribe().get(objectType).getDescribe().getSObjectType();
//find all the fields for this object type
Map<String, Schema.SobjectField> ObjectFieldsMap = objectDef.getDescribe().fields.getMap();
//this method can handle updates or inserts. If a record ID was passed in,
//cast the object as the type represented by the ID. If not, just create a
//new object of the type found in the object describe.
if(recordId != null)
{
updateObj = objectDef.newSobject(recordid);
}
else
{
updateObj = objectDef.newSobject();
}
// populate the object's fields by looping over all the params in the rest request.
for (String key : sObjectData.keySet())
{
// only add params if they are valid field on the object
if (ObjectFieldsMap.containsKey(key))
{
//figure out the type of this field so we can cast it to the correct type
string fieldType = ObjectFieldsMap.get(key).getDescribe().getType().name().ToLowerCase();
//since I don't know how to do, or if it's even possible to do dynamic casting we need a
//series of if statments to handle the casting to numeric types. I think all the others should
//be fine if left as a string. Dates might explode, not sure.
if(fieldType == 'currency' || fieldType == 'double' || fieldType == 'percent' || fieldType == 'decimal' )
{
updateObj.put(key, decimal.valueOf(string.valueOf(sObjectData.get(key)).trim()));
}
else if(fieldType == 'boolean')
{
updateObj.put(key, Boolean.valueOf(sObjectData.get(key)));
}
else if(fieldType == 'date')
{
updateObj.put(key, date.valueOf(sObjectData.get(key)));
}
else
{
updateObj.put(key, sObjectData.get(key));
}
}
else
{
system.debug('Invalid field: '+ key + ' for object type ' + objectType);
}
}
//update/insert the object
upsert updateObj;
//return the saved object.
return updateObj;
}
//take one of those wrapper objects and format it by wrapping it in a callback if needed, and serializing
//the result into json.
public static blob formatResponse(restResponseWrapper responseData)
{
string response;
String callback = RestContext.request.params.get('callback');
if(callback != null)
{
response = callback + '(' + JSON.serialize(responseData) + ');';
}
else
{
response = JSON.serialize(responseData);
}
return blob.valueOf(response);
}
//just put some random mathemtical expression here that will only be known by the client application and
//this class. I know it's not super secure, but it's better than nothing. Will at least stop super novice
//script kiddies.
public static long getAuthToken()
{
return DateTime.Now().getTime() - 42131 / 3 * 8;
}
@isTest
global static void testRespondentPortal()
{
// set up the request object
System.RestContext.request = new RestRequest();
System.RestContext.response = new RestResponse();
//First lets try and create a contact.
RestContext.request.requestURI = '/respondentPortal/post/contact';
RestContext.request.params.put('firstname','test');
RestContext.request.params.put('lastname','guy');
RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
//send the request
respondentPortal.doGet();
respondentPortal.restResponseWrapper createResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
//make sure the create was successful
system.assertEquals(true,createResponse.success,createResponse.message);
//Okay, now lets try and get that contact we created
System.RestContext.request = new RestRequest();
string query = 'select name, id from contact where id = \''+createResponse.sObjects[0].Id+'\'';
RestContext.request.requestURI = '/respondentPortal/get';
RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
respondentPortal.doGet();
respondentPortal.restResponseWrapper queryResponse = (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
system.assertEquals(true,queryResponse.success,queryResponse.message);
system.assertEquals(1,queryResponse.sObjects.size());
//cool, now lets do an update on that contact
System.RestContext.request = new RestRequest();
RestContext.request.requestURI = '/respondentPortal/put/contact/'+createResponse.sObjects[0].Id;
RestContext.request.params.put('firstname','frank');
RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
respondentPortal.doGet();
respondentPortal.restResponseWrapper updateResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
system.assertEquals(true,updateResponse.success,updateResponse.message);
system.assertEquals(1,updateResponse.sObjects.size());
system.assertEquals('frank',updateResponse.sObjects[0].get('firstname'));
//and finally we'll delete them
System.RestContext.request = new RestRequest();
RestContext.request.requestURI = '/respondentPortal/delete/contact/'+createResponse.sObjects[0].Id;
RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
respondentPortal.doGet();
respondentPortal.restResponseWrapper deleteResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
system.assertEquals(true,deleteResponse.success,deleteResponse.message);
//now try to make a call without an auth token. it should bomb
System.RestContext.request = new RestRequest();
RestContext.request.requestURI = '/respondentPortal/get';
RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
respondentPortal.doGet();
respondentPortal.restResponseWrapper noAuthTokenResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
system.assertEquals(false,noAuthTokenResponse.success,noAuthTokenResponse.message);
//now try to make a call with a bad auth token. it should bomb
System.RestContext.request = new RestRequest();
RestContext.request.requestURI = '/respondentPortal/get';
RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
RestContext.request.params.put('authToken','12345678');
respondentPortal.doGet();
respondentPortal.restResponseWrapper badAuthTokenResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
system.assertEquals(false,badAuthTokenResponse.success,badAuthTokenResponse.message);
//try to make a call to a non existing http verb
System.RestContext.request = new RestRequest();
RestContext.request.requestURI = '/respondentPortal/herpDerp';
RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
respondentPortal.doGet();
respondentPortal.restResponseWrapper badVerbResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);
system.assertEquals(false,badVerbResponse.success,badVerbResponse.message);
blob jsonResponse = formatResponse(badVerbResponse);
}
}
And here is a basic test page to show you how to invoke it with javascript and handle the results, along with how to generate a matching auth token.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="core.js"></script>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Respondent Portal JS Library Test</title>
<script>
// JavaScript Document
var portal = new Object();
portal.restEndPoint = 'https://yourendpoint.testbed.cs7.force.com/yoursite/services/apexrest/respondentPortal/';
portal.runQuery = function(queryString,callback)
{
var jqxhr = $.getJSON(portal.restEndPoint+'get/?callback=?', {authToken:portal.generateAuthToken(),query: encodeURI(queryString)}, function(response) {
console.log('queryRan');
callback(response);
})
}
portal.generateAuthToken = function generateAuthToken()
{
return Math.round(new Date().getTime() - 42131 / 3 * 8);
}
portal.runQuery('select name, id from contact',function(result){
console.log('Callback ran');
console.log(result);
});
</script>
</head>
<body>
</body>
</html>
The basic structure of a call being
https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/%5BhttpVerb%5D/%5BobjectType%5D/%5Bid%5D?sObjectfield=value&othersObjectfield=otherValue
So to perform an update you might call.
To do a create it would be
A delete would be
and finally a query.
https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/get/?query=select id, name from contact
Also, a nice feature of the update and create, you don’t have to worry about only passing in valid sObject fields. It will use everything it can, and discard the rest. So if you arn’t sure if a field exists, or if you have rights to it, no worries. The program won’t exlode. It might not perfectly support dates or relationships yet though.
Any of those calls can accept a ?callback=functionName param which will cause the service to return the data in a callback which can be used for doing JSONP style transactions. EX
<script> $.getJSON("https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/get/?query=select id, name from contact&callback=?", function(data) { $('#data').html(data); }); </script>
Of course this service is a big ass security risk, so you’d likely want to at least add some kind of password to your requests, or something. Maybe a key that cycles daily that both your class and your javascript can calculate? I dunno. At least now you can get at your org data using javascript, which to me, is a big win.
EDIT: The new version of the code posted above includes a super basic authToken system. All it requires is that the client application and the REST service have a pre agreed upon method for generating a one time use key per request. I just used epoch time with some totally random changes to it, and then added a 10 second buffer. It’s pretty lame I know, but it’s better than nothing at least. Try and protect the formula you use to generate the key.
There ya go. Still a little rough around the edges, but should be enough to get you started.
Salesforce Site.com Design Principals for a Web Developer.
So Site.com has been out a while now (and it’s awesome), and while only a handful of companies are using for production websites, many more are playing with it or at least considering it. It’s a really cool product with a lot of power, and it’s kind of a bummer it wasn’t been more widely adopted yet. More adoption would likely mean more features, more developer articles and other cool stuff being built using it. However, there are probably reasons it’s been a little sluggish, and if I had to guess I’d say part of the slow uptake is due to the fact that people;
A) Don’t understand what it does
B) Don’t trust that it can do what it says it will
C) Don’t know how to approach it
D) Insane cost
I know all 4 reasons did slow up a little (even though we ended up launching a production website before site.com was even GA, so I guess we couldn’t have been that slow). However we did end up figuring it out, using it, and are really enjoying it. Though it wasn’t all smooth sailing and the hardest parts weren’t technical, they were design related. So I’d like to share with you a few of the lessons I learned and the stumbling blocks I ran into as a developer trying to write a system marketing could use.
First up, what is Site.com? Site.com is a content management system with a powerful WYSIWYG editor built in. It allows non technical people to create and manage small websites that have limited amounts of interactivity (I mean it doesn’t write javscript for you of course, the menu creator is a bit limited, though some CSS wizardry can make it pretty slick, and there is no native apex access. Maybe in the future though). It uses a template system where you can create templates with defined editable areas which pages can be cloned from. The idea being that your website has a few core layouts (maybe like a landing/splash page, a home page, and some detail pages). Those templates are probably designed by a programmer/designer type with some development background and then the pages cloned from the template should be easily modifiable by your marketing team, or any other non technical people. When changes are made they stay in your sandbox until they are published, similar to doing a commit/push in programmer terms.
This idea of templates is a big deal. For myself as a ‘old fashioned’ developer I had always used large amounts of CSS to control the look and feel of my pages. Of course the idea there being that one simple change in your CSS could easily propagate to all the linked pages. It ensured a consistent look and feel, and also reduced page weight. So this is how I first approached using site.com. I prototype’d my pages on my local machine by following the PSD given to me by our designer. I’d write up a functional example of the PSD using just pure HTML and CSS. After the prototype was as pixel perfect as I cared to get it, I’d being ‘translating’ it into Site.com. Div’s became panels, spans became content areas and basically the result would be perfect, but it sucked.
Why did it suck? Well remember, doing things the old way meant that no style information was contained in the page itself. It was all in the CSS. I usually would even setup images as CSS divs with the background-image property. Now say marketing wants to change that image (as they always do) well guess what, thanks to the way I had things build they’d have to go dig in the css, find the right ID or class and modify the hard coded image path. That’s terrible! Or what if they want to change the padding or margin of a div/panel? Back to the CSS. What was the saving grace of web developers in the past had become the bane of marketing now. Not only was it difficult for them to modify, there was also the serious risk of them breaking something else. CSS is fragile stuff and a fix one place can be an overlapping element somewhere else! So it seems we are in a catch 22 here. We can’t use much CSS, or else marketing can’t really manage the site very well. We can’t just not use it or else the site will become disjointed and have the same issues websites did before CSS. Or so it seemed.
Remember those templates I mentioned. I for a long time under estimated their usefulness. I thought they were a simple toy meant for those ‘non developer types’ so I essentially had been dismissing them. How wrong I was. If you change your thinking into looking at a template as a layout structure with embedded CSS all of a sudden it all becomes easy. Build your page as a template and go ahead and put in the images and divs. Setup as much as you like within the template and made the areas editable that you want marketing to be able to change. Use CSS sparingly to provide easily reusable styles for headers, subscripts, body text and such. Then create pages from your templates and let marketing go nuts. This way you retain some of the benefits of CSS by styling some of the really shared elements (namely text elements, and perhaps some of the container divs and very basic layout structure of your site) but still retain enough flexibility for other users to manage. Here, let’s look at an example.
Say I wanted a a background image for my banner. It’s going to have a background image and a top margin. As a developer who is trying to follow general best practice for regular websites, I might opt to something like this.
<style> #actionBanner { padding-top:20px; padding-left:10px; width:290px; height:281px; background-image:url(actionBanner.png); margin-bottom:19px; background-repeat:no-repeat; } </style> <div id="actionBanner"></div>
Looks nice right? I mean the HTML has no styling attached to it, it’s only structural as it should be. The CSS is clean, easy to read. But now think of it in terms of site.com. If your users actually want to use an image that isn’t actionBanner.png for the background on only one page, they are going to have to go dig in the CSS file to either create a new class, or change the existing one. Changing the existing one would change it for any page that uses that banner, which in this case they don’t want. So now you are back to modifying CSS and have totally defeated the point of using site.com (which in my eyes is to get marketing off your back when it comes to website changes). So what can we do? Using a site.com template, we can simply insert the image in the template. Set the CSS properties on the image itself, and save the template. Now pages that are cloned from that template have the provided image by default, but the users have the ability to override them since it’s marked as editable. They can’t screw up the template so you always have that as a backup, and each page can be customized by simply modifying the properties of the image element. It’s genius!
Now you might be saying, wait this is just like going back to the old days. You have embedded styles in your pages, this is terrible! Not so fast. Remember the reason we hated embedded styles in the first place was because they were unmanageable. You’d have to modify each individual page if you wanted a change. That is not the case with templates. When you have a non editable area in a template, any change to that is immediately propagated down to any page cloned from it. Like I said, templates can be viewed as a hybrid HTML CSS definition. Modifying the inline CSS on your template is the same as modifying a CSS class.
So in short, these are basically the take aways.
1) Prototype your whole website on your local machine in regular HTML/CSS if you can. This will help you figure out what things really will be shared among all pages (like font faces, basic layout elements, paragraph spacing, etc). Also, you’ll have an easily portable copy of your website in case you decide to migrate to another hosting platform. When crating this prototype, remember it will be going into site.com though, so don’t be afraid to use IMG tags, or even a few font tags if they are one off styles.
2) Converting your HTML pages into site.com pages is pretty easy if you’ve done it the right way. Divs will become panels. Spans and P tags will generally become content areas. Try to avoid having editable content as a direct child of a panel/div as it may make styling a bit more difficult and you’ll lose some control over styling. Your marketing users would be able to adjust the panel/div if they are able to adjust it’s content so they could end moving it around or whatever. Just do a panel/div that is not editable, then inside of it create your content area that is editable.
3) Use CSS sparingly. Embed anything that isn’t totally globally shared within the templates. Otherwise you’re going to have marketing users digging around in your CSS and likely messing it up. Avoid this at all costs.
4) Come up with a reliable naming scheme for your resources, especially images. Site.com is flat, there are no folders. If you are used to using folders to organize your resources, you might be in for a bit of a rough ride, or at least a messy architecture. Before you write a single line of code, decide how everything will be named. Images, SWF files, javascript includes, custom fonts, everything exists in the same “folder” so account for that.
5) Last but not least, work with site.com not against it. I know us developer types frequently have a hard time trusting tools to work right and we often think we know better. In this case, trying to do it your way will likely cause you more work. Use the features available. Understand how site.com ‘wants’ you to work and then follow that design pattern.
I hope this helps some. I’ll probably be doing a few more articles on site.com including how to do ajax links and maybe a few other things. Till next time!
Salesforce Siteforce user configurable ajax links
So I’ve been doing a big project recently, using the new Salesforce siteforce builder. For those who are unaware siteforce is basically a content management tool that allows regular non developer users to develop and manage websites, in theory anyway. Of course any website of significant complexity/usefulness is going to require a developer to at least make some CSS and get some templates in place for the end user. One of the biggest things the users would do is edit content and links. Always with the updating of links, I tell ya. Problem is siteforce links are all just regular HREF links. In this ajax powered age, who wants that? I mean we want fast loading, partial page reloads, ajax baby! So how do I let a non coding user create ajax links to fetch the content and inject it into the page? Simple, you let them make links as normal, and use some javascript to modify them.
1) User creates standard HREF link with HREF pointing to desired page.
2) Have some javascript modify the links at runtime to remove the HREF attribute, and replace with an onclick function
3) onclick function fetches the content that was found in the original HREF and injects it into the page where desired.
My implementation of this idea looks like this.
<script type="text/javascript"> $(document).ready(function(){ $('.ajaxLink').each(function(index) { var linkTarget = $(this).attr('href'); $(this).attr('href','#') $(this).click(function(){ loadContent(linkTarget,'news_content'); return false; }); }); jQuery.ajaxSetup({ beforeSend: function() { $('#loadingDiv').show() }, complete: function(){ $('#loadingDiv').hide() }, error: function() { alert('Error loading page');}, success: function() {} }); }); function loadContent(contentPath,contentTarget) { console.log(contentPath + ' ' + contentTarget); $.get(contentPath, function(data) { $('#'+contentTarget).fadeOut('fast',function(){ $("#"+contentTarget).html(data); $("#"+contentTarget).fadeIn() }) }) } </script>
and the HTML
<style> #news_picker { width:20%; height:100%; overflow:auto; float:left; } #news_details { width:79%; height:100%; overflow:auto; float:left; } #loadingDiv { background-image:url(ajaxLoader.gif); background-repeat:no-repeat; background-position:center; z-index:150; display:none; } </style> <div id="news_picker"> <div class="listHeader">All of our news</div> <a href="news1.html" class="ajaxLink" >Ajax link override</a> </div> <div id="news_details"> <div id="loadingDiv">Content Loading, Please Wait.</div> <div id="news_content"> I am news data</div> </div>
this will transform any link with a class of ‘ajaxLink’ and convert it from a regular link into an ajax loading link. Right now it is coded to push the fetched content into a div called ‘news_content’ (you could make this dynamic, or even per link by including some attribute in the link itself that tells the function where to put the fetched content). You may want to add special case handling for content other than text/html, such as detecting if the requested resource is an image, and wrap it in an img tag, etc. Anyway, hope this helps someone, I thought it was pretty cool to allow users to easily create Ajax links 😛