Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

<?xml version="1.0" encoding="utf-8"?>
<html>

(OpenSocial API v0.8)
_ Dan Holevoet and Arne Roomann-Kurrik, OpenSocial Team
Novembro 2009_

This tutorial will introduce you to gadgets and OpenSocial, and will walk you through the steps required to build a simple social gadget where you can give gifts to your friends. In addition, you will be introduced to some of the more advanced features of the OpenSocial API.

For better understanding of this tutorial, it is suggested to read The Requesting-Data-Tutorial /wiki/spaces/a/pages/527082 first.

You can find the complete sample code in the opensocial-resources project on Google Code.

Gadget basics

At their core, social gadgets are XML files, sometimes known as gadget specifications. Here is a simple "Hello World" gadget (helloworld.xml), that illustrates the basic sections of a specification:

<source lang="javascript">

Panel
Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8" ?>

<Module>
<ModulePrefs

 <Module>
   <ModulePrefs title="Hello World!">

<Require

     <Require feature="opensocial-0.8" />


   </ModulePrefs>


   <Content type="html">


     <![CDATA[


       Hello, world!


     ]]>


   </Content>


 </Module>

...


In the "Hello World" example, you can see several sections which control the features and design of the gadget.

  • Code Block<Module> indicates that this XML file contains a gadget. Code Block
  • <ModulePrefs> contains information about the gadget, and its author.
  • Code Block<Require feature="opensocial-0.8" /> denotes a required feature of the gadget — in this case, the OpenSocial API (v0.8). Code Block
  • <Content type="html"> indicates that the gadget's content type is HTML. This is the recommended content type for OpenSocial containers, but gadgets for other containers such as iGoogle support other content types.code
  • <![ CDATA[...]]> contains the bulk of the gadget, including all of the HTML, CSS, and JavaScript (or references to such files). The content of this section should be treated like the content of the Code Blockbody tag on a generic HTML page.

Running your first gadget

<div style="background: #fff; padding: 0 0 10px 10px; width: 40%; float: right; margin: 0 0 10px 10px; ">
<div style="padding: 0 10px; background: #fafafa; border: 1px solid #ccc;">

Notes on using the GGE (Google Gadget Editor)

The GGE has not yet been updated for compatibility with the OpenSocial API, so features like "preview" will not function when using the GGE to edit OpenSocial gadget specifications. For more information on using the GGE, see the gadget API Developer's Guide .

</div>
</div>

...

Section
Column

Now that you know what a basic social gadget looks like, it's time to take it a step further and actually install it within an OpenSocial container. This tutorial uses the iGoogle developer sandbox. (The Getting Started Guide has information on using other OpenSocial containers.) Here are the things you need in order to get up and running. Don't worry, this tutorial will guide you through getting everything.

Set up hosting for your gadget file

It's best to start with a simple gadget while walking through the steps to get up and running. Copy the

