Oh my god. It's full of code!

Posts tagged “Navigation

Experimental: Progressively enhance links for ajax with bookmarking and back button support!

Hey All,

So this is a bit of an ‘in progress/experimental’ kind of post. I think many of us have run into this dilemma. We want to make an awesome ajax powered website with fast loading pages and neat interface. Problem is ajax content sucks for search engine optimization, can be tricky to get bookmarking to work, and of course the back and forward buttons cause problems too. All this seems like it might make ajax a bad idea for navigation, but it’s just too cool to give up. So how can we resolve all these issues and use the awesome ajax navigation we want to? We address each challenge one by one (or just skip the bottom and copy and paste my full code example. Whatever works for you).

Progressive Enhancement (AKA dealing with shitty browsers or search engine)
My first attempts with Ajax navigation where simply to replace the href attributes of my links with javascript function calls. This really is the most straight forward approach, but the most flawed as well. Anyone who doesn’t have javascript support, or search engines trying to crawl your site won’t be able to follow your links. Your site won’t get indexed, and you’ll be abandoned by the search engine gods. Also, if you are using any kind of CMS (such as site.com from Salesforce as I am) the links created will be standard links. Your website people would have to call on you all the time to change their links, if it’s even possible! The answer to all of these problems is progressive enhancement. Use javascript to transform your regular links into javascript ajax links. This ensures that those people/bots not using javascript can still browse your site in the traditional manner. So for this example, my regular link might look like

<a href='contactUs' title='contactUs' class='ajaxLink'>Contact Us</a>

Pretty simple standard link. However I’ve added a class to it that will easily allow me to select it later with some jQuery magic later on. Now we need some javascript to turn that link from a plain jane href into a sexy ajax link. Something like this oughtta do the trick.

    $('.ajaxLink').each(function(index){
        $(this).attr('href', $(this).attr('href').replace('/','#'));
    });
    
    $('.ajaxLink').bind('click', function() {
            loadLink($(this).attr('href').replace('#','/')) 
    });  

(Yes I know it’s a bit sloppy with the replace statement. With the CMS we are using it’s a flat hierarchy, so I don’t need to worry about multiple slashes in the URL. Also I’m purposely leaving it a little less than maximally elegant to increase readability for my readers. I know I could consolidate the two loops.)

What’s happening here is that we are using jQuery to modify every link that has the ajaxLink class to replace the initial slash (which all my links will have) with a # sign. That # is magical. It’s called a hash mark and it’s original use was to make bookmarkable locations in your document. You click the link with the # you go to that location in the same document designated by the #. The # and it’s content is never sent to the server, it exists entirely client side (not that that really matters right now though). So when a user clicks it, their URL changes in their browser now, but it doesn’t cause a page reload. You hear that? Let me repeat. IT CHANGES THE URL, BUT DOES NOT CAUSE A PAGE RELOAD. That’s important. The second part creates a bound function that when you click our link it’s going to call a function called loadLink which expects to receive a valid URL (relative to the current document) so we need to flip the hash back to being a slash (I guess we could probably leave the slash out and just remove the # but whatever). We now have a system that will leave function links for those without javascript and transform them into ajax links for those who do. Sweet.

Bookmarking and unique page urls (The magic of the hash)
You may ask why even bother with the hash at all if we are just flipping it back to a slash. The reason is since it causes the URL to change, the user now has something they can bookmark. It also gives each page a unique URL with which to access it. As the user is navigating around your site, if they end up at a some buried 3 level deep page but the URL hasn’t changed at all they have no idea where they are really at. They don’t have a bookmarkable link, or one they can share with their friends. Of course the each page does have it’s own unique URL (thats how search engines and non javascript browsers will get to them) but your ajax enabled users won’t know that without the hash. With our function we wrote above, regular links now act as javascript and since they links have a # in front of them, the browser treats them as anchors. The URL changes when the link is clicked, but no page reload is preformed. This is a good thing. But wait, just because the hash is in the URL that doesn’t mean it’s really doing anything yet. If someone bookmarks your page with the hash in it, but you don’t have any handler for it nothing really happens. When our page loads we need to check and see if there is a hash in the URL. If so load the page indicated by it, if not then just load your default page. That functionality looks a bit like this.

