Google Docs Help: Collaboration In Blogger Using Google Docs Tutorial Part I

To refresh since last time, this is a series of tutorials on building a mini-Digg system using Blogger and Google Docs as a platform...







To refresh since last time, this is a series of tutorials on building a mini-Digg system using Blogger and Google Docs as a platform. No hosting account, (other than a blog page), is required for this web app, and readers/users don't have to have any special account, (not even a gmail account). What's the price of admission? The example pages that deploy the system all relate to various immigration bills that will effect high tech industry workers. Please write your congress-people no matter what side of the issue you're on. There, admission paid. Now for the fun stuff!

As described in the previous article, the tutorial is divided into several installments. This installment will cover the following basics:

What the System Does So Far (More Cool Stuff on the Way)

Go try one of the collaboration pages out to get a feel for the features: HR 5642 Collaboration Page.

The collaboration system lets readers submit interesting articles to the blog page. These articles are then displayed for all other readers. Readers can also vote on articles clicking on a set of thumb buttons to make each article more or less popular. The infrastructure for the system is built on top of a few very simple, and very free technologies:

  • Google Docs
    • Spreadsheets
    • Forms for spreadsheet input
  • Google Data Visualization API
    • Using the spreadsheet interface, (currently the only interface… lucky us!)
  • Blogger

The technologies that enable each feature are described briefly below, using the following screen shot as a guide:

Article Submission

This form accepts input for new stories. After clicking 'Submit' and 'View Updates' readers can see their stories displayed in the browsing area. Submit is enabled by Google Docs Spreadsheet Forms.

Browsing Area

In this area, stories are displayed that have been previously submitted. The number of stories displayed in each table can be narrowed using the 'Article Search' form. Users can also vote on stories in the browsing area using the voting buttons. Each browsing area on the page is a separate html div. The browsing areas are populated with lists of articles using JavaScript to tailor the html table used for the list. JavaScript also calls the Google Visualization API to query the underlying spreadsheet for submitted articles.

Article Search

Readers can type a search term and narrow down the articles displayed in the browsing area to only those that contain the entire search term. This is the only form on the page so far that was not created from a Google Docs Spreadsheet Form. It's just a simple HTML form, and it will be covered in a later installment of the tutorial.

Voting Buttons

Users can vote stories up or down using the thumb buttons. At present, each user can vote as many times as they want. To see the effects of their votes combined with the votes of all other readers, they can update the vote count by pressing the 'Votes' button. The voting buttons use Google Docs Spreadsheet Forms. They may not look like it, but they are forms. They just have all their input fields hidden and filled in by JavaScript.

Page Layout

The page is divided into three identical areas. One of those areas is shown in the picture above. Each area contains a div, and two supporting forms. The div is shown in blue below, and the two forms are highlighted in orange. The div's purpose is to hold the table of articles.

I'm not an HTML or web design expert, so my explanation of what a div is will contain only what's necessary to understand the code. A div is an html container that can be used to contain and organize other html elements like images, text and tables. A div has several attributes that can be use to describe it. For our purposes, the most important one is the id. Using the id attribute of a div, JavaScript can place new HTML content into that div with a line of code like:

document.getElementById('newsdiv').innerHTML = html.join('');

Where 'newsdiv' is the id of the div that the new HTML will be written into. If you refresh one of the collaboration pages in your browser, you will notice that the divs that hold the article tables flash to blank for a short period. That is because the application is still calculating the HTML to be inserted for each article list. If we take a look at the HTML for each div that is loaded with the page, it is very, very simple:

<div id="otherblog" style="padding: 1em 0pt; overflow-y: scroll; overflow-x: hidden; text-align: left; height: 300px;"></div>

It's just an empty div that's 300 pixels high with a vertical scroll bar, but no horizontal one. The application described here uses the line of JavaScript shown above to add the HTML article table between the opening and closing div tags. Each time we modify the article data, (by submitting a new article, or voting for example), the application updates the contents of the appropriate div by writing a new table into it.

The Application's Data Model

So far, the system utilizes a single Google Docs Spreadsheet. The spreadsheet stores the following fields for each reader-submitted article:

  • Article hyperlink, 'Link'
  • Article Title, 'Title'
  • Article Type, 'Type', (for these pages, the value is restricted to News, OtherBlog, or ThisBlog)
  • GoodBad, (used to calculate the votes for the article… to be covered in a later tutorial)
  • Comment, (for the not-yet-enabled comment feature)