Code Block
helloworld.xml
Column
Info
titleNotes on using the GGE (Google Gadget Editor

The GGE has not yet been updated for compatibility with the OpenSocial API, so features like "preview" will not function when using the GGE to edit OpenSocial gadget specifications. For more information on using the GGE, see the gadget API Developer's Guide .

Set up hosting for your gadget file

It's best to start with a simple gadget while walking through the steps to get up and running. Copy the helloworld.xml example above into a new plain text file on your computer.

...

Using your own hosting is preferred — the flexibility it offers will be greater than free hosting. However, if you don't have your own hosting, and are willing to offer your gadget under an open source license, use Google Code: Project Hosting. Finally, if neither of those options is possible, use the GGE or another alternative.

_NOTOC_

<div style="background: #fff; padding: 0 0 10px 10px; width: 40%; float: right; margin: 0 0 10px 10px; ">
<div style="padding: 0 10px; background: #fafafa; border: 1px solid #ccc;">

Notes on intranet hosting

Files hosted within corporate or private LANs will not be sufficient for the purposes of testing gadgets within iGoogle. The file must be visible to the public Internet so iGoogle can fetch the gadget specification. iGoogle uses a number of public servers to fetch gadget specifications, so simply adjusting your firewall to allow iGoogle isn't feasible. Host the files on an externally accessible server.

Once you've chosen your hosting and have it configured, upload the

Code Block
helloworld.xml

file to the host. Make sure the file is readable to the outside world by typing the URL in a browser window and confirming that the XML is shown to you.
</div>
</div>

Setting up the iGoogle environment

Next, log into iGoogle and create an account if you don't have one. Once your account is created, enter the sandbox. You will need to enter some basic information, and click the sign-up button. Access is granted instantly. Then, within the sandbox click on the "Personalize this page" link that will take you to a page that controls installation of gadgets on your account. Next, scroll down and click the "Add feed or gadget" link. Enter the publicly accessible URL that you entered in your browser before (the location of your gadget specification) and click "Add". Then navigate back to the sandbox. If you see "Hello, world!" then you've successfully installed your first gadget. To see your gadget in canvas view, click the name of your gadget ("Hello World!") in the left navigation menu.

If something went wrong...

If you receive an error message when adding the gadget, iGoogle is most likely having difficulty retrieving the file from your hosting service. Double check that the file is externally visible and that the XML is copied exactly as above. If you receive an error after installation, and you've already double checked the contents of the file, it is possible that the iGoogle sandbox is undergoing maintenance — wait a few moments and try refreshing your browser window.

...

Setting up the iGoogle environment

Section
Column

Next, log into iGoogle and create an account if you don't have one. Once your account is created, enter the sandbox. You will need to enter some basic information, and click the sign-up button. Access is granted instantly. Then, within the sandbox click on the "Personalize this page" link that will take you to a page that controls installation of gadgets on your account. Next, scroll down and click the "Add feed or gadget" link. Enter the publicly accessible URL that you entered in your browser before (the location of your gadget specification) and click "Add". Then navigate back to the sandbox. If you see "Hello, world!" then you've successfully installed your first gadget. To see your gadget in canvas view, click the name of your gadget ("Hello World!") in the left navigation menu.

If something went wrong...

If you receive an error message when adding the gadget, iGoogle is most likely having difficulty retrieving the file from your hosting service. Double check that the file is externally visible and that the XML is copied exactly as above. If you receive an error after installation, and you've already double checked the contents of the file, it is possible that the iGoogle sandbox is undergoing maintenance — wait a few moments and try refreshing your browser window.

If you're making changes to your gadget spec but not seeing them reflected in the iGoogle sandbox, this is because the iGoogle sandbox currently caches your gadget spec to minimize the load on your server. Great for an application in production, but this can be a pain when you're actively developing your app. You can bypass the caching mechanism by un-checking the "Cached" checkbox next to your gadget in the developer gadget (also known as "My Gadgets", #tools see below.

Column
Info
titleNotes on intranet hosting

Files hosted within corporate or private LANs will not be sufficient for the purposes of testing gadgets within iGoogle. The file must be visible to the public Internet so iGoogle can fetch the gadget specification. iGoogle uses a number of public servers to fetch gadget specifications, so simply adjusting your firewall to allow iGoogle isn't feasible. Host the files on an externally accessible server.

Once you've chosen your hosting and have it configured, upload the helloworld.xml file to the host. Make sure the file is readable to the outside world by typing the URL in a browser window and confirming that the XML is shown to you.

Adding the developer tools

Once you've installed your first gadget, there are several other gadgets you should install that will help you with development.

To install a tab containing these gadgets to your iGoogle sandbox, follow this link. A description of each gadget and its purpose can be found in the developer's guide along with installation instructions.

Adding some friends...

The rest of this tutorial relies on having friends available in the sandbox, so take a moment to walk through the process of adding a new friend using the Sandbox Friends gadget. To begin, click on the "Sandbox Friends" link in the left navigation bar. In the contacts manager that is presented, click on the new contact icon on the top left, and enter details for the iGoogle account with which to become friends, and save the details. Next, click on the "Friends" group in the leftmost column, and add your new contact. This contact is now listed as your friend.

An important note is that iGoogle supports asynchronous relationships, so while you may have someone listed as a friend that person might not list you as a "Friends". In order for this relationship to be mutual, the other contact must follow the same process as above to add your account to their "Friends" group. For the purposes of this tutorial, it is best to only include mutual friends in your "Friends" group.

Writing your first social application

Now it's time to bite into something a bit meatier, your first social application. This tutorial will help you write a simple application to give "gifts" to your friends. When the gadget is finished you will be able to:

  • Give simple gifts to your friends.
  • See the gifts you have given your friends.
  • See the gifts friends have given you.

Setting up the basics

If you're starting a new gadget, you should create a new XML file for it — call it <tt>giftsgifts.xml</tt>xml. Begin with the usual XML boilerplate, and include the social API. Give the gadget a title as well, "Gifts," something reflective of the purpose of the application (the samples will amend the version number to help you keep track of the iterations in this lab). Here's what your shell of a gadget looks like:<source lang="javascript">

Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8"?>

...

Panel
<ModulePrefs

<Module>
  <ModulePrefs title="Gifts part 0 - Boilerplate">


    <Require feature="opensocial-0.8"/>


  </ModulePrefs>


  <Content type="html">


    <![CDATA[


    ]]>


  </Content>

</Module>

</source>This gadget doesn't accomplish a lot, and in fact, accomplishes less than the "Hello World" gadget. However, it sets up the basis for the next, important steps.

Complete gadget specification for version 0

_NOTOC_

<div style="background: #fff; padding: 0 0 10px 10px; width: 40%; float: right; margin: 0 0 10px 10px; ">
<div style="padding: 0 10px; background: #fafafa; border: 1px solid #ccc;">the "Hello World" gadget. However, it sets up the basis for the next, important steps.

Complete gadget specification for version 0

Inline JavaScript vs. external JavaScript

For small gadgets, it's often easier to include all the JavaScript calls for a gadget in the same XML file as the HTML. However, for larger gadgets, this can become cumbersome, so it can be helpful to offload JavaScript function definitions into a separate file.
</div>
</div>

Listing friends

Section
Column

If you're going to give gifts to your friends, you're first going to need to let the application discover who your friends are. In this section we will create a gadget that will list all of the current viewer's friends.

Panel

First, create a function that is called once the gadget is loaded. For now, it, will only perform one

...

job, but later will start multiple steps for initialization.

Column
Info
titleInline JavaScript vs. external JavaScript

For small gadgets, it's often easier to include all the JavaScript calls for a gadget in the same XML file as the HTML. However, for larger gadgets, this can become cumbersome, so it can be helpful to offload JavaScript function definitions into a separate file.

Code Block
xml
xml

gadgets.util.registerOnLoadHandler(init);

...


 
function init()

...

Panel
 {
  loadFriends();

}

</source>Now, of course, there needs to be a function to actually load the friend data. The following function creates a new data request object, then populates it with specific types of data that you'll need: the viewer and the viewer's friends. Notice that in order to request friends, the code constructs an <tt>IdSpec</tt> IdSpec object. An <tt>IdSpec</tt> IdSpec is used when you need to specify one or more people in a group (in this case, the viewer's friends). Then, it sends the request to the server, and gives it the name of a function to call when the data is returned.<source lang="javascript">
function

Code Block
xml
xml

function loadFriends()

...

Panel
var req =
 {
  var req = opensocial.newDataRequest();


  req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), 'viewer');

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  req.send(onLoadFriends);

...


}

The callback function, <tt>onLoadFriends</tt> onLoadFriends, will take the data that the server has returned, and display it on the page. The simple check for <tt>personperson.getId()</tt> assures that only mutual friends are loaded.<source lang="javascript">
function

Code Block
xml
xml

function onLoadFriends(data)

...

Panel
var viewer =
 {
  var viewer = data.get('viewer').getData();


  var viewerFriends = data.get('viewerFriends').getData();

 
  html = new Array();

...

Panel

  html.push('<ul>');


  viewerFriends.each(function(person) {


    if (person.getId())
{
 {
      html.push('<li>' + person.getDisplayName() + "</li>")
;
}
;
    }
  });


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


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

...


}

Several <tt>div</tt> div elements have been inserted within the gadget specification as entry points for the new HTML.<source lang="javascript">

Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8"?>

...

Panel
<ModulePrefs

<Module>
  <ModulePrefs title="Gifts part 1 - Friends">


    <Require feature="opensocial-0.8"/>


  </ModulePrefs>


  <Content type="html">


    <![CDATA[

<script

      <script type="text/javascript">


        /* ... *
/
</script>
/
      </script>
      '''<div id='main'>

Your friends:
<div

        Your friends:
        <div id='friends'></div>


      </div>


    ''']]>


  </Content>

</Module>

...


Complete gadget specification for version 1

Giving gifts

Now it's time to implement the raison d'être of your gadget, giving gifts. In this section, we will modify the gadget to allow the viewer to give a gift to one of their friends.

...

First, you'll need to modify the basic HTML in the gadget specification so that it can insert new information for gift giving into the layout. The resultant XML looks like this:<source lang="javascript">

Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8"?>

...

Panel
<ModulePrefs

<Module>
  <ModulePrefs title="Gifts part 2 - Send Gifts">


    <Require feature="opensocial-0.8"/>


  </ModulePrefs>


  <Content type="html">


    <!
[CDATA[
<script
[CDATA[
      <script type="text/javascript">


        /* ... */


      </script>

<div

      <div id='main'>'''

<div

        <div id='give'>

<form

          <form id='gift_form'>

Give <span

            Give <span id='gifts'></span> to <span id='friends'></span>. <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>


          </form>


        </div>'''


      </div>


    ]]>


  </Content>

</Module>

</source>Now that there are nice hooks into the HTML, modify the output of the friends list into a set of <tt>option</tt> option tags for use within a <tt>select</tt> select tag. This will allow you to select a friend to receive a gift.<source lang="javascript">
function

Code Block
xml
xml

function onLoadFriends(data)

...

Panel
var viewer =
 {
  var viewer = data.get('viewer').getData();


  var viewerFriends = data.get('viewerFriends').getData();

 
  html = new Array();

...

Panel

  html.push('<select id="person">');


  viewerFriends.each(function(person) {


    if (person.getId())
{
 {
      html.push('<option value="', person.getId(), '">', person.getDisplayName(), '</option>');

}

    }
  });


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


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

...


}

Next, you'll need to create another selection menu of gifts you can give. The sample uses a selection of different types of nuts, but you can feel free to use whatever you like. A small update to the initialization function calls this function when the page loads.<source lang="javascript">
var globalGiftList = 'a cashew

Code Block
xml
xml

var globalGiftList = ['a cashew nut', 'a peanut', 'a hazelnut', 'a red pistachio nut'];

...


 
function makeOptionsMenu()

...

Panel
var html = new
 {
  var html = new Array();


  html.push('<select id="nut">');


  for (var i = 0; i < globalGiftList.length; i++) {


    html.push('<option value="', i, '">', globalGiftList[i], '</option>');

}

  }
  html.push('</select>');


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

}