$(document).ready(function()
{
          var hashMark = getHash();
          if(hashMark.length > 0)
          {
              loadLink (hashMark);  
          }          
    });    
});

function getHash() 
{
          var hash = window.location.hash;
          return hash.substring(1); // remove #
}

Pretty simple. All you are doing is saying when the document loads, see if there is a hash mark. If so, load the link indicated by it (by passing the hash mark content to the loadLink function). This works great. Now you can have bookmarkable links that actually work. But the back button is still broken….

Dealing with the back button

Man, I love jQuery. Every time I have some crazy issue to deal with, it’s got my back. Like if me and jQuery were in a bar and some big biker dude was trying to hassle me, jQuery would like tap on his shoulder, the biker would turn around and jQuery would just knock like all his teeth out with one right hook. I’d then jQuery a drink and we’d talk about how much mootools sucks (just kidding, I don’t know anything about it). Anyway where I’m going with this is that something that could be really hard to do jQuery makes really easy for us. What we need to do to get the back button to work is to detect when the hash in the URL changes. When a user clicks back or forward using your links the only thing that is going to change is that hash mark content. Nothing gets sent to the server. There is no get/post request going on here. Many hackey approaches are out there from disabled the back button to overriding it’s behavior. Thankfully we aren’t savages. We have an elegant solution. It looks like this.

          $(window).bind('hashchange', function() {
              var hashMark = getHash();
              if(hashMark.length > 0)
              {
                 loadLink (hashMark);  
              } 
          });

Just that easy. A topic that has stumped top web developers for years all wrapped up 7 lines. This just says bind a function to the hashchange function. When it changes, get the hash and pass it to the loadLink function. Boom. Done.

Loading the content (The grand finally)

So we are just about home now. We have progressive enchantments, bookmarking/link sharing ability, and even back/forward functionality. But now we need to actually load the content. Here is one last issue to deal with. Since all of your pages contain all the content/styles/scripts needed to be seen on their own (again for the non ajax users) if you try and load the entire page when you click the link you are going to end up with recursive pages nesting inside each other, duplicate script errors all kinds of crazy shit. So you need to leave the full pages intact for your non ajax users, but still be able to extract just the content you want from it to display on your page. Here we are going to use a little bit of jQuery’s find magic to extract just the content we want.

function loadLink(link)
{
     try
     {
         $("#leftText").html('');
         $('#loader').show();
         
         $.get(link, function(html) {
               $('#loader').hide();
               $("#leftText").html($(html).find("#leftText").html());
         });
     }
    catch(ex)
    {
        console.log(ex);
        $('#loader').hide();
    }
}

So here is whats happening here; the function loadLink expects to receive a valid URL fragment to load. It’s going to blank out my content area (which is called leftText) and then show an ajax loading spinner. jQuery is going to create a get request to get the link and with the result it’s going to extract the content from it’s leftText div, and insert it into this pages leftText div. Since every page is structured basically the same, it works pretty slick. That’s it. You’re done! Of course these scripts need some refining, error handling, edge case handling but I’ll leave that to the reader. The hard shit is done, what do you want me to do your whole job for you? XD Below is the entire script. Enjoy!

$(document).ready(function () {

        markupLinks();

        $(window).bind('hashchange', function () {
            var hashMark = getHash();
            if (hashMark.length > 0) {
                loadLink(hashMark);
            }
        });

        var hashMark = getHash();
        if (hashMark.length > 0) {
            loadLink(hashMark);
        }
});



function markupLinks() {
    $('.ajaxLink').each(function (index) {
        $(this).attr('href', $(this).attr('href').replace('/', '#'));
    });

    $('.ajaxLink').bind('click', function () {
        loadLink($(this).attr('href').replace('#', '/'))
    });
}

function loadLink(link) {
    try {
        $("#leftText").html('');
        $('#loader').show();

        $.get(link, function (html) {
            $('#loader').hide();
            $("#leftText").html($(html).find("#leftText").html());
        });
    } catch (ex) {
        console.log(ex);
        $('#loader').hide();
    }
}

function getHash() {
    var hash = window.location.hash;
    return hash.substring(1); // remove #
}

