OpenSocial Tutorial

(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 /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:

<?xml version="1.0" encoding="UTF-8" ?>
 <Module>
   <ModulePrefs title="Hello World!">
     <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.

  • <Module> indicates that this XML file contains a gadget.
  • <ModulePrefs> contains information about the gadget, and its author.
  • <Require feature="opensocial-0.8" /> denotes a required feature of the gadget — in this case, the OpenSocial API (v0.8).
  • <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.
  • <![ 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 body tag on a generic HTML page.

Running your first gadget

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.

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 .

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.

To host the gadget externally, you will need a place to upload the file. Fortunately, there are many free places to upload gadget specifications, and Google provides two:

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.

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.

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.

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 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 gifts.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:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts part 0 - Boilerplate">
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
    ]]>
  </Content>
</Module>

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

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

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.

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.

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.

gadgets.util.registerOnLoadHandler(init);
 
function init() {
  loadFriends();
}

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 IdSpec object. An 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.

function loadFriends() {
  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, onLoadFriends, will take the data that the server has returned, and display it on the page. The simple check for person.getId() assures that only mutual friends are loaded.

function onLoadFriends(data) {
  var viewer = data.get('viewer').getData();
  var viewerFriends = data.get('viewerFriends').getData();
 
  html = new Array();
  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 div elements have been inserted within the gadget specification as entry points for the new HTML.

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts part 1 - Friends">
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
      <script type="text/javascript">
        /* ... */
      </script>
      '''<div id='main'>
        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:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts part 2 - Send Gifts">
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
      <script type="text/javascript">
        /* ... */
      </script>
      <div id='main'>'''
        <div id='give'>
          <form id='gift_form'>
            Give <span id='gifts'></span> to <span id='friends'></span>. <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>
          </form>
        </div>'''
      </div>
    ]]>
  </Content>
</Module>

Now that there are nice hooks into the HTML, modify the output of the friends list into a set of option tags for use within a select tag. This will allow you to select a friend to receive a gift.

function onLoadFriends(data) {
  var viewer = data.get('viewer').getData();
  var viewerFriends = data.get('viewerFriends').getData();
 
  html = new Array();
  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.

var globalGiftList = ['a cashew nut', 'a peanut', 'a hazelnut', 'a red pistachio nut'];
 
function makeOptionsMenu() {
  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() {
  loadFriends();
  makeOptionsMenu();
}

To tie all of this together, implement 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.

var globalGivenGifts = {};
 
function giveGift() {
  var nut = document.getElementById('nut').value;
  var friend = document.getElementById('person').value;
 
  globalGivenGifts[friend] = nut;
  var json = gadgets.json.stringify(globalGivenGifts);
 
  var req = opensocial.newDataRequest();
  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 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.

You'll need to update the global list of gifts in two instances. First, you'll need to update it when you load the gadget, to see all gives you've given previously. Second, you'll need to update it when you give a new gift, to both make sure that the gift was sent, and to keep your local object fresh.

First, add two requests onto your dataRequest object in 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.

function giveGift() {
  var nut = document.getElementById('nut').value;
  var friend = document.getElementById('person').value;
 
  globalGivenGifts[friend] = nut;
  var json = gadgets.json.stringify(globalGivenGifts);
 
  var req = opensocial.newDataRequest();
  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" });
  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');'''
  req.send(onLoadFriends);
}

If you modify your initial request to load friend data, you can reuse the onLoadFriends function to handle both paths of execution.

function loadFriends() {
  var req = opensocial.newDataRequest();
  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" });
  '''req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts', opt_params), 'data');'''
  req.send(onLoadFriends);
}

Your onLoadFriends function now calls another function that will handle the display of your given gifts.

function onLoadFriends(data) {
  var viewer = data.get('viewer').getData();
  var viewerFriends = data.get('viewerFriends').getData();
  var giftData = data.get('data').getData();
 
  html = new Array();
  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 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).

function updateGiftList(viewer, data, friends) {
  var json = null;
  if (data[viewer.getId()]) {
    json = data[viewer.getId()]['gifts'];
  }
 
  if (!json) {
    globalGivenGifts = {};
  }
  try {
    globalGivenGifts = gadgets.json.parse(gadgets.util.unescapeString(json));
  } catch (e) {
    globalGivenGifts = {};
  }
 
  var html = new Array();
  html.push('You have given:');
  html.push('<ul>');
  for (i in globalGivenGifts) {
    if (i.hasOwnProperty) {
      html.push('<li>', friends.getById(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:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts part 3 - Showing Gifts">
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
      <script type="text/javascript">
        /* ... */
      </script>
      <div id='main'>
        <div id='give'>
          <form id='gift_form'>
            Give <span id='gifts'></span> to <span id='friends'></span>. <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>
          </form>
        </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.

Adding the ability to see what others have sent you isn't too difficult, but it does require some clever use of persistent storage. Currently an application can only write to the persistent storage of the viewer of the gadget, so this gadget stores all the gifts you've given in the 'gifts' field of the viewer's application data. When one of your friends views her instance of the gadget, the gadget will have to seek out the gifts that she has received by checking the 'gifts' field of each of her friends. In short, you can only write to your own storage, but you can read the application data of your friends (as long as they have the app installed, too).

To start, add another hook into the HTML as a placeholder for the list of received gifts.

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts part 4 - Showing What You Got">
    <Require feature="opensocial-0.8"/>
  </ModulePrefs>
  <Content type="html">
    <![CDATA[
      <script type="text/javascript">
        /* ... */
      </script>
      <div id='main'>
        <div id='give'>
          <form id='gift_form'>
            Give <span id='gifts'></span> to <span id='friends'></span>. <a href="javascript:void(0);" onclick='giveGift();'>Give!</a>
          </form>
        </div>
        <div id='given'></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 loadFriends to request the application data for the viewer's friends.

function loadFriends() {
  var req = opensocial.newDataRequest();
  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" });
  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');
 
  '''req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');'''
  req.send(onLoadFriends);
}

Then, update 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.)

function giveGift() {
  var nut = document.getElementById('nut').value;
  var friend = document.getElementById('person').value;
 
  givenGifts[friend] = nut;
  var json = gadgets.json.stringify(givenGifts);
 
  var req = opensocial.newDataRequest();
  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" });
  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');
 
  '''req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');'''
  req.send(onLoadFriends);
}

Third, update the callback function 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, updateReceivedList.

function onLoadFriends(data) {
  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();
  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);
  '''updateReceivedList(viewer, viewerFriendData, viewerFriends);'''
}

The final change, implementing updateReceivedList closely parallels 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.

function updateReceivedList(viewer, data, friends) {
  var viewerId = viewer.getId();
 
  var html = new Array();
  html.push('You have received:<ul>');
  friends.each(function(person) {
    if (data[person.getId()]) {
      var json = data[person.getId()]['gifts'];
 
      var gifts = {}
      if (!json) {
        gifts = {};
      }
      try {
        gifts = gadgets.json.parse(gadgets.util.unescapeString(json));
      } catch (e) {
        gifts = {};
      }
 
      for (i in gifts) {
        if (i.hasOwnProperty &amp;&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 giveGift, passing in both the gift and the friend who'll receive it.

function giveGift() {
  var nut = document.getElementById('nut').value;
  var friend = document.getElementById('person').value;
 
  globalGivenGifts[friend] = nut;
  var json = gadgets.json.stringify(globalGivenGifts);
 
  var req = opensocial.newDataRequest();
  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" });
  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');
 
  req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');
  req.send(onLoadFriends);
 
  '''postActivity(nut, friend);'''
}

Now, you might notice that the new function 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 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 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.

'''var globalFriends = {};'''
 
function onLoadFriends(data) {
  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();
  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;'''
 
  updateGiftList(viewer, giftData, viewerFriends);
  updateReceivedList(viewer, viewerFriendData, viewerFriends);
}

Now that this the helpful information is in the global scope, 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 createActivity function will trigger a page refresh. Since a refresh is not necessary here, the postActivity method supplies an empty function to execute instead.

function postActivity(nut, friend) {
  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() {});
}

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 giveGift function, that blindly overwrites the value of globalGivenGiftsfriend 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:

function giveGift() {
  var nut = document.getElementById('nut').value;
  var friend = document.getElementById('person').value;
 
  '''if (!globalGivenGifts) {
    globalGivenGifts = {};
  }
  if (!globalGivenGifts[friend]) {
    globalGivenGifts[friend] = new Array();
  }
  globalGivenGifts[friend].push(nut);'''
  var json = gadgets.json.stringify(globalGivenGifts);
 
  var req = opensocial.newDataRequest();
  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" });
  req.add(req.newFetchPersonAppDataRequest(viewer, 'gifts'), 'data');
 
  req.add(req.newFetchPersonAppDataRequest(viewerFriends, 'gifts', opt_params), 'viewerFriendData');
  req.send(onLoadFriends);
 
  postActivity(nut, friend);
}

Now, when you give a gift, the 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, globalGivenGiftsfriend 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, 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 updateGiftList you must add an additional loop when iterating over globalGivenGifts so that the code looks like the following:

function updateGiftList(viewer, data, friends) {
  var json = null;
  if (data[viewer.getId()]) {
    json = data[viewer.getId()]['gifts'];
  }
 
  if (!json) {
    globalGivenGifts = {};
  }
  try {
    globalGivenGifts = gadgets.json.parse(gadgets.util.unescapeString(json));
  } catch (e) {
    globalGivenGifts = {};
  }
 
  var html = new Array();
  html.push('You have given:');
  html.push('<ul>');
  for (i in globalGivenGifts) {
    if (i.hasOwnProperty) {
      '''for (j in globalGivenGifts[i]) {
        if (j.hasOwnProperty) {
          html.push('<li>', friends.getById(i).getDisplayName(), ' received ', globalGiftList[globalGivenGifts[i][j]], '</li>');
        }
      }'''
    }
  }
  html.push('</ul>');
  document.getElementById('given').innerHTML = html.join('');
}

Previously globalGivenGiftsi 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 updateReceivedList is modified in a similar manner:

function updateReceivedList(viewer, data, friends) {
  var viewerId = viewer.getId();
 
  var html = new Array();
  html.push('You have received:<ul>');
  friends.each(function(person) {
    if (data[person.getId()]) {
      var json = data[person.getId()]['gifts'];
 
      var gifts = {}
      if (!json) {
        gifts = {};
      }
      try {
        gifts = gadgets.json.parse(gadgets.util.unescapeString(json));
      } catch (e) {
        gifts = {};
      }
 
      for (i in gifts) {
        if (i.hasOwnProperty &amp;&amp; i == viewerId) {
          '''for (j in gifts[i]) {
            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 giftsi represents gifts given to you by your friend, the extra loop then displays those on the page.

And, just like that, your application now supports giving multiple gifts.

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 gadgets.io.makeRequest function to grab remote data.

First, add a method that requests a data file from a URL:

function requestGiftList(url) {
  var params = {};
  params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.JSON;
  gadgets.io.makeRequest(url, onGiftList, params);
}

The 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 makeRequest call, you'll see a reference to a nonexistent callback function named onGiftList which is not yet implemented. You'll do so now, by adding the following code:

function onGiftList(data) {
   if (data.data && data.data.length) {
     globalGiftList = data.data;
   }
}

If the response is appropriately formatted as an array, the onGiftList method will assign the returned data to the 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 requestGiftList method is called before any other requests are made. You'll also need to change onGiftList to continue application execution once it is finished.

Change your code to look like the following:

function onGiftList(data) {
   if (data.data && data.data.length) {
     globalGiftList = data.data;
   }
   '''loadFriends();
   makeOptionsMenu();'''
};
 
function init() {
   '''requestGiftList("http://example.com/gifts.json");'''
}

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

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

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

[
  "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 gifts.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 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 makeRequest call can be made.
  2. Wait for the 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.

Depending on the server and network conditions, this could mean that users may have to wait a couple of seconds before using the application — certainly not ideal!

Thankfully, you can request that the container preload this data as soon as it begins rendering the application. By adding a preload, this data will be instantly available to the application once it finishes loading and the extra latency will be eliminated.

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

<ModulePrefs title="Gifts part 7 - Working with Remote content">
    <Require feature="opensocial-0.8"/>
    '''<Preload href="http://example.com/gifts.json" />'''
</ModulePrefs>

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

That's it! With this line in place, the call to makeRequest will return data to 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 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.

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Gifts part 8 - Views">
    <Require feature="opensocial-0.8"/>
    '''<Require feature="views" />'''
    <Preload href="http://example.com/gifts.json" />
  </ModulePrefs>
  '''<Content type="html" view="home,profile,canvas">'''
    <![CDATA[
      <script type="text/javascript">
        /* ... */
      </script>
      <div id='main'>
        '''<div id='give' style='display: none;'>'''
          <form id='gift_form'>
            Give <span id='gifts'></span> to <span id='friends'></span>. <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>
          </form>
        </div>
        <div id='given'></div>
        <div id='received'</div>
        '''<div id='more' style='display: none;'>
          <a href='javascript:void(0);' onclick='navigateToCanvas();'>More</a>
        </div>'''
      </div>
    ]]>
  </Content>
</Module>

With these changes in place, modify onLoadFriends to render different sections of the above HTML depending on which view is currently selected. To simplify things, add a global variable globalView to keep track of the current view.

var globalView = gadgets.views.getCurrentView().getName();
 
function onLoadFriends(data) {
  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 == "canvas") {'''
    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);
}

The new version of 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 navigateToCanvas function that's called when the viewer clicks the "More" button the in "home" view.

function navigateToCanvas() {
  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 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.

Now your application should function properly in both the "home" and "canvas" views of iGoogle, presenting a limited amount of information in the "home" view and a link to the full-screen "canvas" view.

Complete gadget specification for version 8 But wait there's more ==

Congratulations, you've officially written an OpenSocial gadget, one that takes advantage of a number of API features including profile information and persistent storage. But, don't cut the party short—there's more work to be done. When you feel up to it, see if you can tackle some of the following issues on your own:

  • Use thumbnail images instead of display names
  • Use images instead of gift text
  • Use 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 getProxyUrl to cache images
  • Get the gadget running on another container that supports opensocial-0.8, like Hi5

And the rest...

Designing great social applications can be difficult, but the next steps are up to you. There's already a lot of documentation available, a helpful group for asking and answering questions, and a blog to keep you up to date. Here's to your application!

  • 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 #opensocial room on Freenode IRC will let you talk to developers in realtime.