Skip to content


jQuery Mobile + CouchDB: Part 7.1 – Authorization and Validation

In my previous article I addressed deleting documents from the albums database within CouchDB using the jquery.couch plugin and hacked around jQuery Mobile a bit to get an external page to act as a dialog without updating the hash location. All great stuff, and we ended off having an application that provided the basics when working with documents – Create Read Update and Delete. After that post, I decided it was high time to throw a wrench into the mix and lock down the admin party we have been having. It’s been a good run…

Wait… so what’s with the versioning of Part 7 in the title?! Its a multipart article of an article series. Barring i don’t break the space-time continuum for breaking up an article into sections, introducing authorization and validation will be spread over a couple posts. There is a lot of information involving set-up and development, plus we will jump into some new concepts – such as jQuery widgets and plugins – that may be a little too much information to digest in a single article.

In the first installment of this multi-part article I intend to address authorization and validation of our document operations so as to not let any old joey-bagadonuts hit the application and start adding, deleting and modifying our precious album list we have been painstakingly curating. If you have been following along, you may have noticed that we have thrown security and user-permitted actions out the window. This article won’t address the real breadth of security that your CouchDB instance should employ, but I will cover moving away from our admin party (anybody can do anything) and introduce the concept of user contexts, as well as how they can be used to validate operations requested for documents in the albums database of our CouchDB instance.

Light Reading

Before I start, there are two excellent articles out there that address authorization and validation in CouchDB that are indespensible. I read them before I started this series while working on another project, but came back to them as a refresher. I recommend you go check them out as they provide a more concise explanation of the work we are about to do (i’ll just touch on the finer points to get this application up and running). They are:

CouchOneWhat’s new in CouchDB 1.0 — Part 4: Security’n stuff: Users, Authentication, Authorisation and Permissions
CouchDB The Definitive GuideValidation Functions

While those articles are an excellent resource for the task at hand, the sites themselves have a wealth of information which I highly suggest perusing, as well. Alright, armed with a little knowledge, lets dig in.

Fix This

If you have already taken your CouchDB instance out of admin party, you can skip this section or read on.

If you have been following along in this series and have only been using the command line to interact with CouchDB, there is a utility application called Futon that ships with CouchDB. I actually browse my CouchDB instance using Futon in CouchOne (neé CouchDBX for Mac). If you have ever visited http://127.0.0.1:5984/_utils, that’s Futon. If you have a version of CouchOne running on your machine, that shows Futon within the browse window. I recommend using CouchOne for local development as it makes it easier to start and stop the service as well as pretty prints out the http calls (not as verbose as i would like, but still useful).

So if you’ve visited Futon and have been running our application that we have built along in the series under admin party (ie. all access), then you may see something of the sort on the right hand side of Futon. Notice the bottom portion:

Futon side panel
[Futon side panel]

We intend to Fix this.

Click that link, and a dialog should appear looking like the following:

Create Server Admin
[Create Server Admin]

Make sure to read what is in that dialog as it is useful and i will not reiterate its information here… we’re just trying to get back to coding people! Enter in whatever username and password you choose. For the purposes of this article series (as the information will be used later) I entered:

u: toddanderson
p: admin123

Click Create to create the admin user and relax. What just happened is that you created an admin that can now do everything everybody used to be able to do. That username and hashed password are now saved in /etc/couchdb/local.ini of your CouchDB instance and is viewable in Futon when you go to Configuration:

Configuration Panel
[Configuration panel]

I have to admit, I am doing this all backtracking. Meaning I took my CouchDB instance off of admin party some time ago. So I am trusting this is still the way to go about it. And I am hoping that since CouchDB 1.0+ there has been a _users database. To check if there is within Futon, go to Overview and one of the first databases listed should be _users. If its there, great! It should probably even have its authentication validation view included so we are all set, and the admin you just created may or may not be automatically entered as a user (i have on mine).

If _users is not available for you… leave a comment and we can work something out.

Creating Users

Remember that rush of power you had when CouchDB was running under admin party? That light may have gone out once you created an admin… but think of it – now you possess the control to create/update/delete as many users as you want. Isn’t it glorious. Now we are going to reign it in a bit and create a new user.

The easiest way to set up a new user is through Futon. If you are still logged in as the admin you just created, log out from the right hand panel and use the Signup link:

Futon Sign Up
[FutonSign Up]

That will open up the Sign Up dialog allowing you to enter a username and password:

Create User Dialog
[Create User Dialog]

I am creating a new user named custardbelly with a super-secret password that will be salted using sha1. That is all handled by the jquery.couch plugin (the same one we have been using in the examples in this series) employed by Futon. If we open up the new user from the _users database we should see the following:

User Entry
[User Entry]

There are a couple things to note here. First off, I took that screen shot after i already assigned a role (“albums-user”) to it. My bad. To do that for you new user, just click on the roles value field and enter “albums-user” (with quotes). That role assignment will be used later when validating documents, so don’t think too hard about it right now.

The other things of note are the auto-generated type field with a value of “user”, and the password_sha and salt fields – auto-generated and populated with values created through the jquery.couch plugin during signup. The _id for a user also has to have the form of reverse-domain for a couchdb user: org.couchdb.user:custardbelly. It must be in that format to be a valid user id so if you ever go about creating a new User manually, either from the _user database New Document option or from the command line, keep that in mind. Last, but not least, the name field is populated with the value of the username (the one entered in the Sign Up dialog through Futon and the value at the end of the _id property). The name property is commonly used in validating documents against a user context which we will get into a little later.

That said, you could create a new user using the database controls in Futon or the command line, but some extra work would be needed to create that sha1 encrypted password. It would be neat to have a script that would do that and output a json file that can be passed in the command line creation of a User, but i won’t go into that right now. Just so you can see what that would look like, the following is an example of how to create a user using the command line:

curl -vX PUT http://toddanderson:admin123@127.0.0.1:5984/_users/org.couchdb.user:custardbelly -d '{"name":"custardbelly", "roles":["albums-user"], "type":"user", "password-sha":"39bc3d994b6a0ce19cb60726b630237d494ae928", "salt":"312b9eb84105e322eb508a559b0000d3"}'

The -d argument takes a valid json string and you could alternatively point to a file using:

-d @custardbelly_user.json

So yeah, a script would be awesome to perform the password encryption and generate that json file with the proper fields. If you have one, let me know. Otherwise it might be a little project for me at a later date. For now, I just use Futon to create new users.

Adding User to Documents

Great. Now we have to associate a user with each album in the albums database. Technically, becaaue we are working with a document-based DMS, we don’t have to go and add a user field to all the album documents we currently have entered. Not having a user field on an album document – though we will include on in all future creates and updates to albums – will not break either party: client or server. But it will be needed for validation on operations. And because we are working with a lovely document-based DMS, its not pulling teeth. For the album documents i currently have in my albums database, i have gone and added a user field with the value of the user name previously created:

Document Update
[Document Update]

It is important to not that the user field value of an album document must match a name field value from a _user database document. This property will be assigned to the user attribute of a user context and be used for comparison on validation. So… human error and misspelling are high at stake in this case when done manually, unfortunately – i’ve definitely been guilty of it and spent hours cursing and pointing at mouses and monitors when only to finally say, “oh… missed the ‘t’ in there. huh“.

Alright. Now we have our album(s) associated with user(s) from the _user database. Let’s take a peek at how validation will occur when performing operations on albums with our client-side jQuery Mobile application. (ah, jQuery Mobile. not much of it in this article of the series, unfortunately. But hold on to your hats… there’s a ton of it in the next!)

Validation

When operations to a document are requested, CouchDB performs, or rather invokes, validation based on the presence of a validate_doc_update script in each /_design document of a database. If the script is not present, there is no validation and anyone (barring the security level assigned to the database) can do whatever they want. So we took CouchDB out of admin party, but that only puts restrictions on operations that require admin credentials once an admin is stored – operations like user creation, user role assignment, database creation, etc, essentially administrative tasks for your CouchDB instance. However, without a validate_doc_updates script in the /_design of our albums CouchApp, anyone can still update an album document however they see fit.

Truthfully, i spend my days as a client-side architect/developer. So i can’t speak well enough about how CouchDB invokes the validate_doc_update and why we are allowed to use JavaScript. There is some type of interpreter that intercepts a create or update (/delete) operation and invokes the validate_update_doc script. If no exception is thrown from that script, it continues along with the operation. Easy concept to grasp and a beautiful design by the CouchDB team… someday i will dig more into how all this comes to happen…

So, we can use good ol’ familiar JavaScript for our validation. When the validate_doc_update is invoked, it is passed two document objects – the “if-all-goes-well” new document and old document – and the user context. The user context object is representative of the current user logged into a session and has property values that can be used for validation on document operations.

To keep you even further from creating the validate_update_doc :) , let’s take a look at what is returned when you create a session:

> curl http://toddanderson:6s0jo772c0kcnwg@127.0.0.1:5984/_session
> {"ok":true,"userCtx":{"name":"toddanderson","roles":["_admin","admin"]},"info":{"authentication_db":"_users","authentication_handlers":["oauth","cookie","default"],"authenticated":"default"}}

So that userCtx object is essentially what is passed during invocation of validation_doc_update. On to the code! Open up your favorite text editor, add the following script and save the file as validate_doc_update.js in the root of your albums couchapp directory (for me, in following with this series, is: /Documents/workspace/custardbelly/couchdb/albums/):

/validate_doc_update.js

function( newDoc, oldDoc, userCtx ) {
  // Load validation script.
  var v = require("vendor/couchapp/lib/validate").init( newDoc, oldDoc, userCtx );

  // Create method to test if valid user.
  v.isAlbumsUser = function() {
    return v.isAdmin() || userCtx.roles.indexOf("albums-user") != -1;
  }

  // Ensure that a current session exists for editing.
  if( !userCtx.name ) {
    v.unauthorized( "You need to be logged in order to do that." );
  }
  else if( !v.isAlbumsUser() ) {
    v.forbidden( "You do not have proper access to edit this document." );
  }

  // Ensure that any updates need to match user.
  var isDeletingWithoutPermission = ( newDoc._deleted && ( oldDoc.user != userCtx.name ) );
  var isUpdatingWithoutPermission = ( newDoc.user != userCtx.name ) || ( oldDoc && ( newDoc.user != oldDoc.user ) );
  // If either non-permission criteria is met, checking delete first...
  if( !v.isAdmin() && isDeletingWithoutPermission ) {
    v.forbidden( "Only the creator of this document has permission to delete." );
  }
  else if( !v.isAdmin && ( !newDoc._deleted && isUpdatingWithoutPermission ) ) {
    v.forbidden( "Only the creator of this document has permission to update." );
  }
  else {
    // If it is being deleted, we are all set.
    if( newDoc._deleted ) return true;

    // Require a user field.
    v.require( "user" );
    // Ensure the assigned user is not changed.
    v.unchanged( "user" );
    // Ensure that user does not have value of undefined.
    v.assert( (newDoc.user != "undefined"), "New documents must have an associated user." );
  }
}

First off, when you create a CouchApp, you get a bunch of scripts available to you in the /vendor directory. We seen and used some of these, most notably in the loader.js from /vender/couchapp/_attachments that is loaded by the index.html document of our jQuery Mobile application. In the first line of our validate_doc_update we require another JavaScript provided through CouchApp – the validate.js. The validate.js script essentially exposes helper methods for determining user roles and validating document updates as well as convenience methods for throwing exceptions (as forbidden or unauthorized). The init() method is a utility method to access a new instance of this validation object and call these methods against the newDoc, oldDoc and userCtx objects.

We add a new method to our validation object to check if the user has a role of is an “albums-user” or is an admin:

/validate_doc_update.js

v.isAlbumsUser = function() {
  return v.isAdmin() || userCtx.roles.indexOf("albums-user") != -1;
}

That is then used to verify that we can go forward in our validation of the document based on the userCtx and documents. It is important to note that a document that is in the process of being deleted is assigned a _deleted property before being passed to the validate_doc_update. That is important to our validation as it will not necessarily be filled with a “user” property, nor is the “user” property necessary on the newDoc to validate the operation. We need to check if a delete operation is valid by comparing the oldDoc to the userCtx:

/validate_doc_update.js

var isDeletingWithoutPermission = ( newDoc._deleted && ( oldDoc.user != userCtx.name ) );
var isUpdatingWithoutPermission = ( newDoc.user != userCtx.name ) || ( oldDoc && ( newDoc.user != oldDoc.user ) );
// If either non-permission criteria is met, checking delete first...
if( !v.isAdmin() && isDeletingWithoutPermission ) {
  v.forbidden( "Only the creator of this document has permission to delete." );
}
else if( !v.isAdmin && ( !newDoc._deleted && isUpdatingWithoutPermission ) ) {
  v.forbidden( "Only the creator of this document has permission to update." );
}

If our validation passes through that, then all that is left is to make sure that we have the correct fields and their values are valid:

/validate_doc_update.js

else {
  // If it is being deleted, we are all set.
  if( newDoc._deleted ) return true;

  // Require a user field.
  v.require( "user" );
  // Ensure the assigned user is not changed.
  v.unchanged( "user" );
  // Ensure that user does not have value of undefined.
  v.assert( (newDoc.user != "undefined"), "New documents must have an associated user." );
}

If we are simply deleting the document, return true to pass the validation, else we ensure that a user field is present, that it has not changed and that it is not undefined. If none of those checks throws an exception, the validation will be complete and the update (create or update) will pass.

What’s cool about Futon is that it will use that validation script when working on documents within Futon… pretty meta. So if you have shut down validation to no-one-at-all-ever, and go and try to update a document in your database, you’ll get an alert and will have to log in as an admin and change your validate_doc_update.

Push Validation to our Albums CouchApp

Alright, with our validate_doc_update script ready to go, its time to push it live. If you have been following along in this series, you are familiar with how we push updates using couchapp. However, with our new privileges that we implemented in this article, we have to do it a little differently. Whoa whoa whoa! Calm down… its not that bad. We just have to pass our admin credentials in the command:

couchapp push albums http://toddanderson:admin123@127.0.0.1:5984/albums

All we did was use basic access authentication to push an update to our couchapp with the credentials of the admin we created previously. Now you have authority. Can you feel the power? Sometimes i think i feel the power, but it could be gas. Not this time, though. I swear.

Security

If you have been playing around with Futon and looked at your albums database, you may have seen a Security… option in the list of actions:

Security Option
[Security Option]

If you log in as the administrator, click option and fill in the credentials with the admin and roles we created previously in this article, you will enforce a login prior to viewing the application:

Security Dialog
[Security Dialog]

If you save that and logout of Futon then try to access the albums database, you will get the following alert:

Security Alert
[Security Alert]

Likewise, if we now browse to our application at http://127.0.0.1:5984/albums/_design/albums/index.html you’ll be presented with this page on landing:

Application Landing
[Application Landing]

That is the session.html shipped with CouchDB that provides an easy way to present a login or signup gateway page for your application. We don’t want that. It breaks the User Experience for our albums application. We are going to present a login/signup only when a user tries to perform an operation that requires session validation. So, if you went ahead and added security, go and roll that back be emptying the fields… just wanted to show you a little more about security.

Now Your Stuck

Uh-oh
[Uh-oh]

LOL. You fell for it. What a ruse! If you have visited your application and tried to either create, update or delete a document, you can’t! That’s the end of this series. See you around!

No, no. Come back. Lower your fists. We’re going to add a login/signup dialog to our jQuery Mobile application so we can go about our business as we had done previously, but this time using user credentials and validation. This post has already gotten rather long, so that will come in the next installment of this mini-series. Just sit back for a bit and feel your gas power rise.

Conclusion

In this first installment of Part 7 mini-series within a series, we address user credentials, validation and security for our database and albums application. All fun stuff, and prior to CouchDB 0.11 a sore point. I won’t go into how I had done it previously, but it boiled down to giving in and keeping the admin party. Maybe i wasn’t smart enough. But thankfully the intelligent people leading CouchDB made me look smart again in later releases.

Next up in the mini-series within a series within Part 7 within this blog within these series of tubes, we will use the information gathered in this article along with our validation script and modify our jQuery Mobile application to present a login/signup dialog when an operation is requested that requires session authentication and user credentials. Hurrah!

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.


as3flobile ScrollViewport and Flex 4 containers? from yeah to eh.

The other day i got an email from Tom Kordys for a little clarification on the as3flobile project and if the ScrollViewport could be used to provide touch scrolling to a DataGroup. My first thought was “no”, then my second thought was “why not?”. The third thought i forget. But then the fourth thought was fire up Flash Builder and take a break.

When i originally started out creating the as3flobile library, the intent was to provide a suite of controls for an AS3 project targeting the Flash Player on a mobile browser. I intentionally pushed Flex to the side for a number of reasons:

  1. Adobe is currently working on and improving Hero (nee Slider).
  2. It runs like crap on mobile (hence the first line item).
  3. I wanted to dive into a solely AS3 personal project again (selfish).

Before i got that email, truthfully, the thought never crossed my mind to intermingle parts from the as3flobile library into Flex; Adobe will deliver their solution in due time and i will then make a decision whether to use Flex or AS3 for my next mobile-web based project. Until then, if need be, i’ll just use AS3. That is still my impression, but after i got that email i was just dying to know if i could enable touch scrolling on Flex 4 containers.

So you’ve read all the way down to here and i still haven’t given an answer…

[view source enabled]
as3flobile viewport container
ScrollViewport with Group containers.

[view source enabled]
as3flobile viewport datagroup
ScrollViewport with virtualized DataGroup container.

Yes, it is possibile! Would i recommend it? Sure, have fun! Would i put it into production? No. No, i would not.

From my tests, Flex containers in the as3flobile ScrollViewport run pretty smoothly on a desktop browser. If that is your target platform and you are looking for touch-scroll on containers, I say try it out. In Flash Player on mobile browser, the scrolling and touch interaction is not smooth enough for my liking, but it may pass your user experience tests… who knows. I believe it is the length of invalidation cycle for Flex that is causing the render hiccups, but i am sure there is some optimization that could be done in as3flobile that may help a little. That said, as3flobile touch-scroll runs pretty smoothly on an AS3 project, so i don’t know how much optimization could be done for me to be comfortable with the rendering performance of Flex containers in an as3flobile ScrollViewport. All said, I will explain how you can target a Flex 4 container as the content for as3flobile ScrollViewport.

as3flobile ScrollViewport

The ScrollViewport control from as3flobile essentially uses its dimensions as a visible area for child content. Its target child content is typed to an InteractiveObject, which is the base display class from which touch/mouse events are received on. Flex containers subclass (through a larger inheritance chain) InteractiveObject so there was no extra effort or cajoling of the as3flobile library to make ScrollViewport work with Flex containers.

With its content set to IntractiveObject, ScrollViewport employs the Strategy Pattern to recognize interactive events on the content and perform scrolling. You can assign custom scroll contexts and strategies to ScrollViewport, but the default context reacts to Mouse events invoking the default scroll strategy. The scroll behaviour is based on the width and height dimensions of the target content for the ScrollViewport. If the height of the content is less than or equal to the height property of ScrollViewport, then vertical scrolling is turned off. Likewise for the width property values and horizontal scrolling.

To set up a viewport for AS3, you would do something of the following:

var container:Sprite = new Sprite();
var bitmap:Bitmap = new Bitmap( new BitmapData( 200, 200, false, 0x000000 ) );
container.addChild( bitmap );

// Direct dimension values
var viewport:ScrollViewport = new ScrollViewport();
viewport.width = 100;
viewport.height = 100;
viewport.content = container;
addChild( viewport );

// OR - Utility convenience instantiation
var viewport:ScrollViewport = ScrollViewport.initWithScrollRect( new Rectangle( 0, 0, 100, 100 ) );
viewport.content = container;
addChild( viewport );

In this example, the InteractiveObject target is the Sprite that has a Bitmap child. The size of the Bitmap (and consequently the parenting size of the Sprite) is larger than the designated size for the viewport. In reality this is a terrible example because you won’t really see the scrolling affect due to a single colored bitmap. An example with a loaded bitmap can be seen at http://www.custardbelly.com/android/froyo/as3flobile.

Targeting Flex 4 containers