Apex Google Powered Navigation Class

Hey all,
Expanding on what I was talking about yesterday I have packaged my google maps navigation tool into an apex class. Using this class you can easily get directions to and from any address using the tried and tested google maps system. Simply provide the origin, destination, desired data return format, and a toggle indicating if you want to expand abbreviations in the directions (like rd to road, nw to northwest, dr to drive, etc).

You have several options for how you want the data to be returned. You can either return the raw xml data from google (kml as they call it) get a nice JSON data structure, a semi-colon separated list, or a semi-colon separated list that is url formated. Invoking the class is super easy.

String directions = getDirections.getDirections('address 1','address 2','dataReturnType', expandAbbreviations);

One thing that I dislike about the current incarnation is that the XML parsing is a bit slow/inefficient. I am not very strong on my XML-fu so currently it must iterate over every XML element to find the desired data. It would be faster to tell it more accuratly where to look, but I don’t really know how. So if you are a smarty pants developer and know how to make the search faster, please feel free to let me know. I’ll be happy to update it and credit you for the fix.

With this tool, you could easily use it as a controller for a visualforce page, and create your own RESTful style webservice, or use it to help build other neat services. It’s a nice developer resource I think.

Download it here or from the projects page.

NOTE: To use this class you will have to add http://maps.google.com to your remote sites in your org. Just go to setup->security->remote sites and add the url.


Coldfusion, Angel.com, Google Maps Directions, and You!

Okay, so even the title is a mouthful, this post is probably going to be insane you are thinking. Well… maybe, but it’s cool stuff. So picture this. You are using Angel.com as an IVR provider. So people call in and talk to a phone machine for data. Now say you want to give directions over the phone. Say you want those directions to be dynamic, and given to the user step by step. So for example you are hosting an event. People pre-register for this event, and have provided their address, which you have stored in a database. Bob Johnson calls in (he has registered and provided his address before) and wants directions from his house to your event center. You might think you’d need a live person to do this. Blasphemy! Have Bob authenticate so we can find his address in the database. Feed that address into an Angel.com variable (if there were a better way to enter addresses over the phone, you wouldn’t even need to pre-register, but because entering data in phones sucks we kind of need their address to already exist somewhere we can get it). Once that variable is in Angel.com, pass it, along with the destination to this tool via URL arguments. This tool will then give step by step directions that the IVR can read aloud back to Bob. He even has the ability to replay each direction, and navigate backward and forward through the steps.

Just copy and paste this and run host it on a ColdFusion server somewhere. It’s ready to be called with all configs just being passed in the URL at runtime.

<cfparam name="url.start" type="string" default="1405+Olive+Ln+N,+Plymouth,+Hennepin,+Minnesota+55447">
<cfparam name="url.end" type="string" default="1111+Cambridge+St.+Hopkins,+MN+55343+(White+Castle)">
<cfparam name="url.stepID" type="integer" default="1">

<!---- The id of the page that calls this webservice in angel.com ---->
<cfparam name="url.thisPage" type="string" default="1">

<!---- the id of the page to go to if this thing errors for some reason --->
<cfparam name="url.failPage" type="string" default="2">

<!---- the id of the page to go when we are all done giving directions ---->
<cfparam name="url.nextPage" type="string" default="3">

<!---- the id of the page to go to if we just can't find directions or a route ---->
<cfparam name="url.reEnterInfoPage" type="string" default="4">

<!---- the id of the page to go to if the person decides they want to talk to a person ---->
<cfparam name="url.transferToCC" type="string" default="5">

<!---- the id of the page to go to if the person wants to hang up---->
<cfparam name="url.disconnectPage" type="string" default="6">

<cfparam name="XMLData" type="string" default="">
<cfparam name="text" type="string" default="">

<!--- Format the directions for sending to google --->
<cfset url.start = replacenocase(url.start, " ", "+")>
<cfset url.end = replacenocase(url.end, " ", "+")>

<cfhttp url="http://maps.google.com/" result="KMLData">
    <cfhttpparam name="saddr" value="#url.start#" type="url">
    <cfhttpparam name="daddr" value="#url.end#" type="url">
    <cfhttpparam name="output" value="kml" type="url">