The only data fields that are discussed in this part of the tutorial are 'Link', 'Title', and 'Type'. The data defined in the data model is entered into the spreadsheet using…

Google Docs Spreadsheet Forms

Google introduced the ability to collect user input into spreadsheets utilizing forms in February. They recently introduced support for embeddable forms: Official Google Docs Blog: Embed your forms. The method used here goes a bit beyond Google's embeddable forms and provides more customization and control. Google has provided a great step-by-step help page on how to create forms, so I won't rehash it here. In short, when a Google spreadsheet user creates a form, one column for each field in the form is automatically added to the spreadsheet. The spreadsheet owner can mail the form to recipients to be filled out, or they can see the form in a web page. When users fill out the form, their responses are automatically stored in the associated Google Docs spreadsheet. The automatically produced form for the mini-digg app looks like:

Obviously, the form looks much different when it's finally utilized in this application. The good news is that the conversion is easy! It's described at length in two articles on this site:

More Cowbell! Record Production Using Google Forms and Charts

and

Cowbells Without Retakes

Have you noticed the heavy black line above each form in the application? It's not just there to be pretty! That's the 'hidden' redirect iframe that the 'Retakes' article discusses.

When a reader submits an article, the 'Link', 'Title' and 'Type' are all automatically stored to the spreadsheet through the magic of Google Docs Forms. You won't see the article type in the submission form though. It is hard coded into a hidden field in each of the three submission forms. This can be seen in the html for the News form:

<form target="newsform" action="http://spreadsheets.google...EIRKKw" method="post">
Link:<input size="15" name="single:0" type="text">
Title:<input size="35" name="single:1" type="text">
<input value="News" name="group:2" type="hidden">
<input value="Submit" type="submit">
<input value="View Updates" onclick="hr5642_initialize_News()" type="button">
</form>

That's the article submission feature. It's that simple. Create a spreadsheet, create a form, tailor the html of the form to your liking and insert it into your web page, sit back and wait for the data to roll in!

Displaying Aritcles

All that's left is to display the submitted articles in an organized fashion on the blog page. This is where the Google Visualization API comes in. The Google visualization API was announced in March of this year. JavaScript can utilize the API to extract data from a Google Docs Spreadsheet. You might be wondering why we couldn't use JavaScript to submit articles as well. So far, the visualization API is for just that, visualization. There are no facilities in the API to write into or modify the contents of a spreadsheet, only read facilities are provided.

You can see the JavaScript source code in its entirety below. The source code is based on the Hello World app provided by Google for the visualization API. It's structured into three sections:

  1. The initialization code. This is the code that runs when the blog page is loaded.
  2. The proxy functions that handle callbacks from the Google visualization API queries.
  3. The function that performs the HTML rendering of the returned query data into the blog page's article divs.

Initialization

The initialization code specifies the location of the APIs provided by Google, loads the visualization API, and calls

google.setOnLoadCallback(hr5642_initialize);

passing it the name of the function that will be executed immediately after the Google API is loaded. In this case, that function is hr5642_initialize. So, application startup looks like:

hr5642_initialize calls three functions. Each function is responsible for querying the underlying Google Docs spreadsheet and using the returned data to populate its associated article list, (div), on the blog page. The three functions are:

  • hr5642_initialize_News()
  • hr5642_initialize_OtherBlog()
  • hr5642_initialize_ThisBlog()

Querying and the Proxy Functions

The three functions above are responsible for submitting the queries to the spreadsheet that will return the specified articles for each div. They are virtually identical and differ only in the article search qualifier, ('ThisBlog' in the code snippet shown below), and the proxy function that they pass as a callback to the Google API, ('handleThisBlogQueryResponseProxy' in the code snippet shown below). The code for hr5642_initialize_ThisBlog is:

function hr5642_initialize_ThisBlog() {

var query = new google.visualization.Query(stQuery);

query.setQuery("select B, C, sum(E) where D ='ThisBlog' group by B, C");

query.send(hr5642_handleThisBlogQueryResponseProxy);

}

The code first creates a new query object from Google API library. This object will be used to hold the query string that specifies what information the application wants to retrieve from the underlying spreadsheet. If you're familiar with SQL, the Google Visualization API query language will be very familiar. For more documentation on the query language check out Google's Query Language Reference. The query string:

select B, C, sum(E) where D ='ThisBlog' group by B, C