The approach to apply the content of the ScrollViewport to a Flex container is relatively the same as in the AS3 example. The only caveat is that you need to listen for a change in the contentWidth and contentHeight properties of the Flex container and refresh the ScrollViewport instance. As well, the ScrollViewport needs to be added as a child of SpriteVisualElement, and the target container re-parented to the ScrollViewport. Here is a quick example:

<?xml version="1.0" encoding="utf-8"?>
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
		 xmlns:s="library://ns.adobe.com/flex/spark"
		 xmlns:mx="library://ns.adobe.com/flex/mx"
		 creationComplete="handleCreationComplete();">

	<fx:Script>
		<![CDATA[
			import com.custardbelly.as3flobile.controls.viewport.ScrollViewport;

			import mx.events.PropertyChangeEvent;

			protected var viewport:ScrollViewport;

			protected function handleCreationComplete():void
			{
				container.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE, handleContentPropertyChange, false, 0, true );

				viewport = new ScrollViewport();
				viewport.width = 100;
				viewport.height = 100;
				viewport.content = container;
				viewportHolder.addChild( viewport );
			}

			protected function handleContentPropertyChange( evt:PropertyChangeEvent ):void
			{
				if( evt.property == "contentWidth" || evt.property == "contentHeight" )
				{
					viewport.refresh();
				}
			}
		]]>
	</fx:Script>

	<s:SpriteVisualElement id="viewportHolder" />

	<s:Group id="container" width="200" height="200">
		<s:Rect width="200" height="200">
			<s:fill>
				<s:LinearGradient>
					<s:entries>
						<s:GradientEntry color="0x000000" ratio="0" />
						<s:GradientEntry color="0xFFFFFF" ratio=".66" />
					</s:entries>
				</s:LinearGradient>
			</s:fill>
		</s:Rect>
	</s:Group>

</s:Group>

The target content – in this example, a Group container with the id of “container” – needs to go through the invalidation cycle within Flex. So it is declared in mark-up as a child of the parent Group. This is important to note, because the target content container cannot be added from a Declarations tag in Flex. It needs to have gone through its invalidation in order to be rendered and re-parented to the ScrollViewport. Once the container is set as the content for the ScrollViewport it is now on the display list of the ScrollViewport.

As mentioned early, in this example, i have assigned a PropertyChangeEvent listener to pick up the change to contentWidth and contentHeight of the container. Once caught, the viewport is refreshed. This is needed in order to properly based its scroll bounds in the strategy. Once that is set, you are already to go.

Here is an example of using this approach which shows vertical and horizontal scroll viewports for Group containers:

[view source enabled]
as3flobile viewport container
ScrollViewport with Group containers.

ScrollViewport and virtualized DataGroup renderers

After drumming up a working example with Group container as content target for ScrollViewport, my mind immediately changed focus to DataGroups. And not just DataGroups, but the need for item renderer recycling using virtualization. If you are unfamiliar with virualization and DataGroups, Adobe has some useful information here.

That gets a little trickier, but not too much. Fortunately the contentWidth and contentHeight property values are updated on a DataGroup based on the dataProvider and the item renderer using virtualization, even though every data renderer for an item in the dataProvider is not present in the layout display stack since they are recycled. In broad terms, this means our scroll strategy will still be a viable solution, but we will need to update the verticalScrollPosition and y position of the DataGroup during the scroll behaviour.

Here is an example of using DataGroup with a virtualized layout:

<?xml version="1.0" encoding="utf-8"?>
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
		 xmlns:s="library://ns.adobe.com/flex/spark"
		 xmlns:mx="library://ns.adobe.com/flex/mx"
		 creationComplete="handleCreationComplete();">

	<fx:Declarations>
		<s:ArrayList id="dp">
			<fx:String>hello</fx:String>
			<fx:String>world</fx:String>
			<fx:String>foo</fx:String>
			<fx:String>bar</fx:String>
			<fx:String>baz</fx:String>
			<fx:String>jello</fx:String>
			<fx:String>biafra</fx:String>
		</s:ArrayList>
	</fx:Declarations>

	<fx:Script>
		<![CDATA[
			import com.custardbelly.as3flobile.controls.viewport.ScrollViewport;

			import mx.events.PropertyChangeEvent;

			protected var viewport:ScrollViewport;

			protected function handleCreationComplete():void
			{
				dataGroup.addEventListener( PropertyChangeEvent.PROPERTY_CHANGE, handleContentPropertyChange, false, 0, true );
				dataGroup.dataProvider = dp;

				viewport = new ScrollViewport();
				viewport.width = 100;
				viewport.height = 100;
				viewport.content = container;
				viewport.scrollChange.add( handleScrollChange );
				viewportHolder.addChild( viewport );
			}

			protected function handleContentPropertyChange( evt:PropertyChangeEvent ):void
			{
				if( evt.property == "contentHeight" )
				{
					container.height = dataGroup.contentHeight;
					contentBackground.height = dataGroup.contentHeight;
					viewport.refresh();
				}
			}

			protected function handleScrollChange( value:Point ):void
			{
				dataGroup.verticalScrollPosition = value.y * -1;
				dataGroup.y = dataGroup.verticalScrollPosition;
			}
		]]>
	</fx:Script>

	<s:SpriteVisualElement id="viewportHolder" />

	<s:Group id="container" width="100">
		<s:Rect id="contentBackground" width="100">
			<s:fill>
				<s:SolidColor color="#DDDDDD" />
			</s:fill>
		</s:Rect>
		<s:DataGroup id="dataGroup"
					 width="100" height="200"
					 clipAndEnableScrolling="true"
					 itemRenderer="spark.skins.spark.DefaultItemRenderer">
			<s:layout>
				<s:VerticalLayout useVirtualLayout="true" gap="2" />
			</s:layout>
		</s:DataGroup>
	</s:Group>

</s:Group>

In this example, we are basing our target content container dimensions on change to the contentHeight property of the DataGroup child. The dataProvider and the item renderer are the basis of the contentHeight property, and once we handle that change, we apply the height properties to the container and contentBackground, then refresh the viewport. The contentBackground is there to act as a grabbable area within the container so we can scroll without having to touched down on an item renderer in the DataGroup directly.

The handler for the scrollChange Signal then updates the verticalScrollPosition of the DataGroup and the y position within the container. This gives the perceived scrolling display incorporated with item renderer recycling (notice the useVirtualLayout property value on the VerticalLayout assigned to the DataGroup).

Here is an example of this working with the status API of Twitter as the data content for a DataGroup:

[view source enabled]
as3flobile viewport datagroup
ScrollViewport with virtualized DataGroup container.

You’ll see some lag when each new item renderer is requested to render, but once it has finished and cached the image requests it will get smoother.

Conclusion

Cheers to Tom Kordys for looking in to as3flobile and sparking the discussion. In short, yeah is it possible to use ScrollViewport and Flex 4 containers. But if you have looked at the examples on a mobile device that supports Flash Player, you may agree with me that it is not production-ready. Maybe there is some optimization i could do one my side, but i feel the lag really is on the Flex invalidation cycle. As well, these examples are pretty basic and there might be some other fine-tuning, not to mention maybe swapping out the container with a Bitmap snapshot of itself while scrolling… something to think about.

Attribution

The examples use the Hi-ReS-Stats component created by Mr.doob.

as3flobile uses the as3-signals library created by Robert Penner.

as3flobile is a set of ActionScript 3 components targeting the Flash Player on mobile devices. You can read more about the project here. To see working examples, visit http://www.custardbelly.com/android/froyo/as3flobile/

Posted in AS3, Flash, Flex, Flex 4, as3flobile.


jQuery Mobile + CouchDB: Part 6 – Deleting Documents

In my previous article, I addressed adding documents to a CouchDB database using the jquery.couch plugin along with a form within the jQuery Mobile framework. If you have been following along (and have been using with the application that is being built) that database might be filling up with album documents by now… but with no way to remove them!

“Why, late on Friday night, did I ever think I needed to catalog Starship’s ‘Knee Deep in the Hoopla’ as ‘my saving grace’?!”

… is one thing that you may say to yourself because there is currently no way to delete a document from the application we have built within this series. However, we have come to the final piece in basic document operations: the D in CRUD. Sit back down. I know it is exciting, but I have a lot of long-winded explanations to write so get ready.

In this article I am going to address adding the ability to delete a document from a CouchDB database. While doing so, I will lightly cover the role that dialogs play in jQuery Mobile and, in the end, hack it to present a dialog (our delete confirmation dialog) as an external page styled as a jQuery Mobile dialog.

In the following examples, I will assume that you have been following along from previous posts and will present updates to existing code.

Deleting Documents

Before we just dive into adding a Delete button somewhere, let’s first think about its context within the application itself. If you have been following along with the previous posts, we have essentially 4 pages in our application:

  • Home – Displays the full list of album documents.
  • Add Album – Provides form to an album document.
  • View Album – Displays information from the album document.
  • Edit Album – Provides form to edit the information on an album document.

From this set of pages we can remove Home and Add Album as candidates to present a Delete button associated with a single document, as they have multiple and no album targets, respectively. So, that leaves us with View Album and Edit Album. A case can definitely be made that a Delete button should be available from the Edit Album page, but I think it would start to get crowded on that page with Save, Cancel and Delete… furthermore, it might be confusing to the User what Cancel actually referred to; cancel delete? cancel edit? I think the Delete button is much more suited to live alongside the Edit button in the View Album page.

So we know where the Delete button is going to reside. The role of the Delete button is to open a confirmation Dialog so the user can confirm their intent on deleting an album document, or deny it; they may have accidentally hit that button. Now an architectural decision needs to be addressed as to how to present this Delete dialog… should we have it as an internal page within the album view page that is served up from our show function in CouchDB, or should it be an external page completely self manageable?

We can make a case for internal with obvious reasons being that a delete dialog is associated with a single document with which you want to perform an operation and our album view page represents a single album document. We also don’t necessarily need the dialog page to be accessed outside of the flow of the application. Meaning, it is not a requirement that the delete dialog is exposed to any user without having to open the application and select an album from the list.

So an internal dialog page might be a good fit, and in most cases I might push for it. Unfortunately, the current state of jQuery Mobile makes the case for an external dialog page an easy one. If we were to add a dialog page to the album view template served up from our show function for an album, the elements would be all screwed up. I do not know if by design that is expected or if it is just a by-product of the templating and dynamic DOM that external pages provides. Either way, things get screwy adding a dialog page along side another page in an external jQuery Mobile page. So external dialog page, ahoy!

Wow, if you read that whole ramble, I am flattered and I hope it made sense. If you didn’t, i dont blame you; more code less talk.

Delete Dialog

We are going to create an external dialog page to present the user the ability to cancel out of the action or confim that they would like to delete the album document from the database. Now, typically when working with the jQuery Mobile framework, a dialog is assigned for a page anchor link with the data-rel attribute value set to “dialog“. For example:

<a href="_show/album-delete/12345" data-icon="minus" data-rel="dialog">Delete</a>

In essence, this attribute not only is responsible for the look-and-feel of the page, but also is an indicator that the url location should not be updated when the dialog page is loaded in the DOM. Makes sense – you shouldn’t have to hit back to get out of a dialog (though Android throws that use-case out the window with its context menus, which i think is still intuitive… but whatever).

But… we can’t do that. We need to use the $.mobile.changePage() method to present the delete dialog page from the album view page. The main reason being relative page locations in relation to show function requests. Basically we would have to invoke the show function to allow for the delete dialog to be presented using a relative path out of the current album view page. An updated example for our application would look like:

<a href="../../_show/album-delete/12345" data-icon="minus" data-rel="dialog">Delete</a>

To me, very ugly and not the correct way to enable this functionality. Furthermore, if we think back to how we implemented the album view page, we decided to clear the page from the DOM after navigating away from it. Essentially, within this context, showing the dialog would wipe the DOM of the album view page and when we choose to either confirm or deny or cancel deletion of the document, we would be returned to a blank page due to the inherent behaviour assigned to the role of a dialog in jQuery Mobile (ie. the previous page is not loaded again using $.mobile.changePage()). Not ideal.

So we’ll just create an external delete dialog maintaining the look and feel by applying styles manually (instead of having the jQuery Mobile framework parse the DOM and apply them intrinsically). That way we can also encapsulate the logic used in deleting a document from the CouchDB database using a view-controller associated with the dialog page.

Delete Dialog show function

With our minds made up on externalizing the Delete Dialog page, we’ll continue on with how we have made all our pages in this series. First up the show function. With your favorite text editor open, create a new document called album-delete.js in the /shows directory of your couchapp application folder (for me that is /Documents/workspace/custardbelly/couchdb/albums). Add the following script and save:

/shows/album-delete.js

function(doc, req) {
    var Mustache = require("vendor/couchapp/lib/mustache");
       var stash = {
            artist: doc.artist,
            title : doc.title,
            document: doc._id
    };
    return Mustache.to_html(this.templates.albumdelete, stash, this.templates.partials.albumdelete);
}

Very similar to our other show functions for the view and edit pages, with just a little less information about the document in the stash. We’ve also got our template and partials declared in the Mustache to_html() call, so let’s go ahead and create those.

Delete Dialog template

As I mentioned earlier, since our dialog is external and will be presented using $.mobile.changePage( ) we loose some niceties of styling pages as dialogs within the jQuery Mobile framework. As such, we are going to roll-up our sleeves and apply some classes and styles directly inline to fake the appearance of the delete album page as a dialog.

With you favorite text editor open, create and save the following mark-up as albumdelete.html in the /templates directory:

/templates/albumdelete.html

<div data-role="page" id="albumdelete" data-backbtn="false" class="ui-dialog ui-body-a">
    <div class="ui-header ui-bar-a ui-corner-top ui-overlay-shadow">
        <h1 class="ui-title">Delete Album?</h1>
        <a id="dialogCloseButton" href="#" data-icon="delete" data-iconpos="notext" style="left: 15px; top: .4em; position: absolute;">Close</a>
    </div>
    <div data-role="content" id="dialogContent" data-identity="{{document}}" class="ui-body-c ui-corner-bottom ui-overlay-shadow">
        <p>Are you sure you want to delete {{artist}}, {{title}}?</p>
        <a id="dialogCancelButton" href="#" data-role="button" data-theme="a">no</a>
        <a id="dialogConfirmButton" href="#" data-role="button" data-theme="c">yes</a>
    </div>
    <div data-role="footer" />
</div>
{{>scripts}}

When you request to load an external page into the DOM, that page – or rather, i should say, the root div with the data-role value of page – is actually wrapped in another parent div with the data-role removed and transfered to the wrapping div. So keep that in the back of your mind as we will need to a little more pixel pushing in the view-controller. For now, understand that this mark-up is going to be wrapped by a div, and the page role of this page will be removed.

Now, in order to style the content of our page as a dialog, we explicitly set the root div class to ui-dialog which just throws in some margins to present more of a self contained box (like a dialog), and ui-body-a essentially being a base style for color and font treatments.

You may also notice the data-backbtn attribute thrown in there. I have rarely gotten it to work for an external page in the current stable release of jQuery Mobile (1.0a2), but i threw it in there and crossed my fingers. In any event, we’ll handle the no back button issue manually, as we only want to display a close button just like all other default dialogs do in jQuery Mobile.

First step in that process: get rid of the header. You may notice that the first div in the children is not assigned a data-role of header or any other value. Instead, i peaked inside the JS and CSS of the jQuery Mobile framework and assigned in-line the class declarations given to a header in the context of a dialog. These classes are responsible for the colorization, size and corner-roundedness (not getting any spell-checking squigglies on that word!) of the div, which houses a custom Close button. Just as I went through and found the classes assigned to a header in a dialog, I went and found the specific styles given to the Close button that is added to the DOM inherently in jQuery Mobile when a dialog is present. I have also done the same for the content, finishing the look-and-feel off with a different background color and bottom rounded corners.

This will look relatively just like any other dialog that is created/modified within the jQuery Mobile framework (at the current release, at least!). However, the background will not grow with its parent size (will be wrapped in a div), so the background treatment assigned to this page will not flow to the size of the page in the browser. To do that we’ll do some jQuery wizardry in the view-controller for this delete dialog page. In order to get there, let’s create our partial for this template.

With you favorite text editor open, create a file named scripts.html, add the following mark-up, and save it in /templates/partials/albumdelete:

/templates/partials/albumdelete/script.html

<script src="../../script/album-delete-dialog.js"></script>

If you haven’t been following along in the series, this partial is just an organizational thing for me. I may be a little anal about where my code resides, and adding the JavaScript within the script declaration is totally acceptable; i just like having my scripts all in one place in a project.

OK. With that set, let’s get on to creating the view-controller for the delete dialog page.

Delete Dialog View-Controller

Before we jump in and create the script, let’s just make sure we are on the same page for the functionality of our delete dialog page. Like most dialogs, it presents the ability to confirm an action or cancel out of that action. So those are two pieces of functionality we need to implement in our view-controller for the page, with the confirmation being a CouchDB transaction. Actually… i think that is about it. Aside from some custom styling we will do in order for the page to be in full, it should be some smooth sailing. I am going to assume that you have been following along in the series and won’t go into the breadth of how the view-controller behaves, but rather the methods which we will implement on it.

With your favorite text editor open, create a new file called album-delete-dialog.js, add the following JavaScript and save in /_attachments/script:

/_attachments/script/album-delete-dialog.js

var AlbumDeleteDialogController = function() {

    function handleDialogViewHide()
    {
        $("#dialogCloseButton").die( "click", handleClose );
        $("#dialogCancelButton").die( "click", handleClose );
        $("#dialogConfirmButton").die( "click", handleDelete );

        var docId = $("#dialogContent").data("identity");
        var dialogCache =  $(document.getElementById("_show/album-delete/" + docId));
        dialogCache.unbind( "pagehide", handleDialogViewHide );
        dialogCache.empty();
        dialogCache.remove();
    }

    function handleDialogView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#dialogContent").data("identity");
        var dialogPage = $(document.getElementById("_show/album-delete/" + docId));
        dialogPage.bind( "pagehide", handleDialogViewHide );
    }

    function handleClose( event )
    {
        event.preventDefault();
        var docId = $("#dialogContent").data("identity");
        $.mobile.changePage( "_show/album/" + docId, "slide", true, true );
        return false;
    }

    function handleDelete( event )
    {
        event.preventDefault();
        var docId = $("#dialogContent").data("identity");
        // First open doc based on ID in order to get full document.
        $db.openDoc( docId, {
            success: function( document ) {
                // Then use the opened doc as reference to remove.
                $db.removeDoc( document, {
                    success: function() {
                        $.mobile.changePage( "#home", "slide", true, true );
                    },
                    error: function() {
                        alert( "Could not remove document with id: " + docId );
                    }
                });
            },
            error: function() {
                alert( "Could not find document with id: " + docId );
            }
        });
        return false;
    }

    return {
        initialize: function() {
            $("#dialogCloseButton").live( "click", handleClose );
            $("#dialogCancelButton").live( "click", handleClose );
            $("#dialogConfirmButton").live( "click", handleDelete);
            // Do pagebefore so when it is shown, it is filled correctly.
            $("div[data-role='page']").live( "pagebeforeshow", function() {
                $("div[data-role='page']").die( "pagebeforeshow" );
                var docId = $("#dialogContent").data("identity");
                var dialogPage = $(document.getElementById("_show/album-delete/" + docId));
                var dialog = $("#albumdelete");
                var h = parseFloat(dialogPage.innerHeight());
                h -= ( parseFloat(dialog.css("border-top-width")) + parseFloat(dialog.css("border-bottom-width")) );
                // define the height based on innerHeight of wrapping parent page and the border styles applied to a dialog.
                dialog.css( "height", h + "px" );
            });
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleDialogView();
            });
        }
    };
}();

function handleDialogReady()
{
    AlbumDeleteDialogController.initialize();
}
$().ready( handleDialogReady )

That might be a lot to digest all at once, but it has the same functionality as the other view-controllers we have created in this series. Essentially, It manages the removal of the page from the DOM on navigation away. A user will be navigated to the previous – album view – page on cancel/close or be taken to the #home page with the updated list of albums after confirmation of delete and success of the transaction from the CouchDB instance. Let’s take a closer look on how that is done:

/_attachments/script/album-delete-dialog.js