...


 
function init()

...

Panel
 {
  loadFriends();


  makeOptionsMenu();

...


}

To tie all of this together, implement <tt>giveGift</tt> giveGift, the function called when a user clicks the "Give!" button in the gadget. The function loads the gift to be given and the friend to give it to, from the form, updates a global object of gifts, and saves this to the persistent storage.<source lang="javascript">
var globalGivenGifts =

Code Block
xml
xml

var globalGivenGifts = {};

...


 
function giveGift()

...

Panel
var nut =
 {
  var nut = document.getElementById('nut').value;


  var friend = document.getElementById('person').value;

 
  globalGivenGifts[friend] = nut;

...

Panel
var json =

  var json = gadgets.json.stringify(globalGivenGifts);

 
  var req = opensocial.newDataRequest();

...

Panel

  req.add(req.newUpdatePersonAppDataRequest("VIEWER", 'gifts', json));


  req.send();

...


}

Complete gadget specification for version 2

Showing your generosity

Although your gift gadget can give gifts, once they're sent, they go into a vacuum and you're never really sure they've been sent. It would be helpful if the gadget could list the gifts that have been given. In this section we will modify the gadget to load the list of gifts that the viewer has sent to other people.

You could cheat a little bit here, and just use the global object <tt>globalGivenGifts</tt> globalGivenGifts, but that would only display gifts that you've given in any one session, because right now it isn't linked to any persistent storage. Also, you wouldn't know whether your requests were actually successful, just that you'd sent them (the JavaScript object is currently updated regardless of success). A global object is a convenient way to store the gifts that you've sent, though, so if you keep it updated by linking to persistent storage, it will serve as a suitable place to load the data.

...

First, add two requests onto your dataRequest object in <tt>giveGift</tt> giveGift to fetch the viewer's information, and the viewer's friends (for association purposes), when it makes the request. Then, take advantage of the opportunity to add a callback to your request to send a gift.<source lang="javascript">
function

Code Block
xml
xml

function giveGift()

...

