drupalgap_640.png

The feedback from the tutorial below, along with the feedback from presenting this tutorial at DrupalCamp Michigan and the University of Michigan Drupal Developers meeting, inspired me to build DrupalGap.

I would recommend checking out the DrupalGap project to see the features described in this tutorial in action, plus new features available in DrupalGap. Otherwise, have fun with the tutorial below, it will provide a solid understanding of the building blocks for Drupal powered mobile applications in PhoneGap with jQuery mobile.

Tutorial Overview

Drupal - Services Setup

Mobile Application - Drupal Services - System Connect

Mobile Application - Drupal Services - User Login/Logout

Mobile Application - Drupal Services - Node Create/Retrieve/Update/Delete

This is a tutorial which describes how to build a simple mobile application that utilizes Drupal 7 web services. The mobile application is built using PhoneGap and JQuery mobile inside the Eclipse IDE using an Android emulator.

The mobile application will allow a user to login and logout from a Drupal site. In addition, the application will allow an authenticated user with proper permissions to CRUD (create, retrieve, update, delete) page node content on a Drupal site.

Prerequisites

Intermediate Drupal Skills

Understanding of RESTful API Calls

Intermediate HTML/CSS/JS/JQuery Skills

Understand JSON

Components

Before getting started, you will want to be familiar with both the Android Hello World and PhoneGap Hello World tutorials. So if you're ready, pull up a chair as we go on this fantastic voyage.

1. Download & Install Drupal

Download Drupal and install it.

2. Download & Enable the Services Module

Download the Services module and enable it.

3. Download SPYC

UPDATE - In Drupal 7, you may now skip all of step #3!

The Services sub module called 'REST Server' needs SPYC (a PHP YAML loader/dumper - "YAML is a human friendly data serialization standard for all programming languages").

Navigate to your site's Services module directory:

cd /var/www/my_drupal_site/sites/all/modules/services

Download SPYC:

wget http://spyc.googlecode.com/svn/trunk/spyc.php -O servers/rest_server/lib/spyc.php

The spyc.php file needs to be inside the rest_server/lib folder in the site's Services module directory. For example:

/var/www/my_drupal_site/sites/all/modules/services/servers/rest_server/lib

4. Enable the REST Server Module

This module comes packaged with Services, enable it as well.

5. Add a Service

Structure -> Services -> Add

Name : my_drupal_services

: my_drupal_services Server : REST

: REST Path to endpoint : my_services

: my_services Debug mode enabled : optional

: optional Session authentication : checked

6. Setup Service Resources

After creating your service, click the 'Edit Resources' link listed next to your service.

Expand each resource set and check the box next to the resources listed below. Leave the alias field empty for each resource you check. Save the resources for your service.

For these examples we will be using the following resources:

node retrieve create update delete

system connect

user login logout



7. Enable REST Server Response Formatters & Request Parsing

Switch to the 'Server' tab and make sure the following are enabled:

Response formatters json

Request parsing application/json application/x-www-form-urlencoded



You can enable other formatters and parsing options as you wish, this tutorial only needs the ones mentioned above.

8. Testing a Resource from Drupal Services

To test our newly created resource(s), we will be using the Poster plugin in Firefox. For this example, we will test our our 'System -> Connect' resource. Open up the Poster plugin for Firefox (make sure you are logged out of your Drupal site in Firefox):

URL: http://localhost/my_drupal_site/?q=my_services/system/connect.json

Content Type: application/x-www-form-urlencoded

Actions: POST

Once you fill out the 'URL' and 'Content Type' fields, click the 'POST' button in the 'Actions' section. A Response window with a status of '200 OK' should show up if everything worked correctly. You should get a JSON response similar to the following:

{ "sessid":"abcdefghijklmnopqrstuvwxyandznowiknowmyabcsnexttimewontyousingwithme", "user":{ "uid":0, "hostname":"127.0.0.1", "roles":{ "1":"anonymous user" }, "cache":0, "session":"", "timestamp":"1321994670" } }

As you can see, the System Connect resource returns JSON with some information about the currently connected user. In this case, we made an anonymous request to the resource, so the user id comes back as zero. Optionally use JSONLint to see a pretty-print of your JSON string to better understand what is contained in the response.

Alrighty, we are ready to move on to some mobile application development with PhoneGap and JQuery Mobile for an Android emulator inside Eclipse.

9. Setup Eclipse, Android Emulator and PhoneGap

If you haven't already done so, you'll need to download and install Eclipse, setup the Android Emulator in Eclipse and have PhoneGap setup to run on your Android Emulator. Please refer to these tutorials for more information:

10. Download JQuery & JQuery Mobile

Download JQuery & JQuery Mobile. Place the JQuery Javascript file and both the Javascript and CSS files for JQuery Mobile inside your /assets/www/ directory in your Android Project.

/assets/www/jquery-1.6.4.min.js

/assets/www/jquery.mobile-1.0.min.js

/assets/www/jquery.mobile-1.0.min.css

11. Include JQuery & JQuery Mobile in the Mobile Application

Inside your Eclipse PhoneGap Android project's /assets/www/index.html file, include the .js files and the .css file like so:

<!DOCTYPE HTML> <html> <head> <title>Hello World</title> <script type="text/javascript" charset="utf-8" src="phonegap-1.2.0.js"></script> <link rel="stylesheet" href="jquery.mobile-1.0.min.css" /> <script src="jquery-1.6.4.min.js"></script> <script src="jquery.mobile-1.0.min.js"></script> </head> <body> <h1>Hello World</h1> </body> </html>

When we run this in our Android emulator it will look something like this:

12. Prepare Empty JavaScript Files

We'll need a few JavaScript files to handle some of the user's interaction as well as to communicate with our Drupal Services.

Create this empty folder:

/assets/www/scripts

Create these empty files:

/assets/www/scripts/dashboard.js /assets/www/scripts/login.js

13. Create a Dashboard Page for the Mobile Application

Create the dashboard page with user login and logout buttons inside the <body></body> and include the dashboard.js file: (/assets/www/index.html)

<div data-role="page" id="page_dashboard"> <script type="text/javascript" charset="utf-8" src="scripts/dashboard.js"></script> <div data-role="header"> <h1>Dashboard</h1> </div><!-- /header --> <div data-role="content"> <p><a href="#page_login" data-role="button" id="button_login">Login</a></p> <p><a href="#" data-role="button" id="button_logout">Logout</a></p> </div><!-- /content --> </div><!-- /page -->