function handleDelete( event )
{
    event.preventDefault();
    var docId = $("#dialogContent").data("identity");
    // First open doc based on ID in order to get full document.
    $db.openDoc( docId, {
        success: function( document ) {
            // Then use the opened doc as reference to remove.
            $db.removeDoc( document, {
                success: function() {
                    $.mobile.changePage( "#home", "slide", true, true );
                },
                error: function() {
                    alert( "Could not remove document with id: " + docId );
                }
           });
        },
        error: function() {
            alert( "Could not find document with id: " + docId );
        }
    });
    return false;
}

We first grab the document id assigned to the data-identity attribute of the content div and use that to open the document from the CouchDB database using the declared $db instance on the index.html (of which this page is loaded into). On success of opening the document, we make a request to remove it from the database using the removeDoc() method from the jquery.couch plugin. Upon success of removal from the CouchDB database, we then navigate back to the #home page where the list is updated accordingly to reflect the removal of the album document.

Now the fun part! In order to have the original div (assigned the data-role of page in our template) fill its background to its wrapping parent (assigned by jQuery Mobile upon load of external page) we need to do some jQuery to grab the dimensions of the wrapping page div and the assigned styles on the dialog to reset the dimensions of the dialog to fill its background to the page.

/_attachments/script/album-delete-dialog.js

// Do pagebefore so when it is shown, it is filled correctly.
$("div[data-role='page']").live( "pagebeforeshow", function() {
    $("div[data-role='page']").die( "pagebeforeshow" );
    var docId = $("#dialogContent").data("identity");
    var dialogPage = $(document.getElementById("_show/album-delete/" + docId));
    var dialog = $("#albumdelete");
    var h = parseFloat(dialogPage.innerHeight());
    h -= ( parseFloat(dialog.css("border-top-width")) + parseFloat(dialog.css("border-bottom-width")) );
    // define the height based on innerHeight of wrapping parent page and the border styles applied to a dialog.
    dialog.css( "height", h + "px" );
});

The dialogPage is the parent div for the external delete dialog page (the template previously created in this article). As mentioned before that is created and wrapped around the external page inherently within the jQuery Mobile framework when the page is loaded into the DOM.

That wrapping parent is assigned the id of the url location shown in the hash, and is accessed using getElementById(). There might be some other tricks to access that div using class*=’ui-page’. It is also assigned a class of ui-page-active, but that is only after it has been shown. Since we assign a handler for pagebeforeshow, that class has yet to be assigned so we can’t access it that way using jQuery. Listening and assigning these property during the pagebeforeshow event also has the added benefit of sizing the dialog background correctly before the user sees it.

Alright. So we have faked our external page to be presented as a dialog normally shown through the jQuery Mobile framework by assigning the data-rel on a page link. Now we have to go about modifying the code so we can show it…

Modifying Album View Page

Way back in the beginning of this article before i started yammering on about this and that, we resolved that the Delete button should be an additional UI piece to the album view page. If you have been following along in this series, we added the Edit button a couple articles back to the album view page. It was represented by a p element with the data-role of button and positioned vertically below the document information fields. Quite ugly, but reasonable for the time.

To get into more of a consistant look and feel, we are going to add a navigation bar to the album view page, just as we have done for the #home page in the last article. The navbar for the album view page will consists of two buttons – Edit and Delete – and we’ll also throw in some styling to give them unique enough color treatments.

In your favorite text editor, open the /templates/album.html document and make the following modifications:

/templates/album.html

<div data-role="page" id="albumview" data-position="inline" data-back="true">
    <div data-role="header" id="albumheader">
        <h1 class="albumtitle">{{title}}</h1>
        <a href="#home" data-icon="grid" class="ui-btn-right">Home</a>
    </div>
    <div data-role="content" id="albumcontent" data-identity="{{document}}">
        <h2 class="artist">{{artist}}</h2>
        <p class="title">{{title}}</p>
        <p class="description">{{description}}</p>
    </div>
    <div id="falseFooter" class="ui-bar-a ui-footer ui-footer-fixed ui-fixed-inline fade" role="content" data-position="fixed">
        <div data-role="navbar">
          <ul class="ui-grid-a">
              <li><a href="#" id="deleteButton" data-icon="minus">Delete</a></li>
              <li><a href="#" id="editButton" data-icon="gear" data-theme="b">Edit</a></li>
          </ul>
        </div>
    </div>
    <div data-role="footer" data-position="fixed" />
</div>
{{>scripts}}

We got rid of the previous Edit button element and added it to a list of buttons in a navbar. Alongside the Edit button, we have added the Delete button. We also added some icons and applied a theme to the Edit button to give it a blue color so as to be immediately distinguishable from an action to delete the album document.

You may notice that we actually did not add the navbar to the footer as we did for the home page. The reason being that there is a bug in the current stable release for jQuery Mobile that screwed up the layout if we did that. Even though I have mentioned bugs in the jQuery Mobile framework, the beauty is we can easily work around most of them by either faking the HTML mark-up or using jQuery to modify properties; that is always a win in my book. So we created a false footer and assigned it the classes that would normally be assigned to a footer from the jQuery Mobile framework, and with a data-position of fixed, it should always appear at the bottom of the page (it won’t really, but we will modify the view-controller to put it there).

Now, we needed to leave the footer tag in this template because, you guessed it, a bug in jQuery Mobile. If we omitted that footer on an external document, the ready event is never fired – bad news. So we threw it in with a data-position of fixed so it is always at the bottom. Woohoo! Our page has been updated. Let’s wired up some operations so we can show our delete album dialog.

Modifying Album View-Controller

Before we get into this, i must profess that we are going to do something gross. I don’t particularly like it, but it is a fast and cheap way to get the result we want. Let’s get into it and see if i can dig myself out…

In your favorite text editor, open the /_attachments/script/album-page.js document and make the following modifications:

/_attachments/script/album-page.js

var AlbumPageController = function() {

    /* RIPPED FROM jquerymobile-1.0a2.js */
    function getOffsetTop(ele)
    {
        var top = 0;
        if (ele)
        {
            var op = ele.offsetParent, body = document.body;
            top = ele.offsetTop;
            while (ele && ele != body)
            {
                top += ele.scrollTop || 0;
                if (ele == op)
                {
                    top += op.offsetTop;
                    op = ele.offsetParent;
                }
                ele = ele.parentNode;
            }
        }
        return top;
    }

    function setTop(el){
        var fromTop = $(window).scrollTop(),
            thisTop = getOffsetTop(el[0]), // el.offset().top returns the wrong value on iPad iOS 3.2.1, call our workaround instead.
            thisCSStop = el.css('top') == 'auto' ? 0 : parseFloat(el.css('top')),
            screenHeight = window.innerHeight,
            thisHeight = el.outerHeight(),
            useRelative = el.parents('.ui-page:not(.ui-page-fullscreen)').length,
            relval;
        if( el.is('.ui-header-fixed') ){
            relval = fromTop - thisTop + thisCSStop;
            if( relval < thisTop){ relval = 0; }
            return el.css('top', ( useRelative ) ? relval : fromTop);
        }
        else{
            //relval = -1 * (thisTop - (fromTop + screenHeight) + thisCSStop + thisHeight);
            //if( relval > thisTop ){ relval = 0; }
            relval = fromTop + screenHeight - thisHeight - (thisTop - thisCSStop);
            return el.css('top', ( useRelative ) ? relval : fromTop + screenHeight - thisHeight );
        }
    }
    /* END RIPPED FROM jquerymobile-1.0a2.js */

    function handleView()
    {
        setTop( $("#falseFooter") );
        $("#editButton").live( "click", handleEdit );
        $("#deleteButton").live( "click", handleDelete );
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumcontent").data("identity");
        var albumPage = $(document.getElementById("_show/album/" + docId));
        albumPage.bind( "pagehide", handlePageViewHide );
    }

    function handleEdit( event )
    {
        // Prevent default link event.
        event.preventDefault();
        // Access document id from data-identity.
        var docId = $("#albumcontent").data("identity");
        // Change page.
        $.mobile.changePage( "_show/album-edit/" + docId, "flip", false, true );
        return false;
    }

    function handleDelete( event )
    {
        // Prevent default link event.
        event.preventDefault();
        // Access document id from data-identity.
        var docId = $("#albumcontent").data("identity");
        // Change page.
        $.mobile.changePage( "_show/album-delete/" + docId, "slideup", false, false );
        return false;
    }

    function handlePageViewHide()
    {
        $("#editButton").die( "click", handleEdit );
        $("#deleteButton").die( "click", handleDelete );

        var docId = $("#albumcontent").data("identity");
        var albumPageCache =  $(document.getElementById("_show/album/" + docId));
        albumPageCache.unbind( "pagehide", handlePageViewHide );
        albumPageCache.empty();
        albumPageCache.remove();
    }

    return {
        initialize : function() {
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleView();
            });
        }
    };
}();

function handlePageViewReady()
{
    AlbumPageController.initialize();
}
$().ready( handlePageViewReady );

You notice those first two methods? Yeah, i just copied and pasted them from the jQuery Mobile script. They are private methods of the “fixedHeaderFooter” plugin of the framework. We needed them to position our false footer navbar, so i ripped ‘em. Wait! Don’t leave. I am not happy about it either. Hopefully they will be exposed utilities in a later release of the framework, or we could go about creating our own so they are accessible outside of this page, but for now you gotta do what you gotta do.

Upon show of the page we use those methods to position the false footer navbar at the bottom. Sure, in the real world a client – our rather your team since you know about it! – would log a bug, but we’re just having fun right? I hope so.

The other modification was to wire up the Delete button to show the delete album dialog page we previously created. We employed the same operations we have done throughout this series to navigating to a page: access the document id and call $.mobile.changePage() to serve up the filled template form CouchDB. Done and done. Let’s get all Salt-N-Pepa up in here and Push It.

Deployment

We modified our application to have the ability to delete an album document from the database by adding a new delete dialog page and updating the UI of the album view page. With these changes saved, we can now push to the CouchDB database using the couchapp utility. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is the CouchApp application directory for these examples). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If all was successful and you now go to http://127.0.0.1:5984/albums/_design/albums/index.html. Select an album from the the list on the landing page, and click the Delete button from the album view page. Choose to delete, cancel or close and you should be navigated to the correct page: #home if delete, album view if cancel/close. These updated and new pages should look somewhat like the following:

updated album view

delete album dialog

Conclusion

In this article, we finished off having the basic operations for handling documents in our application. We can now create, read, update and delete documents from the CouchDB database. Along the way, we covered a few ways to hack things together for an application utilizing the jQuery Mobile framework; some were work-arounds due to bugs, some were just design decisions. In any event, I hope it was informative if not fun.

Next up: I am wavering between authentication or attachments… you’ll have to wait and see!

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.


jQuery Mobile + CouchDB: Part 5 – Adding Documents

In my previous post, I addressed editing documents using a form in jQuery Mobile and updating the document in the CouchDB database using the jquery.couch plugin. So far, if you have been following along, the posts have only addressed the RU of CRUD (Create Read Update Delete) from the client application aspect. The list of albums are read upon launch, with a single album read on detail, and last post addressed updating a target document. Its high-time we start throwing C & D into that mix… but hold on. One at a time. First one will be fun. The second one will make you cry. That’s not true.

In this article I am going to address the ability to create a new album document, which in and of itself is not entirely different from editing. While doing so, I will address adding a button bar as a footer using jQuery Mobile grid layout and a few tidbits here and there to get things to work, as well as continue to gush over the beauty of using a document-oriented database system… because there’s changes ahead!

In the following examples, I will assume that you have been following along from previous posts and present updates to existing code.

Adding Documents

In the previous posts, I discussed the difference between internal and external pages in jQuery Mobile and made a fair assessment (i hope) to create the pages of our client application as externally-loaded into the DOM. This allows us to see the beauty of templates and show functions and peek a little into how a CouchDB instance, which is an HTTP server itself, can be used to serve up a client the communicates with a target CouchDB database.

Well, we are not going to do that for the album add page. Instead, we will declare the page internally and not worry about removing it form the DOM to prevent page caching. The reason is that, though it will look very similar to the album edit page in mark-up, it differs slightly in its usage. The album add page has no document association and is not served up based on a document id. So using a show function would essentially return a 404. We could have it as an external page, but I got to thinking… will we need the ability for a user to hit the album add page outside of the context of the client application? Meaning, should we restrict the ability to add documents to a single client? I am choosing to, but you can certainly run with it and make it external. For me it makes more sense to only allow the index.html to be hit and the sole starting point for the Albums application.

To start, open up that old index.html file from the /_attachments directory in your favorite text editor. We are initially just going to modify the page declarations in the body tag, so the following modifications are a subset of lines from the index.html. The whole file will be presented later; for now save the following changes:

_attachments/index.html

<body>
    <div id="home" data-role="page">
        <div data-role="header"><h1>Albums</h1></div>
        <div data-role="content">
            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b"></ul>
        </div>
        <div data-role="footer" class="ui-bar"><h4>a list of albums</h4></div>
    </div>
    <div id="addAlbum" data-role="page">
        <div data-role="header"><h1>Add Album</h1></div>
        <div data-role="content">
            <form id="albumAddForm" action="#" method="get">
                <div data-role="fieldcontain">
                  <label for="artist">Artist:</label>
                     <input id="addArtistField" name="artist" type="text" />
              </div>
              <div data-role="fieldcontain">
                  <label for="title">Title:</label>
                     <input id="addTitleField" name="title" type="text" />
              </div>
              <div data-role="fieldcontain">
                  <label for="description">Description:</label>
                  <textarea id="addDescriptionField" name="description" cols="40" rows="8"></textarea>
              </div>
              <div class="ui-body ui-body-b">
                  <fieldset class="ui-grid-a">
                      <div class="ui-block-a">
                          <a href="#home" id="addCancelButton" data-role="button" data-theme="d">Cancel</a>
                      </div>
                      <div class="ui-block-b">
                          <a href="#" id="addSubmitButton" data-role="button" data-theme="a">Submit</a>
                      </div>
                  </fieldset>
              </div>
          </form>
        </div>
    </div>
</body>

As i mentioned earlier, if you have been following along with the previous posts, the addAlbum page is very similar to the external album edit page (created in the last post); the only difference being that the fields/buttons have different ids and the header is left to default in displaying the Back button.

There is associated script with the internal addAlbum page that handles submitting a new album document to the CouchDB database, and there is no time like the present to shut me up and dive right in. Make the following changes to the previously created handleDocumentReady() JavaScript method within index.html:

_attachments/index.html

function handleDocumentReady()
{
    $("#home").bind( "pagebeforeshow", refreshAlbums );
    refreshAlbums();

    $("#addSubmitButton").live( "click", function( event ) {
        event.preventDefault();
        var document = {};
        document.artist = $("input#addArtistField").val();
        document.title = $("input#addTitleField").val();
        document.description = $("textarea#addDescriptionField").val();
        document.creation_date = ( new Date() ).getTime();
        $db.saveDoc( document, {
                success: function() {
                    $.mobile.changePage( "#home", "slidedown", true, true );
                },
                error: function() {
                    alert( "Cannot save new document." );
                 }
        });
        return false;
    });
}

Essentially, we add a click event handler to the submit button, create a new document object, fill in the proper fields then use the $db instance (established on load as the albums databased from the CouchDB instance) to save the new document. You may notice that saving a new document and updating an existing document are both done using the same function from the jquery.couch plugin: saveDoc(). That function will check for an _id on the document object (first argument) and determine if a POST or PUT is required. That is all handled internally in saveDoc() so you don’t have to worry.

creation_date

You may have also noticed that i said “proper fields” and the document object has a property that was not on the previous documents we created from the first post. Where did this creation_date property come from? And why won’t it throw an error on save? Short answer: we are working with a document-oriented database, so we are not tied to a schema and a pre-defined set of fields in a table. So anything can be added to any document?! Isn’t that a little wild-west?! Maybe, but we are making a pretty focused client application here where we know what fields we want to present to the user; but it is a good point and a solid argument to not keep your CouchDB instance in admin-party and to create good validation functions. For now, we are not concerned about security or validation and are having some fun.

OK, so why did we add creation_date? The reason is related to the map function for /views/albums. If you remember way back to the first article, we created a map function for our albums view that basically returned a key and value. The key being the _id of each document. That key is used to sort the returned array of documents. That key is also automatically generated for us when we create a new document. Hence, a case could be made that the order of added documents will not correlate to the descending order of the sorted key list (_id of each document in the database). In order to be able to properly present the list of albums in the order that they were added to the database, the creation_date property is now being added to each new document. I use the time in milliseconds as the value for creation_date because i feel that most (if not all) client-side languages will know how to use that number and format the date as required.

Updating albums view

Well… now there is the issue of returning and displaying album documents sorted by creation_date. Fortunately it is an easy issue to resolve: we are going to update the key value returned from the map function of the albums view.

Open up /views/albums/map.js in your favorite text editor and change the previously-saved emit() invocation to the following:

/views/albums/map.js

function(doc) {
  emit( ( new Date(doc.creation_date) ).getTime(), doc );
}

We are now using the creation_date property as the key for each document to return. CouchDB will sort on this key and return a list of documents from the albums database in our CouchDB instance. You may notice that we are resolving to a new instance of Date using the creation_date property value and then returning the same value using getTime(). Seems a little superfluous. However, it is just a sort of future-proofing if a requirement comes down the pipe that creation_date needs to be saved in some other format. Who knows.

Sorting

If we were to push our changes and requested the list of album documents, you would notice that the documents are sorted by CouchDB in ascending order (oldest creation_date first). Now, we could create a reduce function to return the list in descending order, but i don’t think that is the best use of reduce seeing as we can easily using JavaScript to reverse the array upon response. So, I am going to make you open up index.html again and add one line with the refreshAlbums() JavaScript method:

/_attachments/index.html

function refreshAlbums()
{
    $("#albums").empty();
    $db.view("albums/albums", {
        success: function( data ) {
                var i;
                var album;
                var artist;
                var title;
                var description;
                var listItem;
                var header;
                var albumLink;
                data.rows.reverse();
                for( i in data.rows )
                {
                    album = data.rows[i].value;
                    artist = album.artist;
                    title = album.title;
                    description = album.description;
                    externalPage = "_show/album/" + album._id;
                    listItem = "<li class=\"album\">" +
                                "<a href=\"" + externalPage + "\">" +
                                    "<h2 class=\"artist\">" + artist + "<\/h2>" +
                                "<\/a>" +
                                "<p class=\"title\">" + title + "<\/p>" +
                                "<p class=\"description\">" + description + "<\/p>" +
                               "<\/li>";
                    $("#albums").append( listItem );
                }
                $("#albums").listview( "refresh" );
            }
    });
}

That’s it! Just reverse the returned rows of album documents from the view request and we have a descending list of albums we have added to the database based on creation_date.

Now the only problem is we have no way of accessing the addAlbum page! Don’t fret. We’re going to add a button bar to the footer of the #home page that will take us there.

Updating #home Footer

We are going to update the default (#home) jQuery Mobile page for our application to display a button bar in the footer. Originally, we just had some text there as a placeholder, but seeing as we want to ability to add album documents from the list view will replace that text with a navbar containing a single button – the add button – that will navigate the user to the album add page we created in the previous section of this article. Doing so will involve a couple user experience issues that we will address and uncover a few more jQuery Mobile goodies.

Open up /_attachments/index.html in your favorite editor and save the following modifications to the #home page:

/_attachments/index.html

<div id="home" data-role="page">
  <div data-role="header" data-position="fixed"><h1>Albums</h1></div>
  <div data-role="content">
      <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b"></ul>
  </div>
  <div data-role="footer" data-position="fixed">
    <div data-role="navbar">
        <ul class="ui-grid-a">
            <li style="width:100%;"><a href="#addAlbum" data-transition="slideup" data-icon="plus">Add Album</a></li>
        </ul>
    </div>
  </div>
</div>

The first thing you may notice is that we have added a data-position attribute to the header and footer. With a value of “fixed“, the jQuery Mobile framework will ensure that they are always in place (header at top, footer at bottom) without regards to scrolling. This will treat the content list as the scrollable area, so when our list grows with all the new album documents that we create a user will always see and be able to access the header and footer (with the add button).

Aside from the data-position attribution, we added a navbar as the content for the footer. The content of the navbar is a list with a single item with its navigational reference to the addAlbum jQuery Mobile page we created previously. Assigning a grid class to the list will layout its items in a sequently manner. We set the width style rule directly for the list item because styling of list items for a navbar are limited to at least 2 items in the jQuery Mobile. To get around that and have a single button, we just set it to have the width of the navbar. Then we also got all fancy with transitions and icons on the link within the list item :)