Panel
var nut =
 {
  var nut = document.getElementById('nut').value;


  var friend = document.getElementById('person').value;

 
  globalGivenGifts[friend] = nut;

...

Panel
var json =

  var json = gadgets.json.stringify(globalGivenGifts);

 
  var req = opensocial.newDataRequest();

...

Panel

  req.add(req.newUpdatePersonAppDataRequest("VIEWER", 'gifts', json));


  '''req.add(req.newFetchPersonRequest("VIEWER"), 'viewer');

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  var viewer = opensocial.newIdSpec({ "userId" : "VIEWER" });

...

Panel

  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');'''


  req.send(onLoadFriends);

}

</source>If you modify your initial request to load friend data, you can reuse the <tt>onLoadFriends</tt> onLoadFriends function to handle both paths of execution.<source lang="javascript">
function

Code Block
xml
xml

function loadFriends()

...

Panel
var req =
 {
  var req = opensocial.newDataRequest();


  req.add(req.newFetchPersonRequest("VIEWER"), 'viewer');
Panel
var viewerFriends =

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  var viewer = opensocial.newIdSpec({ "userId" : "VIEWER" });

...

Panel

  '''req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts', opt_params), 'data
');
');'''
  req.send(onLoadFriends);

}

</source>Your <tt>onLoadFriends</tt> Your onLoadFriends function now calls another function that will handle the display of your given gifts.<source lang="javascript">
function

Code Block
xml
xml

function onLoadFriends(data)

...

Panel
var viewer =
 {
  var viewer = data.get('viewer').getData();


  var viewerFriends = data.get('viewerFriends').getData();


  var giftData = data.get('data').getData();

 
  html = new Array();

...

Panel

  html.push('<select id="person">');


  viewerFriends.each(function(person) {


    if (person.getId())
{
 {
      html.push('<option value="', person.getId(), '">', person.getDisplayName(), '</option>');

}

    }
  });


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


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

 
  '''updateGiftList(viewer, giftData, viewerFriends);

...

'''
}

Now, you'll need to write an <tt>updateGiftList</tt> updateGiftList function to update the global object and display the results, when the gadget gets back data. The sample function below is robust enough to not throw an exception when the list is blank, but bad data will cause the global list of gifts to be blank (and fail silently).<source lang="javascript">
function

Code Block
xml
xml

function updateGiftList(viewer, data, friends)

...

Panel
var json = null;
if (data
 {
  var json = null;
  if (data[viewer.getId()]) {


    json = data[viewer.getId()]['gifts'];

}

...


  }
 
  if (!json)

...

Panel
globalGivenGifts =
 {
    globalGivenGifts = {};

}
try {
globalGivenGifts =

  }
  try {
    globalGivenGifts = gadgets.json.parse(gadgets.util.unescapeString(json))
;
} catch
;
  } catch (e) {


    globalGivenGifts = {};

}

...


  }
 
  var html = new Array();

...

Panel

  html.push('You have given:');


  html.push('<ul>');


  for (i in globalGivenGifts) {


    if (i.hasOwnProperty)
{
 {
      html.push('<li>', friends.getById
(info)
(i).getDisplayName(), ' received ', globalGiftList[globalGivenGifts
(
[i]], '</li>');

}
}

    }
  }
  html.push('</ul>');


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

...


}

The last thing you'll need is a hook in the HTML where you can insert the list of given gifts:<source lang="javascript">

Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8"?>

...

Panel
<ModulePrefs

<Module>
  <ModulePrefs title="Gifts part 3 - Showing Gifts">


    <Require feature="opensocial-0.8"/>


  </ModulePrefs>


  <Content type="html">


    <![CDATA[

<script

      <script type="text/javascript">


        /* ... */


      </script>

<div

      <div id='main'>

<div

        <div id='give'>

<form

          <form id='gift_
form'>
Give <span
form'>
            Give <span id='gifts'></span> to <span id='friends'></span>. <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>


          </form>


        </div>

<div

        '''<div id='given'></div>

'''
      </div>


    ]]>


  </Content>

</Module>

...


Complete gadget specification for version 3

Showing gifts you've received

So far, the gift gadget sends gifts to your friends, but your friends have no way of knowing they've received them. When they see their gift application, it doesn't tell them what other people have sent them, just what they, themselves have sent. In this section we will modify the gadget to list items that the viewer's friends have given him or her.

...

To start, add another hook into the HTML as a placeholder for the list of received gifts.<source lang="javascript">

Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8"?>

...

Panel
<ModulePrefs

<Module>
  <ModulePrefs title="Gifts part 4 - Showing What You Got"
>
<Require
>
    <Require feature="opensocial-0.8"/>


  </ModulePrefs>


  <Content type="html">


    <![CDATA[

<script

      <script type="text/javascript">


        /* ... */


      </script>

<div

      <div id='main'>

<div

        <div id='give'>

<form

          <form id='gift_form'>

Give <span

            Give <span id='gifts'></span> to <span id='friends'></span>. <a href="javascript:void(0);" onclick='giveGift();'>Give!</a>


          </form>


        </div>

<div

        <div id='given'></div>

<div

        '''<div id='received'></div>

'''
      </div>


    ]]>


  </Content>

</Module>

...


Next, you'll need to make a number of small changes to the functions that load persistent data. First, update <tt>loadFriends</tt> loadFriends to request the application data for the viewer's friends.<source lang="javascript">
function

Code Block
xml
xml

function loadFriends()

...

Panel
var req =
 {
  var req = opensocial.newDataRequest();


  req.add(req.newFetchPersonRequest("VIEWER"), 'viewer');
Panel
var viewerFriends =

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  var viewer = opensocial.newIdSpec({ "userId" : "VIEWER" });

...

Panel

  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');

 
  '''req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');

...

Panel
'''
  req.send(onLoadFriends);

}