asks the API to return a set of rows that contain B, the stored article's hyperlink, 'Link', and C, the stored article's title, 'Title'. The 'group by' qualifier tells the query to only return unique combinations of hyperlink and title, eliminating duplicates that might have been entered by someone clicking on the article submission button twice. The final query term, 'sum(E)', is used by the voting update feature. That will be covered in future installment of the tutorial. A snippet of the spreadsheet that the query string queries can be seen below:

As you can see, the query language utilizes the column headings of the spreadsheet, not the field names inserted when the spreadsheet input form is created.

The last line of code calls the send function of the query object. The only argument of the send function is the proxy callback function that Google will call when the query results are ready. The code execution here looks like:

The proxy functions are all identical, there only purpose is to call the method that renders the html in each div, passing along the query response from the Google API and an argument '2' that specifies which div is to be rendered. For example:

function hr5642_handleThisBlogQueryResponseProxy(response){

hr5642_handleQueryResponse(response, 2);

}

The single argument to the proxy function is required by the Google API and holds the data returned by the API from the spreadsheet in response to the JavaScript query.

Rendering the HTML

In the interest of time and space, the voting and search features will be discussed in another installment of the tutorial. The code for them is intentionally not described here, although it is shown in the full listing below. However, these omissions shouldn't obscure the explanations provided. If they do, please leave a comment, and I'll try to clear it up.

At a Glance: The HTML rendering code is very similar to the "Hello World" App from Google. It uses a simple for loop to walk through the returned query results and write an HTML table row entry for each result.

In Detail: Immediately upon entering the function, we call the Google API to get a data table object from the returned query results:

var data = response.getDataTable();

Next, the switch block decides what div the resulting HTML should be written into based on the div_to_use input argument to the rendering function, (hr5642_handleQueryResponse, in the code shown here).

The HTML is created for the table itself and the header row and pushed into an array of strings, called 'html'. Then the code enters the aforementioned for loop, (no pun intended). To retrieve data from the data table, the following API call is used:

data.getFormattedValue(row, 0)

row is the loop increment variable and specifies which row of data, (reader-submitted article), the code is operating on. The second argument specifies which column in the row to retrieve. Our query asked for B, C, and the sum of E:

select B, C, sum(E) where D ='ThisBlog' group by B, C

So, a column number of 0 returns B, the 'Link'. 1 returns C, the 'Title', and 2 returns sum(E), the count of votes.

Various HTML tags are pushed into the 'html' array along with the data to create the desired html table rows on the blog page, and the loop is repeated for the next row of query data until all the data returned for the query has been rendered. Finally, when the for loop is exited, the ending tag for the html table is added to the 'html' string array. The newly rendered HTML is then inserted into the appropriate div on the blog page by calling:

document.getElementById(stListID).innerHTML = html.join('');

Finally, The Source Code!

This listing is the JavaScript source code. For the full html listing as well, simply view the page source of one of the collaboration pages.

<script type="text/javascript" src="http://www.google.com/jsapi"></script>

<script type="text/javascript">

google.load("visualization", "1");


function hr5642_initialize() {

hr5642_initialize_ThisBlog();

hr5642_initialize_OtherBlog();

hr5642_initialize_News();

}


var stQuery = 'http://spreadsheets.google.com/tq?key=pvFXGB-79Kl2AewIPEIRKKw&gid=0&pub=1';

var formaction = '<form target=\"thisblogform\" action=\"http://spreadsheets.google.com/formResponse?key=pvFXGB-79Kl2AewIPEIRKKw\" method=\"post\">';

var stAction = '';

var news_div = 0;

var otherblog_div = 1;

var thisblog_div = 2;


function hr5642_initialize_ThisBlog() {

var query = new google.visualization.Query(stQuery);

query.setQuery("select B, C, sum(E) where D ='ThisBlog' group by B, C");

query.send(hr5642_handleThisBlogQueryResponseProxy);

}


function hr5642_initialize_OtherBlog() {

var query = new google.visualization.Query(stQuery);

query.setQuery("select B, C, sum(E) where D ='OtherBlog' group by B, C");

query.send(hr5642_handleOtherBlogQueryResponseProxy);

}


function hr5642_initialize_News() {

var query = new google.visualization.Query(stQuery);

query.setQuery("select B, C, sum(E) where D ='News' group by B, C");

query.send(hr5642_handleNewsQueryResponseProxy);

}