The only drawback to this solution is that we dynamically fill our list upon load. Unfortunately this updates the y position of the footer by (n*list item height). So if we kept our page like this, we’d lose the footer off the page once the list is filled :( Wait, don’t leave… we can easily fix that!

With the /_attachments/index.html file still open in your favorite text editor, add the following line after the list refresh():

/_attachments/index.html

function refreshAlbums()
{
    $("#albums").empty();
    $db.view("albums/albums", {
        success: function( data ) {
                var i;
                var album;
                var artist;
                var title;
                var description;
                var listItem;
                var header;
                var albumLink;
                data.rows.reverse();
                for( i in data.rows )
                {
                    album = data.rows[i].value;
                    artist = album.artist;
                    title = album.title;
                    description = album.description;
                    externalPage = "_show/album/" + album._id;
                    listItem = "<li class=\"album\">" +
                                "<a href=\"" + externalPage + "\">" +
                                    "<h2 class=\"artist\">" + artist + "<\/h2>" +
                                "<\/a>" +
                                "<p class=\"title\">" + title + "<\/p>" +
                                "<p class=\"description\">" + description + "<\/p>" +
                                "<p class=\"date\">" + new Date( album.creation_date ) + "<\/p>" +
                               "<\/li>";
                    $("#albums").append( listItem );
                }
                $("#albums").listview( "refresh" );
                $.fixedToolbars.show();
            }
    });
}

The fixedToolbars controller is available from jQuery Mobile with several public methods exposed for dealing with the header and footer toolbars. We are using show() to force an update in placement once the list has been refreshed. This will affectively put the footer (with its add button) back to the bottom where it belongs and is accessible. Hack? Maybe. But it works for now (see versions at end of post :) ).

So that is it… except for one thing.

Finishing Up

I can be a stickler. The addAlbum page is not removed from the DOM once navigated away from like the other pages we have created. We did that previously to ensure we were getting the correct returns from the show function based on document _id; jQuery Mobile was caching those pages in the DOM so it never went back out with the update _id. However, our case here is a little different.

The addAlbum page is not associated with a document upon view. It just saves a document to the CouchDB database using the jquery.couch plugin. As such, if a user navigates away from the page – either from an explicit cancel/close or directed back to #home upon save success – we should clear out those input fields. Otherwise, when a user navigates to the addAlbum page again, we’ll always see the last input. That shouldn’t be the case. The user should start fresh each time. To do that, we’ll just listen to when the page is hidden in the DOM and clear the fields.

With /_attachments/index.html still open in your favorite text editor, save the following modifications to the handleDocumentReady() JavaScript function:

/_attachments/index.html

function handleDocumentReady()
{
    $("#home").bind( "pagebeforeshow", refreshAlbums );
    refreshAlbums();

    $("#addSubmitButton").live( "click", function( event ) {
        event.preventDefault();
        var document = {};
        document.artist = $("input#addArtistField").val();
        document.title = $("input#addTitleField").val();
        document.description = $("textarea#addDescriptionField").val();
        document.creation_date = ( new Date() ).getTime();
        $db.saveDoc( document, {
                success: function() {
                    $.mobile.changePage( "#home", "slidedown", true, true );
                },
                error: function() {
                    alert( "Cannot save new document." );
                 }
        });
        return false;
    });

    $("#addAlbum").bind( "pagehide", function() {
       $("input#addArtistField").val( "" );
       $("input#addTitleField").val( "" );
       $("textarea#addDescriptionField").val( "" );
    });
}

Similarly to how we listened for navigation away from the other external pages, we assign an even handler for the “pagehide” event and clear the input fields. That will be invoked upon back/cancel and successful commit (via the $.mobile.changePage() method), so we can ensure that the fields are always presented empty upon hitting the addAlbum page.

So we are all on the same page (pun… intended?), here is the updated /_attachments/index.html file in all its modified glory:

<!DOCTYPE html>
<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div id="home" data-role="page">
          <div data-role="header" data-position="fixed"><h1>Albums</h1></div>
          <div data-role="content">
              <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b"></ul>
          </div>
          <div data-role="footer" data-position="fixed">
            <div data-role="navbar">
                <ul class="ui-grid-a">
                    <li style="width:100%;"><a href="#addAlbum" data-transition="slideup" data-icon="plus">Add Album</a></li>
                </ul>
            </div>
          </div>
      </div>
      <div id="addAlbum" data-role="page">
          <div data-role="header"><h1>Add Album</h1></div>
          <div data-role="content">
              <form id="albumAddForm" action="#" method="get">
                  <div data-role="fieldcontain">
                    <label for="artist">Artist:</label>
                       <input id="addArtistField" name="artist" type="text" />
                </div>
                <div data-role="fieldcontain">
                    <label for="title">Title:</label>
                       <input id="addTitleField" name="title" type="text" />
                </div>
                <div data-role="fieldcontain">
                    <label for="description">Description:</label>
                    <textarea id="addDescriptionField" name="description" cols="40" rows="8"></textarea>
                </div>
                <div class="ui-body ui-body-b">
                    <fieldset class="ui-grid-a">
                        <div class="ui-block-a">
                            <a href="#home" id="addCancelButton" data-role="button" data-theme="d">Cancel</a>
                        </div>
                        <div class="ui-block-b">
                            <a href="#" id="addSubmitButton" data-role="button" data-theme="a">Submit</a>
                        </div>
                    </fieldset>
                </div>
            </form>
          </div>
      </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

      $db = $.couch.db("albums");

      function handleDocumentReady()
      {
          $("#home").bind( "pagebeforeshow", refreshAlbums );
          refreshAlbums();

          $("#addSubmitButton").live( "click", function( event ) {
              event.preventDefault();
              var document = {};
              document.artist = $("input#addArtistField").val();
              document.title = $("input#addTitleField").val();
              document.description = $("textarea#addDescriptionField").val();
              document.creation_date = ( new Date() ).getTime();
              $db.saveDoc( document, {
                      success: function() {
                          $.mobile.changePage( "#home", "slidedown", true, true );
                      },
                      error: function() {
                          alert( "Cannot save new document." );
                      }
              });
              return false;
          });

          $("#addAlbum").bind( "pagehide", function() {
              $("input#addArtistField").val( "" );
              $("input#addTitleField").val( "" );
              $("textarea#addDescriptionField").val( "" );
          });
      }

      function refreshAlbums()
      {
          $("#albums").empty();
          $db.view("albums/albums", {
            success: function( data ) {
                    var i;
                    var album;
                    var artist;
                    var title;
                    var description;
                    var listItem;
                    var header;
                    var albumLink;
                    data.rows.reverse();
                    for( i in data.rows )
                    {
                        album = data.rows[i].value;
                        artist = album.artist;
                        title = album.title;
                        description = album.description;
                        externalPage = "_show/album/" + album._id;
                        listItem = "<li class=\"album\">" +
                                    "<a href=\"" + externalPage + "\">" +
                                        "<h2 class=\"artist\">" + artist + "<\/h2>" +
                                    "<\/a>" +
                                    "<p class=\"title\">" + title + "<\/p>" +
                                    "<p class=\"description\">" + description + "<\/p>" +
                                    "<\/li>";
                        $("#albums").append( listItem );
                    }
                    $("#albums").listview( "refresh" );
                    $.fixedToolbars.show();
                }
            });
      }
      $(document).ready( handleDocumentReady );

  </script>
</html>

Deployment

We modified our application to utilize a show function to serve the albumadd page up within a jquery Mobile application. With these changes saved, we can now push to the CouchDB database using the couchapp utility. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is the CouchApp application directory for these examples). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If all was successful and you now go to http://127.0.0.1:5984/albums/_design/albums/index.html, we’ll still have our old familiar list and our new navbar items in the footer. Click the Add Album button to open the form and save some information. The document should be saved to the CouchDB database and the application will direct you back to the #home page with the updated list of albums.

index.html#addAlbum

index.html#home

Conclusion

In this article, we added another important piece to working with documents from a database – Create. Along the way we uncovered a little about view maps from CouchDB and how they are sorted. We also found that the jquery.couch plugin handles create and update of documents through the same method – saveDoc() – on a database instance. As well, as it pertains to visible changes, we employed fixed toolbars, added custom footer content and discussed a little about the decision between using internal and external pages as it pertains to User Experience and application design. Hopefully, it all worked out.

Next up: we got the CRU… we need to the D.

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.


jQuery Mobile + CouchDB: Part 4 – Editing Documents

In the previous post, I addressed using templates to generate the jQuery Mobile external pages. Though it addressed a fundamental (imo) part of serving up HTML from the CouchDB instance and had a few nice tricks for jQuery Mobile, it essentially was organization and cleanup so I could move forward in developing the application without it getting to cluttered for my development and workflow.

In this article, I am going to take the templating structure established in the previous post and add another external page to the jQuery Mobile application – one in which I will be able to edit the document served up from the CouchDB database. Along with this, I’ll discuss assigning event handlers to DOM elements using jQuery, adding buttons to footers and headers, and clearing external jQuery Mobile pages from cache to ensure a user is looking at the most recent changes to a document.

Edit Template

In the previous post, we moved from returning HTML from CouchDB using a show function and string manipulation to moving mark-up over to a template and utilizing Mustache to render the served up jQuery Mobile external page. We are going to use the same technique to deliver another page that will allow a user to edit a target CouchDB document, with the option to either cancel or submit their changes.

Open up your favorite text editor, add the following snippet and save the file as albumedit.html in the /templates directory of your albums couchapp (eg. /Documents/workspace/custardbelly/couchdb/albums/templates/albumedit.html):

/templates/albumedit.html

<div data-role="page" id="albumedit" data-nobackbtn="true">
    <div data-role="header" id="albumheader">
        <a id="cancelBackButton" href="#" data-icon="delete">Close</a>
        <h1 class="albumtitle">{{title}}</h1>
    </div>
    <div data-role="content" data-theme="c" style="padding:0em;">
        <form id="albumform" action="#" method="get" data-identity="{{document}}">
            <div data-role="fieldcontain">
                <label for="artist">Artist:</label>
                <input id="artistField" name="artist" type="text" value="{{artist}}" />
            </div>
            <div data-role="fieldcontain">
                <label for="title">Title:</label>
                <input id="titleField" name="title" type="text" value="{{title}}" />
            </div>
            <div data-role="fieldcontain">
                <label for="description">Description:</label>
                <textarea id="descriptionField" name="description" cols="40" rows="8">{{description}}</textarea>
            </div>
            <div class="ui-body ui-body-b">
                <fieldset class="ui-grid-a">
                    <div class="ui-block-a">
                        <p id="cancelButton" data-role="button" data-theme="d">Cancel</p>
                    </div>
                    <div class="ui-block-b">
                        <p id="submitButton" data-role="button" data-theme="a">Submit</p>
                    </div>
                </fieldset>
            </div>
        </form>
    </div>
    <div data-role="footer" />
</div>
{{>scripts}}

That is some pretty hefty mark-up in relation to the relatively small (element count-wise) pages we have previously created. What should be familiar are the {{mustache}} tokens which will be dynamically filled with content using mustache.js in our soon-to-be created show function for the album edit page; but there are a few jQuery Mobile tidbits in here that we should take a little deeper of a look at.

Page & Header

/templates/albumedit.html

<div data-role="page" id="albumedit" data-nobackbtn="true">
    <div data-role="header" id="albumheader">
        <a id="cancelBackButton" href="#" data-icon="delete">Close</a>
        <h1 class="albumtitle">{{title}}</h1>
    </div>

A data-role of page is set as usual to declare a jQuery Mobile page and an id attribute is assigned for a reason that will be discussed shortly. What is of note in the page div is the data-nobackbtn attribute. If set the a value of true, this declaration will remove the default Back button from the header and allow us to customize the options. For this case, we have replaced the Back button with a Close button:

/templates/albumedit.html

<a id="cancelBackButton" href="#" data-icon="delete">Close</a>

Without specifying a style class, the Close button will be positioned to the left in the header. The href is set to an empty anchor. Essentially this is just a placeholder for an action defaulted to a link in the header. When we create the controller script for the album edit page later in this article, we will prevent the default behaviour and handle the close manually.

Content Form

jQuery Mobile has some great organization and styling when it comes to form elements. Positioning and aligning fields is all handled by the framework and, unless you want to get fancy, you basically just have to declare the desired element or assign the desired role/layout in order to achieve a fairly simple form. What we have done for our album edit page template is pretty basic:

/templates/albumedit.html

<form id="albumform" action="#" method="get" data-identity="{{document}}">
    <div data-role="fieldcontain">
        <label for="artist">Artist:</label>
        <input id="artistField" name="artist" type="text" value="{{artist}}" />
    </div>
    <div data-role="fieldcontain">
        <label for="title">Title:</label>
        <input id="titleField" name="title" type="text" value="{{title}}" />
    </div>
    <div data-role="fieldcontain">
        <label for="description">Description:</label>
        <textarea id="descriptionField" name="description" cols="40" rows="8">{{description}}</textarea>
    </div>
    <div class="ui-body ui-body-b">
        <fieldset class="ui-grid-a">
            <div class="ui-block-a">
                <p id="cancelButton" data-role="button" data-theme="d">Cancel</p>
            </div>
            <div class="ui-block-b">
                <p id="submitButton" data-role="button" data-theme="a">Submit</p>
            </div>
        </fieldset>
    </div>
</form>

The input fields are populated with the dynamic values from our show function (discussed later) and two buttons are presented to either Cancel or Submit any changes to those values. By marking each div with a data-role of fieldcontain, the label/input pair will be styled and aligned with each other div with a horizontal bar to delimit them vertically. The Cancel and Submit buttons are held in a fieldset with a grid layout assigned:

/templates/albumedit.html

<fieldset class="ui-grid-a">
    <div class="ui-block-a">
        <p id="cancelButton" data-role="button" data-theme="d">Cancel</p>
    </div>
    <div class="ui-block-b">
        <p id="submitButton" data-role="button" data-theme="a">Submit</p>
    </div>
</fieldset>

By assigning a ui-block value to each div in the fieldset, they will essentially be sized at 50% within the grid and positioned accordingly – Cancel to the left of Submit. You may notice that the “buttons” are not a link elements nor button elements. The reason being that both of those within a context of a form element were somehow being overridden to always perform a submit action. We will want to trap the user interaction with these elements in order to either a) reset the field values or b) submit our changes to the CouchDB database. So, they have been declared as p elements with a data-role of button so as to have the proper styling, but not the default action role of submittal of form without being captured and prevented first.

We will get into the submission of an edited document in a bit when we discuss the associated script with the album edit page, but until then take notice of the Partial inclusion in our albumedit template:

/templates/albumedit.html

{{>scripts}}

… and let’s dive into creating our show function for editable album pages.

Edit Show Function

If you have been following along with the previous post, you will remember that we removed the string-manipulated mark-up from the show function for the album view page and replaced it with Mustache templating. The show function we will create for the album edit page will largely look the same as the one created for the album view page. In fact, the only difference will be the target template and partial include.

Open up your favorite text editor, create a new file, add the following snippet and save the file as album-edit.js in the /shows directory of your albums couchapp (eg. /Documents/workspace/custardbelly/couchdb/albums/shows/album-edit.js):

/shows/album-edit.js

function(doc, req) {
    var Mustache = require("vendor/couchapp/lib/mustache");
    var stash = {
            artist: doc.artist,
            title : doc.title,
            description: doc.description,
            document: doc._id
    };
    return Mustache.to_html(this.templates.albumedit, stash, this.templates.partials.albumedit);
}

Again, if you have been following along with previous posts, this would look relatively familiar. Essentially, we are using the mustache.js template plugin to deliver HTML mark-up of the previously created albumedit.html jQuery Mobile page. The associated document is retrieved from the CouchDB database by passing the _id of the document in the request (eg. http://127.0.0.1:5984/albums/_design/albums/_show/
album-edit/db04eb7e5c845ee0aa791ae1ed001e4d
).

The previously created /templates/albumedit.html will be used as the template and is supplied as the first argument in Mustache.to_html(). The stash object (second argument) is used to dynamically filled the values of the fields upon load and the specified properties of the album document are available for editing: artist name, album title and personal description. The third argument is the Partial includes directory that will be substituted in the {{>}} declarations of the /templates/albumedit.html document. There is only one Partial defined in albumedit.html{{>scripts}} – which will essentially declare a JavaScript file with actions associated with the album edit page.

Scripts Partial

Open up your favorite text editor and save the following snippet as scripts.html in /templates/partials/albumedit:

/templates/partials/albumedit/scripts.html

<script src="../../script/album-edit-page.js"></script>

If you have followed along with create the show function, template and partial for the album view page, this again will look similar. We are essentially loading an associated JavaScript file for the album edit page. As explained in the previous post, the script could be included here and not redirected to a js file, but for my own organizational habits I create a separate JavaScript file and have it residing in the /_attachments/script/ directory of my albums couchapp (eg. /Documents/workspace/custardbelly/couchdb/albums/
_attachments/script/album-edit-page.js
).

album-edit-page.js

If we think about the role of our album edit page in the scheme of the application, we want to present the user with the ability to edit the album document from the CouchDB albums database. The form will initially be filled with the latest editable values and the user has the ability to either submit changes or cancel any pending changes. A Cancel action will return the fields back to the original values and return the user to the album view page. To ensure that the user is presented with the latest changes to the document from the database, we will also need to clear the page from the page cache just as we have done with the album view page. With these requirements in mind, let’s start creating the associated script for the album edit jQuery Mobile page served up from the album-edit show function.

AlbumEditPageController

We’ll start by creating a quasi-view-controller for the album edit page in the similar fashion as the one used for the album view page (created in the previous post). Initially, it will handle recognizing once the jQuery Mobile page div is shown and remove it from the page cache once it is hidden (by navigating away from the page).

Open your favorite text editor and save the following snippet as album-edit-page.js in the /_attachments/script/ directory:

/_attachments/script/album-edit-page.js

var AlbumEditPageController = function() {

    function handleEditPageViewHide()
    {
        var docId = $("#albumform").data("identity");
        var pageCache =  $(document.getElementById("_show/album-edit/" + docId));
        pageCache.unbind( "pagehide", handleEditPageViewHide );
        pageCache.empty();
        pageCache.remove();
    }

    function handleEditView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumform").data("identity");
        var albumPage = $(document.getElementById("_show/album-edit/" + docId));
        albumPage.bind( "pagehide", handleEditPageViewHide );
    }

    return {
        initialize: function() {
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleEditView();
            });
        }
    };
}();

function handleEditPageReady()
{
    AlbumEditPageController.initialize();
}
$().ready( handleEditPageReady )

Once the page is loaded, the AlbumEditPageController is initialized an a pageshow event handler is assigned to recognize when the albumedit jQuery Mobile page is shown. We access the page in the DOM using the standard data-role attribute value for a jQuery Mobile page. I stumbled upon this solution from these two forum posts – http://forum.jquery.com/topic/force-page-update and http://forum.jquery.com/topic/binding-events-to-buttons-in-a-dialog – and seem to be the current agreed upon solution for accessing a loading jQuery Mobile page. In the handleEditView() handler, the page in the DOM is accessed using document.getElementById() and assigned an event handler for pagehide to clear it from the page (ie. DOM) cache. Aside from the external page id used to access the element, this is pretty much identical to the solution from the previous post.

One of the requirements for the album edit page is the ability to cancel and clear any edited fields by the user. In order to do so, we will stored a local object with the provided name/value pairs so the fields can be easily reverted.

With album-edit-page.js open in your favorite text editor, make the following modifications and save the file:

/_attachments/script/album-edit-page.js

var AlbumEditPageController = function() {

    var editableAlbum;

    function handleEditPageViewHide()
    {
        editableAlbum = null;

        var docId = $("#albumform").data("identity");
        var pageCache =  $(document.getElementById("_show/album-edit/" + docId));
        pageCache.unbind( "pagehide", handleEditPageViewHide );
        pageCache.empty();
        pageCache.remove();
    }

    function handleEditView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumform").data("identity");
        var albumPage = $(document.getElementById("_show/album-edit/" + docId));
        albumPage.bind( "pagehide", handleEditPageViewHide );

        storeUneditedDocument();
    }

    function storeUneditedDocument()
    {
        var artist = $("input#artistField").val();
        var album = $("input#titleField").val();
        var description = $("textarea#descriptionField").val();
        editableAlbum = {artist:artist, album:album, description:description};
    }

    return {
        initialize: function() {
           $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleEditView();
            });
        }
    };
}();