</source>Then, update <tt>giveGift</tt> giveGift to do the same. (Remember that these two entry points to updating data rely on one callback function, so the data needs to be consistently fetched.)<source lang="javascript">
function

Code Block
xml
xml

function giveGift()

...

Panel
var nut =
 {
  var nut = document.getElementById('nut').value;


  var friend = document.getElementById('person').value;

givenGiftsfriend = nut;

Panel
var json =

 
  givenGifts[friend] = nut;
  var json = gadgets.json.stringify(givenGifts);

 
  var req = opensocial.newDataRequest();

...

Panel

  req.add(req.newUpdatePersonAppDataRequest("VIEWER", 'gifts', json));


  req.add(req.newFetchPersonRequest("VIEWER"), 'viewer');
Panel
var viewerFriends =

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  var viewer = opensocial.newIdSpec({ "userId" : "VIEWER" });

...

Panel

  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');

 
  '''req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');

...

Panel
'''
  req.send(onLoadFriends);

...


}

Third, update the callback function <tt>onLoadFriends</tt> onLoadFriends to pull the data of the owner's friends out of the returned data, and pass it along to the function that will do the real work, <tt>updateReceivedList</tt>.<source lang="javascript">
function updateReceivedList.

Code Block
xml
xml

function onLoadFriends(data)

...

Panel
var viewer =
 {
  var viewer = data.get('viewer').getData();


  var viewerFriends = data.get('viewerFriends').getData();


  var giftData = data.get('data').getData();

var viewerFriendData =

  '''var viewerFriendData = data.get('viewerFriendData').getData();

...

'''
 
  html = new Array();

...

Panel

  html.push('<select id="person">');


  viewerFriends.each(function(person) {


    if (person.getId())
{
 {
      html.push('<option value="', person.getId(), '">', person.getDisplayName(), '</option>');

}

    }
  });


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


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

 
  updateGiftList(viewer, giftData, viewerFriends);

...

Panel

  '''updateReceivedList(viewer, viewerFriendData, viewerFriends);
'''
}

</source>The final change, implementing <tt>updateReceivedList</tt> updateReceivedList closely parallels <tt>updateGiftList</tt> updateGiftList, but rather than iterating once through the list of gifts you've sent, iterates once through the gifts each of your friends have sent, and pulls out just the ones for you. These are collected nicely, and displayed.<source lang="javascript">
function

Code Block
xml
xml

function updateReceivedList(viewer, data, friends)

...

Panel
var viewerId =
 {
  var viewerId = viewer.getId();

 
  var html = new Array();

...

Panel

  html.push('You have received:<ul>');


  friends.each(function(person) {


    if (data[person.getId()])
{
var json = data
 {
      var json = data[person.getId()]['gifts'
;

var gifts = {}

Panel
if
];
 
      var gifts = {}
      if (!json)
{
gifts =
 {
        gifts = {};

}
try {
gifts =

      }
      try {
        gifts = gadgets.json.parse(gadgets.util.unescapeString(json));

} catch

      } catch (e)
{
gifts =
 {
        gifts = {};

}

for (i in gifts) {

Panel
if

      }
 
      for (i in gifts) {
        if (i.hasOwnProperty &amp;&
i h1.
amp; i == viewerId)
{
 {
          html.push('<li>', globalGiftList[gifts
(
[i]], ' from ', person.getDisplayName(), '</li>');

}
}
}
});

        }
      }
    }
  });
  html.push('</ul>');


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

...


}

Complete gadget specification for version 4

Bragging about it

So, now your friends know how generous you are because they can see the peanuts you gave them, but the real coup de grâce would be if you could show this off to everyone. The good news is that you can, if you post your giving in the activity stream. These activities will be displayed in different ways depending on the container. For example, the container may show activities on an "updates" page or in a gadget.

The first step towards posting an activity is simple, add a call to a new function at the end of <tt>giveGift</tt> giveGift, passing in both the gift and the friend who'll receive it.<source lang="javascript">
function

Code Block
xml
xml

function giveGift()

...

Panel
var nut =
 {
  var nut = document.getElementById('nut').value;


  var friend = document.getElementById('person').value;

 
  globalGivenGifts[friend] = nut;

...

Panel
var json =

  var json = gadgets.json.stringify(globalGivenGifts);

 
  var req = opensocial.newDataRequest();

...

Panel

  req.add(req.newUpdatePersonAppDataRequest("VIEWER", 'gifts', json));


  req.add(req.newFetchPersonRequest("VIEWER"), 'viewer');
Panel
var viewerFriends =

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  var viewer = opensocial.newIdSpec({ "userId" : "VIEWER" });

...

Panel

  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');

 
  req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');

...

Panel

  req.send(onLoadFriends);

 
  '''postActivity(nut, friend);

...

'''
}

</source>Now, you might notice that the new function <tt>postActivity</tt> postActivity isn't being given the friend's name, or the name of the viewer, which it might conceivably need. Unfortunately, both of these bits of information are outside of the scope of the function call, because <tt>giveGift</tt> giveGift is called when the user clicks a button, not when the API is being used.

There are two ways to go about solving this issue. The first solution is to pass more information into the form, including the real names of all the friends, and the id of the viewer. The second, and probably more practical solution, is to put some of the application data into the global scope, so that <tt>postActivity</tt> postActivity can access it anytime it wants. It's likely that other parts of your application will want some bits from persistent storage (or the viewer's id) at some point where that data isn't passed into the current function, so having some data stored at the global level of the gadget is often a good, and convenient, idea.

For the purposes of sending messages to the activity stream, put the collection of friends in the global scope.

<source lang="javascript">
var globalFriends = {};

function

Code Block
xml
xml

'''var globalFriends = {};'''
 
function onLoadFriends(data)

...