(we'll take care of button visibility rules later)

14. Drupal Services - System Connect

Next we'll make a call to our Drupal System Connect Service on the dashboard page. If the returned result indicates the user is anonymous we will show the login button and hide the logout button. If the returned results indicates the user is authenticated we will hide the login button and show the logout button. (/assets/www/scripts/dashboard.js)

WARNING: When trying to reach localhost from an Android Emulator, I had to use 10.0.2.2 instead.

WARNING: There is a security issue with Drupal Services prior to the 3.4 release of the module. Service calls for authenticated users that use POST, PUT or DELETE now need a CSRF token included in the request header. The code in this tutorial has NOT been updated to reflect this change! More Information

var nid; // global node id variable $('#page_dashboard').live('pageshow',function(){ try { $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/system/connect.json", type: 'post', dataType: 'json', error: function (XMLHttpRequest, textStatus, errorThrown) { alert('page_dashboard - failed to system connect'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { var drupal_user = data.user; if (drupal_user.uid == 0) { // user is not logged in, show the login button, hide the logout button $('#button_login').show(); $('#button_logout').hide(); } else { // user is logged in, hide the login button, show the logout button $('#button_login').hide(); $('#button_logout').show(); } } }); } catch (error) { alert("page_dashboard - " + error); } });

Now when we visit the dashboard, the appropriate button will be displayed depending on the user's authentication status.

15. Create a Login Page

Create a login page inside the <body></body> and include the login.js file: (/assets/www/index.html)

<div data-role="page" id="page_login"> <script type="text/javascript" charset="utf-8" src="scripts/login.js"></script> <div data-role="header"> <h1>Login</h1> </div><!-- /header --> <div data-role="content" class='content'> <div> <label for="page_login_name">Username</label> <input type="text" id="page_login_name" /> </div> <div> <label for="page_login_pass">Password</label> <input type="password" id="page_login_pass" /> </div> <fieldset> <div><button type="button" data-theme="b" id="page_login_submit">Login</button></div> </fieldset> </div><!-- /content --> </div><!-- /page -->

16. Drupal Services - User Login

Now that we have our login button and page setup, let's setup the login form submission and corresponding call to our User Login Drupal Service. (/assets/www/scripts/login.js)

$('#page_login_submit').live('click',function(){ var name = $('#page_login_name').val(); if (!name) { alert('Please enter your user name.'); return false; } var pass = $('#page_login_pass').val(); if (!pass) { alert('Please enter your password.'); return false; } // BEGIN: drupal services user login (warning: don't use https if you don't have ssl setup) $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/user/login.json", type: 'post', data: 'username=' + encodeURIComponent(name) + '&password=' + encodeURIComponent(pass), dataType: 'json', error: function(XMLHttpRequest, textStatus, errorThrown) { alert('page_login_submit - failed to login'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { $.mobile.changePage("index.html", "slideup"); } }); // END: drupal services user login return false; });

Now try logging into your Drupal site by filling out the login form. Once logged in, the app changes the page back to the dashboard, then the dashboard only displays the logout button since you are now logged in.

17. Drupal Services - User Logout

Now let's setup the logout button click handler and corresponding user logout service call: (/assets/www/scripts/dashboard.js)

$('#button_logout').live("click",function(){ try { $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/user/logout.json", type: 'post', dataType: 'json', error: function (XMLHttpRequest, textStatus, errorThrown) { alert('button_logout - failed to logout'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { alert("You have been logged out."); $.mobile.changePage("index.html",{reloadPage:true},{allowSamePageTranstion:true},{transition:'none'}); } }); } catch (error) { alert("button_logout - " + error); } return false; });

Once the user logout service call finishes, the app reloads the index.html page. Alrighty, our app now has some basic user services available from our Drupal 7 site, neat-o!

18. Drupal Services - Node Create

Let's now add a 'Create Page' button and corresponding page that we can use to create a page node on our Drupal site. ( Note : Ideally we would want to only show this button if the user has the 'Basic page: Create new content' permission, but for now we can rely on the Services module to make sure the user calling the resource has the appropriate permissions specified in Drupal 'People -> Permissions')

First, add a button to the dashboard: (/assets/www/index.html -> div#page_dashboard)

<p><a href="#page_node_create" data-role="button" id="button_page_create">Create Page</a></p>

Make the button visible to only authenticated users: (/assets/www/scripts/dashboard.js -> system connect - success callback function)

// if user is not logged in... $('#button_page_create').hide(); // hide the page create button // if user is logged in... $('#button_page_create').show(); // show the page create button

(Note: Ideally we would want to only show this button if the user has the 'Basic page: Create new content' permission, but for now we can rely on the Services module to make sure the user calling the resource has the appropriate permissions specified in Drupal 'People -> Permissions')

Next, create the corresponding page and form for the 'Create Page' button, we will only use the title and body field on the form in these examples: (/assets/www/index.html)

<div data-role="page" id="page_node_create"> <script type="text/javascript" charset="utf-8" src="scripts/node_create.js"></script> <div data-role="header"> <h1>Create Page</h1> </div><!-- /header --> <div data-role="content" class='content'> <div> <label for="page_node_name">Title</label> <input type="text" id="page_node_name" /> </div> <div> <label for="page_node_body">Body</label> <textarea id="page_node_body"></textarea> </div> <fieldset> <div><button type="button" data-theme="b" id="page_node_create_submit">Submit</button></div> </fieldset> </div><!-- /content --> </div><!-- /page -->

Now let's create the .js file necessary to handle the submission of the form and the communication to our Drupal Service Node Create Resource: (/assets/www/scripts/node_create.js)

$('#page_node_create_submit').live('click',function(){ var title = $('#page_node_title').val(); if (!title) { alert('Please enter a title.'); return false; } var body = $('#page_node_body').val(); if (!body) { alert('Please enter a body.'); return false; } // BEGIN: drupal services node create login (warning: don't use https if you don't have ssl setup) $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/node.json", type: 'post', data: 'node[type]=page&node[title]=' + encodeURIComponent(title) + '&node[language]=und&node[body][und][0][value]=' + encodeURIComponent(body), dataType: 'json', error: function(XMLHttpRequest, textStatus, errorThrown) { alert('page_node_create_submit - failed to login'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { $.mobile.changePage("index.html", "slideup"); } }); // END: drupal services node create return false; });

Alright, we've got the ingredients necessary to make one good tasting Drupal page node, let's try it!

If all goes well, then the 'data' variable fed into our success callback function from our node create resource call should look something like this:

{ "nid":"1", "uri":"http://10.0.2.2/my_drupal_site/?q=my_services/node/1" }

Sweet, we've successfully created a page node from our Android app. Now what?

19. Drupal Services - Node Retrieve

The node we previously created won't do us much good if we can't retrieve it from Drupal and use it on our mobile app. The data that was returned to our previous success callback contained the node id, in this example the node id is 1. So we can use the Service's node retrieve resource. We'll use the Poster plugin in Firefox for a quick demonstration:

As you can see, we'll be using a GET on http://localhost/my_drupal_site/?q=my_services/node/1.json to retrieve our node with an id of 1. We shall get a JSON response that looks something like this:

{ "vid": "1", "uid": "1", "title": "Hello Android", "log": "", "status": "1", "comment": "1", "promote": "0", "sticky": "0", "nid": "11", "type": "page", "language": "und", "created": "1322764158", "changed": "1322764158", "tnid": "0", "translate": "0", "revision_timestamp": "1322764158", "revision_uid": "1", "body": { "und": [ { "value": "Kablamo!", "summary": "", "format": "filtered_html", "safe_value": "<p>Kablamo!</p>

", "safe_summary": "" } ] }, "rdf_mapping": {...}, "cid": "0", "last_comment_timestamp": "1322764158", "last_comment_name": null, "last_comment_uid": "1", "comment_count": "0", "name": "tyler", "picture": "0", "data": "b:0;", "path": "http://localhost/my_drupal_site/?q=node/1" }

This is all well and good when we know our node id, but what if we wanted a more dynamic approach to retrieving our content?

Download the Views Datasource module then enable the Views JSON module that comes with it. Create a view called 'My Drupal Pages' (my_drupal_pages), filter it to nodes of type page, sort by newest first, and add a node id field to the view. Change the views 'Format' to 'JSON data document' and use the default settings. Then add a 'Page' display to the view and set the 'Path' to 'my_drupal_pages', like so:

Now we can use the Poster plugin again to test her out with a GET on http://localhost/my_drupal_site/?q=my_drupal_pages, like this:

And that shall return us some JSON like this:

{ "nodes": [ { "node": { "Nid": "1", "title": "Hello Android" } } ] }

Sweet nectar, now we have a way to dynamically retrieve our page node(s).

Please note , you may have to follow step 5F in this tutorial to get Views Datasource to output the JSON correctly.

20. Mobile Application - Displaying the Node

Alright, now let's show our page node on the mobile application. We need to add a page to our mobile app, that when loaded, retrieves our page node(s) and shows it in a clickable list:

So we make an app page for a list of nodes:

<div data-role="page" id="page_node_pages"> <script type="text/javascript" charset="utf-8" src="scripts/node_pages.js"></script> <div data-role="header"> <h1>My Drupal Pages</h1> </div><!-- /header --> <div data-role="content" class='content'> <ul data-role="listview" data-theme="a" data-inset="true" id="page_node_pages_list"></ul> </div><!-- /content --> </div><!-- /page -->

Additionally, we need an app page that will be used to display an actual node ( complete with node edit and delete buttons [will implement button handlers soon] ):

<div data-role="page" id="page_node_view"> <script type="text/javascript" charset="utf-8" src="scripts/node_view.js"></script> <div data-role="header"> <h1></h1> </div><!-- /header --> <div data-role="controlgroup"> <a href="#page_node_update" data-role="button" id="button_node_edit">Edit</a> <a href="#" data-role="button" id="button_node_delete">Delete</a> </div> <div data-role="content" class='content'></div><!-- /content --> </div><!-- /page -->

Create two .js files. One to call our Views Data Source JSON and display a list of our page node(s).

/assets/www/scripts/node_pages.js

$('#page_node_pages').live('pageshow',function(){ try { $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_drupal_pages", type: 'get', dataType: 'json', error: function (XMLHttpRequest, textStatus, errorThrown) { alert('page_node_pages - failed to retrieve pages'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { $("#page_node_pages_list").html(""); $.each(data.nodes,function (node_index,node_value) { console.log(JSON.stringify(node_value)); $("#page_node_pages_list").append($("<li></li>",{"html":"<a href='#page_node_view' id='" + node_value.node.Nid + "' class='page_node_pages_list_title'>" + node_value.node.title + "</a>"})); }); $("#page_node_pages_list").listview("destroy").listview(); } }); } catch (error) { alert("page_node_pages - " + error); } }); $('a.page_node_pages_list_title').live("click",function(){ nid = $(this).attr('id'); // set the global nid to the node that was just clicked });

The other to handle the retrieval and display of an actual page node:

/assets/www/scripts/node_view.js

$('#page_node_view').live('pageshow',function(){ try { $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/node/" + encodeURIComponent(nid) + ".json", type: 'get', dataType: 'json', error: function (XMLHttpRequest, textStatus, errorThrown) { alert('page_node_view - failed to retrieve page node'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { console.log(JSON.stringify(data)); $('#page_node_view h1').html(data.title); // set the header title $('#page_node_view .content').html(data.body.und[0].safe_value); // display the body in the content div } }); } catch (error) { alert("page_node_view - " + error); } });

Add a button on the dashboard to navigate to the list of our page node(s) and set its visibility in dashboard.js, like previous buttons:

<p><a href="#page_node_pages" data-role="button" id="button_view_pages">View Pages</a></p>

// if user is not logged in... $('#button_view_pages').hide(); // hide the view pages // if user is logged in... $('#button_view_pages').show(); // show the view pages

Let's have a look at our newly created list, with only one node. Now we can click on our page node in the list and go to our node view page.

21. Mobile Application - Updating a Node

Following previous examples, we will need a page as well as .js file to handle the updating of a node using our mobile app. Of course we could make this more dynamic and share the same code as the node create example, but in the interest of time and simplicity, we'll use some duplicate code, for shame:

Add this to the dashboard index.html:

<div data-role="page" id="page_node_update"> <script type="text/javascript" charset="utf-8" src="scripts/node_update.js"></script> <div data-role="header"> <h1>Edit Page</h1> </div><!-- /header --> <div data-role="content" class='content'> <div> <label for="page_node_update_title">Title</label> <input type="text" id="page_node_update_title" /> </div> <div> <label for="page_node_update_body">Body</label> <textarea id="page_node_update_body"></textarea> </div> <fieldset> <div><button type="button" data-theme="b" id="page_node_update_submit">Submit</button></div> </fieldset> </div><!-- /content --> </div><!-- /page -->

Create a new .js file /assets/www/scripts/node_update.js, then use it to implement the retrieval of the node so it can be added to the form and also to implement the form submission and corresponding node update resource call, ended with a redirect back to the node view page. This node update resource works just like the node create resource, except you use a 'PUT' and include the node id the service call url:

$('#page_node_update').live('pageshow',function(){ try { $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/node/" + encodeURIComponent(nid) + ".json", type: 'get', dataType: 'json', error: function (XMLHttpRequest, textStatus, errorThrown) { alert('page_node_update - failed to retrieve page node'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { console.log(JSON.stringify(data)); $('#page_node_update_title').val(data.title); $('#page_node_update_body').val(data.body.und[0].value); } }); } catch (error) { alert("page_node_update - " + error); } }); $('#page_node_update_submit').live('click',function(){ var title = $('#page_node_update_title').val(); if (!title) { alert('Please enter a title.'); return false; } var body = $('#page_node_update_body').val(); if (!body) { alert('Please enter a body.'); return false; } $.ajax({ url: "http://10.0.2.2/my_drupal_site/?q=my_services/node/" + encodeURIComponent(nid) + ".json", type: 'put', data: 'node[type]=page&node[title]=' + encodeURIComponent(title) + '&node[language]=und&node[body][und][0][value]=' + encodeURIComponent(body), dataType: 'json', error: function(XMLHttpRequest, textStatus, errorThrown) { alert('page_node_update_submit - failed to update node'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { $.mobile.changePage("#page_node_view", "slideup"); } }); return false; });

So we can now edit our node by clicking the edit button on the node view page:

22. Mobile Application - Deleting a Node

And we'll wrap things up by destroying the evidence... I mean node. Since the delete button was on the node view page, we'll add the delete button handler code to the node_view.js file. The node delete resource operates very similar, except you use a 'DELETE' instead of a 'GET':

$('#button_node_delete').live("click",function(){ if (confirm("Are you sure you want to delete this node?")) { try { $.ajax({ url: "http://10.0.2.2/drupalcampmi/?q=my_services/node/" + encodeURIComponent(nid) + ".json", type: 'delete', dataType: 'json', error: function (XMLHttpRequest, textStatus, errorThrown) { alert('page_node_view - failed to delete page node'); console.log(JSON.stringify(XMLHttpRequest)); console.log(JSON.stringify(textStatus)); console.log(JSON.stringify(errorThrown)); }, success: function (data) { console.log(JSON.stringify(data)); $.mobile.changePage("index.html", "slideup"); } }); } catch (error) { alert("button_node_delete - " + error); } } else { return false; } });

Then we can try deleting our page node:

Well then, now you've gone and deleted the node, nice one.

Conclusion

Drupal is cool

PhoneGap is cool

JQuery Mobile is cool

Well there you have it folks. An introductory lesson on how to make a mobile application in PhoneGap using JQuery Mobile and restful Drupal 7 services. I hope this helps you get started on your mobile application and Drupal deeds for humanity. Now go out there and make some cool apps!

If you'd like to see some Drupal Services in action on the iPhone you can check out what some associates and I have been working on. It is a disc golf scoring application called Discasaurus that is available in the iPhone market (Android coming soon) that utilizes restful Drupal 6 services. It also has a corresponding website to match. So if you like Drupal and disc golf, check it out! Right now the app is written in native iOS and we have a working prototype built with PhoneGap for Android.