function handleEditPageReady()
{
    AlbumEditPageController.initialize();
}
$().ready( handleEditPageReady )

An editableAlbum member is declared and updated upon pageshow in the storeUneditedDocument() method. With the original values stored in this externally-immutable we can ensure that whenever a user decides to cancel the editing of the album document we can revert the field values to the original document values. Let’s hook up the cancelBackButton and cancelButton elements declared in the albumedit.html template with click event handlers in order to handle the revert action back to the original values.

Save the following modifications to album-edit-page.js:

/_attachments/script/album-edit-page.js

var AlbumEditPageController = function() {

    var editableAlbum;

    function handleEditPageViewHide()
    {
        $("#cancelButton").die( "click", handleCancelEdit );
        $("#cancelBackButton").die( "click" );
        editableAlbum = null;

        var docId = $("#albumform").data("identity");
        var pageCache =  $(document.getElementById("_show/album-edit/" + docId));
        pageCache.unbind( "pagehide", handleEditPageViewHide );
        pageCache.empty();
        pageCache.remove();
    }

    function handleEditView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumform").data("identity");
        var albumPage = $(document.getElementById("_show/album-edit/" + docId));
        albumPage.bind( "pagehide", handleEditPageViewHide );

        storeUneditedDocument();
    }

    function storeUneditedDocument()
    {
        var artist = $("input#artistField").val();
        var album = $("input#titleField").val();
        var description = $("textarea#descriptionField").val();
        editableAlbum = {artist:artist, album:album, description:description};
    }

    function revertEdits()
    {
        $("input#artistField").val( editableAlbum.artist );
        $("input#titleField").val( editableAlbum.album );
        $("textarea#descriptionField").val( editableAlbum.description );
    }

    function handleCancelEdit()
    {
        revertEdits();
    }

    return {
        initialize: function() {
            $("#cancelButton").live( "click", handleCancelEdit );
            $("#cancelBackButton").live( "click", function( event ) {
                event.preventDefault();
                handleCancelEdit();
                return false;
            });
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleEditView();
            });
        }
    };
}();

function handleEditPageReady()
{
    AlbumEditPageController.initialize();
}
$().ready( handleEditPageReady )

In this snippet, we have wired the two cancelable button elements up to invoke the handleCancelEdit() method upon a click event. All revertEdits() does is reset the field values based on the stored values in editableAlbum. We also take care to kill those event listeners when the page is hidden. Now that we have one requirement fulfilled (cancel-ability) for the album edit page, let’s move on to original intent of the page – submitting document changes.

Submit

To submit changes to the target album document form the album edit page we will use the database API provided in the jquery.couchdb library plugin. That library is automatically loaded from the loader script created by couchapp if remember back to the first post where we accessed the list of available album documents from the albums database in our CouchDB instance. We resolved the $db member in the index.html to the albums database when the page is loaded. When submitting from the album edit page, we will just use that instance to load, modify and save the target document back to the database.

Make the following modifications to album-edit-page.js and save:

/_attachments/script/album-edit-page.js

var AlbumEditPageController = function() {

    var editableAlbum;

    function handleEditPageViewHide()
    {
        $("#cancelButton").die( "click", handleCancelEdit );
        $("#cancelBackButton").die( "click" );
        $("#submitButton").die( "click" );
        editableAlbum = null;

        var docId = $("#albumform").data("identity");
        var pageCache =  $(document.getElementById("_show/album-edit/" + docId));
        pageCache.unbind( "pagehide", handleEditPageViewHide );
        pageCache.empty();
        pageCache.remove();
    }

    function handleEditView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumform").data("identity");
        var albumPage = $(document.getElementById("_show/album-edit/" + docId));
        albumPage.bind( "pagehide", handleEditPageViewHide );

        storeUneditedDocument();
    }

    function storeUneditedDocument()
    {
        var artist = $("input#artistField").val();
        var album = $("input#titleField").val();
        var description = $("textarea#descriptionField").val();
        editableAlbum = {artist:artist, album:album, description:description};
    }

    function saveDocument( document )
    {
        $db.saveDoc( document, {
            success: function( response )  {
                updateEditableAlbum( document );
            },
            error: function() {
                alert( "Cannot save document: " + document._id );
            }
        });
    }

    function updateEditableAlbum( document )
    {
        editableAlbum.artist = document.artist;
        editableAlbum.album = document.album;
        editableAlbum.description = document.description;
    }

    function revertEdits()
    {
        $("input#artistField").val( editableAlbum.artist );
        $("input#titleField").val( editableAlbum.album );
        $("textarea#descriptionField").val( editableAlbum.description );
    }

    function handleCancelEdit()
    {
        revertEdits();
    }

    return {
        initialize: function() {
            $("#cancelButton").live( "click", handleCancelEdit );
            $("#cancelBackButton").live( "click", function( event ) {
                event.preventDefault();
                handleCancelEdit();
                return false;
            });
            $("#submitButton").live( "click", function( event ) {
                var docId = $("#albumform").data("identity");
                $db.openDoc( docId, {
                    success: function( document ) {
                        document.artist = $("input#artistField").val();
                        document.album = $("input#titleField").val();
                        document.description = $("textarea#descriptionField").val();
                        saveDocument( document );
                    },
                    error: function() {
                        alert( "Cannot open document: " + docId );
                    }
                });
            });
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleEditView();
            });
        }
    };
}();

function handleEditPageReady()
{
    AlbumEditPageController.initialize();
}
$().ready( handleEditPageReady )

A click event handler is assigned to the submitButton element, from which the document is loaded and assigned the associated field values. Upon update to the new values, $db.saveDoc() is invoked with a success handler that updates the privately held editableAlbum object. This ensures that the values are properly restored back to any saved changes if the user decides to cancel at any time after the successful save.
Almost there. We will just finish off our controller by navigating the user to the album view page upon Cancel or Submit.

The following is the full album-edit-page.js file with the last modifications highlighted. Save the following changes to album-edit-page.js:

/_attachments/script/album-edit-page.js

var AlbumEditPageController = function() {

    var editableAlbum;

    function handleEditPageViewHide()
    {
        $("#cancelButton").die( "click", handleCancelEdit );
        $("#cancelBackButton").die( "click" );
        $("#submitButton").die( "click" );
        editableAlbum = null;

        var docId = $("#albumform").data("identity");
        var pageCache =  $(document.getElementById("_show/album-edit/" + docId));
        pageCache.unbind( "pagehide", handleEditPageViewHide );
        pageCache.empty();
        pageCache.remove();
    }

    function handleEditView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumform").data("identity");
        var albumPage = $(document.getElementById("_show/album-edit/" + docId));
        albumPage.bind( "pagehide", handleEditPageViewHide );

        storeUneditedDocument();
    }

    function navigateToAlbumPage( docId )
    {
        $.mobile.changePage( "_show/album/" + docId, "slide", true, true );
    }

    function storeUneditedDocument()
    {
        var artist = $("input#artistField").val();
        var album = $("input#titleField").val();
        var description = $("textarea#descriptionField").val();
        editableAlbum = {artist:artist, album:album, description:description};
    }

    function saveDocument( document )
    {
        $db.saveDoc( document, {
            success: function( response )  {
                updateEditableAlbum( document );
                navigateToAlbumPage( document._id );
            },
            error: function() {
                alert( "Cannot save document: " + document._id );
            }
        });
    }

    function updateEditableAlbum( document )
    {
        editableAlbum.artist = document.artist;
        editableAlbum.album = document.album;
        editableAlbum.description = document.description;
    }

    function revertEdits()
    {
        $("input#artistField").val( editableAlbum.artist );
        $("input#titleField").val( editableAlbum.album );
        $("textarea#descriptionField").val( editableAlbum.description );
    }

    function handleCancelEdit()
    {
        revertEdits();
        var docId = $("#albumform").data("identity");
        navigateToAlbumPage( docId );
    }

    return {
        initialize: function() {
            $("#cancelButton").live( "click", handleCancelEdit );
            $("#cancelBackButton").live( "click", function( event ) {
                event.preventDefault();
                handleCancelEdit();
                return false;
            });
            $("#submitButton").live( "click", function( event ) {
                var docId = $("#albumform").data("identity");
                $db.openDoc( docId, {
                    success: function( document ) {
                        document.artist = $("input#artistField").val();
                        document.album = $("input#titleField").val();
                        document.description = $("textarea#descriptionField").val();
                        saveDocument( document );
                    },
                    error: function() {
                        alert( "Cannot open document: " + docId );
                    }
                });
            });
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleEditView();
            });
        }
    };
}();

function handleEditPageReady()
{
    AlbumEditPageController.initialize();
}
$().ready( handleEditPageReady )

In navigateToAlbumPage() we use $.mobile.changePage() to navigate the user to the external album view page using the show function of our CouchDB application based on the _id of the target document that is loaded in the album edit page.

That should be it… aside from one thing. Although (once we push our changes) we can access the page using the #_show/album-edit/${id} location, as the application currently stands, there is no way to access the album edit page from any other page in the application. To remedy that, we will make some modifications to the album.html template and the album-page.js script to navigate the user to the album edit page.

Accessing the Album Edit Page

I am going to assume that you have been following along with previous posts here and that you already have the /templates/album.html and /_attachments/script/album-page.js files. We are going to make modifications to them to include an edit button in the album view page and assign a click handler to navigate to the album edit page.

album.html

Open up the /templates/album.html file in your favorite text editor and save the following modifications:

/templates/album.html

<div data-role="page" id="albumview" data-position="inline" data-back="true">
    <div data-role="header" id="albumheader">
        <h1 class="albumtitle">{{title}}</h1>
        <a href="#home" data-icon="grid" class="ui-btn-right">Home</a>
    </div>
    <div data-role="content" id="albumcontent" data-identity="{{document}}">
      <h2 class="artist">{{artist}}</h2>
      <p class="title">{{title}}</p>
      <p class="description">{{description}}</p>
      <p id="editbutton" data-role="button" data-theme="a">Edit</p>
    </div>
    <div data-role="footer" />
</div>
{{>scripts}}

The only modification to the album.html template e have made is the inclusion of an Edit button which is a p element attributed with the data-role of button. The jQuery Mobile framework will handle styling the Edit button to have consistency in look-and-feel with the other buttons throughout the application. Now let’s hook up a click event listener for that button element to take the user to the album edit page.

album-page.js

Open up /_attachments/script/album-page.js in your favorite text editor and save the following modifications:

/_attachments/script/album-page.js

var AlbumPageController = function() {

    function handleView()
    {
        $("#editbutton").live( "click", handleEdit );

        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumcontent").data("identity");
        var albumPage = $(document.getElementById("_show/album/" + docId));
        albumPage.bind( "pagehide", handlePageViewHide );
    }

    function handleEdit( event )
    {
        // Prevent default link event.
        event.preventDefault();
        // Access document id from data-identity.
        var docId = $("#albumcontent").data("identity");
        // Change page.
        $.mobile.changePage( "_show/album-edit/" + docId, "slide", false, true );
        return false;
    }

    function handlePageViewHide()
    {
        $("#editbutton").die( "click", handleEdit );

        var docId = $("#albumcontent").data("identity");
        var albumPageCache =  $(document.getElementById("_show/album/" + docId));
        albumPageCache.unbind( "pagehide", handlePageViewHide );
        albumPageCache.empty();
        albumPageCache.remove();
    }

    return {
        initialize : function() {
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleView();
            });
        }
    };
}();

function handlePageViewReady()
{
    AlbumPageController.initialize();
}
$().ready( handlePageViewReady );

The handleEdit() handler navigates to the album edit page using the $.mobile.changePage() method pointing to the page created by the show function using the target document id that is used to show the album view page.

Now that we have implemented a way to navigate to the album edit page from the application through the album view page, we can deploy our changes to the CouchDB instance for our albums application.

Deployment

Our Albums application has been modified to allow a user to modify properties of an album document from an album edit page. That page can be accessed by clicking an Edit button from the album view page accessible from a selection of an album from the list presented on the landing index.html. With these modifications saved, we can now push to the CouchDB database using the couchapp utility. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is the CouchApp application directory for these examples). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If all was successful and you now go to http://127.0.0.1:5984/albums/_design/albums/index.html, click on an album from the list and select to edit that document. From the album edit page you are able to either cancel any edits or submit the changes to the target album document. What a thing of beauty :) Let’s take a look at the current state of the application:

index.html (aka #home):
index.html

album view page:
album view page

album edit page:
album edit page

Conclusion

This post got a lot longer than I had anticipated… which just goes to show what a wind-bag i am :) All joking aside, I think we covered some good ground, especially about saving modifications to a document back to the CouchDB database. We also covered, again, how to monitor pages so as to remove them from cache and ensure that the user is always presented with the latest document information. We also delved into assigning event listeners to button elements. All good fun… well, hopefully, it was to you.

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.


jQuery Mobile + CouchDB: Part 3 – Templates and Mustache.js

In the previous post, I covered displaying a document from a CouchDB database in the context of a jQuery Mobile page. A difference between local vs external jQuery Mobile pages was discussed, and it was concluded that external pages was beneficial for the client-side application. In finding this happy medium, I also talked about show functions in CouchDB and how they serve up documents upon request.

In this article, I am going to cover another aspect of delivering HTML content from CouchDBtemplates. While doing so, I will modify the current document views to provide a cleaner solution (imo) for delivering and rendering document-based jQuery Mobile pages. Hopefully, this will set the basis for creating additional views for the application…

Templates

In the previous post, I covered using show functions to deliver HTML content as a jQuery Mobile page from a CouchDB instance. From the previous show function example for the Albums application, I basically constructed a string that would be interpreted as HTML mark-up using the values from the document. While this is a perfectly valid solution, its probably not the best if we want to reuse parts of that HTML or if the mark-up gets a little too unwieldy for string manipulation based on the request. To provide a little sanity as we start making more page documents for the application, we going to start using templates to stub out the mark-up of an HTML document and fill content dynamically from the show functions.

mustache.js

If you have been following along in the tutorials, you may remember that in the loader.js script there were a few JavaScript files being loaded that were not being referenced within the current application. One of those was mustache.js. I didn’t talk about it at the time and I said we could probably get rid of it for now, but might as well leave it in… hopefully you left it in. If not, you’re screwed. Kidding :)

I don’t want to go into a mustache explanation as there are already some great articles out there – especially this one from CouchOne – but I will say that mustache.js is a templating engine for rendering any type of content. For our purposes, we are going to use the to_html() function of Mustache to marry our dynamic album document values with an HTML template and deliver pages from our respective show functions from the CouchDB database.

Album Page Template

Our previous version of album.js from the /show directory of our Albums couchapp looked like the following:

/show/album.js

function(doc, req) {
  var html = "<div data-role=\"page\" id=\"albumview\">" +
                       "<div data-role=\"header\" id=\"albumheader\">" +
                           "<h2 class=\"albumtitle\">" + doc.title + "<\/h2>" +
                       "<\/div>" +
                       "<div data-role=\"content\" id=\"albumcontent\">" +
                           "<h2 class=\"artist\">" + doc.artist + "<\/h2>" +
                           "<p class=\"title\">" + doc.title + "<\/p>" +
                           "<p class=\"description\">" + doc.description + "<\/p>" +
                       "<\/div>" +
                       "<div data-role=\"footer\" \/>" +
                   "<\/div>";
  return html;
}

That served our purpose at the time, but such string manipulation for each page we are going to serve up makes my eyes bleed :) As such, we will use mustache.js to populate document values in an HTML template. First order of business: Remove that string construct from /shows/album.js and put it into a template, replacing the values with {{mustache}} directives.

Create a new directory called templates in your couchapp directory for the Albums application (for me that is at /Documents/workspace/custardbelly/couchdb/albums). Create a new HTML document in your favorite text editor and save the following as album.html in the /templates directory:

/templates/album.html

<div data-role="page" id="albumview" data-position="inline" data-back="true">
    <div data-role="header" id="albumheader">
        <h1 class="albumtitle">{{title}}</h1>
        <a href="#home" data-icon="grid" class="ui-btn-right">Home</a>
    </div>
    <div data-role="content" id="albumcontent" data-identity="{{document}}">
        <h2 class="artist">{{artist}}</h2>
        <p class="title">{{title}}</p>
        <p class="description">{{description}}</p>
    </div>
    <div data-role="footer" />
</div>

Essentially, we have just moved our mark-up from one document to another; the difference being that we are now employing {{mustache}}s as placeholders for dynamic content values. You may notice that the names used in the mustaches are just the property names of the document – similar to the previous albums.js example. In fact there are the property names of the object that will be passed to the Mustache engine to generate our page.

Some added jQuery Mobile element content has been added to the new album page document, as well. We added the data-position and data-back attributes to the page div in order to preserve the back button and add a Home page button to the header. This will allow us to always be able to get back to the index.html Home page from an album page.

<a href="#home" data-icon="grid" class="ui-btn-right">Home</a>

We will need to modify the page div in index.html to have an id value of “home”, but setting that hash as the href for the Home button in the header of our album page will, essentially, enable that link to direct the user back to the page content of the parenting document -index.html.

In album.html we have also declared a data-identity for the content div and provided a {{document}} value. This will be the document._id from the album.js show function and we will later see how it will be used. Back to our album.js document…

Album Page Show Function

Open up the /show/album.js document in your favorite text editor and make replace the current content with the following:

/show/album.js

function(doc, req) {
    var Mustache = require("vendor/couchapp/lib/mustache");
    var stash = {
        artist: doc.artist,
        title : doc.title,
        description: doc.description,
        document: doc._id
    };
    return Mustache.to_html(this.templates.album, stash);
}

As mentioned previously, CouchApp includes the Mustache JavaScript file automatically when generating a new couchapp application. In this example we first assign a reference to the Mustache module through a require() call for the file in the vendor/couchapp/lib directory. A generic stash object is created to assign property values that are dynamically populated into the album.html template (previous snippet) using the Mustache:to_html method.

I am not entirely clear on how it is possible to reference files and directories with the this keyword in a show function, but i can only assume that couchapp has generated an object tree based on the file structure from which one can access values. If you do know, please leave a comment. In any event, the template file and the defining object are the first two arguments for Mustache:to_html. Upon request for an album document, this will return the album.html page dynamically populated with the values for the document related to the id in the request URL (eg. http://127.0.0.1:5984/albums/_design/albums/index.html#_show
/album/db04eb7e5c845ee0aa791ae1ed000fe8
).

Hiccup

There may be a slight problem in this solution however…. Though CouchDB does request-cacheing based on header ETags on documents, jQuery Mobile also does cacheing of pages (or rather it accesses ‘pages’ already loaded into the DOM). So there may be a time that an album has been edited during the application session and jQuery Mobile serves up old information as it never went back out the make a GET request to CouchDB, utilizing this show function. We could make a request for the document each time the page is shown in the DOM using the jquery.couch library, but that may be unnecessary overhead; not to mention a waste of the templating engine.

Another solution, and one I think is viable and worth consideration, is to remove the page from the jquery Mobile page cache when this page is hidden in the view (when you navigate to another page). In order to do so, we will need to access the cached page in the DOM using JavaScript. Instead of adding this JavaScript directly in the HTML template file, we will use another templating feature available in MustachePartials.

Partials

Partials are, essentially, just a way to include common mark-up or code or what have you into the target template page. It’s important to remember that partials are rendered in first so any {{mustache}}s in the partial file will also be filled. For the purposes of our example application, we are just going to include mark-up to load a script. It may be a little overkill at the moment, but I wanted to show the use of partials as they can be very handy.

Open up your favorite text editor and save the following snippet as scripts.html in /templates/partials/album.

/templates/partials/album/scripts.html

<script src="../../script/album-page.js"></script>

[02-06-2010: Thank you to Steve and Stacy Braxton for leaving a comment noting that the filepath to scripts.html for the album page was incorrectly shown in italics. The correct filepath is now displayed. Thanks!]

Real boring stuff and not using the full breath of what Partials can do. Basically we are just loading in an associated JavaScript file with our page from the show function. Great. Now we have to create another file. I know, I know. It may appear convoluted, but in my head it is much cleaner and organized. The script we are about to create, could be included in this partial or in the template, but i prefer to work with another separate JavaScript file whose purpose I know based on its extension – its got script in it.

