Notes on Mobile
Back on track

Hairshirt or not…. the experiment rolls forward!

Now that I got the JSON bug solved worked around I can get back to fleshing out the non-XPages version of the Customers On the GO! app.

The next page is the contact details. The XPages version of this page wasn’t too complicated and I wouldn’t normally dedicate an entire post about it, but this will give me a chance to walk through my Notes document as JSON function I talked about yesterday.

After clicking the ‘Contacts’ link at the bottom of the app, you are sent to the ‘contactsList.html’ page. The page itself is almost identical to the app’s home screen. The content for the page is collected using the JSON formatted output of a view in the CRM applciation using the following JS:

function loadContactsPageData(){
	var cat = getURLParameter("c");
	var parentId = ('00000000'+getURLParameter("documentId")).slice(-8);
	// Get the list data.
	
    $.getJSON('http://10.1.1.39/unplugged/tdtool.nsf/dataProxy?OpenAgent&db=unplugged/rm.nsf&v=JSONContacts&RestrictToCategory='+cat,
        
    	function(data) {
    		var contactArray = convertViewEntryToJSONObject(data);
    		$('#contactDetailsTemplate')                  // Select the template. 
    			.tmpl(contactArray)              // Bind it to the data. 
    			.appendTo('#contactsContainer');   // Render the output.
    		
    		$('#contactsContainer').listview('refresh');
       }
   );
}

The contact template is very basic as well and looks like this:

	<script id="contactDetailsTemplate" type="text/x-jquery-tmpl">
		<li>
		<a href="contact.html?openfileresource&documentId=${noteid}">
		{{if fpjrFunction_WB=="Developer"}}
		<img style="jobImage" src='designer.png'>
		{{else}}
		<img style="jobImage" src='admin.png'>
		{{/if}}
		<span style='contactName'>${fpnhName_WB}</span>
    	<br/><span style='contactDetails'>${fpjrPosition_WB}</span>
    	<br/><span style='contactDetails'>${fppbPhone_WB}</span>
		{{if fppbXtn_WB}}
    	<span style='contactDetails'>  Ext: ${fppbXtn_WB}</span>
		{{/if}}
		</a>
		</li>
	</script>

Not that complicated, and very similar to the first page, so it was really easy to throw together. 

Each item in the above list has a link that brings you to the ‘contact.html’ page where the individual contacts information is displayed. In the XPages version, there was nothing much to this page, and really didn’t offer any more information than the list did. Just the customers name, job role, phone number, and a big button called ‘Call’. So here is where the dataProxy agent comes into play. To get a document all I need is the following URL:
  unplugged/tdtool.nsf/dataProxy?OpenAgent&db=unplugged/crm.nsf/&outputformat=JSON&documentId=00000000

(Of course I would use the real note id, but this is an example). This URL I can use in my JQuery.getJSON call in the same way I was using it with the views.

NotesDocument.js

To make it easier to deal with the document object I create a few helper functions/objects. First was a little JS function to wrap the getJSON call for documents:

function getNotesDocument( db, docid, callback ){
	var url = 'http://10.1.1.39/unplugged/tdtool.nsf/dataProxy?OpenAgent';
	url += "&db=" + db;
	url += "&documentId=" + docid;
	url += "&outputformat=JSON";
    $.getJSON(url,function(data){
    	var doc = new NotesDocument(data);
    	callback(doc);
    });
}

This function takes 3 parameters, the db and docid as strings and the callback that the getJSON function should call. The passed in callback will be called with a NotesDocument object though instead of the direct results from the getJSON call itself. 

The NotesDocument object is simple and only has two functions, a constructor and a method called getItem. This is what it looks like:

var NotesDocument = function(val){
	this.JSON = val.document;
	this.items = {};
	// convert the array of items to named properties in this object
	// this will make accessing them a bit faster than looping through
	// the entire list looking for the item by name
	for( var i = 0; i< this.JSON.item.length; i++ ){
		for( val in this.JSON.item[i] ){
			if( val !== "name"){
				// values get put into a field named the same as their data type
				this.items[this.JSON.item[i].name] = this.JSON.item[i][val];
			}
		}
	}
	delete this.JSON.item; // no need to store the items twice
};

