By Frank Zammetti
Editor's note: Frank Zammetti is the author of "Practical Palm Pre webOS Projects," and an active member of the webOS developer community. If you've been looking for a good example of client-server development for webOS, then this Palm Developer Center exclusive tutorial is for you. Frank will take you through the basics of developing an application that "lives in the clouds", meaning it has a wbeOS component as well as a server-side component to it.
You can find the source code at Palm's Github repository.Enjoy!
Palm's webOS has provided developers a revolutionary new platform with which to exploit the full potential of today's powerful smartphones, and other mobile devices. The Internet provides a mechanism for creating distributed applications that is unparalleled in computing history. Like the perfect combination of peanut butter and chocolate, webOS and the Internet complement each other perfectly.
In this two-part article I'll show you how to build an application that lives in the cloud but that you can still take with you wherever you go. You'll learn about a number of topics including REST, DWR, data synchronization and more.
In the first half of this article we looked at the basics of what the woswiki application would do and we then moved right on to the code that runs on the server. We looked at GAE and what it offers, as well as DWR and REST approaches to AJAX.
In this, the second half, we'll move on to the webOS part of woswiki and see how it ties together with the server component from part 1.
Let's begin by looking at the application's overall directory structure. As you can see, it's a perfectly typical webOS application:
We'll be exploring each of the source files you see here, but a brief overview is certainly in order. This application consists of three scenes: wikiList, viewArticle and editArticle. There is also a dialog, addWiki. Each of those has an assistant naturally, as well as a view definition HTML file. The core code of the application itself is found in the aptly-named woswiki.js file.
There are no images used in this application, so the images directory has been left unexpanded. There is a single stylesheet for the application, which is pretty typical for a webOS application.
There is also that somewhat mysterious /dwr directory out there with two files in it, engine.js and WikiDelegate.js, and obviously you can guess that's DWR-related code. Exactly what those files are however is a mystery we'll get to shortly!
The usual lineup of files can be found in the root directory and they primarily deal with configuring the application and telling Mojo and webOS what they need to know about it. Let's take a quick look at those, even though they are very much typical. We'll skip the icon.png file, the icon for the application, and jump to the JSON files. The required appinfo.json file is there, and it contains the following:
{ "id": "com.etherient.woswiki", "version": "1.0.0", "vendor": "Etherient", "type": "web", "main": "index.html", "title": "webOS Wiki", "icon": "icon.png" }
There shouldn't be any surprises there; it's a perfect ordinary appinfo.json file. We also have a sources.json file:
[ {"source": "app/assistants/stage-assistant.js"}, { "scenes": "wikiList", "source": "app/assistants/wikiList-assistant.js" }, {"source": "app/assistants/addWikiDialog-assistant.js"}, {"source": "app/woswiki.js"}, {"source": "app/dwr/engine.js"}, {"source": "app/dwr/WikiDelegate.js"}, { "scenes": "viewArticle", "source": "app/assistants/viewArticle-assistant.js" }, { "scenes": "editArticle", "source": "app/assistants/editArticle-assistant.js" } ]
Again, it's perfectly ordinary and simply imports the source files for the three scenes that make up the application (wikiList, viewArticle, and editArticle) as well as a global JavaScript file woswiki.js that, as well see, has some common code needed across scenes.
There's also some DWR-specific code being imported here that I alluded to earlier, namely engine.js and WikiDelegate.js. As we saw earlier when we looked at the server-side code, the dwr.xml configuration file tells DWR which Java classes can be called remotely from JavaScript. However, that's only part of the equation. The other part is the client-side engine and its proxies.
The engine is just a bit of JavaScript that knows how to communicate with the DWR servlet on the server using its own internal protocols. The engine works in conjunction with JavaScript proxies, which are JavaScript representations of the Java object that can be called remotely. DWR generates these proxies automatically. Usually, in a more typical web application, you would import both engine.js and the proxies for the remotable classes into your page with normal <script> tags. The interesting thing is that the URL for those tags would point to the DWR servlet! The servlet recognizes a few special URLs, namely one with /engine.js in it and one with /interface/* in it, where the wildcard is the name of the remotable class with a .js extension (like WikiDelegate.js for example).
Now, I could have easily done that in this application too. I could have had those <script> tags in index.html and loaded them from the server each time the application is run. However, part of what I wanted to accomplish with this application is to allow for offline wiki usage, so I couldn't count on the server being there to serve those files.
So, I ran woswiki in the local development server, and then opened up a browser and pointed to the appropriate URLs (namely, http://127.0.0.1:8888/dwr/engine.js and http://127.0.0.1:8888/dwr/interface/WikiDelegate.js). I then saved the content of the response into the files engine.js and WikiDelegate.js respectively and added them to the webOS application in the /dwr directory. This way, I have the engine and proxy objects available and can load them as part of sources.json, just like any other JavaScript in a webOS application. Without doing this, the engine.js and proxy stubs would need to be loaded from index.html with URLs pointing to the server, which means that if the server is down then the app can't even be started. That's the reason for pulling those files into the project, and doing so makes them available for loading via sources.json like all the others.
Lastly, there's also an optional framework_config.json file present:
{ "logLevel": 99, "debuggingEnabled": true, "timingEnabled": false, "logEvents": false, "escapeHTMLInTemplates" : true }
This allows us to see all messages outputted using Mojo.Log.*() functions. It also turns on debugging messages and scene transition timing messages but turns off logging of events. Finally, HTML is set to be escaped in templates. These are all pretty standard settings for a webOS application in development mode, although you may want to tweak them in a final shipping application for performance and/or security reasons.
There is a single stylesheet for the application and it's relatively simple, containing only four style classes:
.cssAddWikiTitle { position : relative; top : -6px; padding-bottom : 10px; text-align : center; font-size : larger; }
When the user wants to add a new wiki, a dialog popup appears and in that dialog is a title. The cssAddWikiTitle is the class applied to that title. It ensures that the title appears at the top of the dialog, in a slightly larger font than usual, and with some space below it before the next bit of content renders.
Right below the title on that same dialog is a divider line and it gets styled with the cssAddWikiSeparator class:
.cssAddWikiSeparator { position : relative; top : -10px; }
This ensures the divider appears right below the title, spaced nicely between the title and the content below it.
Also on that dialog, there is some instruction text above where the user enters a username and password. That text is styled with the aptly-named cssInstructions class:
.cssInstructions { font-size : small; padding-left : 4px; padding-right : 4px; padding-bottom : 4px; }
The font size is small, which seems to look nicer to me, and padding is added just for aesthetics.
When an article is viewed, the articles text is inserted into a <div>. However, to ensure that linebreaks in the content render as linebreaks, the content needs to be wrapped in a <pre> tag, or in the equivalent CSS styling, which is what the cssArticleView class provides:
.cssArticleView { position : absolute; left : 0px; top : 0px; width : 100%; white-space : pre; }
This class is applied to the <div> where the article content is inserted and is styled so that it takes up the entire width of the screen. The white-space:pre specification ensures those linebreaks work as expected.
Since a webOS application kicks off by loading an HTML file, usually index.html in the root of the application as is the case here, let's take a look at that starting point now:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <title>wOSWiki</title> <script src="/usr/palm/frameworks/mojo/mojo.js" type="text/javascript" x-mojo-version="1" /> <link href="/stylesheets/weboswiki.css" media="screen" rel="stylesheet" type="text/css" /> </head> <body></body> </html>
We need Mojo of course, and we need the main stylesheet, so both are imported and that's all we need to do.
The application's real executable code kicks off with the stage, which naturally has its own assistant in the stage-assistant.js file:
function StageAssistant() { } StageAssistant.prototype.setup = function() { woswiki.init(); this.controller.pushScene("wikiList"); };
There are two tasks to accomplish here. First, the global code that really is the core of the application is initialized via the call to the init() method of the woswiki object. We'll be diving headlong into that code next. The other task is to push the first scene, which is the wikiList scene. As its name implies, this is where the list of available wikis that the user has subscribed to are listed, and where new wikis can be added.
The code in the woswiki.js file builds an object named woswiki that contains code shared and used throughout the application. It's a plain old JavaScript object defined like so:
var woswiki = { };
Within this definition are a number of fields and methods. The fields that are present are:
Following the fields are a series of methods, beginning with init():
init : function() { woswiki.db = openDatabase("woswiki", "", "woswiki", 65536); woswiki.db.transaction((function (inTransaction) { inTransaction.executeSql( "CREATE TABLE IF NOT EXISTS wikis (title TEXT, url TEXT, " + "username TEXT, password TEXT); GO;", [], function(inTransaction, inResultSet) { }, woswiki.dbErrorHandler ); inTransaction.executeSql( "CREATE TABLE IF NOT EXISTS wikiArticles (wiki TEXT, " + "title TEXT, articleText TEXT, lastEditedBy TEXT, " + "lastEditedDateTime INTEGER" + "); GO;", [], function(inTransaction, inResultSet) { }, woswiki.dbErrorHandler ); })); dwr.engine.setTimeout(1000 * 5); setInterval(woswiki.backgroundSync, 1000 * 60 * 5); }
This is called from the stage assistant and has a couple of tasks to accomplish. The first is to open the local SQLite database and store the reference in that db field mentioned earlier. With that reference in hand, the tables for the database are created, if they don't exist already. Note that the success callback for the database executeSql() calls do nothing. There's nothing to do in that case so they are just empty methods. However, the failure callback references another method in the woswiki object, dbErrorHandler(). This is a common paradigm that you'll see throughout the code, and we'll see what that method does shortly.
The structure of the tables are very simple: each wiki that is known to the application gets an entry in the wikis table, which just has in it the title, URL, username and password that the user will have to enter when they add a wiki. All articles for a given wiki are stored in the wikiArticles table. The link between the two is that wikis.title equals wikiArticles.wiki. For a given article we store its title, its text and the username and timestamp associated with the last edit. Note that the locking fields that we saw on the Article object as part of the server code are not present here because they aren't necessary. The locking mechanism is strictly a server-side mechanism so there's no need to store that data here.
Two more tasks need to be accomplished once the database is set up, the first being DWR-related. Any remote method invocation you make via DWR is subject to a timeout period after which you can have some code execute. By default, no timeout occurs, meaning a call that hangs for some reason (like a network issue) will simply hang in JavaScript as well. (At best, the request will take a long time, and the UI may well be blocked during that time.) So, I set a timeout of five seconds by default. The dwr.engine object represents the core client-side engine of DWR and it exposes a number of properties and methods, setTimeout() being one of them. Most of the rest aren't required for this application, but if you're interested, you can get all that information from the DWR documentation.
Finally, an interval is kicked off that executes every five minutes. This is a background process that will synchronize the local database and the remote server for a given wiki.
Before we get to that though, let's jump back to the dbErrorHandler() method I mentioned:
dbErrorHandler : function(inTransaction, inError) { Mojo.Controller.errorDialog( "DB ERROR - (" + inError.code + ") : " + inError.message ); }
That was hardly worth waiting for! Because a failure against the local database is catastrophic, and very likely not something that can be recovered from, letting the user know what happened it about the best we can do. An argument could be made for exiting the application at that point, just like an argument could be made for building some sort of complicated retry mechanism into all the database access code. I think this is a reasonable compromise though for a situation that really shouldn't ever happen anyway.
Next we find a method named activateLinks():
activateLinks : function(inText) { var linkStart = inText.indexOf("~!"); while (linkStart != -1) { var linkEnd = inText.indexOf("!~", linkStart); if (linkEnd != -1) { var articleTitle = inText.substring(linkStart + 2, linkEnd); inText = inText.substring(0, linkStart) + "<" + "a href=\"javascript:void(null);\" "+ "onClick=\"" + "woswiki.linkClicked('" + articleTitle + "');\">" + articleTitle + "<" + "/a>" + inText.substr(linkEnd + 2); } linkStart = inText.indexOf("~!"); } return inText; }
This method is responsible for taking the text of an article, scanning it for links to other articles and activating them, which means making them links the user can click. How links are handled is dependant on which wiki software you use. Some simply take any word starting with a capital letter that isn't the first word of a sentence to be a link, others require some sort of marker character (such as a word beginning with an exclamation point). 'Ive chosen something slightly more verbose because my experience is that many wikis make it too easy to create links and you wind up doing so when you don't intend to, and then you have to use some special marker to indicate something should not be a link, which is counterintuitive to me. So, what 'Ive done is that any text in the form ~!xxx!~ is a link to an article, where xxx is the title of that article. With that in mind, you can see that the code is a relatively simple scanning algorithm to find such strings and replace them with <a> tags with an onClick handler. The string returned by this method is the string passed into it with all ~!xxx!~ strings in it transformed into links.
Now, those links have onClick event handlers and they reference the next method in the woswiki object, namely linkClicked():
linkClicked : function(inTitle) { $("viewArticle_divScrim").show(); woswiki.getArticle(inTitle, null, function(inResponse) { woswiki.currentArticle = inResponse; $("viewArticle_divArticleTitle").innerHTML = inResponse.title; $("viewArticle_divArticle").innerHTML = woswiki.activateLinks(inResponse.articleText); $("viewArticle_divScrim").hide(); } ); }
First, a scrim is shown over the current scene (which will always be the viewArticle scene). Next, the getArticle() method of the woswiki object is called, which will be the next thing we looked at. To finish the discussion of linkClicked() first though, note that the call to getArticle() accepts the title of the article to view, the username of the user (but only if they are trying to edit the article, which doesn't apply right now so null is passed), and then a function to execute when the article is ready to view (as you may have surmised, getArticle() results in remote DWR call). The callback function is passed as an anonymous inline function, and it has a couple of things to accomplish. First, a reference to the Article object returned by the call is stored in the currentArticle field of the woswiki object. Next, the title of the article is set in the header of the viewArticle scene. After that, the article itself is inserted into the viewArticle_divArticle <div> that is in the viewArticle scene (as well see later). Note that the content of the article is first processed by activateLinks() to make those links clickable by the user. Finally, the scrim is hidden and the user is able to get back to work.
Now, let's jump back to that getArticle() method that we just saw used, and see whats going on in it:
getArticle : function(inTitle, inUsername, inCallback) { WikiDelegate.getArticle(inTitle, inUsername, { errorHandler : function(inMessage, inException) { woswiki.db.transaction((function (inTransaction) { inTransaction.executeSql( "SELECT * FROM wikiArticles WHERE wiki=? AND title=?; GO;", [ woswiki.currentWiki, inTitle ], function(inTransaction, inResultSet) { var article = null; if (inResultSet.rows.length != 0) { article = { wiki : inResultSet.rows.item(0).wiki, title : inResultSet.rows.item(0).title, articleText : inResultSet.rows.item(0).articleText, lastEditedBy : inResultSet.rows.item(0).lastEditedBy, lastEditedDateTime : inResultSet.rows.item(0).lastEditedDateTime }; } else { article = { wiki : woswiki.currentWiki, title : inTitle, articleText : "This is a new article. Please edit me!", lastEditedBy : null, lastEditedDateTime : 0 }; } article.lockedBy = woswiki.username; article.lockedDateTime = new Date().getTime(); inCallback(article); }, woswiki.dbErrorHandler ); })); }, callback : function(inResponse) { inCallback(inResponse); } }); }
This method is complicated a little bit by the desire to be able to use the wiki when no network connection is present. This method serves as an abstraction layer of sorts that allows the rest of the code to not be aware of whether the network is available or not or where the article came from. When the network is available, the server is consulted for all operations, and thats the happy path, so to speak, the way the application works best. However, if there is no connection available it should still work, but against the local database. This method allows us to deal with that.
First, a DWR call is made by calling the getArticle() method of the WikiDelegate object. This is the JavaScript proxy object for the WikiDelegate object on the server. The requested article title and password are passed to this method as the first two arguments. Note that the username argument will be null if the user clicked a link and is simply viewing an article, but as we'll see later, this method is also used when the user wants to edit the article, and in that situation username would not be null.
The third and final argument to the remote getArticle() method call is an object with two methods in it. In DWR parlance, this is referred to as a call metadata object, but that's really just a fancy way of saying its an object that contains the callback function to execute when the call returns, and optionally some other call-specific things. In this case, one of those call-specific items is an errorHandler callback. This will be executed if any sort of error occurs, most importantly for our purposes, a timeout.
If a timeout occurs then the article needs to be retrieved from the local database. If it's found, then a new object is created based on its values, and that's what will ultimately be returned to the caller. If the article isn't found (which could happen if the user just edited an article and added a new link and there's still no network connection available), then a new object is again created, but this time with the requested title and some temporary text inserted into it. Regardless of how the article object was created, we lock it for editing for the current user. After the if branch that handles those two cases, the callback function that was passed into the getArticle() call is called, passing the article.
Think about that for a moment. It means that whenever getArticle() is called from any scene, the caller is assured of always getting an article object back, and that caller wouldn't know (and shouldn't care anyway) whether the object came from the server or the local database. That detail is completely abstracted away from the caller, exactly as intended.
To close out the getArticle() method is the callback method in that metadata object. This is executed if the article was returned by the server, and in that case it's a simple matter of calling the callback passed into getArticle(). No further work needs to be done here in that case.
Getting an article is all well and good, but updating an article is important too, and that's where the updateArticle() method comes in:
updateArticle : function(inCallback) { woswiki.currentArticle.lastEditedBy = woswiki.username; woswiki.currentArticle.lastEditedDateTime = new Date().getTime(); woswiki.db.transaction((function (inTransaction) { inTransaction.executeSql( "INSERT INTO wikiArticles (wiki, title, articleText, " + "lastEditedBy, lastEditedDateTime) values (?, ?, ?, ?, ?); GO;", [ woswiki.currentWiki, woswiki.currentArticle.title, woswiki.currentArticle.articleText, woswiki.currentArticle.lastEditedBy, woswiki.currentArticle.lastEditedDateTime ], function(inTransaction, inResultSet) { }, woswiki.dbErrorHandler ); })); WikiDelegate.updateArticle(woswiki.currentArticle, { errorHandler : function(inMessage, inException) { inCallback(woswiki.currentArticle); }, callback : inCallback }); }
Somewhat counter-intuitively, at least to my way of thinking, the updateArticle() method is actually shorter and simpler than getArticle(). The currentArticle fields points to the article being edited since its the current article logically, so the lastEditedBy and lastEditedDateTime fields are first updated to reflect the current user and date/time. Next, the article is written to the local database.
Note that upon successful completion of this operation, nothing happens. The reason is that the other task that needs to be accomplished, sending the article to the server, isn't dependant on it having been saved to the database. The server save will occur in parallel to the local save, as you can see next. The updateArticle() method of the WikiDelegate object is called, resulting in a DWR remote call to the server, passing to it the current article object. It's a safe assumption that this call will take longer than the local database save, so upon successful completion the callback passed into woswiki.updateArticle() is called.
The getArticle() and updateArticle() methods represent the two core functions needed to perform the essential wiki operations. There is one method remaining in the woswiki object that is key to the whole thing working, and that's backgroundSync(). It is responsible for synchronizing the local database with the remote server for a given wiki. Its the function that was set up to fire every five minutes in the init() method. It begins innocently enough:
backgroundSync : function() { if (woswiki.synchronizerRunning || !woswiki.currentWiki) { return; } Mojo.Log.error("##### backgroundSync() starting"); woswiki.synchronizerRunning = true; WikiDelegate.listAll(false, { timeout : 1000 * 120, errorHandler : function(inMessage, inException) { Mojo.Log.error("##### backgroundSync() - AJAX error 1: " + inMessage + " - " + inException); }, callback : function(inResponse) {
A quick check is done to confirm that the synchronizer isnt already running (which could happen if the last run is still going, having taken longer than five minutes so far) or if no wiki is currently selected (synchronization occurs on a per-wiki basis and only when one is selected, which helps to keep background activity to a minimum to preserve battery life as much as possible).
If those conditions are passed though, the woswiki.synchronizerRunning flag is set to true so that if the interval fires this method again before its done we won't wind up with two simultaneous update requests happening. The first real work that's done is to retrieve from the server a list of all articles for this wiki. As we saw earlier, since false is passed to the listAll() method, we'll only get back minimal Article objects that contain just the articles title and lastEditDateTime. This call could take more than the five seconds that by default all DWR calls are allotted, and you'll recall we set that in init() as our default. Here, as part of the call metadata object, that default timeout period is being overridden to allow this call to take longer (two minutes in fact, which should be quite enough for even a large wiki; it would likely run afoul of the GAE operation timeout limit long before this timeout anyway). If an error occurs, we just log that fact. The user doesn't need to know, because there's really nothing they could do about it anyway.
As a small digression, astute readers will probably protest that you'd never have this method running simultaneously under any conditions, because JavaScript is single-threaded and setInterval() wouldn't fire the method again until the previous execution completes. I agree this is true. But, what if Palm decides to implement multithreading down the road with respect to setInterval(), as I've seen discussed at various times by different JavaScript engine designers? In that case you'd have a problem. So, using the synchronizerRunning flag may be superfluous today, but it certainly doesn't do any harm to implement it this way and be a little bit future-proof.
When the response comes back successfully though, we execute the following code:
woswiki.db.transaction((function (inTransaction) { inTransaction.executeSql( "SELECT title, articleText, lastEditedBy, lastEditedDateTime " + "FROM wikiArticles WHERE wiki=?; GO;", [ woswiki.currentWiki ], function(inTransaction, inResultSet) { try { var i = null; var localDBArticles = [ ]; var localDBArticlesMap = { }; for (i = 0; i < inResultSet.rows.length; i++) { var article = inResultSet.rows.item(i); localDBArticles.push(article); localDBArticlesMap[article.title] = article; } var serverArticles = inResponse; var serverArticlesMap = { }; for (i = 0; i < serverArticles.length; i++) { serverArticlesMap[serverArticles[i].title] = serverArticles[i]; }
The idea here is that to synchronize content, the code will be comparing what's on the server and what's in the local database and transferring what's missing from each (or what's been updated in each) both ways. So, since the first thing done was to get the list of articles on the server, it stands to reason that the next thing to do is to get that same list of what's in the local database.
Once both lists are in hand we have some initial work to do in order to make things easier later. What we have is two arrays of Article objects, one representing the server and one representing the local database. If we're going to compare the two, think about how you'd write that code. You'd be iterating over one array or the other and then looking into the other array to see if each article in the first is present. So you're talking about a loop inside of a loop. Depending on how big these arrays are, that could be slow, and even if it's a small set of articles it's certainly not the most efficient way. So, what's done here is that each of the arrays is transformed into a map, keyed by title. This way, we can iterate over one of the arrays and do simple direct lookups into the map representation of the other list of article. That'll be more efficient and make the code simpler to boot.
What follows is four blocks of code representing four potential synchronization conditions. These are: an article is present on the server that isn't present in the local database; an article is present in the local database that isn't present on the server; an article on the server is newer than the local database; an article in the local database is newer than on the server. Each of these conditions are unique, but quite similar, so you'll see a lot of similarity between these four blocks of code (and if you guessed that I wrote one, got it working, and then copied it and made subtle changes to get the other three conditions to work, give yourself a cigar!). The first block deals with articles on the server that aren't in the local database:
for (i = 0; i < serverArticles.length; i++) { Mojo.Log.error("##### backgroundSync() - " + "Loop A (new articles on server): " + i); var serverArticle = serverArticles[i]; var localDBArticle = localDBArticlesMap[serverArticle.title]; if (!localDBArticle) { Mojo.Log.error("##### backgroundSync() - Adding server " + "article '" + serverArticle.title + "' to local DB"); woswiki.db.transaction((function (inTransaction) { inTransaction.executeSql( "INSERT INTO wikiArticles (wiki, title, articleText, " + "lastEditedBy, lastEditedDateTime) values " + "(?, ?, ?, ?, ?); GO;", [ woswiki.currentWiki, serverArticle.title, serverArticle.articleText, serverArticle.lastEditedBy, serverArticle.lastEditedDateTime ], function() { }, function(inTransaction, inError) { Mojo.Log.error( "##### backgroundSync() - DB error 2a: " + inTransaction + " - " + inError ); } ); })); } }
This is probably the simplest condition to deal with. We iterate over the array of server articles, and for each one, we look it up in the map of local articles. If it's not found then we do a simple database insert to add it.
Dealing with articles in the local database that aren't on the server is only slightly more complex:
for (i = 0; i < localDBArticles.length; i++) { Mojo.Log.error("##### backgroundSync() - " + "Loop B (new articles in local DB): " + i); var localDBArticle = localDBArticles[i]; var serverArticle = serverArticlesMap[localDBArticle.title]; if (!serverArticle) { Mojo.Log.error("##### backgroundSync() - Adding local " + "article '" + localDBArticle.title + "' to server"); WikiDelegate.updateArticle(localDBArticle, { errorHandler : function(inMessage, inException) { Mojo.Log.error( "##### backgroundSync() - AJAX error 2a: " + inMessage + " - " + inException ); }, callback : function() { } }); } }
In this case we have to make a remote call, so the updateArticle() method of the WikiDelegate object is called, which weve seen in action before. Note that the woswiki.updateArticle() abstraction method is not used because we don't need the abstraction: we know we're trying to write to the server definitively in this case.
Things get slightly more challenging when we try to see if there are newer versions of articles on the server than what we have in the local database:
for (i = 0; i < serverArticles.length; i++) { Mojo.Log.error("##### backgroundSync() - Loop C " + "(more recent versions on server): " + i); var serverArticle = serverArticles[i]; var localDBArticle = localDBArticlesMap[serverArticle.title]; if (serverArticle && localDBArticle && serverArticle.lastEditedDateTime > localDBArticle.lastEditedDateTime) { Mojo.Log.error("##### backgroundSync() - Updating local " + "article '" + serverArticle.title + "' from server"); woswiki.db.transaction((function (inTransaction) { inTransaction.executeSql( "INSERT INTO wikiArticles (wiki, title, articleText, " + "lastEditedBy, lastEditedDateTime) values " + "(?, ?, ?, ?, ?); GO;", [ woswiki.currentWiki, serverArticle.title, serverArticle.articleText, serverArticle.lastEditedBy, serverArticle.lastEditedDateTime ], function() { }, function(inTransaction, inError) { Mojo.Log.error( "##### backgroundSync() - DB error 2b: " + inTransaction + " - " + inError ); } ); })); } }
It's still just iterating over the array of articles on the server and looking them up in the map of local articles. A this point we know we'll find it, since the first two blocks of code will have synchronized anything that's missing, so now we have to see if the server version is newer. To do this we just need to compare the lastEditedDateTime value from the server version of the Article object with the local version. If it's more recent then we do an insert again into the database and use the server version of the Article object for the values. Note that SQLite on webOS is nice enough to not complain if we're inserting a record that's already there; it'll just happily overwrite it. This can be bad in some circumstances of course, but for our needs here it's just right!
Finally, we have to do the same sort of check in the other direction:
for (i = 0; i < localDBArticles.length; i++) { Mojo.Log.error("##### backgroundSync() - Loop D " + "(more recent versions in local DB): " + i); var localDBArticle = localDBArticles[i]; var serverArticle = serverArticlesMap[localDBArticle.title]; if (localDBArticle && serverArticle && localDBArticle.lastEditedDateTime > serverArticle.lastEditedDateTime) { Mojo.Log.error("##### backgroundSync() - Updating server " + "article '" + localDBArticle.title + "' from local DB"); WikiDelegate.updateArticle(localDBArticle, { errorHandler : function(inMessage, inException) { Mojo.Log.error( "##### backgroundSync() - AJAX error 2b: " + inMessage + " - " + inException ); }, callback : function() { } }); } }
In the same way, a newer version of an article in the local database is sent to the server via the WikiDelegate.updateArticle() method again.
All thats left is some final cleanup code:
} catch (e) { Mojo.Log.error("##### backgroundSync() - Process error: " + e); } woswiki.synchronizerRunning = false; }, function(inTransaction, inError) { Mojo.Log.error( "##### backgroundSync() - DB error 1: " + inTransaction + " - " + inError ); }
All of the processing code is wrapped in a try/catch so that we can log an error for any problems that occur, and then that unnecessary flag is set to false again. Finally, we have to handle the possibility of an error doing the read from the local database, so that's the purpose of that last function there. Again, this occurring should be one of those slim-to-none kinds of things, but we can at least log a message, just in case.
With the core code found in woswiki.js dissected, we can now move on to the scenes that make up the UI of the application, beginning with the wikiList scene. This scene, as you can see here, is the one the user sees when the application is initially launched.
It's a perfectly ordinary scene with a perfectly ordinary List widget. The list uses the Add item at the bottom paradigm, as opposed to menu items of some sort. You can swipe to delete any wiki in the list if you wish. Being an ordinary and not very complex scene, the view markup is quite sparse:
<div id="main" class="palm-hasheader"> <div class="palm-header">Your Wiki List</div> </div> <div id="wikiList_lstWiki" x-mojo-element="List"></div>
Yep, just a header and a <div> for the List is all it is. The assistant for the scene is also pretty simple, but we'll take it little by little since it's a bit longer than the view markup:
function WikiListAssistant() { this.wikiListTapBind = this.wikiListTap.bind(this); this.wikiListAddBind = this.wikiListAdd.bind(this); this.wikiListDeleteBind = this.wikiListDelete.bind(this); };
It's always good form to create binds around event handlers once at the start so as to avoid performance hits and potential memory leaks, so that's what we start with in the constructor. The fields of the assistant that store the bind references appear after the constructor in the object definition. Note that although the constructor appears before these field definitions, the following definitions will actually be executed first, when the .js file is loaded, so although it may look like the fields get populated with values and then get null'd out, it is in fact the other way around regardless of the order the code appears here.
WikiListAssistant.prototype.wikiListTapBind = null; WikiListAssistant.prototype.wikiListAddBind = null; WikiListAssistant.prototype.wikiListDeleteBind = null;
As we'll see later, when the user wants to add a wiki, a dialog is used. This dialog is going to need a reference to this assistant to do its thing, so we have a field for that:
WikiListAssistant.prototype.addWikiDialog = null;
We'll of course also need a model for the List widget, so that appears next:
WikiListAssistant.prototype.lstWikiListModel = { items : [ ] };
Now we're ready to see the methods, beginning with the ubiquitous setup() method:
WikiListAssistant.prototype.setup = function() { this.controller.setupWidget("wikiList_lstWiki", { itemTemplate : "wikiList/list-item", addItemLabel : "Add...", swipeToDelete : true, }, this.lstWikiListModel); };
Nothing unusual there, just you're everyday, run-of-the-mill List widget setup. As mentioned, we want to be able to swipe to delete, so that option is turned on, and we want to be able to use an add item, so the text for that is set. The list-item is the template for the items in our List, and in keeping with the theme, it too is quite simple:
<div class="palm-row" x-mojo-tap-highlight="momentary"> <div class="palm-row-wrapper textfield-group"> <div class="truncating-text">#{title}</div> <div class="truncating-text">#{url}</div> </div> </div>
Two rows of text, one for title and one for url, surrounded by Palm-supplied standard style classes, is all we need.
The activate() method is next, and there's a little more to see there:
WikiListAssistant.prototype.activate = function() { woswiki.currentWiki = null; woswiki.currentArticle = null; dwr.engine._defaultPath = null; WikiDelegate._path = null; this.controller.listen("wikiList_lstWiki", Mojo.Event.listTap, this.wikiListTapBind ); this.controller.listen("wikiList_lstWiki", Mojo.Event.listAdd, this.wikiListAddBind ); this.controller.listen("wikiList_lstWiki", Mojo.Event.listDelete, this.wikiListDeleteBind ); this.updateList(); };
The first thing that's done is to reset some variables, namely those pointing to the current wiki and article. We also reset some DWR-related stuff, but let's skip those for now as we'll see them later and it will make more sense to explain them then. The point to doing all of this though is that whenever the scene is activated, as happens if the user performs the back gesture from the viewArticle scene, they are effectively exiting the wiki they're in and preparing to select another.
After those resets are done, the event listeners for the List are set up, using the binds we saw earlier. Finally, updateList() is called to show the list of wikis, and that method is what we find next in the code:
WikiListAssistant.prototype.updateList = function() { woswiki.db.transaction(( function (inTransaction) { inTransaction.executeSql( "SELECT * FROM wikis", [ ], function (inTransaction, inResultSet) { this.lstWikiListModel.items = [ ]; for (var i = 0; i < inResultSet.rows.length; i++) { this.lstWikiListModel.items.push(inResultSet.rows.item(i)); } this.controller.modelChanged(this.lstWikiListModel); }.bind(this), woswiki.dbErrorHandler ); }.bind(this) )); };
It's a simple read from the wikis table in the local SQLite database, then pushing each item from the returned results, if there were any, onto the just-cleared array of items in the List's model. When that's done, we call modelChanged(), and the List will now be showing any wikis available to the user.
Going hand-in-hand with the activate() method is the deactivate() method:
WikiListAssistant.prototype.deactivate = function() { this.controller.stopListening("wikiList_lstWiki", Mojo.Event.listTap, this.wikiListTapBind ); this.controller.stopListening("wikiList_lstWiki", Mojo.Event.listAdd, this.wikiListAddBind ); this.controller.stopListening("wikiList_lstWiki", Mojo.Event.listDelete, this.wikiListDeleteBind ); };
This removes the event listeners from the List. Since doing this requires us to pass the same function reference as when the listener was set up, that's yet another reason the function binds are stored as fields in this assistant, as we saw earlier.
Now we begin to look at the event handlers for the list, the first of which is the tap handler:
WikiListAssistant.prototype.wikiListTap = function(inEvent) { dwr.engine._defaultPath = inEvent.item.url + "/dwr"; WikiDelegate._path = inEvent.item.url + "/dwr"; woswiki.currentArticle = null; woswiki.currentWiki = inEvent.item.title; woswiki.username = inEvent.item.username; woswiki.password = inEvent.item.password; Mojo.Controller.stageController.pushScene("viewArticle"); };
Now we can talk about those DWR settings we saw rest in the activate() method. Typically, when you use DWR in a web page, you're requesting both the engine.js and interface .js files, those being the proxies for your remote objects, from the DWR servlet itself. This is because DWR requires some dynamic information be inserted into them, in this case, the path to the servlet. However, since I've embedded the DWR engine.js and WikiDelegate.js files in the application itself, these paths won't be properly set for whatever wiki we select. We need to set that path dynamically, and that's precisely what is being done here. The _defaultPath field in the dwr.engine object and the _path field in the WikiDelegate object are set to point to the URL associated with the wiki that was selected, and in this way we're mimicking what we would have gotten from the server if we were getting these files from the DWR servlet.
Once the DWR work is done we have some other setup to perform. This involves storing the name of the selected wiki, ensuring no article is recorded as currently being viewed, and storing the username and password for the wiki on the woswiki object. As soon as that's done it's time to show the "home" article for the selected wiki, which is a consequence of pushing the viewArticle scene.
We'll get to that scene later, but first we have two more List event handlers to look at:
WikiListAssistant.prototype.wikiListAdd = function(inEvent) { this.addWikiDialog = this.controller.showDialog({ template : "wikiList/add-wiki-dialog", preventCancel : true, assistant : new AddWikiDialogAssistant(this) }); };
Tapping the Add item in the List results in the addWiki dialog being shown, but we're going to be looking at that next so we won't get into detail on it here. Note however that the dialog cannot be cancelled out of; that is, the user can't use the back gesture to exit it. This is done intentionally because as you'll see, there is a Cancel button on that dialog that serves the same purpose.
The final event handler is for deleting a wiki:
WikiListAssistant.prototype.wikiListDelete = function(inEvent) { woswiki.db.transaction(( function (inTransaction) { inTransaction.executeSql( "DELETE FROM wikis WHERE title=?", [ inEvent.item.title ], function (inTransaction, inResultSet) { }, woswiki.dbErrorHandler ); inTransaction.executeSql( "DELETE FROM wikiArticles WHERE wiki=?", [ woswiki.currentWiki ], function (inTransaction, inResultSet) { }, woswiki.dbErrorHandler ); } )); };
The item is of course removed from the List automatically, but we still have to delete it from the database, which is a simple matter of executing a SQL DELETE statement. Note that we have to delete both the record describing the wiki in the wikis table as well as all the articles for it in the wikiArticles table, so there are two SQL statements to execute.
As we just saw a little bit back, the addWiki dialog is shown to the user when they tap the Add item on the List on the wikiList scene. This dialog looks like this:
As also mentioned, the back gesture cannot be used to exit this dialog; the Cancel button serves that purpose. However, as we'll see, the Cancel button is disabled when the wiki is being verified, which is a REST call. This is on purpose: we don't want the user exiting this dialog until the AJAX requests comes back from the server in one form or another. Otherwise, we might be in a situation where the request comes back but the specified handler is no longer in scope. This would either be an error in the UI or, at best, an error in the logs. Better to avoid that entirely, and simply not letting the user exit the dialog until the operation has finished is an easy way to do that.
I'm jumping ahead a little bit though. Let's look at the view markup for this dialog first:
<div class="cssAddWikiTitle">Add A Wiki</div> <div class="palm-dialog-separator cssAddWikiSeparator"></div> <div class="palm-row"> <div class="palm-row-wrapper"> <div class="textfield-group" x-mojo-focus-highlight="true"> <div class="title"> <div class="label">Title</div> <div id="addWiki_txtTitle" x-mojo-element="TextField"></div> </div> </div> </div> </div> <div class="palm-row"> <div class="palm-row-wrapper"> <div class="textfield-group" x-mojo-focus-highlight="true"> <div class="title"> <div class="label">URL</div> <div id="addWiki_txtURL" x-mojo-element="TextField"></div> </div> </div> </div> </div> <div class="cssInstructions" style="padding-top:10px;"> Enter your username and password for this wiki. If you do not yet have an account, one will be created for you. </div> <div class="palm-row"> <div class="palm-row-wrapper"> <div class="textfield-group" x-mojo-focus-highlight="true"> <div class="title"> <div class="label">Username</div> <div id="addWiki_txtUsername" x-mojo-element="TextField"></div> </div> </div> </div> </div> <div class="palm-row"> <div class="palm-row-wrapper"> <div class="textfield-group" x-mojo-focus-highlight="true"> <div class="title"> <div class="label">Password</div> <div id="addWiki_txtPassword" x-mojo-element="TextField"></div> </div> </div> </div> </div> <table width="100%"><tr> <td width="50%"><div id="addWiki_btnSave" x-mojo-element="Button"></div></td> <td width="50%"><div id="addWiki_btnCancel" x-mojo-element="Button"></div></td> </table>
It's pretty unremarkable markup, so to speak! Really just some basic HTML with the requisite webOS markers to turn <div>'s into widgets later. I'm sure some would protest the user of a <table> to organize the buttons at the bottom rather than a pure CSS approach, so feel free to write me and tell me all about it.
The assistant for this scene is where things get a bit more interesting, beginning with its constructor:
function AddWikiDialogAssistant(inParentAssistant) { this.parentAssistant = inParentAssistant; this.saveTapBind = this.saveTap.bind(this); this.cancelTapBind = this.cancelTap.bind(this); this.validationSuccessBind = this.validationSuccess.bind(this); this.validationProblemBind = this.validationProblem.bind(this); this.on0HandlerBind = this.on0Handler.bind(this); this.getArticlesBind = this.getArticles.bind(this); };
Earlier, I mentioned that this assistant was going to need a reference to the assistant for the wikiList scene, and indeed that reference is passed into the constructor and stored in the parentAssistant field of this object. We'll see how that's used later. There are also a few event handler binds found here, some of which are for widgets, like saveTapBind and cancelTapBind. The rest are related to the AJAX (REST) call we're going to be making.
The fields referenced in the constructor are defined next:
AddWikiDialogAssistant.prototype.parentAssistant = null; AddWikiDialogAssistant.prototype.saveTapBind = null; AddWikiDialogAssistant.prototype.cancelTapBind = null; AddWikiDialogAssistant.prototype.validationSuccessBind = null; AddWikiDialogAssistant.prototype.validationProblemBind = null; AddWikiDialogAssistant.prototype.on0HandlerBind = null; AddWikiDialogAssistant.prototype.getArticlesBind = null;
The models for the widgets are as well:
AddWikiDialogAssistant.prototype.txtWikiTitleModel = { value : null }; AddWikiDialogAssistant.prototype.txtWikiURLModel = { value : null }; AddWikiDialogAssistant.prototype.txtUsernameModel = { value : null }; AddWikiDialogAssistant.prototype.txtPasswordModel = { value : null }; AddWikiDialogAssistant.prototype.btnSaveModel = { disabled : false, label : "Save", buttonClass : "affirmative" }; AddWikiDialogAssistant.prototype.btnCancelModel = { disabled : false, label : "Cancel", buttonClass : "negative" };
Since the two buttons will be manipulated when a wiki is being added, we'll need to have access to these models. So unlike in some other spots where I have the models being anonymously-inlined with the event handler setups, this time they are members of the assistant.
The setup() method is the first method we encounter:
AddWikiDialogAssistant.prototype.setup = function() { this.parentAssistant.controller.setupWidget("addWiki_txtTitle", { focusMode : Mojo.Widget.focusSelectMode, textCase : Mojo.Widget.steModeLowerCase }, this.txtWikiTitleModel ); this.parentAssistant.controller.setupWidget("addWiki_txtURL", { focusMode : Mojo.Widget.focusSelectMode, textCase : Mojo.Widget.steModeLowerCase }, this.txtWikiURLModel ); this.parentAssistant.controller.setupWidget("addWiki_txtUsername", { focusMode : Mojo.Widget.focusSelectMode, textCase : Mojo.Widget.steModeLowerCase }, this.txtUsernameModel ); this.parentAssistant.controller.setupWidget("addWiki_txtPassword", { focusMode : Mojo.Widget.focusSelectMode, textCase : Mojo.Widget.steModeLowerCase }, this.txtPasswordModel ); this.parentAssistant.controller.setupWidget("addWiki_btnSave", { type : Mojo.Widget.activityButton }, this.btnSaveModel ); this.parentAssistant.controller.setupWidget("addWiki_btnCancel", { }, this.btnCancelModel ); };
The four TextBox widgets are first set up, all in a very similar way. I didn't want any of them to do auto-capitalization, so textCase is set to Mojo.Widget.steModeLowerCase. After the TextBoxes are set up, the two Button widgets are as well. The Save button is set as type Mojo.Widget.activityButton so that it can have a Spinner to indicate work is in progress.
The activate() method is here as well of course:
AddWikiDialogAssistant.prototype.activate = function() { this.parentAssistant.controller.listen("addWiki_btnSave", Mojo.Event.tap, this.saveTapBind); this.parentAssistant.controller.listen("addWiki_btnCancel", Mojo.Event.tap, this.cancelTapBind); this.btnSaveModel.label = "Save"; this.btnSaveModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnSaveModel); this.parentAssistant.controller.get( "addWiki_btnSave").mojo.deactivate(); };
Tap event handlers are set up on the two buttons, and the label of the Save button is reset to its default state. Likewise, the button is enabled and the Spinner is turned off.
Any scene that is activated will likely need to be deactivated too, and this one is no exception:
AddWikiDialogAssistant.prototype.deactivate = function() { this.parentAssistant.controller.stopListening("addWiki_btnSave", Mojo.Event.tap, this.saveTapBind); this.parentAssistant.controller.stopListening("addWiki_btnCancel", Mojo.Event.tap, this.cancelTapBind); };
The event listeners are removed to clean up memory and bit and that's it.
Now it gets interesting! When the Save button is tapped, it's time to try to add the wiki:
AddWikiDialogAssistant.prototype.saveTap = function(inEvent) { var wikiTitle = this.txtWikiTitleModel.value; var wikiURL = this.txtWikiURLModel.value; var username = this.txtUsernameModel.value; var password = this.txtPasswordModel.value; if (wikiTitle == null || wikiTitle.blank()) { Mojo.Controller.getAppController().showBanner({ messageText : "Please enter a title", soundClass : "alerts" }, { }, ""); this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); return; } if (wikiURL == null || wikiURL.blank()) { Mojo.Controller.getAppController().showBanner({ messageText : "Please enter a URL", soundClass : "alerts" }, { }, ""); this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); return; } if (username == null || username.blank()) { Mojo.Controller.getAppController().showBanner({ messageText : "Please enter a username", soundClass : "alerts" }, { }, ""); this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); return; } if (password == null || password.blank()) { Mojo.Controller.getAppController().showBanner({ messageText : "Please enter a password", soundClass : "alerts" }, { }, ""); this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); return; }
This first chunk of code performs some basic validations to ensure the user entered something in all four fields. Note that the blank() method, which is added to the String object by the Prototype library that ships with Mojo, is used for this purpose. If any of them are blank then a banner notification is shown and the Spinner on the Save button, which was activate by virtue of the button being tapped, is deactivated.
Assuming those validations pass, it's time to get to work, which begins with updating the UI to indicate work is in progress:
this.btnSaveModel.label = "Working..."; this.btnSaveModel.disabled = true; this.parentAssistant.controller.modelChanged(this.btnSaveModel); this.btnCancelModel.disabled = true; this.parentAssistant.controller.modelChanged(this.btnCancelModel);
The Spinner on the Save button will have started automatically, but we still need to disable the button and change its label. Similarly, the Cancel button needs to be disabled, as per our earlier discussion about requiring this operation to complete before the user leaves the scene.
After that comes the first of the real work:
if (wikiURL.indexOf("http://") == -1) { wikiURL = "http://" + wikiURL; } if (wikiURL.charAt(wikiURL.length - 1) == "/") { wikiURL = wikiURL.substr(wikiURL, wikiURL.length - 1); } this.txtWikiURLModel.value = wikiURL; this.parentAssistant.controller.modelChanged(this.txtWikiURLModel); new Ajax.Request(wikiURL + "/rest/woswiki", { method : "get", on0 : this.on0HandlerBind, onSuccess : this.validationSuccessBind, onException : this.validationProblemBind, onFailure : this.validationProblemBind }); };
First, the URL that was entered is examined. If it doesn't begin with http:// then that is prepended to it. Similarly, if the last character is a forward slash then that is removed. The final version of the string is inserted back into the URL field, just to keep up appearances for the user.
Next, an AJAX request is made. This uses the Ajax object provided by the Prototype library. The URL is the URL that was just constructed, followed by /rest/woswiki, forming a proper REST URL that our server-side code will recognize and process. We also have to ensure the proper HTTP method is used, so we explicitly set it to GET (which is the default, but no harm in really making sure). Finally, a batch of event handlers are passed to the Request() method. The onSuccess, onException and onFailure handlers are, I suspect, obvious, but the on0 one most likely isn't. This handler kicks in if no network connection is available.
Mojo of course has a way to test for that situation, but because the function to do so is service-based, meaning its asynchronous, implementing it frankly would have made this code mode complicated and difficult to follow, which is something I try to avoid when writing a learning article like this. The on0 handler however serves essentially the same purpose, although it doesn't happen immediately as the Mojo check would, meaning the scrim may be up for a few seconds here before the on0 handler fires. It's not a big deal in my opinion, at least not in this situation.
As I'm sure you can surmise, most of the rest of the code comprises the event handlers for this call, and that on0Handler is the first we encounter as we walk through to code:
AddWikiDialogAssistant.prototype.on0Handler = function (inTransport) { this.btnSaveModel.label = "Save"; this.btnSaveModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnSaveModel); this.btnCancelModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnCancelModel); this.parentAssistant.controller.get( "addWiki_btnSave").mojo.deactivate(); Mojo.Controller.getAppController().showBanner({ messageText : "Server unreachable", soundClass : "alerts" }, { }, ""); };
All we can really do in this situation is to reset the UI so the user can try again, and show a banner notification to let them know what happened. They are free to try again, hopefully after confirming network connectivity. This condition can also occur if the URL entered doesn't point to a valid woswiki instance, or is malformed in some way.
Assuming woswiki is running at the specified URL though, the onSuccess handler, which is the validationSuccess() method (as reference by the validationSuccessBind field), will be called:
AddWikiDialogAssistant.prototype.validationSuccess = function(inTransport) { if (inTransport.responseText && inTransport.responseText.strip() == "woswiki_1.0") { new Ajax.Request(this.txtWikiURLModel.value + "/rest/user/" + encodeURIComponent(this.txtUsernameModel.value), { method : "post", parameters : { password : this.txtPasswordModel.value }, on0 : this.on0HandlerBind, onSuccess : this.getArticlesBind, onException : this.validationProblemBind, onFailure : this.validationProblemBind } ); } else { this.validationProblem(inTransport); } };
First we check that the string returned by the server is what we expect. This allows for the possibility that there is a newer version of woswiki running on the server than what this webOS client knows about. The user would have to update their client, or the woswiki server component could just return this same string, which would indicate that even though the server code is newer, the older client can work with it just fine. It's just a little bit of flexibility for down the road, nothing more, because by virtue of having gotten any successful response at all, we all but know there is a woswiki instance running at the URL. (Someone could have an app that responds to the same URL of course, but that's not terribly likely.)
Once the string is confirmed to be correct then the next step is to validate the user credentials. This means either that the username/password entered are valid, or that the username is new to the wiki, in which case the server will automatically add the user and things will proceed as if the credentials were valid (because, in effect, they must be!). So, another Ajax.Request() call is made, this time to our /rest/user URL, with the username appended to the URL. This time we're POST'ing the request, and we have to pass the password too, and that's done as part of the POST body by way of the parameters attribute allowed by the Ajax.Request() method. This time, the onSuccess handler is the getArticlesBind, which wraps the getArticles() method (which I've broken up into chunks, since it's a little on the long side):
AddWikiDialogAssistant.prototype.getArticles = function(inTransport) { if (inTransport.responseText && inTransport.responseText.strip() == "ok") { dwr.engine._defaultPath = this.txtWikiURLModel.value + "/dwr"; WikiDelegate._path = this.txtWikiURLModel.value + "/dwr";
First, we confirm that the username/password that was entered was either valid or resulted in a new user being created, both of which are indicated by the ok string being returned. In that case, we first have to do the same sort of DWR hacking that we saw earlier to get the engine and interface stubs pointing to the correct URL. Once that's done, we call WikiDelegate.listAll():
WikiDelegate.listAll(true, { timeout : 1000 * 120, errorHandler : function(inMessage, inException) { }, callback : function(inResponse) { var callbackCount = 0;
This time, note that we pass true as the first argument. This indicates to the server that we want all the data for each article, including its text. This is because we're going to be inserting all the articles into the local database, as you can see:
woswiki.db.transaction( function (inTransaction) { inTransaction.executeSql( "INSERT INTO wikis (title, url, username, password) " + "VALUES (?, ?, ?, ?); GO;", [ this.txtWikiTitleModel.value, this.txtWikiURLModel.value, this.txtUsernameModel.value, this.txtPasswordModel.value ], function(inTransaction, inResultSet) { callbackCount = callbackCount + 1; if (callbackCount == (inResponse.length + 1)) { this.finishAdd(); } }.bind(this), woswiki.dbErrorHandler );
Before we look at that code, note that callbackCount variable that's set to zero before the database transaction begins. At a high level, two things are being done here. First, the wiki itself is being added to the wikis table, which is what the above block of code is doing. However, at the same time, we're going to be adding each article to the wikiArticles table. All of this happens in parallel, and asynchronously. So, we don't know precisely which database function callback will wind up being called last, and we need to know that in order to finish things up.
That's where the callbackCount variable comes in. Because of the way all these functions are context-bound, they'll all have access to that variable. So, in the callback of every database function we'll increment it and then see if it matches the total number of articles, plus one for the add of the wiki itself. Whichever callback finds that to be the case knows it's the last one to fire and can do the code required at the end.
You can see that logic in the callback for the database operation to add the wiki. The finishAdd() method gets called when the final condition is met.
Likewise, when we add each of the articles:
for (var i = 0; i < inResponse.length; i++) { inTransaction.executeSql( "INSERT INTO wikiArticles (wiki, title, articleText, " + "lastEditedBy, lastEditedDateTime) " + "VALUES (?, ?, ?, ?, ?); GO;", [ this.txtWikiTitleModel.value, inResponse[i].title, inResponse[i].articleText, inResponse[i].lastEditedBy, inResponse[i].lastEditedDateTime ], function(inTransaction, inResultSet) { callbackCount = callbackCount + 1; if (callbackCount == (inResponse.length + 1)) { this.finishAdd(); } }.bind(this), woswiki.dbErrorHandler ); }
The same logic is performed for each write to the database, one for each article returned by the server.
Now, going back a few steps, if the username and password weren't valid then we'll hit the else clause of the outer if statement:
} else { this.btnSaveModel.label = "Save"; this.btnSaveModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnSaveModel); this.btnCancelModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnCancelModel); this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); Mojo.Controller.getAppController().showBanner({ messageText : "Login failed or name in use", soundClass : "alerts" }, { }, ""); }
The UI gets reset, as we've seen previously, and the appropriate banner message is displayed. Note that we really don't know at this point if the user intended to add a new user and the username just happened to be taken already or if they intended to log in with an existing account but has the password wrong, so we put up a generic message to cover both cases. It's good security practice not to give away too much information about why a login failed anyway.
Another possible failure outcome is if any sort of error response occurs, such as an HTTP 500 error thrown by the server. In this case, the validationProblem() method is executed:
AddWikiDialogAssistant.prototype.validationProblem = function(inTransport) { this.btnSaveModel.label = "Save"; this.btnSaveModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnSaveModel); this.btnCancelModel.disabled = false; this.parentAssistant.controller.modelChanged(this.btnCancelModel); this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); Mojo.Controller.getAppController().showBanner({ messageText : "No wiki found at specified URL", soundClass : "alerts" }, { }, ""); };
Note that this can occur for either of the REST requests, but they are handled the same, which is just to reset the UI and show a banner message. I take this condition to mean there is no woswiki instance at the specified URL, and that's what the message reflects. Of course, there could be other reasons to hit this method, things like server misconfigurations and such, but since the user doesn't have much recourse either way I think this message suffices.
Finally, when the wiki has been validated, the user validated and all articles and the wiki itself added to the database, the finishAdd() method fires:
AddWikiDialogAssistant.prototype.finishAdd = function() { this.parentAssistant.controller.get("addWiki_btnSave").mojo.deactivate(); this.parentAssistant.addWikiDialog.mojo.close(); this.parentAssistant.addWikiDialog = null; this.parentAssistant.updateList(); };
At this point we're all done; everything went fine, so the dialog can be closed. Note that before doing that however, the Spinner on the Save button is deactivated. This may seem pointless, and I'd pretty much agree, but not doing so results in an error message showing up in the logs.
Once the dialog is closed, the List of wikis needs to be refreshed. Because this is a dialog and not a scene, the activate() method of the wikiList scene won't fire as it would if we were popping a scene, so we have to manually call it. This, finally, is the reason for storing a reference to that assistant!
There is one very small method floating around these parts, and that's the cancelTap() method:
AddWikiDialogAssistant.prototype.cancelTap = function(inEvent) { this.finishAdd(); };
Piggybacking off the finishAdd() method is just a way for a lazy coder like me to save some typing! It does everything we need, so there's not much point in duplicating code, I figure.
When the user taps a link in an article, or when a wiki is first selected, the viewArticle scene is pushed and it looks like this:
The header pill up top shows the title of the article. The Home button simply loads the home article again so the user can start from ground zero, and the Edit button lets him edit the current article. Note that adding a new article is a two-step process: edit an article to add a link, then click the link to view the article.
The markup for the viewArticle scene is as follows:
<div id="main" class="palm-hasheader"> <div class="palm-header" id="viewArticle_divArticleTitle"></div> </div> <div id="viewArticle_divArticle" class="cssArticleView"></div> <div class="palm-scrim" id="viewArticle_divScrim"> <div id="viewArticle_divSpinner" x-mojo-element="Spinner"></div> </div>
Like most of the others we've seen, it's not at all rocket science. The viewArticle_divArticle is a plain old <div> and is where the article's text will be inserted. The nice thing here is that because we're inserting the text via innerHTML, the user can enter HTML when editing the article in order to have rich formatting. Wikis often have their own syntax for things like that, but using plain HTML like this is a simple way to achieve the same thing.
The assistant for the view starts out very similarly to the others we've looked at:
function ViewArticleAssistant() { }; ViewArticleAssistant.prototype.menuModel = { items : [ { label : "Home", command : "home" }, { }, { label : "Edit", command : "edit" } ] };
Although the command menu doesn't get manipulated, I still have the model hanging off the assistant itself. It could just as easily have been anonymously inlined with the setupWidget() call in setup():
ViewArticleAssistant.prototype.setup = function() { this.controller.setupWidget("viewArticle_divSpinner", { spinnerSize : "large" }, { spinning : true } ); this.controller.setupWidget(Mojo.Menu.commandMenu, null, this.menuModel); };
The activate() method follows that:
ViewArticleAssistant.prototype.activate = function() { if (woswiki.currentArticle == null) { woswiki.getArticle("home", null, function(inResponse) { woswiki.currentArticle = inResponse; $("viewArticle_divArticleTitle").innerHTML = inResponse.title; $("viewArticle_divArticle").innerHTML = woswiki.activateLinks(inResponse.articleText); $("viewArticle_divScrim").hide(); } ); } else { $("viewArticle_divArticle").innerHTML = woswiki.activateLinks(woswiki.currentArticle.articleText); } };
When this scene is pushed there either is a current article (which would be the case when coming back from the editArticle scene), or it can be null (if the wiki was just selected). In the latter case, the home article is retrieved. The response will be an Article object, so we can set the header pill's content to the title attribute of the Article object, and display the articleText attribute in the previously-discussed <div>. If there is a current article already then there's no need to call getArticle(). We can just display the text again (which may have been updated if we're coming back from the editArticle scene) and the title will already be properly set.
When either of the command menu buttons is tapped, the handleCommand() method will be executed:
ViewArticleAssistant.prototype.handleCommand = function(inEvent) { if (inEvent.type == Mojo.Event.command) { switch (inEvent.command) { case "home": woswiki.currentArticle = null; $("viewArticle_divScrim").show(); this.activate(); break; case "edit": Mojo.Controller.stageController.pushScene("editArticle"); break; } } };
The Home button simply shows the scrim on the scene and calls activate(), which you can of course do independent of the scene really being activated because it's just a plain old JavaScript method after all! When Edit is tapped, that's when the editArticle scene is pushed, and that's our next (and final!) destination.
The editArticle scene is, naturally, where the current article is edited. Rather than try to have a single scene where you could view and edit, which would have been entirely possible, I decided to separate them to break the code up a bit. The scene looks like this:
Like the viewArticle scene, the article's title is in the pill header, and below that is a little note to remind the user how to do links. Below that is a TextField where the article's text will be edited. It is set up to auto-expand it's height as the user types, so they can enter as much or as little as they like. The markup that makes the magic happen is this:
<div id="main" class="palm-hasheader"> <div class="palm-header" id="editArticle_divArticleTitle"></div> </div> <div class="cssInstructions"> To create a link to another article, do this:<br> ~!xxx!~<br> where xxx is the title of the article (with NO spaces!) </div> <div> <div class="palm-list"> <div class="palm-row single"> <div class="palm-row-wrapper textfield-group" x-mojo-focus-highlight="true"> <div class="title"> <div id="editArticle_txtText" x-mojo-element="TextField"></div> </div> </div> </div> </div> </div> <div class="palm-scrim" id="editArticle_divScrim"> <div id="editArticle_divSpinner" x-mojo-element="Spinner"></div> </div>
As usual, it's very basic markup. Clearly, this is an application that could be made a lot fancier, and I highly encourage you to do so!
The assistant looks almost identical to the viewArticle scene to start off:
function EditArticleAssistant() { }; EditArticleAssistant.prototype.menuModel = { items : [ { label : "Cancel", command : "cancel" }, { }, { label : "Save", command : "save" } ] }; EditArticleAssistant.prototype.editModel = { value : null };
This time there's a model for the TextField, but aside from that it looks
like that last scene. Similarly, the
setup()
As mentioned, the user can type as much text as they want. The
multiline
attribute set to true allows for that. Activating the scene has a little more behind it: The current article is retrieved, which may seem a little weird, but it's
done so that we know we have the latest version of it if network connectivity
is available. This has a more important goal though, and that's to lock the
article for editing. This of course won't come into play if the server can't
be reached. In that case the user effectively gets an automatic edit lock
since their changes will only be saved locally. When the server is reachable
though, this getArticle() call results in the
lockedBy and
lockedDateTime
fields on the Article object in the database being updated to reflect the
user now editing the article, or it results in a message saying the article
can't be edited. The logic goes like this: if the Article object returned by the call to
getArticle() has a
lockedBy value that matches the current
username, then it's recorded as the current article, and the current contents
are inserted into the TextField. Note that
activateLinks() is not called in this case, as we
want to be editing the plain text, not the viewable version. However, if the
Article object's lockedBy value doesn't
match the current username, then a
banner message is shown saying someone has a lock, and the scene is popped. The last bit of code to look at is the
handleCommand() for the scene's
command menu: The Cancel button is handled simply by popping the scene. Nothing needs to be
reset or loaded or anything, since no changes will have been saved anywhere at
this point. The Save button, on the other hand, requires some work. The scene is
scrimmed, the articleText field of the
current Article object is updated with what the user entered, and then
updateArticle() is called on the woswiki
object. This will result in an Article object being returned, whether the
save was successful against the server only, both server and local database,
or local database only. It will also occur if the save could not be completed
due to a failed lock. This can happen if one user got an edit lock, took too
long to save changes and another user got a lock in the meantime. When this
happens, the Article object that comes back has lockedBy set to null and
lockedDateTime set to zero. In this case, a banner message is shown and the
scene is popped, with the currentArticle
field now pointing to the returned,
and updated, Article object. Note that usability-wise this isn't good because
the user's changes are simply lost. Consider that an opportunity for
improvement as a learning exercise! I hope that this article has provided you with a good example of doing
client-server, cloud-based applications in webOS. I've tried to demonstrate a
number of important concepts including GAE, REST, DWR, synchronization and
online/offline support. As I said early on, this is by no stretch of the imagination a fully-featured
wiki application, but thats part of the beauty of it: its ripe for
expansion! If you're a developer who likes to learn by doing, now is the
perfect opportunity to take the code I've written and build upon it! Doing so
will provide you with practical experience working with the concepts this
article has presented. I hope you find this article useful and that it helps you get a leg-up in
your webOS development. Now get out there and get coding! -Frank W. Zammetti
EditArticleAssistant.prototype.setup = function() {
this.controller.setupWidget("editArticle_divSpinner",
{ spinnerSize : "large" }, { spinning : true }
);
this.controller.setupWidget(Mojo.Menu.commandMenu, null, this.menuModel);
this.controller.setupWidget("editArticle_txtText",
{ focusMode : Mojo.Widget.focusSelectMode, multiline : true },
this.editModel
);
};
EditArticleAssistant.prototype.activate = function() {
woswiki.getArticle(woswiki.currentArticle.title, woswiki.username,
function(inResponse) {
if (woswiki.username == inResponse.lockedBy) {
$("editArticle_divArticleTitle").innerHTML = inResponse.title;
woswiki.currentArticle = inResponse;
this.editModel.value = inResponse.articleText;
this.controller.modelChanged(this.editModel);
} else {
Mojo.Controller.getAppController().showBanner({
messageText : "Article locked by " + inResponse.lockedBy,
soundClass : "alerts"
}, { }, "");
this.controller.stageController.popScene();
}
$("editArticle_divScrim").hide();
}.bind(this)
);
};
EditArticleAssistant.prototype.handleCommand = function(inEvent) {
if (inEvent.type == Mojo.Event.command) {
switch (inEvent.command) {
case "cancel":
this.controller.stageController.popScene();
break;
case "save":
$("editArticle_divScrim").show();
woswiki.currentArticle.articleText = this.editModel.value;
woswiki.updateArticle(
function(inResponse) {
if (inResponse.lockedBy == null &&
inResponse.lockedDateTime == 0) {
Mojo.Controller.getAppController().showBanner({
messageText : "Not saved, edit lock not yours" ,
soundClass : "alerts"
}, { }, "");
}
woswiki.currentArticle = inResponse;
this.controller.stageController.popScene();
}.bind(this)
);
break;
}
}
};
Wrapping things up