album-page.js

You may have looked at the path in the src attribute value from the Partial we just created and noticed that it is a relative path that goes out a couple directories. That is going back to the _attachments directory to access the album-page.js file from the script folder. The _attachments directory also houses the style folder in which we added the jQuery Mobile styles in the first post.

We’re going to create an add the album-page.js file to the /_attachments/script directory and it will provide the ability to access and clear the page from the page cache of jQuery Mobile so as to always serve up a the page from CouchDB (based on its own cacheing mechanism) using the show function. Open up your favorite text editor and save the following snippet as album-page.js in /_attachments/script:

/_attachments/script/album-page.js

var AlbumPageController = function() {

    function handleView()
    {
        // Watch for bound hide of page to clear from cache.
        var docId = $("#albumcontent").data("identity");
        var albumPage = $(document.getElementById("_show/album/" + docId));
        albumPage.bind( "pagehide", handlePageViewHide );
    }

    function handlePageViewHide()
    {
        var docId = $("#albumcontent").data("identity");
        var albumPageCache =  $(document.getElementById("_show/album/" + docId));
        albumPageCache.unbind( "pagehide", handlePageViewHide );
        albumPageCache.empty();
        albumPageCache.remove();
    }

    return {
        initialize : function() {
            $("div[data-role='page']").live( "pageshow", function() {
                $("div[data-role='page']").die( "pageshow" );
                handleView();
            });
        }
    };
}();

function handlePageViewReady()
{
    AlbumPageController.initialize();
}
$().ready( handlePageViewReady );

In essence, we are just creating a view controller for our jQuery Mobile page served up from the show function. Once the page is recognized as part of the DOM, the controller is initialized and an event handler is assigned to the pageshow event for the current page – the album.html served up. We access the page in the DOM using the standard data-role attribute value for a jQuery Mobile page. I stumbled upon this solution from these two forum posts – http://forum.jquery.com/topic/force-page-update and http://forum.jquery.com/topic/binding-events-to-buttons-in-a-dialog – and seem to be the current agreed upon solution for accessing a loading jQuery Mobile page.

Now, it should be noted that setting that handler will actually set a handler on all divs with the data-role attributed as “page”. But since we are only concerned with the pageshow event, we know that the current target is the album page. Once that event is captured, we remove the handler and assign a pagehide event handler to clear the page from the jQuery Mobile (ie. DOM) cache and remove the elements from the DOM.

Since the album page is an external page (defined in the script in index.html when building the list items), its registered in the DOM and accessed using the _show path based on the document ID. So, when the page is fully loaded we access that page recognized in the DOM using the getElementById() method:

var docId = $("#albumcontent").data("identity");
var albumPage = $(document.getElementById("_show/album/" + docId));

Accessing the page as such, we can empty all its elements and remove it once the page has been fully hidden from the view in the page transition of jQuery Mobile:

 var docId = $("#albumcontent").data("identity");
 var albumPageCache =  $(document.getElementById("_show/album/" + docId));
 albumPageCache.unbind( "pagehide", handlePageViewHide );
 albumPageCache.empty();
 albumPageCache.remove();

That essentially will make a request each time to CouchDB for the album document page and not rely on the cacheing of pages within jQuery Mobile. This will also open up the ability to assign and manage handlers for other events, such a requesting pages for editing a document… but we’ll broach that subject in another post ;) For now, let’s go back and update our album.js and album.html documents to include the partial.

[01-28-2010: Thank you to IR for leaving a comment alerting me to the fact that i totally missed out on updating the neccessary files after creating the partial]

Modifying album.js and album.html

Open up /shows/album.js in your favorite text editor and make the following modification to pass the partial directory in Mustache.to_html():

/shows/album.js

function(doc, req) {
    var Mustache = require("vendor/couchapp/lib/mustache");
    var stash = {
        artist: doc.artist,
        title : doc.title,
        description: doc.description,
        document: doc._id
    };
    return Mustache.to_html(this.templates.album, stash, this.templates.partials.album);
}

That last argument in to_html() will allow you to stub out the documents included in the /album folder of the partials directory in the album.html template. We’ll need to update that document as well for our scripts partial to be included.

Open up /templates/album.html and make the following modification:

/templates/album.html

<div data-role="page" id="albumview" data-position="inline" data-back="true">
    <div data-role="header" id="albumheader">
        <h1 class="albumtitle">{{title}}</h1>
        <a href="#home" data-icon="grid" class="ui-btn-right">Home</a>
    </div>
    <div data-role="content" id="albumcontent" data-identity="{{document}}">
        <h2 class="artist">{{artist}}</h2>
        <p class="title">{{title}}</p>
        <p class="description">{{description}}</p>
    </div>
    <div data-role="footer" />
</div>
{{>scripts}}

A slight modification to the syntax on how you would include object property values, the partial is included by appending the name of the partial document to include with a greater than character. That will essentially write the content of the document inline in the template where it is declared.

Now we just need to make a small modification to the index.html file and then deploy our modified application to the CouchDB instance so we can view our wonderful changes (which won’t look like we changed a thing).

Modifying index.html

We added a Home button to the header bar of the album page so we could always navigate back to the Home page. Currently the album page is accessible from the album list on the Home page, but who knows, maybe that won’t always be the case in the future. In any event, we made the change and assigned the href value for the Home page button with the #home anchor. Currently that will go nowhere. We’ll set that as the div id on the main jQuery Mobile page in index.html.

Open up the /_attachments/index.html file in your favorite text editor and make the following modifications the #home page div:

/_attachments/index.html

<div id="home" data-role="page">
    <div data-role="header"><h1>Albums</h1></div>
    <div data-role="content">
        <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b"></ul>
    </div>
    <div data-role="footer" class="ui-bar"><h4>a list of albums</h4></div>
</div>

[02-05-2010: Thank you to Otetz for leaving a comment about the missing call to refresh the list prior to shoe on the index.html file]
With index.html still open in your favorite text editor, add the following line to the handleDocumentReady() method in the inline JavaScript:

function handleDocumentReady()
{
    $("#home").bind( "pagebeforeshow", refreshAlbums );
    refreshAlbums();

Save that, and now we can add a Home button to any page and will always be directed back to that div with the list of albums from our CouchDB database. Cool. Time to deploy.

Deployment

We modified our show function to use templates and created a script to empty the page and its elements from the DOM upon hide within the jQuery Mobile context and are assured that each access of an album document will contain the latest changes. With these modifications saved, we can now push to the CouchDB database using the couchapp utility. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is the CouchApp application directory for these examples). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If all was successful and you now go to http://127.0.0.1:5984/albums/_design/albums/index.html, we’ll still have our old familiar list and still access each album by clicking on a list item. Basically it performs exactly as it had before, but we cleaned up a little on our end… a benefit to us as the developer and a benefit to the user though not visually noticeable… it still looks rather ugly :)

Conclusion

We delved a little deeper into how to serve up pages from CouchDB using templates and partials (thanks to Mustache!) and also touched on accessing pages from DOM cache in the context of jQuery Mobile framework. All nice stuff, but nothing really has changed in how the application worked for the end user. We ensured that album pages always had the correct and latest content, but we haven’t opened up the possibility to edit the document and commit changes. That’s to come :) Just wanted to lay the groundwork for easing into adding more pages to our application. Hopefully you found some of this useful.

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.


jQuery Mobile + CouchDB: Part 2 – Document View

In the previous post for this series, I covered getting up and running with jQuery Mobile as a client-side application for a CouchDB database. Along with examples for the DHTML documents, I also covered/talked about some great tools that help along with the workflow of creating a client-side application and working with a CouchDB instance. I am going to continue using those tools – mainly CouchApp – and cover the differences between local and external jQuery Mobile pages and how to show a single document from the albums list. If you have not already, I recommend checking out the first part in this series: jQuery Mobile + CouchDB: Part 1 – Getting Started. Some tools and directory structures are discussed in that post that will not be explained in this post.

Introduction

Looking at the current state of the client-side application created for communicating with a CouchDB instance, the landing page loads all the documents from a target database and displays them in a jQuery Mobile list.

The database for the application holds album documents and a simple view was created that would return every document in the database. Currently the URL for viewing that list of documents from the albums view is http://127.0.0.1:5984/albums/_design/albums/index.html.

Now i can peruse the list just fine, but what if the list items do not show all the fields pertaining to a document? (* they do now, but that is the beauty of CouchDB… the album document schema can change without having me curl up and start crying under my desk!) Good question, me. The application should direct the user to a detail page which shows a single document upon selection from an item in the list. Nice answer, you.

In this post, I will be covering how to create and navigate to a detail page for a single document from the list for our jQuery Mobile + CouchDB application. I’ll hope to address the benefits of local and external pages in the jQuery Mobile framework and how to go about displaying pages using the show functions available in the design for a CouchDB database.

Decisions, Decisions

A jQuery Mobile application is comprised of pages, loosely discussed in the previous post. The framework allows for a developer to declare the content of each page inline in the same document (local, internally linked) or point to another HTML document (external) by providing a link as the content of a page. For some quick examples:

Local:

index.html

...
<body>
    <div id="home" data-role="page">
        <div data-role="header">Home</div>
        <div data-role="content">
            <p>Hello World!</p>
            <p><a href="#signin">Sign in?</a>
        </div>
        <div data-role="footer">welcome</div>
    </div>
    <div id="signin" data-role="page">
        <div data-role="header">Sign up</div>
        <div data-role="content">
            <p><a href="#home">Nevermind</a></p>
        </div>
        <div data-role="footer">come in</div>
    </div>
</body>
...

In this example, two pages are declared (home and signin), and each have a link in their content that points to the other. By setting the href to the value of the id attribute of a div element with a data-role of page, preceded by a hash (#), the framework knows to display the content of that page upon request – updating the location and history as well.

External:

index.html

...
<body>
    <div id="home" data-role="page">
        <div data-role="header">Home</div>
        <div data-role="content">
            <p>Hello World!</p>
            <p><a href="pages/signin.html">Sign in?</a>
        </div>
        <div data-role="footer">welcome</div>
    </div>
</body>
...

/pages/signin.html

<body>
    <div id="signin" data-role="page">
        <div data-role="header">Sign up</div>
        <div data-role="content">
            <p><a href="../index.html">Nevermind</a></p>
        </div>
        <div data-role="footer">come in</div>
    </div>
</body>

[* NOTE :: If you try to develop an application using externally linked pages on your local disk, you will get an error that looks somewhat like the following:

XMLHttpRequest cannot load file:///Users/{user}/{location}/index.html. Origin null is not allowed by Access-Control-Allow-Origin.

The reason is that file:// requests produce a null Origin. Someone more familiar with jquery Mobile could hopefully give a fuller explanation, but a null Origin throws an error on transitioning pages. When developing i deploy the client to my localhost for testing.*]

In this example, the landing page has a link that points to an external HTML document (signin.html). With a div element attributed as a page in the external HTML document, the location is updated to /index.html#pages/signup.html.

Both approaches have their advantages and disadvantages. The biggest advantage for internal pages is transition speed, while the biggest disadvantage for internal pages is readability; and those are switched for external pages. At least those are what i perceive as being the selling points for internal vs external, aside from the URLs…

I would choose external most of the time, mainly because it adds the ability to view a page without being in the flow of a whole page set in a single document. Sure, the URL is way uglier (ie index.html#pages/signup.html as opposed to index.html#signup) but hopefully i could then bug someone smarter than me to pretty up the URLs with some rewrites.

So, in conclusion, I lean toward external pages… but i will offer up both solutions so you can see how each would work.

Local Album Page View

First things first, fire up your favorite text editor and open the /_attachments/index.html page from your CouchApp directory for the albums application. We are going to add another page div to the document and display the album detail locally:

<!DOCTYPE html>
<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
        <div data-role="header"><h2>Albums</h2></div>
        <div data-role="content">
            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
        </div>
        <div data-role="footer">Footer</div>
    </div>
    <div data-role="page" id="albumview">
        <div data-role="header" id="albumheader">
            <h2 class="albumtitle"></h2>
        </div>
        <div data-role="content" id="albumcontent"></div>
        <div data-role="footer" />
    </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">
      // forget about this for now.
  </script>
</html>

In this example, we have added another page and given it the id attribute value of “albumview“. When an item from the albums list is clicked, we’ll notify the framework to navigate to that page and display the corresponding content. The JavaScript was removed for this example just to clear away the clutter from what the intention was in making a new page. Just so we know we are working with the same content, here is the full updated document with inline script:

<!DOCTYPE html>
<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
        <div data-role="header"><h2>Albums</h2></div>
        <div data-role="content">
            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
        </div>
        <div data-role="footer">Footer</div>
    </div>
    <div data-role="page" id="albumview">
        <div data-role="header" id="albumheader">
            <h2 class="albumtitle"></h2>
        </div>
        <div data-role="content" id="albumcontent"></div>
        <div data-role="footer" />
    </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

      $db = $.couch.db("albums");

      function handleDocumentReady()
      {
          refreshAlbums();
      }

      function refreshAlbums()
      {
          $("#albums").empty();
          $db.view("albums/albums",
            { success: function( data ) {
                    var i;
                    var album;
                    var artist;
                    var title;
                    var description;
                    var listItem;
                    for( i in data.rows )
                    {
                        album = data.rows[i].value;
                        artist = album.artist;
                        title = album.title;
                        description = album.description;
                        listItem = "<li>" +
                                    "<h2 class=\"artist\">gt;" + artist + "<\/h2>" +
                                    "<p class=\"title\">gt;" + title + "<\/p>" +
                                    "<p class=\"description\">gt;" + description + "<\/p>";
                        $("#albums").append( listItem );
                    }
                    $("#albums").listview( "refresh" );
                }
            });
      }

      $(document).ready( handleDocumentReady );

  </script>
</html>

As it stands, the HTML that is constructed and appended as a list item to the albums list does not have any clickable navigation. As shown in previous examples, to add navigation to internal pages, we’ll add a link with an href to a page preceded by a hash (#) to each listItem HTML snippet.

ListItem HTML

Now we could just update the HTML string as such:

listItem = "<li>" +
                 "<a href=\"#albumview\"><h2 class=\"artist\">gt;" + artist + "<\/h2><\/a>" +
                 "<p class=\"title\">gt;" + title + "<\/p>" +
                 "<p class=\"description\">gt;" + description + "<\/p>";

With the link wrapping the artist header, upon user-click the document would navigate to the albumview page. The problem with this solution is that the albumview is not populated with any content. We don’t know which list item was clicked or any other relevant information related to the target document. To access that, we’ll switch up how the HTML snippet is created and assign a click handler to each link in order to update a variable accessible between pages.

Make the following modifications the the HTML snippet within the for… loop of refreshAlbums():

for( i in data.rows )
{
    album = data.rows[i].value;
    artist = album.artist;
    title = album.title;
    description = album.description;
    listItem = $("<li/>", {
                        class: "album"
                    });
    header = "<h2 class=\"artist\">" + artist + "<\/h2>";
    albumLink = $("<a/>", {
                            href: "#albumview",
                            "data-identity": album._id,
                            click: function() {
                                albumId = $(this).data("identity");
                            }
                    });
    albumLink.append( header );
    listItem.append( albumLink );
    listItem.append( "<p class=\"title\">" + title + "<\/p>" );
    listItem.append( "<p class=\"description\">" + description + "<\/p>" );
    $("#albums").append( listItem );
}

In this snippet, we have resolved listItem to an HTML entity we can append to and construct a click-able albumLink using object notation. a data-identity property is set in the link to access and assign an albumId which will later be used to request the document once we have landed on the albumview page. The content elements are largely the same as they were previously for the listItem, but we have wrapped the header in a link element.

Navigation

We will need to bind an event prior to showing the albumview page in order to request the document and display it once we land on the albumview page. The following snippet is the full document with those changes highlighted:

<!DOCTYPE html>
<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
        <div data-role="header"><h2>Albums</h2></div>
        <div data-role="content">
            <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
        </div>
        <div data-role="footer">Footer</div>
    </div>
    <div data-role="page" id="albumview">
        <div data-role="header" id="albumheader">
            <h2 class="albumtitle"></h2>
        </div>
        <div data-role="content" id="albumcontent"></div>
        <div data-role="footer" />
    </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

      var albumId;
      $db = $.couch.db("albums");

      function handleDocumentReady()
      {
          $("#albumview").bind( "pagebeforeshow", openAlbum );
          refreshAlbums();
      }

      function refreshAlbums()
      {
          $("#albums").empty();
          $db.view("albums/albums",
            { success: function( data ) {
                    var i;
                    var album;
                    var artist;
                    var title;
                    var description;
                    var listItem;
                    for( i in data.rows )
                    {
                        album = data.rows[i].value;
                        artist = album.artist;
                        title = album.title;
                        description = album.description;
                        listItem = $("<li/>", {
                                        class: "album"
                                        });
                        header = "<h2 class=\"artist\">" + artist + "<\/h2>";
                        albumLink = $("<a/>", {
                                        href: "#albumview",
                                        "data-identity": album._id,
                                        click: function() {
                                                albumId = $(this).data("identity");
                                            }
                                        });
                        albumLink.append( header );
                        listItem.append( albumLink );
                        listItem.append( "<p class=\"title\">" + title + "<\/p>" );
                        listItem.append( "<p class=\"description\">" + description + "<\/p>" );
                        $("#albums").append( listItem );
                    }
                    $("#albums").listview( "refresh" );
                }
            });
      }

      function openAlbum()
      {
          $("#albumcontent").children().remove();
            $db.openDoc( albumId,
                  { success: function( data ) {
                          var artist = data.artist;
                          var title = data.title;
                          var description = data.description;
                          var html = "<h2 class=\"artist\">" + artist + "<\/h2>" +
                                        "<p class=\"title\">" + title + "<\/p>" +
                                        "<p class=\"description\">" + description + "<\/p>";
                          $("#albumcontent").append( html );
                          $("#albumheader .albumtitle").text( title );
                  }
              });
      }

      $(document).ready( handleDocumentReady );

  </script>
</html>

In this snippet we have bound the “pagebefoeshow” event to the openAlbum() method handler within the handleDocumentReader() method:

$("#albumview").bind( "pagebeforeshow", openAlbum );

This will invoke openAlbum() prior to transitioning to the albumview page, allowing us to request the document based on the albumId and add elements to the content of the albumview page:

function openAlbum()
{
    $("#albumcontent").children().remove();
     $db.openDoc( albumId,
            { success: function( data ) {
                    var artist = data.artist;
                    var title = data.title;
                    var description = data.description;
                    var html = "<h2 class=\"artist\">" + artist + "<\/h2>" +
                                  "<p class=\"title\">" + title + "<\/p>" +
                                  "<p class=\"description\">" + description + "<\/p>";
                    $("#albumcontent").append( html );
                    $("#albumheader .albumtitle").text( title );
            }
        });
}

… nothing to unfamiliar… and nothing to pretty :) .

Deployment

We can now push our changes to the CouchDB database using the couchapp utliity. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is my CouchApp application directory). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If that was successful and you now visit http://127.0.0.1:5984/albums/_design/albums/index.html, you will be presented with the list of albums. Click on a list item, and you should be navigated to the albumview page which will display the detail of the document selected from the list. It may look something like the following:

I intentionally left the browser wide (and not the simulated size of a mobile device) so you can how see the hash is added to the URL once on the albumview page: http://127.0.0.1:5984/albums/_design/albums/index.html#albumview.

This is all well and good, but it leads to a problem (in my opinion) as to directly sending a user to the albumview with a populated document. As it stands, an album detail can only be seen once a user has clicked on an item from the list. If you just pasted the http://127.0.0.1:5984/albums/_design/albums/index.html#albumview URL in a new browser window, the content would be empty. Not good User Experience.

Fortunately, there is an alternative. We can add a show function to our application in CouchDB to navigate to an external page that has a (somewhat) uglier URI but allows for you to land on a detail page without going through the landing page of the application. yay!

External Album Page View

In the browser, up to this point through the series, we have seen pages that return the JSON object representing rows of documents (ie. http://127.0.0.1:5984/albums/_all_docs) and used design views to create a default landing page to interact with the albums database (http://127.0.0.1:5984/albums/_design/albums/index.html). But there is more to CouchDB. Using shows and lists, you can serve up a document in any type of representation, including HTML. They are restricted to only GET requests, so you can’t save a document in a different format, but you can certainly return a document in another format.

If you look within the /albums directory we created using CouchApp, you may notice a couple folders we have largely been ignoring while we toiled away in /_attachments and /vendorshows and lists. We are not going to look into lists at the moment, but we will create a show function in our application design to return the HTML representation of a document marked-up as a jquery Mobile page. With this scenario, we can treat the show function as an external page view and have a ugl(ier)y URL, but one we can send to anyone and not have to go through list item selection from the main page.

Show Function

As we are moving away from internal pages to external pages for our application, we are going to have each page returned in a show function of our design. The first order of business to return a album detail page upon selection from the list, is to move the HTML markup for albumview into a show design document in the /shows directory of our albums CouchApp application.

Open up your favorite text editor and create a new JavaScript document. Enter in the following function and save it as album.js in the /shows folder:

function(doc, req) {
  var html = "<div data-role=\"page\" id=\"albumview\">" +
                       "<div data-role=\"header\" id=\"albumheader\">" +
                           "<h2 class=\"albumtitle\">" + doc.title + "<\/h2>" +
                       "<\/div>" +
                       "<div data-role=\"content\" id=\"albumcontent\">" +
                           "<h2 class=\"artist\">" + doc.artist + "<\/h2>" +
                           "<p class=\"title\">" + doc.title + "<\/p>" +
                           "<p class=\"description\">" + doc.description + "<\/p>" +
                       "<\/div>" +
                       "<div data-role=\"footer\" \/>" +
                   "<\/div>";
  return html;
}

Each show document must declare the function(doc, reg) method. This is the standard function of a show document which is invoked on a request through http://127.0.0.1:5984/albums/_design/albums/_show/album/${_id} and handed the full corresponding document from the database and the HTTP request object.

If we look at the index.html page we worked on in the previous section (Local Album Page View), we’ll see that what is returned from this show function is really just a mixture of the mark-up for the albumview page and the openAlbum() method. You may notice that we are not sending back a full HTML document from this show function. That is alright for now. This mark-up will be treated as a page and housed and styled by our index.html page. We can probably do some fancy string assembly based on the request path, but for now we are just gonna leave it as an insertion into the jquery Mobile application.

Modifying index.html

Now that we have a show function serving up our abumview pages, we need to clean up our index.html document and change our albumview link from internal to external. Open up the /_attachments/index.html document in your favorite editor and make the following modifications:

<!DOCTYPE html>
<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
          <div data-role="header"><h2>Albums</h2></div>
          <div data-role="content">
              <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
          </div>
          <div data-role="footer">Footer</div>
      </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

      $db = $.couch.db("albums");

      function handleDocumentReady()
      {
          refreshAlbums();
      }

      function refreshAlbums()
      {
          $("#albums").empty();
          $db.view("albums/albums",
            { success: function( data ) {
                    var i;
                    var album;
                    var artist;
                    var title;
                    var description;
                    var listItem;
                    var externalPage;
                    for( i in data.rows )
                    {
                        album = data.rows[i].value;
                        artist = album.artist;
                        title = album.title;
                        description = album.description;
                        externalPage = "_show/album/" + album._id;
                        listItem = "<li class=\"album\">" +
                                            "<a href=\"" + externalPage + "\">" +
                                                "<h2 class=\"artist\">" + artist + "<\/h2>" +
                                            "<\/a>" +
                                            "<p class=\"title\">" + title + "<\/p>" +
                                            "<p class=\"description\">" + description + "<\/p>";
                                        "<\/li>";
                        $("#albums").append( listItem );
                    }
                    $("#albums").listview( "refresh" );
                }
            });
      }

      $(document).ready( handleDocumentReady );

  </script>
</html>

There’s no highlighting for what we removed in that snippet, but we took out the albumview page declaration from the body and removed the openAlbum() method (as well as its binding on “pagebeforeshow“). The listItem is now back to an HTML string that is appending to the albums listview. Instead of adding a click handler to a link, the href is attributed as an external jQuery Mobile page which is is a query to show the associated document with the _id. Simple, clean.

Deployment

We modified our application to utilize a show function to serve the albumview page up within a jquery Mobile application. With these changes saved, we can now push to the CouchDB database using the couchapp utility. Open a terminal and navigate to the directory where you create your CouchApp applications (for me that is /Documents/workspace/custardbelly/couchdb and in there i have a folder named albums which is the CouchApp application directory for these examples). Enter the following command to push the changes to the CouchDB instance:

couchapp push albums http://127.0.0.1:5984/albums

If all was successful and you now go to http://127.0.0.1:5984/albums/_design/albums/index.html, we’ll still have our old familiar list. Click on an item, and we should be navigated to a detail (or albumview) page somewhat like the following:

I left the browser wide again so you can see the new URL for viewing a single album document: http://127.0.0.1:5984/albums/_design/albums/index.html#_show/album/${doc._id}. Pretty neat. Now you can even copy that #show page URL and paste it in another window and you should be taken directly to that detail page. You may notice that we still get the back button in the upper left of the header. That is because of the hash (#) in the URL which the jquery Mobile framework interprets as being a page linked from another. So a false history. If you clicked Back from the page, it would not go to the index.html page. A minor flaw in our design that we are going to let go for now :) Hey, we just got all this neat stuff working… we’ll work out the kinks later. There’s more fun stuff to work on.

Conclusion

Well, i said the subsequent posts after the initial Getting Started would be shorter… i lied :) In any event, we created a detail page for a single document and offloaded the page loading to an external jquery Mobile page utilizing the show function of the albums design. A nice solution with an uglyURL (we can address that later) that can be passed to other people so they can land on that single page without going through the whole application from the beginning. Since I prefer the external page solution, any continuing posts on this example will utilize the show function to serve, at least, the album detail page.

Now what if we wanted to edit the fields of that document… I am on the edge of my seat as i write this. Literally. Someone stacked a bunch of wrapping stuff on this chair and left me little room. I could have moved it, but i was just too excited to talk about forms…

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Articles in this series:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

Posted in CouchDB, jquery, jquery-mobile.


jquery Mobile + CouchDB: Part 1 – Getting Started

Intro

I have been digging using CouchDB as my back-end choice for personal projects for a while now and truly do believe it is the best solution when choosing a non-relational database management system. While discovering my affinity for CouchDB, i started developing as3couchdb – an open-source library written in ActionScript 3 that allows Flash to communicate to a CouchDB instance which can be found on github. I think as3couchdb is in a pretty stable place at the moment and have built a couple applications and have been happy with it. I am going to continue developing it, and if you do use it and find problems/suggestions please let me know.

That said, i have never been one to focus on something for too long (without getting paid to, of course :) ), without getting distracted by wanting to learn something new. I knew CouchDB shipped with a jQuery plugin, so i thought i would play around with making a DHTML client for some of my projects. Then i thought, why the hell not throw in the jquery Mobile framework for the fun of it? If we are gonna do it, let’s do it.

So that is what i have set out to do. I have finished some basics of an application and am looking to get more into the nitty gritty. I figured what better way to do this than to document it; not only purposed as a storage of how to do something, but *hopefully* a place for some people to chime in and tell me how to do it better :)