</cfhttp> 


<cfoutput>
    <cftry>
        <cfset XMLData = xmlParse(KMLData.FileContent)>
        <cfset totalNumberOfSteps = arraylen(XMLData.kml.Document.XmlChildren)-4>
        <cfset text = XMLData.kml.Document.XmlChildren[stepID+3].XmlChildren[1].XmlText>
        <cfset text = text&" . .">
        <!--- Some extra text formatting for reading over the IVR. You can easily add more abbreviations here if there
              are some I forgot --->
        <cfset text = replacenocase(text, " LN ", " Lane ")>
        <cfset text = replacenocase(text, " BLVD ", " Boulevard ")>
        <cfset text = replacenocase(text, " RD ", " Road ")>
        <cfset text = replacenocase(text, " ST ", " Street ")>
        <cfset text = replacenocase(text, " Ave ", " Avenue ")>
        <cfset text = replacenocase(text, " NW ", " North West ")>
        <cfset text = replacenocase(text, " NE ", " North East ")>
        <cfset text = replacenocase(text, " SE ", " South East ")>
        <cfset text = replacenocase(text, " SW ",  "South West")>            
        <cfset text = replacenocase(text, " N ", " North ")>
        <cfset text = replacenocase(text, " E ", " East ")>
        <cfset text = replacenocase(text, " W ", " West ")>
        <cfset text = replacenocase(text, " S ",  "South ")>

        
        <cfsavecontent variable="PromptMessage">
            #text#
            <cfif stepID LT totalNumberOfSteps>
                Press 1 for the next direction. Press 2 to repeat this direction. Press 3 to hear the previous direction.
                <cfelse>
                    You have reached your destination.
                    Press 1 to disconnect. Press 2 to repeat this direction. Press 3 to hear the previous direction.
            </cfif>    
        </cfsavecontent>
    
        <cfif stepID LT totalNumberOfSteps>
            <cfsavecontent variable="XMLLinks">
                <LINK dtmf="1" returnValue="#stepID+1#" destination="#url.thisPage#" />
                <LINK dtmf="2" returnValue="#stepID#" destination="#url.thisPage#" />
                <LINK dtmf="3" returnValue="#stepID-1#" destination="#url.thisPage#" />
            </cfsavecontent>
            
            <cfelse>
                <cfsavecontent variable="XMLLinks">
                    <LINK dtmf="1" returnValue="#url.nextPage#" destination="#url.thisPage#" />
                    <LINK dtmf="2" returnValue="#stepID#" destination="#url.thisPage#" />
                    <LINK dtmf="3" returnValue="#stepID-1#" destination="#url.thisPage#" />
                </cfsavecontent>            
        </cfif>
        
        
        <cfset counter = 1>
        <cfset VariablesObject[counter] = structnew()>
        <cfset VariablesObject[counter]["Name"] = "totalDirections">
        <cfset VariablesObject[counter]["Value"] = totalNumberOfSteps>

         

        <cfcatch type="any">
            <cfset promptMessage = "Sorry we couldn't find a route with the information supplied. Press 1 to try a different direction method. Press 2 to disconnect, or press 3 to be transferred to customer care.">
            <cfsavecontent variable="XMLLinks">
                <LINK dtmf="1" returnValue="#reEnterInfoPage#" destination="#failPage#" />
                <LINK dtmf="2" returnValue="#disconnectPage#" destination="#url.thisPage#" />
                <LINK dtmf="3" returnValue="#transferToCC#" destination="#url.thisPage#" />
            </cfsavecontent>        
            

            <cfset VariablesObject[1] = structnew()>
            <cfset VariablesObject[1]["Name"] = "ErrorType">
            <cfset VariablesObject[1]["Value"] = cfcatch.Type>        

            <cfset VariablesObject[2] = structnew()>
            <cfset VariablesObject[2]["Name"] = "ErrorMessage">
            <cfset VariablesObject[2]["Value"] = cfcatch.Message>

            <cfset VariablesObject[3] = structnew()>
            <cfset VariablesObject[3]["Name"] = "ErrorDetails">
            <cfset VariablesObject[3]["Value"] = cfcatch.Detail>                                    
        </cfcatch>
    </cftry>    
    
    <cfset ReturnObject = printQuestionReturnVariables('stepID',PromptMessage,XMLLinks,url.failPage,VariablesObject)>