Panel
var viewer =
 {
  var viewer = data.get('viewer').getData();


  var viewerFriends = data.get('viewerFriends').getData();


  var giftData = data.get('data').getData();


  var viewerFriendData = data.get('viewerFriendData').getData();

 
  html = new Array();

...

Panel

  html.push('<select id="person">');


  viewerFriends.each(function(person) {


    if (person.getId())
{
 {
      html.push('<option value="', person.getId(), '">', person.getDisplayName(), '</option>');

}

    }
  });


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


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

globalFriends = viewerFriends;


 
  '''globalFriends = viewerFriends;'''
 
  updateGiftList(viewer, giftData, viewerFriends);

...

Panel

  updateReceivedList(viewer, viewerFriendData, viewerFriends);

}

</source>Now that this the helpful information is in the global scope, <tt>postActivity</tt> postActivity can be fully functional. Posting to the activity stream is quite simple — the creation request consists of an activity, a priority, and a callback function. Create an activity with a title along the lines of "You gave your friend a red pistachio." The priority tells the container to post the activity if the user has given it permission to do so, or ask if for permission. Finally, the callback function is an optional parameter, but if it's omitted the <tt>createActivity</tt> createActivity function will trigger a page refresh. Since a refresh is not necessary here, the <tt>postActivity</tt> postActivity method supplies an empty function to execute instead.<source lang="javascript">
function

Code Block
xml
xml

function postActivity(nut, friend)

...

Panel
var title = 'gave ' +
 {
  var title = 'gave ' + globalFriends.getById(friend).getDisplayName() + ' ' + globalGiftList[nut];


  var params = {};


  params[opensocial.Activity.Field.TITLE] = title;


  var activity = opensocial.newActivity(params)


  opensocial.requestCreateActivity(activity, opensocial.CreateActivityPriority.HIGH, function() {});

}

</source>Complete gadget specification for version 5

Giving multiple gifts

At this point, your gift giving application is relatively robust, but it's missing some of the polish that befits a professional application. The first thing you'll want to tackle is the inability to give more than one gift to any single person.

Currently, this limitation is imposed by the <tt>giveGift</tt> giveGift function, that blindly overwrites the value of <tt>globalGivenGiftsglobalGivenGiftsfriend</tt> whenever you give a new gift. To allow giving multiple gifts, you must treat this value as an array instead of a single value, pushing a new element into the array while keeping the old values.

The new version of your code is as follows:<source lang="javascript">
function

Code Block
xml
xml

function giveGift()

...

Panel
var nut =
 {
  var nut = document.getElementById('nut').value;


  var friend = document.getElementById('person').value;

 
  '''if (!globalGivenGifts)

...

Panel
globalGivenGifts =
 {
    globalGivenGifts = {};

}
if

  }
  if (!globalGivenGifts[friend]) {


    globalGivenGifts[friend] = new Array();

}
globalGivenGiftsfriend

  }
  globalGivenGifts[friend].push(nut);'''


  var json = gadgets.json.stringify(globalGivenGifts);

 
  var req = opensocial.newDataRequest();

...

Panel

  req.add(req.newUpdatePersonAppDataRequest("VIEWER", 'gifts', json));


  req.add(req.newFetchPersonRequest("VIEWER"), 'viewer');

 
  var viewerFriends = opensocial.newIdSpec({ "userId" : "VIEWER", "groupId" : "FRIENDS" });


  var opt_params = {};


  opt_params[opensocial.DataRequest.PeopleRequestFields.MAX] = 100;


  req.add(req.newFetchPeopleRequest(viewerFriends, opt_params), 'viewerFriends');

 
  var viewer = opensocial.newIdSpec({ "userId" : "VIEWER" });

...

Panel

  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');

 
  req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');

...

Panel

  req.send(onLoadFriends);

 
  postActivity(nut, friend);

...


}

Now, when you give a gift, the <tt>giveGift</tt> giveGift function checks to see if there are already saved gifts for the recipient. If there are, the new gift is simply added to the array. If there are no prior gifts, <tt>globalGivenGiftsglobalGivenGiftsfriend</tt> is initialized as an array, then the gift is added to that array. Because this data structure is stored as JSON, the changes to your data structure are transparent to the API, and you don't need to change any of the calls that pass data to and from the container.

With multiple gifts nicely tucked away in an array, the next step is to pull that data back out to display in the interface. There are two places that need to be changed, <tt>updateGiftList</tt> and <tt>updateReceivedList</tt> updateGiftList and updateReceivedList. In both functions, you'll need to modify the loop that iterates over each set of gifts to pull out multiple gifts instead of just one.

In <tt>updateGiftList</tt> updateGiftList you must add an additional loop when iterating over <tt>globalGivenGifts</tt> globalGivenGifts so that the code looks like the following:<source lang="javascript">
function

Code Block
xml
xml

function updateGiftList(viewer, data, friends)

...

Panel
var json = null;
if (data
 {
  var json = null;
  if (data[viewer.getId()]) {


    json = data[viewer.getId()]['gifts'
;
}

...

];
  }
 
  if (!json)

...

Panel
globalGivenGifts =
 {
    globalGivenGifts = {};

}
try {
globalGivenGifts =

  }
  try {
    globalGivenGifts = gadgets.json.parse(gadgets.util.unescapeString(json));


  } catch (e) {


    globalGivenGifts = {};

}

...


  }
 
  var html = new Array();

...

Panel

  html.push('You have given:');


  html.push('<ul>');


  for (i in globalGivenGifts) {


    if (i.hasOwnProperty)
{
 {
      '''for (j in globalGivenGifts[i])
{
if
 {
        if (j.hasOwnProperty)
{
 {
          html.push('<li>', friends.getById
(info)
(i).getDisplayName(), ' received ', globalGiftList[globalGivenGifts
(
[i][j]], '</li>');

}

        }
      }'''

}
}

    }
  }
  html.push('</ul>');


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

}