This happens to be the first in a series of posts that document building a “simple” DHTML application using the jQuery Mobile framework and a CouchDB back-end. Here’s to hoping the intro for subsequent posts are not as long.

CouchDB

CouchDB is a schema-free document-oriented database HTTP server. If you have not heard the term NoSQL, it is used to refer to a database management system (DBMS) that differs from the widely-used relational model; started out as meaning a DBMS that does not providing a SQL interface, but became a term to refer to any non-relational DBMS, of which CouchDB is often grouped with when talking about NoSQL. I won’t go into a lengthy discussion of CouchDB – so I will say, in basic terms, CouchDB is a document-based DBMS. This will make more sense once we start looking at the API for the CouchDB jQuery plugin.

Before we get into building a front-end for our application, we need to set up a database to play around with. I am assuming here that you have installed CouchDB on your local machine and have it running and accessible at http://127.0.0.1:5984. If you don’t and want to set it up, download CouchDB and peruse the appendix on this link – http://guide.couchdb.org/draft – or install from source from this link -http://guide.couchdb.org/draft/source.html

CouchApp

Though it is not necessary in creating and modifying views and DHTML pages served up from CouchDB, i tend to use the CouchApp command line tool when working with and managing a CouchDB database.

You can read more about CouchApp on their main wiki and checkout the project at /couchapp/couchapp on github. CouchApp is basically a set of utilities that makes creating and modifying views of a CouchDB database instance easy (or i should say easier). Not to mention (the best part), you can create all your views, etc locally, make sure you got it right then push replication remotely and you are all set. Beauty.

When pushing design documents to a CouchDB database in this and following example posts, I will be using the couchapp command line tool as it is quick and easy and a great little utility, but everything done with it can be easily done using curl if you wish.

Our Album Database

First things first, let’s set up a database we can start working with. I use CouchDBX, which i recommend if you are on a Mac. Don’t know if there is an equivalent for Windows or Linux, so if you do know, leave a comment please. Otherwise, just hitting up http://127.0.0.1:5984/_utils, the Futon app, should work in getting started.

Create a new database called albums… or whatever you want to call it. I will be building album views in these examples but you could easily make them something else. Once we have our albums database created, lets create a couple album documents since we are going to have a view that shows a list of albums. The make up of an album document is as such:

{
    "artist" : {
        "type" : "string"
    }
    "title" : {
        "type" : "string"
    }
    "description" : {
        "type" : "string"
    }
}

… pretty basic for our purposes. Using that schema, add a couple album documents to the databse. I just threw in the following document entries:

{"artist":"Thin Lizzy", "title":"Fighting", "description":"Bona-fide rock classic."}
{"artist":"David Bowie", "title":"Honky Dory", "description":"Doesn't get much better than this."}

… and now you know a little bit about my musical tastes :) Any way, we’ve got our database and a few documents residing in it. Lets create a client-side application using CouchApp and push it to the albums database.

Our Albums CouchApp

With CouchApp properly installed, we’ll move over to using the command line tools to create our Albums application, create views and push the documents to our CouchDB database.

Application

With the terminal open, navigate to a directory that you deem worthy to create applications for CouchDB in. For example, on Mac, that for me is usually /Documents/workspace/custardbelly/couchdb. Enter in the following command to generate an albums application to which we will add some views and later push to the albums database:

couchapp generate app albums

If all goes well, a new folder named albums will be in the target directory and will contain files such as the following:

couch app application diretory

I won’t cover all of what those files pertain to, but basically those files will be pushed to the albums database and stored based on the _id file value – CouchApp defaults to _design/${application} and for this example will be _design/albums.

Now, if we were to push this application to our CouchDB database as it stands now and visited http://127.0.0.1:5984/albums/_all_docs we would get returned a page that shows the JSON object of rows of available documents with the default key/values. Maybe useful to some, but not a good user experience for looking at documents in our database. To do this we can create a view using CouchApp that will help in presenting a page that lists the available documents in a nice readable manner.

View

In the terminal, enter the following command to generate a view for all albums in the database:

couchapp generate view albums albums

With that command, we have used CouchApp to create a view named albums (the last argument) in the database albums (argument after view). If you take a look at the views directory in the albums application folder, you will see that a new folder called albums has been created containing the map.js and reduce.js files.

couch app application diretory

I won’t go over all that is involved in map/reduce in CouchDB, so this is some good information if you are interested: http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views. For now, we are just going to modify the map.js file and delete the reduce.js file so it returns a list of the album documents in the database.

map.js

Open up the /albums/views/albums/map.js file in you favorite text editor and add the following line within the function() statement:

function( doc ) {
    emit( doc._id, doc );
}

When you visit http://127.0.0.1:5984/albums/_design/albums/_view/albums a JSON object will be returned with a key/value pairing for each document in the database with the key being the _id assigned to the document and the value being the full document object. In essence, when you create a view, you are creating a rule of how you want documents from the database returned to you. For this simple example, we are just going to return all the documents trusting that they will all be albums. You can create more views (and more applications) as you see fit. For now, we’ll just work with all the documents and wire up a page to view them.

Let’s go ahead and push the albums application to our albums database:

couchapp push albums http://127.0.0.1:5984/albums

If successful, that should return:

2010-12-06 17:37:24 [INFO] Visit your CouchApp here:
http://127.0.0.1:5984/albums/_design/albums/index.html

If you go and visit http://127.0.0.1:5984/albums/_design/albums/index.html, you will notice that shows absolutely no relevance to our application or our database :) If you go to http://127.0.0.1:5984/albums/_design/albums/_view/albums you will now see our full JSON object that we will use when we modify that index.html.

The index file that you were originally directed to is the index.html file in the /albums/_attachments directory. If you open that file up in a text editor, you will see that it is using the couch.app jQuery plugin to construct the elements on the default page. We can modify this HTML document now and use the jQuery Mobile plugin to present our list of albums beautifully (no guarantees on the beautiful part:)).

Albums Application User Interface

CouchDB ships with a version of jQuery and a plugin to interact with a CouchDB instance. They (and other scripts for serialization and futon) can be found in your installation, which on my make is in the following location:

/opt/local/share/couchdb/www/script

If we look at the default index.html page created for us when we used CouchApp to create our albums application, we see that it is using a loader to load in all those necessary scripts:

<html>
  <head>
    <title>My New CouchApp< itle>
    <link rel="stylesheet" href="style/main.css" type="text/css">
  </link></title></head>
  <body>
    <div id="account"></div>

    <h1>Generated CouchApp</h1>

    <div id="profile"></div>
    <div id="items"></div>

    <div id="sidebar">
      <p>Edit welcome message.</p>
      <p>Ideas: You could easily turn this into a photo sharing app, or a grocery list, or a chat room.</p>
    </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">
    $.couch.app(function(app) {
      $("#account").evently("account", app);
      $("#profile").evently("profile", app);
      $.evently.connect("#account","#profile", ["loggedIn","loggedOut"]);
      $("#items").evently("items", app);
    });
  </script>
</html>

That loader.js file can be found in the /albums/vendor/couchapp/_attachments directory and looks like the following:

function couchapp_load(scripts) {
  for (var i=0; i < scripts.length; i++) {
    document.write('<script src="'+scripts[i]+'">< \/script>')
  };
};

couchapp_load([
  "/_utils/script/sha1.js",
  "/_utils/script/json2.js",
  "/_utils/script/jquery.js",
  "/_utils/script/jquery.couch.js",
  "vendor/couchapp/jquery.couch.app.js",
  "vendor/couchapp/jquery.couch.app.util.js",
  "vendor/couchapp/jquery.mustache.js",
  "vendor/couchapp/jquery.evently.js"
]);

As we intend to use jQuery Mobile for our user interface, we will modify this loader.js file to include the latest jQuery libray and the jQuery Mobile plugin.

Modifying loader.js

If you have not already done so, download the latest releases of jQuery and jQuery Mobile. As of this writing those are:

  1. jQuery-1.4.4.js
  2. jQuery.mobile-1.0a2.js

Copy those files from wherever you downloaded them to into the /albums/vendor/couchapp/_attachments/ directory.

Open up loader.js from that same directory in you favorite text editor and make the following changes:

function couchapp_load(scripts) {
  for (var i=0; i < scripts.length; i++) {
    document.write('<script src="'+scripts[i]+'">< \/script>')
  };
};

couchapp_load([
  "/_utils/script/sha1.js",
  "/_utils/script/json2.js",
  "vendor/couchapp/jquery-1.4.4.js",
  "/_utils/script/jquery.couch.js",
  "vendor/couchapp/jquery.couch.app.js",
  "vendor/couchapp/jquery.couch.app.util.js",
  "vendor/couchapp/jquery.mustache.js",
  "vendor/couchapp/jquery.evently.js",
  "vendor/couchapp/jquery.mobile-1.0a2.js"
]);

Save the changes to loader.js. We could probably get rid of the mustache.js and evently.js includes, but we’ll forget about that for now.

Now that we have our loader loading the proper scripts to display a jQuery Mobile interface, we need to modify the index.html file to display the list of album documents from our CouchDB database.

Modifying index.html

Firstly, we’ll need to add the necessary css and image files that came with the jQuery Mobile download so we get the default look-and-feel.

Copy the jquery.mobile-1.0.a2.css file and the images/ directory from your jQuery Mobile download into the /albums/_attachments/style/ folder (residing along with the default main.css file generated using CouchApp).

Open up the index.html file in the /albums/_attachments directory with your favorite text editor, and start off with a clean slate by removing the inline JavaScript and div tags in the body of the HTML document. Also declare the jquery.mobile-1.0a2.css file in your stylesheets. The index.html file should now look somewhat like the following:

<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>

  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

  </script>
</html>

JQuery Mobile Page

This post is already getting pretty long as it is, and i don’t want to discuss the finer parts of jQuery Mobile so the following modifications might not have the fullest explanations as to why we are adding some elements, but i will try at times to explain.

To begin, a jQuery Mobile application is comprised of pages. You can declare all your pages in a single HTML document or load other pages (saved as HTML documents) into divs. A page is denoted by a data-role attribute with the value of “page”. The jQuery Mobile framework handles this pagination and its browser history for you, so there isn’t much to worry about except for how you want you pages to look… well there is more to worry about, but we won’t for the time being :)

The landing page for our albums application will display the list of album documents. To do so, modify the index.html file by adding a page div to the body of the HTML document:

<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
          <div data-role="header"><h2>Albums</h2></div>
          <div data-role="content">
              <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
          </div>
          <div data-role="footer">Footer</div>
      </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

  </script>
</html>

The make up of a jQuery Mobile page contains a content div and optional header and footer divs. In this example we have included the header and footer with some very basic information. What is of note is the content div (marked with the data-role of “content“). The content of our landing page is a list maked with a data-role of “listview” which will be populated with the document result form our CouchDB database query for all album documents.

With our jQuery Mobile elements in place, we will add the JavaScript to communicate with our CouchDB instance and request the album documents from our albums database.

Communication

As mentioned previously, CouchDB ships with a jquery plugin to ease the communication with a CouchDB instance. That script (jquery.couch.js) along with scripts for serialization are loaded within the loader.js.

First things first, we need to assign a handler for when the document has become available and we declare our database reference as the albums database form the CouchDB instance:

<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
          <div data-role="header"><h2>Albums</h2></div>
          <div data-role="content">
              <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
          </div>
          <div data-role="footer">Footer</div>
      </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

      $db = $.couch.db("albums");

      function handleDocumentReady()
      {
          // request album documents...
      }

      $(document).ready( handleDocumentReady );

  </script>
</html>

The $(document).ready() event hook is a standard part of jQuery to recognize when the DOM has been loaded and we can begin any operations/transactions. In this example, the handleDocumentReady() method is defined as the handler for that event. Within that handler we will add the communication with the $db instance to request our album documents and add them to the listview of the content.

The following snippet is a full implementation of making a request for those documents and adding them to the listview:

<html>
  <head>
    <title>My Albums</title>
    <link rel="stylesheet" href="style/main.css" type="text/css">
    <link rel="stylesheet" href="style/jquery.mobile-1.0a2.css" type="text/css"/>
  </head>
  <body>
    <div data-role="page">
          <div data-role="header"><h2>Albums</h2></div>
          <div data-role="content">
              <ul id="albums" data-role="listview" data-theme="c" data-dividertheme="b" />
          </div>
          <div data-role="footer">Footer</div>
      </div>
  </body>
  <script src="vendor/couchapp/loader.js"></script>
  <script type="text/javascript" charset="utf-8">

      $db = $.couch.db("albums");

      function handleDocumentReady()
      {
          refreshAlbums();
      }

      function refreshAlbums()
      {
          $("#albums").empty();
          $db.view("albums/albums",
            { success: function( data ) {
                    var i;
                    var album;
                    var artist;
                    var title;
                    var description;
                    var listItem;
                    for( i in data.rows )
                    {
                        album = data.rows[i].value;
                        artist = album.artist;
                        title = album.title;
                        description = album.description;
                        listItem = "<li>" +
                                    "<h2 class=\"artist\">gt;" + artist + "<\/h2>" +
                                    "<p class=\"title\">gt;" + title + "<\/p>" +
                                    "<p class=\"description\">gt;" + description + "<\/p>";
                        $("#albums").append( listItem );
                    }
                    $("#albums").listview( "refresh" );
                }
            });
      }

      $(document).ready( handleDocumentReady );

  </script>