#trim(ReturnObject)#
</cfoutput>


<cffunction name="printQuestionReturnVariables" access="remote" hint="Print Question Data For Angel IVR with returnable variables">
    <cfargument name="varName" default="none" type="string">
    <cfargument name="promptMessage" default="none" type="string">
    <cfargument name="linkMessage" default="none" type="string">
    <cfargument name="failPage" default="failPagePlaceholder" type="string">
    
    <!--- This is an array of structures, with keys "name" and "value" --->
    <!--- EX variables[1].Name = Gender --->
    <!--- EX variables[1].Value = Male --->
    <cfargument name="variablesToInclude" default="" type="any" required="no">
    
    <!--- create, scope and set the loop counter variable used below --->
    <cfset var i = 0>
    
    <cfoutput>
        <cfsavecontent variable="ReturnMessage">
            <ANGELXML>
                <QUESTION var="#ucase(varname)#">
                    <PLAY>
                        <PROMPT type="text">
                            #promptMessage#
                        </PROMPT>
                    </PLAY>
                    <RESPONSE>
                        <KEYWORD>
                            #linkMessage#
                        </KEYWORD>
                    </RESPONSE>
    
                    <ERROR_STRATEGY type="nomatch" reprompt="true">
                        <PROMPT type="text"> Sorry I did not get that. </PROMPT>
                        <PROMPT type="text"> I still did not get that. </PROMPT>
                        <PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>
                        <GOTO destination="/25" />
                    </ERROR_STRATEGY>
                    <ERROR_STRATEGY type="noinput" reprompt="true">
                        <PROMPT type="text"> Sorry I did not get that. </PROMPT>
                        <PROMPT type="text"> I still did not get that. </PROMPT>
                        <PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>
                        <GOTO destination="/25" />
                    </ERROR_STRATEGY>
                </QUESTION>        
                <cfif IsArray(arguments.variablesToInclude)>
                    <VARIABLES>
                        <cfloop from="1" to="#arraylen(arguments.variablesToInclude)#" index="i">
                            <cftry>
                                <cfif structkeyexists(arguments.variablesToInclude[i],'Name') and  structkeyexists(arguments.variablesToInclude[i],'value')>
                                    <VAR name="#ucase(arguments.variablesToInclude[i]['Name'])#" value="#arguments.variablesToInclude[i]['Value']#" />
                                </cfif>
                                <cfcatch type="any">
                                    <cfset ErrorData = structnew()>
                                    <cfset ErrorData.Error = cfcatch>
                                    <cfset ErrorData.Arguments = arguments>
                                    <cfset ErrorData.form = form>
                                    
                                    <!--- You might wanna email this data to yourself or something --->
                                 
                                </cfcatch>    
                            </cftry>                            
                        </cfloop>

                    </VARIABLES>
                </cfif>    
                
            </ANGELXML>
        </cfsavecontent>
    </cfoutput>
    
    <cfreturn ReturnMessage>
</cffunction>

So really what’s happening here is that the page gets called with some URL variables. Those variables are used to construct a google maps http request. That request actually prints out XML. We take the XML and clean it up a little bit and format it. Then print it off in a nice Angel XML package so it can be read by the system. The page just provides one step at a time, and uses a recursive style setup to just continually give directions until there are no more.

If you just want to use the step by step direction giving, you can of course easily remove all the Angel XML junk and just access the #return# variable and do whatever you like with it. This could easily be adapted to an online direction giver, or for Twilio, SMS, whatever. For now it is ColdFusion based, but I may try and convert it to an Apex class in the not too distant future. Depending on how the user is interacting with this, you could even remove the need to have the address pre stored. You could for example have a dedicated number where users just text their current address and you text them back step by step directions to your office or whatever. Really the sky is the limit here.

Here is a sample to see how it works. The demo has some extra stuff to make it more “person usable” instead of stripped down to be consumed by a computer.
Example of ColdFusion/Google Maps/Angel.com dynamic directions

Anyway, hope you guys think this is cool. It was a lot of fun to write!