</source>Previously <tt>globalGivenGiftsglobalGivenGiftsi</tt> was a single gift, but now it is an array of gifts. The new loop iterates through that array and displays all of the gifts.

The code in <tt>updateReceivedList</tt> updateReceivedList is modified in a similar manner:<source lang="javascript">
function

Code Block
xml
xml

function updateReceivedList(viewer, data, friends)

...

Panel
var viewerId =
 {
  var viewerId = viewer.getId();

 
  var html = new Array();

...

Panel

  html.push('You have received:<ul>');


  friends.each(function(person) {


    if (data[person.getId()])
{
var json = data
 {
      var json = data[person.getId()]['gifts'
;

var gifts = {}

Panel
if
];
 
      var gifts = {}
      if (!json)
{
gifts =
 {
        gifts = {};

}
try {
gifts =

      }
      try {
        gifts = gadgets.json.parse(gadgets.util.unescapeString(json
));
} catch
));
      } catch (e)
{
gifts =
 {
        gifts = {};

}

for (i in gifts) {

Panel
if

      }
 
      for (i in gifts) {
        if (i.hasOwnProperty &amp;&amp; i == viewerId)
{
 {
          '''for (j in gifts[i])
{
if (j.hasOwnProperty) {
 {
            if (j.hasOwnProperty) {
              html.push('<li>', globalGiftList[gifts
(
[i][j]], ' from ', person.getDisplayName(), '</li>');

}

            }
          }'''

}
}
}
});

        }
      }
    }
  });
  html.push('</ul>');


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

...


}

In this function, the additional loop is added inside the logic to loop through your friends' given gifts. When <tt>giftsgiftsi</tt> represents gifts given to you by your friend, the extra loop then displays those on the page.

...

Complete gadget specification for version 6

Getting a list of gifts from a remote server

Up to this point, the list of gifts available to your users has been coded directly into the application. It would be nice to enable the application to retrieve a list of gifts from a remote web page, so that adding and removing gifts is as easy as editing a data file on your server. Fortunately, this can be easily implemented by using the <tt>gadgetsgadgets.io.makeRequest</tt> makeRequest function to grab remote data.

First, add a method that requests a data file from a URL:<source lang="javascript">
function

Code Block
xml
xml

function requestGiftList(url)

...

Panel
var params =
 {
  var params = {};


  params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;


  gadgets.io.makeRequest(url, onGiftList, params);

...


}

The <tt>requestGiftList</tt> requestGiftList method accepts a URL as a parameter and tries to pull JSON-encoded data from the URL. JSON is a lightweight data interchange format based off of JavaScript. Later in this section, we'll be creating a JSON encoded data file and hosting it for this application to use.

Once the request is made, you'll need to use the response to set the list of gifts. In the newly-added <tt>makeRequest</tt> makeRequest call, you'll see a reference to a nonexistent callback function named <tt>onGiftList</tt> onGiftList which is not yet implemented. You'll do so now, by adding the following code:<source lang="javascript">
function

Code Block
xml
xml

function onGiftList(data)

...

Panel
if
 {
   if (data.data && data.data.length)
{
globalGiftList =
 {
     globalGiftList = data.data;

}

...


   }
}

If the response is appropriately formatted as an array, the <tt>onGiftList</tt> onGiftList method will assign the returned data to the <tt>globalGiftList</tt> globalGiftList variable.

Now you have a mechanism to update the list of gifts from a remote server, but you need to hook it into the existing application flow or else these new methods will never be called. Since the other methods in the application rely on the gift array being set before they execute, you'll need to change some existing code to make the <tt>requestGiftList</tt> requestGiftList method is called before any other requests are made. You'll also need to change <tt>onGiftList</tt> onGiftList to continue application execution once it is finished.

Change your code to look like the following:<source lang="javascript">
function

Code Block
xml
xml

function onGiftList(data)

...

Panel
if
 {
   if (data.data && data.data.length)
{
globalGiftList =
 {
     globalGiftList = data.data;

}

   }
   '''loadFriends();


   makeOptionsMenu();'''

};

...


 
function init()

...

Panel
 {
   '''requestGiftList("http://example.com/gifts.json");
'''
}

</source>Now, the <tt>loadFriends</tt> and <tt>makeOptionsMenu</tt> loadFriends and makeOptionsMenu methods won't be called until the request for the remote gift data returns.

Pay extra attention to the <tt>requestGiftListrequestGiftList("http://example.com/gifts.json");</tt> line of code— you'll need to change this URL to an appropriate place on your server where you can host the <tt>giftsgifts.json</tt> json file, which we'll be creating next.

Once you have a location for <tt>giftsgifts.json</tt>json, paste the following into your text editor and save or upload it to the appropriate place:

<source lang="javascript">
[

Panel

"a cashew nut",
"a peanut",
"a hazelnut",
"a red pistachio nut",
"a mendacious mongongo",
"a crazy coconut",
"a happy horse-chestnut",
"a beautiful brazil nut"

]
</source>

Code Block

[
  "a cashew nut",
  "a peanut",
  "a hazelnut",
  "a red pistachio nut",
  "a mendacious mongongo",
  "a crazy coconut",
  "a happy horse-chestnut",
  "a beautiful brazil nut"
]

If you load the application now, you'll have a bunch of new gifts that you can share with your friends! Additional entries can be made simply by updating <tt>giftsgifts.json</tt>json.

This new functionality is great to have and makes maintaining the application much easier, but there's one major flaw with our implementation — we've increased the load time! By injecting the <tt>makeRequest</tt> makeRequest call into the application flow, and waiting for it to finish before requesting the social data, users now have to:

  1. Wait for the application to finish loading so that the <tt>makeRequest</tt> makeRequest call can be made.
  2. Wait for the <tt>makeRequest</tt> makeRequest call to finish and return data so that social data can be requested.
  3. Wait for the social data request to finish so that the application can be rendered.