google.setOnLoadCallback(hr5642_initialize);


function hr5642_handleNewsQueryResponseProxy(response){

hr5642_handleQueryResponse(response, 0);

}


function hr5642_handleOtherBlogQueryResponseProxy(response){

hr5642_handleQueryResponse(response, 1);

}


function hr5642_handleThisBlogQueryResponseProxy(response){

hr5642_handleQueryResponse(response, 2);

}


function hr5642_handleQueryResponse(response, div_to_use) {

if (response.isError()) {

alert('Error in query: ' + response.getMessage() + ' ' + response.getDetailedMessage());

return;

}

var data = response.getDataTable();

var html = [];

var stVotesUpdateFunc = "";

var stListID = "";

switch(div_to_use){

case 0:

stVotesUpdateFunc = 'hr5642_initialize_News()';

stListID = 'news';

break;

case 1:

stVotesUpdateFunc = 'hr5642_initialize_OtherBlog()';

stListID = 'otherblog';

break;

case 2:

stVotesUpdateFunc = 'hr5642_initialize_ThisBlog()';

stListID = 'thisblog';

break;

}

html.push('<table border=\"1\"><tr><th>Article</th><th><input value=\"Votes\" onclick=\"');

html.push(stVotesUpdateFunc);

html.push('\" type=\"button\"></th><th></th><th></th></tr>');

for (var row = 0; row < data.getNumberOfRows(); row++){

var stName = data.getFormattedValue(row, 1);

var srchIndex = 0;

var srchLength = 0;

switch(div_to_use){

case 0:

srchIndex = stName.indexOf(hr5642_getSrch('srchnews'));

srchLength = hr5642_getSrch('srchnews').length;

break;

case 1:

srchIndex = stName.indexOf(hr5642_getSrch('srchotherblog'));

srchLength = hr5642_getSrch('srchotherblog').length;

break;

case 2:

srchIndex = stName.indexOf(hr5642_getSrch('srchthisblog'));

srchLength = hr5642_getSrch('srchthisblog').length;

break;

}

if((srchIndex != -1) || (srchLength == 0)){

html.push('<tr><td><a href=\"');

html.push(data.getFormattedValue(row, 0));

html.push('\">');

html.push(data.getFormattedValue(row, 1));

html.push('</a></td><td> ');

html.push(data.getFormattedValue(row, 2));

html.push('</td><td>');

html.push(formaction);

html.push('<input size=\"15\" name=\"single:0\" type=\"hidden\" value=\"');

html.push(data.getFormattedValue(row, 0));

html.push('\"><input size=\"35\" name=\"single:1\" type=\"hidden\" value=\"');

html.push(data.getFormattedValue(row, 1));

html.push('\"><input value=\"ThisBlog\" name=\"group:2\" type=\"hidden\">');

html.push('<input type=\"hidden\" name=\"single:3\" value=\"-1\"><input value=\"\" class=\"downbutton\" type=\"submit\"></form></td><td>');



html.push(formaction);

html.push('<input size=\"15\" name=\"single:0\" type=\"hidden\" value=\"');

html.push(data.getFormattedValue(row, 0));

html.push('\"><input size=\"35\" name=\"single:1\" type=\"hidden\" value=\"');

html.push(data.getFormattedValue(row, 1));

html.push('\"><input value=\"ThisBlog\" name=\"group:2\" type=\"hidden\">');

html.push('<input type=\"hidden\" name=\"single:3\" value=\"1\"><input value=\"\" class=\"upbutton\" type=\"submit\">');

html.push('</form>');

html.push('</td>');

html.push('</tr>');


}

}

html.push('</table>');

document.getElementById(stListID).innerHTML = html.join('');

}


function hr5642_getSrch(my_value){

var stSearch = document.getElementById(my_value).value;

return stSearch;

}




</script>

Thanks!

If you've made it this far, you're a trooper! Thanks for the your time and interest. Please leave any feedback, ideas, or suggestions as comments below. And don't forget! Contact your representatives!



Comments

Thanks for Publishing My Article

Hi Christine and Marie,
Thanks for re-publishing my article as a knol piece. I don't mind, the more the merrier, but I'm curious as to how I can get author attribution for the original piece published at:
http://copaseticflow.blogspot.com/2008/05/testtesttest.html
Thanks again!

Last edited Oct 16, 2008 2:28 PM
Report abusive comment