</html>

To get a deeper view, lets look in detail at the method that handles loading the album documents in the script which is invoked from the handleDocumentReady() handler:

function refreshAlbums()
{
    $("#albums").empty();
    $db.view("albums/albums",
    { success: function( data ) {
            var i;
            var album;
            var artist;
            var title;
            var description;
            var listItem;
            for( i in data.rows )
            {
                album = data.rows[i].value;
                artist = album.artist;
                title = album.title;
                description = album.description;
                listItem = "<li>" +
                            "<h2 class=\"artist\">" + artist + "<\/h2>" +
                            "<p class=\"title\">" + title + "<\/p>" +
                            "<p class=\"description\">" + description + "<\/p>";
                $("#albums").append( listItem );
            }
            $("#albums").listview( "refresh" );
        }
    });
}

First and foremost, as we might use this method in later implementations of the application to refresh the list, we make a call to empty the content of the listview:

$("#albums").empty();

Following that, we request the albums view from the database instance by calling $db.view() and pointing to the albums view that we created and pushed to the CouchDB database using CouchApp.

$db.view("albums/albums"

… and then resolve a successful result to a function that handles interpreting each JSON object related to to an album document so as to add them as list items to the listview:

{ success: function( data ) {
            var i;
            var album;
            var artist;
            var title;
            var description;
            var listItem;
            for( i in data.rows )
            {
                album = data.rows[i].value;
                artist = album.artist;
                title = album.title;
                description = album.description;
                listItem = "<li>" +
                            "<h2 class=\"artist\">" + artist + "<\/h2>" +
                            "<p class=\"title\">" + title + "<\/p>" +
                            "<p class=\"description\">" + description + "<\/p>";
                $("#albums").append( listItem );
            }
            $("#albums").listview( "refresh" );
        }
}

The for..in loop in this example goes reads each document object and appends a list item to the listview, after which we make a call to refresh the view.

Deployment

With the index.html file modified and ready to go live, we can use the couchapp utility to push an update to the CouchDB instance so we can view our new mobile-based application in a browser:

couchapp push albums http://127.0.0.1:5984/albums

.. if all goes well you should see the following printed out :

2010-12-07 11:46:55 [INFO] Visit your CouchApp here:
http://127.0.0.1:5984/albums/_design/albums/index.html

Now, if you do visit that link, it should redirect you to http://127.0.0.1:5984/albums/_design/albums/index.html and you should see something like the following:

couch app mobile application

Pretty, no? No. But maybe in later posts we can modify that. For now its up and running. yay!

Conclusion

Well, if you made it this far down, you have been a trooper. Hopefully this has been of some help in getting up and started using CouchDB, CouchApp and jQuery Mobile to provide a mobile-based User Interface for documents in a CouchDB database. Since this is the first in a series of posts in working with jQuery Mobile and CouchDB it is pretty lengthy as it addresses set-up and the beginning of an application of which we can continue to work. Good news is, later posts on this topic will be shorter :) and address more of the finer details of jQuery Mobile and CouchDB communication. At least that is the hope :) .

Future Topics

I am looking forward to adding more posts for this topic as i go about discovering how to create a mobile-based solution for working with documents from a CouchDB database. The tentative list is as follows:

  1. Getting Started
  2. Displaying a page detail of a single album.
  3. Templates and Mustache
  4. Displaying an editable page of an album.
  5. Creating and Adding an album document.
  6. Deleting an album document
  7. Authorization and Validation – Part 1
  8. Authorization and Validation – Part 2

Full source for albums couchapp here.

As those post go live, this list will be updated with the related links.

[Note] This post was written against the following software versions:
CouchDB – 1.0.1
CouchApp – 0.7.2
jQuery – 1.4.4
jQuery Mobile – 1.0a2
If you have found this post and any piece has moved forward, hopefully the examples are still viable/useful. I will not be updating the examples in this post in parellel with updates to any of the previously mentioned software, unless explicitly noted.

Posted in CouchDB, jquery, jquery-mobile.


Flex 4.5 (Hero) – Pausing Session Restoration

While Flex 4.5 (Hero) provides an API for persisting session data within View objects, as discussed in my previous post, MobileApplication dispatches events at the initialization and deactivation of the application that allow for you to perform any subsequent custom handling of persistent data – such as re-logging a user back into a service on relaunch of the application.

[[NOTE] It should be noted that this post will be discussing some of the finer points of the persistant data API available, as of this writing, with the free trial download of Flash Builder “Burrito” and the included Flex SDK Version 4.5.0 build 17689]

When the sessionCachingEnabled property of MobileApplication is declared as true, the following events are broadcast:

/* dispatched in MobileApplication +initialize() */
FlexEvent.APPLICATION_RESTORING
FlexEvent.APPLICATION_RESTORE
/* dispatched in MobileApplication -applicationDeactivateHandler() */
FlexEvent.APPLICATION_PERSISTING
FlexEvent.APPLICATION_PERSIST

In responding to these events, a client can serialize any other custom data to the disk that is relevant between sessions (launch, close, re-launch) of your application – be it as a Shared Object, which the PersistenceManager writes to, or a file using the File API.

When working with persisted data between View objects, what essentially is happening is the serialization of the NavigationStack. The NavigationStack is, in rough terms, a history manager. It holds a list of ViewHistoryData objects that pertain to the progression of View requests; popping and pushing the ViewHistoryData objects from the stack as the user progresses through the application.

When responding to the session-persistent events, you most likely will not be modifying that stack that is being serialized between View objects already for you (using the data property and serializeData() and deserializePersistedData() methods discuss in my previous post). Though you could, you could hijack that Shared Object from the PersistenceManager and overwrite the work it had previously done, but most likely the serialized NavigationStack will be left untouched or at the very least only inspected.

What had originally got me looking into working with these session-persistent events was to have the ability to re-log in a user who had previously been logged into a service in a prior session. It is a common practice and user experience and one the user rarely thinks about. Think about every time you open a Twitter client on your device. The session is most-likely not kept alive while you have it closed and are playing Falling Balls. But when you re-launch it, you don’t (typically) have to log back in. You can begin sending tweets again. Right away.

This can be achieved in a Flex Hero MobileApplication, but you will have to suspend the “restoring” of the application upon initialization in order to read in any custom persisted data and log a user into a service. I’ll touch on that later, but for now let’s look at how you would first serialize custom data when the application is being deactivated:

protected var _currentUser:CustomUser;
protected var storedUserFilename:String = "currentUser.obj";
...
// Event handler for FlexEvent.APPLICATION_PERSIST of MobileApplication.
protected function handleApplicationPersist( evt:Event ):void
{
	var user:CustomUser = _currentUser;
	var file:File = File.applicationStorageDirectory.resolvePath( storedUserFilename );
	var stream:FileStream = new FileStream();
	stream.open( file, FileMode.WRITE );
	stream.writeObject( user );
	stream.close();
}

Pretty familiar if you have worked with the File API. Nothing actually new in regards to 4.5, just writing the serializable CustomUser object to the application storage directory. It’s another topic on how to maintain the reference to _currentUser, so for the purposes of this example we’ll just say that object is available and managed here. If you are familiar with the File API, you probably are aware that CustomUser needs to be registered with a class alias in order to be properly written to and read back in from the file system as that ActionScript object. That can be achieved by wither declaring a [RemoteClass] meta-tag or the registerClassAlias() method. [*Fun fact*: MobileApplication registers the ViewNavigator and NavigationStack classes upon initialization in the registerPersistenceClassAliases() method]. Here’s a quick exmaple of using the [RemoteClass] meta:

package com.custardbelly.examples.model
{
	[RemoteClass(alias="com.custardbelly.examples.model.CustomUser")]
	public class CustomUser
	{
		public var username:String;
		public var password:String;
 
		public function CustomUser() {}
	}
}

One thing to remember when serializing ActionScript objects, don’t have any non-defaulted constructor parameters. You will get an error when the object is being de-serialized.

Back to the task at hand – logging a user back into the service on relaunch of the application (start of a new session). In order to do that, the “restoring” process of the application needs to be suspended so as to inspect the serialized “history stack” and determine if the user is being brought back to the “login” view or not. If not, the application should log the user back in and resume to the View persisted at the top of the stack from a previous session:

// [Sublass of MobileApplication]
 
// Event handler for FlexEvent.APPLICATION_RESTORING on MobileApplication
protected function handleApplicationRestoring( evt:Event ):void
{
	// Stop any subsequent work so we can determine if we need to log a user back in. 
	evt.preventDefault();
 
	// Access the serialized navigator state, maintained between view navigation.
	var savedState:Object = persistenceManager.getProperty("navigatorState");
	// Check if the topview from the stack is the view we deliver as the login view.
	if( topViewIsNotLogIn( savedState ) )
	{
		try
		{
			// Access a file we serialized to the disk that represents the previously logged-in user.
			var file:File = File.applicationStorageDirectory.resolvePath( storedUserFilename );
			var stream:FileStream = new FileStream();
			stream.open( file, FileMode.READ );
			var user:CustomUser = stream.readObject() as CustomUser;
			if( user != null )
			{
				// Log user back into service.
				// Upon complete, make sure to call:
				// restoreApplicationState();
			}
		}
		catch( e:Error )
		{
			// Most likely file does not exist.
			restoreApplicationState();
		}
	}
	else
	{
		// Else continue on our way.
		restoreApplicationState();
	}
}
 
// Determines if the topView in our perstisted stack is not what we consider to be the log in view.
protected function topViewIsNotLogIn( savedState:Object ):Boolean
{
	if( savedState == null ) return false;
 
	// The index within our view stack that we consider as the log in view.
	const loginViewIndex:int = 0;
	// The serialized stack.
	var stack:NavigationStack = savedState.navigationStack as NavigationStack;
	if( stack != null )
	{
		use namespace mx_internal;
		// If we have a stack to inspect.
		if( stack.source && stack.source.length > 0 )
		{
			var topView:ViewHistoryData = stack.topView;
			var loginView:ViewHistoryData = stack.source[loginViewIndex] as ViewHistoryData;
			return ( topView != loginView );
		}
	}
	return false;
}

One point to grasp is that the event object dispatched for FlexEvent.APPLICATION_RESTORING needs to be halted so we can do some work before the MobileApplication proceeds with business as usual. That is done by calling event.preventDefault(). That actually flips a flag in the -initialize() method of MobileApplication, telling it to not proceed with restoring.

The other point, is that, once halted, MobileApplication:restoreApplicationState() must be called at a later time in order to proceed with initialization. In this example, if it is determined that the user is being brought to the “log in” View on re-launch, then the stored user is not logged back in and restoreApplicationState() is invoked to proceed. If it is determined that the user will be taken to another View, then log in to the service must be done and, upon success, then invoke restoreApplicationState().

[Tip: If you start working with persistent data and you just want to clear it from time to time while developing in order to know that you are working with it properly, call persistenceManager.clear() in your FlexEvent.APPLICATION_RESTORING handler and all your wishes will come true.]

Since Flex 4.5 is beta, you can’t have an example without seeing use namespace mx_internal :) Half-kidding, but for those unfamiliar with that namespace, some methods and properties in the SDK are declared with the mx_internal access specifier to denote that they are in limbo of being given the proper access. So in a later version of the SDK, they may be changed to public, private, protected or any other custom specifier. For this example, in order to access the source property of NavigationStack we needed to resolve to mx_internal. Hopefully it will be opened up at a later date, or at least a public accessor for that list.

So there you have it. The best practice on notifying the user that you are logging them back into a service upon relaunch? I haven’t found the best solution that meets my needs yet, but I do not recommend pushing a View to the stack of the ViewNavigator during that restore suspension. It caused some weird UI glitches. Probably the best bet is to use the PopUpManager – but do not use the ProgressBar in that… bad things happen. Hopefully a new ProgressBar is on the horizon for Flex 4.5.

Posted in AIR, Burrito, Flex, Flex 4.5.


Flex 4.5 (Hero) – Session-Persistent View Presenters

As I had hinted at in my previous post, before i went down the rabbit-hole of state data with regards to the life-cycle of a View object, I had intended to write about persisting Supervising Presenters for Views within an application session. Before I go much farther, i do want to note that Paul Williams has some excellent posts up on different presenter patterns within the context of a Flex application which can be found at http://blogs.adobe.com/paulw/. Specifically, I am taken by the Supervising Presenter, though i am not totally sold… which I hope to address in this post, but my main intent here is to show how I re-use a presenter for a View object to cut down on memory within an application session.

I may touch upon the life-cycle of a View and how state data is maintained in this post, but not to the extent in my previous article. I want to talk more about moving away from “code behind” in Flex-based mobile applications and implementing session-persistent presenters for Views.

If you want to skip the yammering, you can view the source of an example here: Mobile (Hero) Supervising Presenter Example

New View instance on each request

Due to the instantiation of a new View instance upon each request from the ViewNavigator within a MobileApplication as discussed in my previous post, I started really considering my choice to always use what is loosely called code behind” in Flex. I’ve always had somewhat of a problem with the implementation but just kept at it because it worked – separated logic into AS file from view mark-up in MXML – and (not always a fast and hard rule) don’t utilize another micro-architecture framework in projects. I’m not going to get into an argument about frameworks, but let’s leave it that i don’t always agree on their necessity when weighed against project requirements, deadlines and multi-developer teams.

So “code behind” always was a minor sore point, but it got the job done and got it done in an organized fashion. But I am really taking it into consideration when optimizing applications for the mobile paradigm, mainly inheritance chain. To extend View for logic and then extend that view-controller for display again just seems like the wrong choice, especially when View is an nth extension of UIComponent already and taking in the inheritance memory point described here: http://help.adobe.com/en_US/flash/cs/using/WSD7F8E1A9-680A-4000-9BA9-D7B01FDD7ECD.html

I started shopping around for Presenter patterns that i felt were the least intrusive (as i mentioned before Paul Williams has some great posts up here) and settled on the Supervising Presenter at the moment.

Yet, seeing as a View is instantiated upon each request from ViewNavigator, I wanted to keep the overhead low and ensure that only one Presenter was created and associated with a view. In other words, upon the first instantiation of a View object, a Presenter is created and associated with the view. Upon destruction of the view, the Presenter reference is notified to stop supervising and is stored in the state data for that View object. When another instance of the View is instantiated, that data is read in and the Presenter is notified to start supervising again. So now we not only have the logic (depends on *how* much logic you determine necessary) separated from the view, but are re-using the same Presenter without having to create a new instance each time a user navigates to that View.

Here are some examples:

ISupervisingPresenter

import flash.events.IEventDispatcher;
import spark.components.View;
 
public interface ISupervisingPresenter extends IEventDispatcher
{
	function supervise( view:View ):void;
	function unsupervise():void;
	function dispose():void;
}

ISupervisingPresenter supervises a single View and can be instructed to unsupervise as well as tear down any loose reference to offer itself up for garbage collection.

ISupervisingPresenter Implementation

import com.custardbelly.example.event.LogInEvent;
import com.custardbelly.example.views.HomeView;
 
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
 
import spark.components.View;
 
[Event(name="success", type="com.custardbelly.example.event.LogInEvent")]
 
[RemoteClass(alias="com.custardbelly.example.presenter.HomePresenter")]
public class HomePresenter extends EventDispatcher implements ISupervisingPresenter
{
	protected var _view:HomeView;
 
	public function HomePresenter() {}
 
	protected function addHandlers():void
	{
		_view.logInButton.addEventListener( MouseEvent.CLICK, handleLogIn, false, 0, true );
	}
 
	protected function removeHandlers():void
	{
		_view.logInButton.removeEventListener( MouseEvent.CLICK, handleLogIn, false );
	}
 
	protected function handleLogIn( evt:Event ):void
	{
		dispatchEvent( new LogInEvent() );
	}
 
	public function supervise( view:View ):void
	{
		_view = ( view as HomeView );
		addHandlers();
	}
 
	public function unsupervise():void
	{
		removeHandlers();
		_view = null;
	}
 
	public function dispose():void
	{
		if( _view ) unsupervise();
		// remove any other references to ensure GC of this instance...
	}
}

When supervising a view, the ISupervisingPresenter implementation essentially assigns event handlers to child controls of that target View. Its up to you how much logic the ISupervisingPresenter holds. For instance i could forgo dispatching an event and just access the ViewNavigator from the target view and tell it to go to another screen.

Supervised View

< ?xml version="1.0" encoding="utf-8"?>
<s :View xmlns:fx="http://ns.adobe.com/mxml/2009" 
		xmlns:s="library://ns.adobe.com/flex/spark" 
		title="home" 
		creationComplete="handleCreationComplete();">
 
	<fx :Script>
		< ![CDATA[
			import com.custardbelly.example.event.LogInEvent;
			import com.custardbelly.example.presenter.HomePresenter;
			import com.custardbelly.example.presenter.ISupervisingPresenter;
 
			import mx.events.FlexEvent;
 
			protected var _data:Object;
			protected var _presenter:ISupervisingPresenter;
 
			// Listen for deactivate in order to assemble session data accessed from View:data()
			protected function handleCreationComplete():void
			{
				addEventListener( FlexEvent.VIEW_DEACTIVATE, handleDeactivate, false, 0, true );
			}
 
			// Is called before navigator.destroy of this instance. Serialize the data property and clear out refs for GC.
			protected function handleDeactivate( evt:FlexEvent ):void
			{
				removeEventListener( FlexEvent.VIEW_DEACTIVATE, handleDeactivate, false );
				serializeSessionData();
				disassemblePresenter();
			}
 
			// Create the supervising presenter.
			protected function  establishPresenter( value:Object ):void
			{
				if( value && value.hasOwnProperty( "presenter" ) )
				{
					_presenter = ( value["presenter"] as ISupervisingPresenter );
				}
				else
				{
					_presenter = new HomePresenter();
					if( value ) value["presenter"] = _presenter;
				}
 
				_presenter.supervise( this );
				addPresenterHandlers( _presenter );
			}
 
			// Clear refs for GC of this instance.
			protected function disassemblePresenter():void
			{
				removePresenterHandlers( _presenter );
				_presenter.unsupervise();
				_presenter = null;
			}
 
			protected function addPresenterHandlers( presenter:ISupervisingPresenter ):void
			{
				presenter.addEventListener( LogInEvent.SUCCESS, handleLogInSuccess, false, 0, true );
			}
			protected function removePresenterHandlers( presenter:ISupervisingPresenter ):void
			{
				presenter.removeEventListener( LogInEvent.SUCCESS, handleLogInSuccess, false );
			}
 
			// Modify session state data to hold reference to the supervising presenter for re-use.
			protected function serializeSessionData():void
			{
				if( _data == null ) _data = {};
				_data["presenter"] = _presenter;
			}
 
			// Navigate to nother view.
			protected function handleLogInSuccess( evt:LogInEvent ):void
			{
				navigator.pushView( WelcomeView );
			}
 
			// Need to override in order to persist state data through session.
			override public function get data():Object
			{
				return _data;
			}
			// Override to read in session state data.
			override public function set data( value:Object ):void
			{
				_data = value;
				establishPresenter( _data );
			}
 
		]]>
	</fx>
 
	<s :navigationContent />
 
	</s><s :layout>
		<s :VerticalLayout paddingLeft="10" paddingRight="10" paddingTop="10" paddingBottom="10" />
	</s>
 
	<s :TextInput id="usernameField" width="100%" />
	<s :TextInput id="passwordField" width="100%" displayAsPassword="true" />
	<s :Button id="logInButton" label="log in" />

In essence, upon first creation of this View instance, the Supervising Presenter is created and assigned through a call to the data property setter (invoked by the ViewNavigator upon creation of the view). When the view is deactivated (by the ViewNavigator before destruction) the view-local _data property is updated to hold a reference to the associated supervising presenter for this view. Upon creation of a new instance, it is read in though the data setter and instructed to begin supervising again.

The Supervising Presenter reference is persisted through the current session of the application and re-used upon each new instance of the View. Though i do hate using Script in MXML, this code does address my concerns in re-use, inheritance and separation of logic while utilizing the session-persistent hook-ins now present in the View API in Flex Hero.

Mobile (Hero) Supervising Presenter Example

Now… i gotta think about getting that Script outta my MXML :)

Posted in AIR, Flex, Flex 4.5.