...

Preloads are remarkably simple, as well. Just add the following line to the <tt>ModulePrefs</tt> ModulePrefs section of the XML spec:

<source lang="javascript">

<ModulePrefs
Panel
Code Block
xml
xml

<ModulePrefs title="Gifts part 7 - Working with Remote content">


    <Require feature="opensocial-0.8"/>

<Preload

    '''<Preload href="http://example.com/gifts.json" />

'''
</ModulePrefs>

...


Once again, make sure to change <tt>http://example.com/gifts.json</tt> to the location of your own <tt>giftgift.json</tt> json file.

That's it! With this line in place, the call to <tt>makeRequest</tt> makeRequest will return data to <tt>onGiftList</tt> onGiftList immediately.

Complete gadget specification for version 7

Taking advantage of views

The most elegant social applications will tailor themselves to fit within their context. OpenSocial applications should behave no differently, and take advantage of the distinct views made available by the container. Within orkut, the available views are "profile" and "canvas". Within iGoogle, the available views are "home" and "canvas". In both cases, the use case of the two views is distinct, and your applications should satisfy those uses.

To start, edit the XML to include the "views" feature in the <tt>ModulePrefs</tt> ModulePrefs section. Then, extend your content section so that explicitly declares its content as applicable to the "home", "profile", and "canvas" views. At the same time, let's perform some minor modifications to help differentiate the content that displays in each view, and provide a link to navigate to the "canvas" view.<source lang="javascript">

Code Block
xml
xml

<?xml version="1.0" encoding="UTF-8"?>

...

Panel
<ModulePrefs

<Module>
  <ModulePrefs title="Gifts part 8 - Views">


    <Require feature="opensocial-0.8"/>

<Require

    '''<Require feature="views" />

<Preload
'''
    <Preload href="http://example.com/gifts.json" />


  </ModulePrefs>

<Content

  '''<Content type="html" view="home,profile,canvas">

'''
    <![CDATA[

<script

      <script type="text/javascript">


        /* ... */


      </script>

<div

      <div id='main'>

<div

        '''<div id='give' style='display: none;'>

<form
'''
          <form id='gift_form'>

Give <span

            Give <span id='gifts'></span> to <span id='friends'></span>. <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>


          </form>

</div>
<div

        </div>
        <div id='given'></div>

<div

        <div id='received'</div>


        '''<div id='more' style='display: none;'>

<a

          <a href='javascript:void(0);' onclick='navigateToCanvas();'>More</a>


        </div>'''


      </div>


    ]]>


  </Content>

</Module>

</source>With these changes in place, modify <tt>onLoadFriends</tt> onLoadFriends to render different sections of the above HTML depending on which view is currently selected. To simplify things, add a global variable <tt>globalView</tt> globalView to keep track of the current view.<source lang="javascript">
var globalView =

Code Block
xml
xml

var globalView = gadgets.views.getCurrentView().getName();

...


 
function onLoadFriends(data)

...

Panel
var viewer =
 {
  var viewer = data.get('viewer').getData();


  var viewerFriends = data.get('viewerFriends').getData();


  var giftData = data.get('data').getData();


  var viewerFriendData = data.get('viewerFriendData').getData();
if (globalView h1.

 
  '''if (globalView == "canvas")
{
html = new
 {'''
    html = new Array();


    html.push('<select id="person">');


    viewerFriends.each(function(person)
{
 {
      html.push('<option value="', person.getId(), '">', person.getDisplayName(), '</option>');


    });


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


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

 
    '''updateGiftList(viewer, giftData, viewerFriends)
;
;
    document.getElementById('give').style.display = "block";


  } else {


    document.getElementById('more').style.display = "block";


  }'''

 
  globalFriends = viewerFriends;

 
  updateReceivedList(viewer, viewerFriendData, viewerFriends);

...


}

</source>The new version of <tt>onLoadFriends</tt> uses <tt>globalView</tt> onLoadFriends uses globalView to unhide some HTML elements in the application, as well as using it as a basis to render the list of given gifts. When in "home" view, only the list of received gifts will be rendered.

Next, implement the <tt>navigateToCanvas</tt> navigateToCanvas function that's called when the viewer clicks the "More" button the in "home" view.<source lang="javascript">
function

Code Block
xml
xml

function navigateToCanvas()

...

Panel
var canvas =
 {
  var canvas = gadgets.views.getSupportedViews()["canvas"];


  gadgets.views.requestNavigateTo(canvas);

...


}

This function asks the container for a list of supported views, as an array indexed by name. It then selects the "canvas" view and passes it into the <tt>requestNavigateTo</tt> requestNavigateTo call. This call is not a guarantee that the view will migrate to the "canvas" view, because it's a request. The exact behavior is container-specific. In iGoogle, the request should be automatic, but some containers might refuse to honor the request, or ask the user for permission.

...

  • Use thumbnail images instead of display names
  • Use images instead of gift text
  • Use <tt>requestNavigateTo</tt> requestNavigateTo to navigate back to the "home" view
  • Use message bundles for i18n
  • Use the Persistence API to cache the HTML for the "home" view
  • Use <tt>getProxyUrl</tt> getProxyUrl to cache images
  • Get the gadget running on another container that supports opensocial-0.8, like Hi5

...

  • The Getting Started Guide has information on using OpenSocial containers other than iGoogle.
  • The OpenSocial documentation will give you in-depth information about the OpenSocial API.
  • The OpenSocial discussion group is a great place to connect with other OpenSocial developers and get answers to any questions you may have.
  • The <tt>#opensocial</tt> #opensocial room on Freenode IRC will let you talk to developers in realtime.

...