NotesDocument.prototype.getItem = function(item){
	if( this.items.hasOwnProperty(item) ){
		return this.items[item];
	}
	return "";
}

This little bit of code lets me access the item names in much the same way as if I was working directly with the built in NotesDocument object in Domino.

Finally the function that gets the document can be reduced down to this bit of JS:

function loadContactPage(){
		
	var cat = ('00000000'+getURLParameter("documentId")).slice(-8);
	// Get the list data.
	getNotesDocument( "unplugged/crm.nsf", getURLParameter("documentId"), 
		function(doc) {
			$('#contactTemplate')                  // Select the template. 
				.tmpl(doc)              // Bind it to the data. 
				.appendTo('#contactDetails');   // Render the output.
			
			// need to re-apply styles so the 'call' button will
			// be shown normally. For some reason the only way to 
			// get this to work with the link button is to 
			// 'refresh' the entire page.
			$('#contactDetail').trigger('create');
		}
	);
}

One of my biggest frustrations with JQuery Mobile is that it doesn’t deal with dynamic content very well. You probably noticed that there is a call to the ‘trigger()’ function above. After much trial and error, and google searching, it was the only way I could get the ‘Call’ button to render as a JQM Button widget and not a link. The reason all this is needed is that JQM loads a page and does all it’s markup tweaking before the getJSON() call returns. As a result, by the time the template code has been processed, JQM thinks it’s done with the page. To get around it you either need to add all the JQM classes and styles to the template code manually, or you need to force JQM to reprocess the page - which is what calling trigger(‘create’) does when you specify the DIV for the page.

And just like the rest of the pages, I add a event handler to call this function when the contactDetails page is loaded:

$(document).bind("mobileinit", function(){
    [...snip...]
	$('#contactDetail').live('pagebeforeshow',function(event, ui){
		loadContactPage();	
	});
    [...snip...]
}

With the Javascript ready to go all that is left is the page template that will display the data from the document. For that I’m going to use the getItem function in the NotesDocument object directly in my page template:

	<script id="contactTemplate" type="text/x-jquery-tmpl">
		<span class='contactName'>${$data.getItem("fpnhName_WB")}</span><br/>
	    <span class='contactDetails'>${$data.getItem("fpjrPosition_WB")}</span><br/>
		<span class='contactDetails'>${$data.getItem("fppbPhone_WB")}</span>
		{{if $data.getItem("fppbXtn_WB")}}
	    <span class='contactDetails'>  Ext: ${$data.getItem("fppbXtn_WB")}</span>
		{{/if}}
		<a id='callButton' href="tel:${$data.getItem("fppbPhone_WB")}" data-icon="grid" data-role="button">Call</a>
	</script>

When the JQuery template code evaluates the template, you can access some of the objects it passes in directly. The object here we are using is the $data object, which corresponds to the data that you passed to the jQuery.tmpl() function. With that ability we can call the getItem() function and get the value out of the field.

I’m feeling pretty happy with what I have up to this point. The dataProxy agent and the JSON document output is working well, and I’ve been able to keep the look and feel of the app intact.

Next up is the license page.

Working around Domino

Seems like I do this a lot… so after running into a bug in the Domino JSON view output I was faced with a few choices. The first, and probably easier option, was to just rewrite the views I was using to fix the encoding problem I ran into. I ended up not using that option because it would mean that I would have to have custom views for all the data I was accessing. Even though I had created the views new for this exercise, ultimately I wanted to transition to preexisting views in the apps once I was ready to move to production. So now that I was faced with having to make design changes to the back end apps anyway, I decided to explore my other option which was to create a custom agent to serve up the data.

A custom agent has a couple disadvantages. First, it will require some special considerations when it comes time to deploy this regarding ACLs and agent signers. Second, it means that I have to re-implement something that already exists in Domino. The first one is just a fact of life and can be dealt with. The second one is more problematic because, I hate doing it, and I won’t be able to implement all the possible options and it will require a lot more testing.

On the up side, a custom agent does have many advantages also. First, I can make the output anything I want, or include as much or as little of the data. Second, I now have a chance to standardize an API that I can easily port/re-implement on other back-end systems without impacting my front end (big plus at the moment). Third, the ReadViewEntries does not support JSONP, which might come in handy at some point in this project I’m sure. And Finally, it can give me access to data a view cannot, like rich text. With these in mind the choice to custom code the agent became much easier to accept.

The dataProxy Agent

I wrote the agent in Java because LotusScript is just no fun anymore, and it gives me the most flexibility when I comes time to port this to some other platform. The basics of the agent was to try and get as close to the ReadViewEntries format as possible so I took the output from that and loaded it up in a text compare program so I could see it side-by-side with my output. Form there I just tweaked my code until I had the same, or nearly the same output. In about an hour I had the following code:

	private void writeViewJSON(Database db, String viewName) throws NotesException {
		// get the view
		View v = db.getView(viewName);
		v.setAutoUpdate(false);
		// get the navigator
		ViewNavigator nav = null;
		if (params.containsKey("RestrictToCategory")) {
			nav = v.createViewNavFromCategory((String) params.get("RestrictToCategory"));
		} else {
			nav = v.createViewNav();
		}

		// write the view out
		PrintWriter ao = getAgentOutput();
		ao.println("Content-type: application/json");
		ao.println("{");
		ao.println("\"@timestamp\": \"" + v.getLastModified() + "\",");
		ao.println("\"@toplevelentries\": \"" + v.getTopLevelEntryCount()
				+ "\",");
		ao.println("\"viewentry\": [");
		ViewEntry e = nav.getFirst();
		for (int i = 0; i < nav.getCount(); i++) {
			ao.println("{");
			ao.println("\"@position\": \"" + e.getPosition('.') + "\",");
			ao.println("\"@unid\": \"" + e.getUniversalID() + "\",");
			ao.println("\"@noteid\": \"" + e.getNoteID() + "\",");
			ao.println("\"@siblings\": \"" + e.getSiblingCount() + "\",");
			ao.println("\"entrydata\": [");
			int writeCount = 0;
			for (int j = e.getIndentLevel(); j < e.getColumnValues().size(); j++) {
				ViewColumn c = (ViewColumn) v.getColumns().elementAt(j);
				if (!c.isHidden()) {
					if (writeCount++ > 0) {
						ao.println(",");
					}
					Vector cv = writeColumn(ao, e, c);

					if (c.isCategory()) {
						break;
					}
				}
			}
			ao.println("]");

			ao.println("}");
			if (i + 1 < nav.getCount()) {
				ao.println(",");
			}
			ao.println();
			e = nav.getNext(e);
		}
		ao.println("]");
		ao.println("}");
	}

Some of the differences, though minor, are to do with categories and the NoteId and UNID. In the Domino output you get special NoteId’s and in my version they are just empty strings. Also, the timestamp that I output is a different format than the domino version.

The agent supports a few URL parameters:

  • s= Lets you specify the server.
  • db=   Lets you specify the database the view is defined in.
  • v= Lets you specify the view to read.

using these, I can get to any view in any database.

That was all I needed at this point on the server. On the client side, it only required a single change to the Javascript to change the URL in the JSON calls. 

Not the end of the story…

This isn’t the end of the agent though. I have also added another function that makes a huge difference in the client that I didn’t have the ability to do before. I will cover that in my next post.

Small tweak to JS from yesterday

In prep for tomorrows post, I made a few tweaks to the Javascript I posted yesterday. I’m posting this now since I think the post tomorrow will be long enough, and I’m not ready to write it up yet. This function is handy in a general sense also, so this way it doesn’t get lost in a longer post.

To get the contacts list I needed to make a few changes to the JS functoin convertViewEntryToJSONObject:

function convertViewEntryToJSONObject(data){
	var entry = [];
	for( var j=0; j<data.viewentry.length; j++ ){
		var obj = {};
		obj.noteid = data.viewentry[j]["@noteid"];
		for(var i=0;i<data.viewentry[j].entrydata.length;i++){
			var prop = data.viewentry[j].entrydata[i]["@name"];
			var val = "";
			if('undefined'=== typeof data.viewentry[j].entrydata[i].text){
				val = data.viewentry[j].entrydata[i].textlist.text[0][0];
			}
			else{
				val = data.viewentry[j].entrydata[i].text[0];
			}

			obj[prop]=val;
		}
		entry.push( obj );
	}
	return entry;
}

The version I needed yesterday worked well when I only got one view entry back. Now it works with views that return multiple entries. This should make the view object I get back a lot easier to work with long term. 

Just the basic details

Like with the XPages version, this page will be pretty simple in it’s overall structure:

<body>
	<div data-role="page"  id="details">
	<div  data-position="fixed" data-role="header">
	<a rel="externa" href="index.html" data-icon="arrow-l" data-iconpos="left">Home</a>
		<h1 id='companyName'></h1>
	</div>	
	<div data-role="content">
	<div id='companyDetails'></div>
	<div id='mapCanvas' style="margin-right:20px;width: 100%; height: 360px;"></div>
	
	<a href="#" data-icon="forward" data-role="button">Directions</a>
	<a href="#" data-icon="grid" data-role="button">Call</a>
	
	</div><!-- /content -->
	<div  data-position="fixed" data-role="footer">
		<div data-role="navbar">
		<ul id='footerNavLinks'>
			<li><a href="#" class="ui-state-persist ui-btn-active">Details</a></li>
			<li><a href="#" onclick="changeCategory('contactList.html')">Contacts</a></li>
			<li><a href="#" onclick="changeCategory('licenseList.html')">Licenses</a></li>
			<li><a href="#" onclick="changeCategory('notesList.html')">Notes</a></li>
		</ul>
		</div><!-- /navbar -->
	</div><!-- /footer -->
</div><!-- /page -->
</body>

In this block of code there are two things to keep an eye on. First is in the header <div>:

<h1 id='companyName'></h1>

Next is the <div> with the id ‘companyDetails’:

<div id='companyDetails'></div>

Also you may have noticed that in the footer, each link contains an onclick handler, but I’ll get to that later.

The two id’s above are going to be where my replacement text ends up. Notice how the replacement target it can be any thing you want, not just a <div>.

Here are the templates I’m going to use:

	<!-- 
		JQM only loads the [data-role='page'] element into the linked pages
		so the templates all need to be here, in that <div> not in the <head>
	-->
	<script id="companyNameTemplate" type="text/x-jquery-tmpl">
		${fcftCompany_WB}
	</script>
	<script id="companyDetailsTemplate" type="text/x-jquery-tmpl">
		<span style="display:none" id="address">${faddLocation_SS}</span>
		<span style="display:none" id="companyName">${fcftCompany_SS}</span>
		<span class='companyAddress'>${faddAddress_SS}</span><br/>
		{{if faddAddress_SS_1.length}}
		<span class='companyAddress'>${faddAddress_SS_1}</span><br/>
		{{/if}}
		<span class='companyAddress'>${faddTown_SS}, ${faddCounty_SS}   ${faddPostcode_SS}, ${faddCountry_SS}</span><br/>
	</script>
	

It is very important that these two <script> blocks go inside the <div data-role=’page’>. The reason, which took me a while to come to realize has to do with the way JQuery Mobile follows links. JQM uses AJAX to load the content of the page pointed to by all <a> tags unless you use the rel=”external” or its variants. This is done so the next page can have fancy transitions. The way a page slides in from the right is an example of this effect. The way it works is that in the background, JQM gets the div from the new page that contains the data-role=’page’ attribute and only loads that into the current page by replacing the current data-role=’page’ div. The effect is neat but it actually can cause really bizarre things to happen if you don’t watch out. For example, in the first page I created, I placed the template blocks in the <head> of my page. In the first version of the details page I did the same thing. After my first test, I couldn’t get the customer name to show up in the title bar and I would be getting ‘object has no method ‘appendTo” errors in the JS Console:

It took a while to figure this out, but the cause was that when JQM loaded details.html page, it only use the data-role=’page’ div. At that point I had two options. The first was to load the template into the <head> of the first page. Since that page is used as the target for the next page to load into, my template will be there. The other option, which I ended up going with, was to put all the things I wanted to load into the page div. I like the second approach because it keeps the ‘page’ more intact. Keeping everything in the first page just seems really messy to me.

The next step is to update the ‘mobileinit’ function in my app.js with the following code for my new details page:

$(document).bind("mobileinit", function(){
	$('#main').live('pageshow',function(event, ui){
		loadMainPageData();
	});
        $('#details').live('pagebeforeshow',function(event, ui){
		loadDetailsPageData();	
		loadMap();		
	});
});

Here is where I hit my first real problem. Getting a list of customers from a view is pretty easy. Getting a single customer document is not. There is no Domino URL parameter to read a document as JSON. This will have to be the first design change I make. I figure I have a couple options though. First option would be to create an agent I can call that returns the document I want in JSON format. The upside to this is the flexibility it would give me as I could add what ever I wanted, and it would make RichText possible. The downside to this is that I would have to be able to run code on the server which means signing the database with the correct Id. I don’t really want to deal with that right now. The second option is to still use a new view and categorize it on the Note ID or customer name of the document I want, and add all the fields I need as columns. This has two distinct advantages - it’s easy to create views and change them without needing special access, and it keeps the native Domino changes to a minimum. Agents require testing and they are not easily ported if I ever want to move the data off of Notes (gasp!! - I’m not in charge of IT so who knows what will happen tomorrow….). So the view I created is really simple. Categorized on NoteID and I added columns for each field I wanted off of the document. I have no RichText in these documents that I need, so this will work fine for now. With the view created I have the following function for loading a customer ‘document’:

function loadDetailsPageData(){
	var cat = ('00000000'+getURLParameter("documentId")).slice(-8);
	// Get the list data.
    $.getJSON('http://10.1.1.39/unplugged/rm.nsf/JSONCustomers?readviewentries&outputformat=json&RestrictToCategory='+cat,
    	function(data) {
	    	var doc=convertViewEntryToJSONObject(data);
	    	$('#companyNameTemplate')                  // Select the template. 
    			.tmpl(doc)              // Bind it to the data. 
    				.appendTo('#companyName');   // Render the output.
    		$('#companyDetailsTemplate')                  // Select the template. 
    			.tmpl(doc)              // Bind it to the data. 
    			.appendTo('#companyDetails');   // Render the output.

      		codeAddress();
       }
   );
}

function convertViewEntryToJSONObject(data){
	var obj = {};
	obj.noteid = data.viewentry[0]["@noteid"];
	for(var i=0;i<data.viewentry[0].entrydata.length;i++){
		obj[data.viewentry[0].entrydata[i]["@name"]]=data.viewentry[0].entrydata[i].text[0];
	}	
	return obj;
}

The document I want is loaded via a URL parameter just like before, and the URL I use is almost the same to get the JSON data. The only difference is that I added ‘RestrictToCategory’ to the url and used the NoteID as the parameter. This data returned by the view JSON is not quite what I want so the function convertViewEntryToJSONObject() takes the normal view JSON

and converts to another object that looks more like a document

Having this faux document object makes it much easier to create and maintain my templates in the html. My template can refer to actual field names just like in the Xpage version, instead of viewentry[0].entrydata[2].text[0]. Much easier to read.

Loading the map and displaying the address happen in exactly the same way, and I was able to use the same functions from earlier to do it. All I did was copy that code from the XPage and put it into the app.js file.

So this is a lot to cover in one post. I know it seems like a lot of code, but everything done today is laying the ground work for all the pages to come, so the rest should be much easier. Next time I’ll finish with this page and take a look at the footer and the links there.

Debugging JavaScript on a mobile phone

Debugging in Chrome or Firefox is pretty easy thanks to tools like the built in Chrome debugger or Firebug. However, once you get your app onto a mobile device it can be very difficult since these tools are not available there. A great option is to use the JavaScript API ‘console.log’ and a tool called jsconsole.com. With JSConsole you can connect to any web page running the JSConsole remote script from within the JSConsole web app. Simply add the following script tag to your HTML:

<script src="http://jsconsole.com/remote.js?random_id"></script>

Where <random_id> is anything you want. Preferably something easy to remember. Then go to http://jsconsole.com and in the box type

:listen <random_id>

Then when you refresh the page on your mobile device, all the console.log output will be echo’ed to the jsconsole.com web page.

More information can be found here:

http://jsconsole.com/remote-debugging.html

Just wanted to share a site I really find useful. With JavaScript being more and more important in the projects I’m working on, it’s really handy to have this around to test little snippets without having to go through all the effort of creating an actual test page.