Social Mashup Tutorial (v0.9)

Overview

You can create engaging social experiences just by mashing up some data on the web with the HTML and JavaScript in a gadget. This tutorial will teach you how to create a social mashup by walking through the implementation of a simple, gift-giving app. You'll learn how to access arbitrary resources on the web as well as how to use App Data, the container's persistent storage, rather than communicating with your own application server.

Prerequisites

  • You'll need a place to host your gadget XML file. You can use any hosting solution as long as the gadget spec is accessible from the web (e.g. not behind a firewall).
  • You'll need a place to test your gadget. Most social networks provide developer sandboxes where you can try out new apps. I used the orkut sandbox while writing this tutorial.

The Profile and Canvas views

OpenSocial apps can define multiple views to generate UI appropriate for the context in which the app is being shown. For example, you can define a profile view to be shown when the app is rendered on a user's profile. The other common view on a typical social network is the canvas view, which is used to render a full page view of the app.

Here's the skeleton for the gift-giving app. The gadget XML file defines two content sections, one for the profile view and one for the canvas.

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Social Mashup Tutorial - Gifts (simple views)">
  </ModulePrefs>
  <Content type="html" view="profile">
    <![CDATA[
      Hello, Gifts! (profile view)
    ]]>
  </Content>
  <Content type="html" view="canvas, default">
    <![CDATA[
      Hello, Gifts! (canvas view)
    ]]>
  </Content>
</Module>

Note that you can define multiple views for each content section, as well as declare a default for containers to use when they don't support the other views you've defined.

The Owner and Viewer

OpenSocial defines two personas, the owner and the viewer. The owner is the user that owns the content on a particular page, while the viewer is the logged in user looking at a page. For example, if Alice is logged into a social network and she's viewing Bob's profile page, then Alice is the viewer and Bob is the owner. If Alice is viewing her own profile page, then she is the owner and the viewer.

It's also possible for Alice to view Bob's instance of an application. Suppose Alice is viewing an app on Bob's profile and clicks a link that takes her to the canvas view. In this case, Alice is the viewer of the canvas view, and Bob is the owner. If Alice is viewing her own instance of an app's canvas page, then she is the owner and the viewer. The latter case, where owner == viewer, is often used to let the user configure the settings of an app.

There are two ways to request user information in OpenSocial, and both have specific scenarios where they are most useful. The first scenario is when the app is rendering on page load. In this case you already know what social data you need to render your app, so you can declare it in the gadget spec using Data Pipelining. The second scenario is when you need to react to some user interaction and update the page. In this case you can use the JavaScript API to dynamically request the information you need. You shouldn't use the JavaScript API for the initial page load because it's much slower for the user: the JavaScript can't even start to execute until the container page has rendered, then there are HTTP round trips from the user's browser to the container for each request you make. Since the data pipelining requests are declared in the gadget spec, the container can prefetch the data and render the app immediately.

Intro to data pipelining

Let's update the gadget to display a greeting to the viewer and print the owners name. Since you already know the data to display (the owner and viewer's name), you can declare your requests using data pipelining tags. To do this, you first need to include <Require feature="opensocial-data"/> in the ModulePrefs element. Then you can add data request tags in the Content section for the profile view:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Social Mashup Tutorial - Gifts (data request tags)">
    <Require feature="opensocial-data"/>
  </ModulePrefs>
  <Content type="html" view="profile">
    <script type="text/os-data" xmlns:os="http://ns.opensocial.org/2008/markup">
      <os:ViewerRequest key="viewer" fields="displayName"/>
      <os:OwnerRequest key="owner" fields="displayName"/>
    </script>
  </Content>
  <!-- Content section for canvas view omitted -->
</Module>

Notice that the data pipelining requests declared within a script tag, where the type is text/os-data. This allows the markup in the Content section to remain valid HTML, and any renderer can choose to parse or ignore the content of the script tag based on the type.

The information returned from data pipelining requests is available in the app's data context. Just like requesting data from the container, there are two ways to access the data context: programmatic access via JavaScript or declarative access via OpenSocial Templates. Again, the JavaScript API is useful for responding to user interaction, but you'll want to use the declarative solution for the initial page render.

Intro to OpenSocial Templates

To use OpenSocial Templates, first add <Require feature="opensocial-templates"/> to the list of features declared in the ModulePrefs element. Just like data pipelining requests, templates are declared in a script tag, but the type is "text/os-template". You can then use variable substitution to access the social data, using the values of the key attributes from the data request tags as variable names. To add a greeting for the viewer and attribution for the owner, you would insert the following script tag in to the Content section for the profile view.

<script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup">
  Hello, {$viewer.displayName}!
  <br/>
  You're looking at {$owner.displayName}'s profile.
</script>

An important thing to note is that many OpenSocial containers don't give you access to the viewer's data if that user hasn't installed your app. It's always good to plan for this case, since many users will not have your app installed the first time they view it (having been redirected from a link in a friend's activity stream or something similar). OpenSocial Templates support conditional statements, so if you can't access the viewer's name to greet them, you can avoid printing out something like "Hello, undefined!". Here's an example of what that template might look like:

<script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup">
  Hello<os:If condition="${viewer.displayName!=null}">, ${viewer.displayName}</os:If>!
  <br/>
  You're looking at {$owner.displayName}'s profile.
</script>

If you've been coding along as we go, your gadget XML file should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Social Mashup Tutorial - Gifts (owner and viewer)">
    <Require feature="opensocial-data"/>
    <Require feature="opensocial-templates"/>
  </ModulePrefs>
  <Content type="html" view="profile">
    <![CDATA[
      <script type="text/os-data" xmlns:os="http://ns.opensocial.org/2008/markup">
        <os:ViewerRequest key="viewer" fields="displayName"/>
        <os:OwnerRequest key="owner" fields="displayName"/>
      </script>
      <script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup">
        Hello<os:If condition="${viewer.displayName!=null}">, ${viewer.displayName}</os:If>!
        <br/>
        You're looking at ${owner.displayName}'s profile.
      </script>
    ]]>
  </Content>
  <Content type="html" view="canvas">
    <![CDATA[
      Hello, Gifts! (canvas view)
    ]]>
  </Content>
</Module>

More complex data and views

Now that you know the basics, let's use data pipelining and OpenSocial Templates to create a more complex user interface in the canvas view to let users give gifts to their friends. Canvas views provide more space for your app to work with and containers are typically less restrictive with what content is allowed. For example, some containers do not allow advertisements or JavaScript in the profile view.

To let users select a gift to give to their friends you'll need a list of gifts for them to choose from, and you'll need access to their friends list. You can get the list of friends from the container, but the list of gifts will need to come from some external source (in this case a text file that contains JSON). The next couple sections will describe how to access data from the container and from the web.

Accessing friends

The first feature to add to the canvas view will be a drop down menu where the user can select a friend to receive their gift. To access a group of people, use a <os:PeopleRequest> tag and specify the users you want to retrieve using the userId and groupId attributes. This application will use userId="@viewer" and groupId="@friends" to request a list of the viewer's friends.

Here's the script tag you can add to the canvas view's Content section to declare the request for the viewer's friends. Notice that the count attribute is used to specify the maximum number of friends to return.

<script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
  <os:PeopleRequest key="viewerFriends" userId="@viewer" groupId="@friends" fields="displayName" count="100"/>
</script>

Accessing the web

For the list of gifts, or any content from the web, use a <os:HttpRequest> tag and specify the URL of the content in the href attribute. This application will use href="http://graargh.returnstrue.com/lane/opensocial/v09/gifts.json" to request a JSON array of available gifts for the user to choose from.

<os:HttpRequest key="gifts" format="json" href="http://graargh.returnstrue.com/lane/opensocial/v09/gifts.json"/>

More OpenSocial Templates

Now that you've requested the data, you can access it using OpenSocial templates for the initial page render. Here is a template that displays two drop down menus, one for selecting a gift, and one for selecting a friend to give it to. Remember, if the current viewer doesn't have the app installed (i.e. they are viewing someone else's canvas view), then the list of friends won't be available and you should display and appropriate message instead of the dropdown menus.

<script type="text/os-template"  xmlns:os="http://ns.opensocial.org/2008/markup">
  <os:If condition="${viewerFriends.list==null}">
    Please install this app so you can give gifts to your friends.
  </os:If>
  <os:If condition="${viewerFriends.list!=null}">  <!-- The gift-giving selection form -->
    <form id='gift_form'>
      Give
      <select id="gift">
        <option repeat="${gifts.content}" var="gift" value="${gift.id}">
          ${gift.name}
        </option>
      </select>
      to
      <select id="person">
        <option repeat="${viewerFriends}" var="person" value="${person.id}">
          ${person.displayName}
        </option>
      </select>
      <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>
    </form>
  </os:If>
</script>

Note that clicking the 'Give!' link invokes a JavaScript function. For now, just use alert to display the selected friend and gift IDs:

<script type="text/javascript">
function giveGift() {
  var msg = ['Give giftId:',
             document.getElementById('gift').value,
             ' to userId:',
             document.getElementById('person').value];
  alert(msg.join(''));
  }
</script>

At this point your gadget XML file should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Social Mashup Tutorial - Gifts (selecting gifts)">
    <Require feature="opensocial-data"/>
    <Require feature="opensocial-templates"/>
  </ModulePrefs>
  <!-- Content section for profile view omitted -->
  <Content type="html" view="canvas">
    <![CDATA[
      <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
        <os:PeopleRequest key="viewerFriends" userId="@viewer" groupId="@friends" fields="displayName" count="100"/>
        <os:HttpRequest key="gifts" format="json" href="http://graargh.returnstrue.com/lane/opensocial/v09/gifts.json"/>
      </script>

      <script type="text/os-template"  xmlns:os="http://ns.opensocial.org/2008/markup">
        <os:If condition="${viewerFriends.list==null}">
          Please install this app so you can give gifts to your friends.
        </os:If>
        <os:If condition="${viewerFriends.list!=null}">  <!-- The gift-giving selection form -->
          <!-- The gift-giving selection form -->
          <form id='gift_form'>
            Give
            <select id="gift">
              <option repeat="${gifts.content}" var="gift" value="${gift.id}">
                ${gift.name}
              </option>
            </select>
            to
            <select id="person">
              <option repeat="${viewerFriends}" var="person" value="${person.id}">
                ${person.displayName}
              </option>
            </select>
            <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>
          </form>
        </os:If>
      </script>

      <script type="text/javascript">
        function giveGift() {
          var msg = ['Give giftId:',
                     document.getElementById('gift').value,
                     ' to userId:',
                     document.getElementById('person').value];
          alert(msg.join(''));
        }
      </script>
    ]]>
  </Content>
</Module>

Working with App Data

Now that you've added the ability for a user to give gifts to their friends, you'll want to add a way to store each transaction so that the next time the user logs in, they'll see all the gifts they've given. OpenSocial containers provide a storage mechanism called App Data, which is a way for your app to store arbitrary key/value pairs for each user.

In this gift-given application, you'll create a JavaScript object with recipientId and giftId properties to represent each transaction. For each user, you'll create an array of these objects that represents all the gifts that user has given. Since App Data only supports storing strings, you need to store this array of JavaScript objects as a JSON string.

In order to perform operations on App Data, you need to include the 'osapi' feature by including the following line in the ModulePrefs element of your gadget XML file:

<Require feature="osapi"/>

Storing information in App Data

When the user clicks the 'Give!' link, the giveGift function should check which gift and friend are currently selected, and store a transaction in App Data that represents the viewer giving a gift to the selected user. To accomplish this, here's a naive implementation of the giveGift function that creates an array with a single element and update the App Data store (clobbering anything that was previously stored in App Data):

<script type="text/javascript">
  function giveGift() {
    // Create a transaction object based on the currently selected values in the drop down menu
    var newTransaction = {
      'recipientId': document.getElementById('person').value,
      'giftId': document.getElementById('gift').value
    }
    var transactions = [];
    transactions.push(newTransaction);

    // Create an object of key/value pairs to store in App Data
    var appdata = {
      'transactions': gadgets.json.stringify(transactions),
    }

    // Store the transactions in App Data (overwriting any previous values)
    osapi.appdata.update({data: appdata,
                          appId: "@app",
                          keys:['transactions'],
                          fields:['transactions']}).execute(giveGiftCallback);
  }

  function giveGiftCallback(response) {
    if (response.error) {
      alert("There was an error updating App Data: " + response.error.message);
    } else {
      alert("App Data updated, please refresh the page to see your update");
    }
  }
</script>

Accessing information from App Data

You can use data pipelining to access the information stored in App Data. When you request App Data, the JavaScript object returned will have properties named after the ID of the user that the data is stored for. In this case you want the viewer's data, so you'll need to add two data pipelining tags – one to request the App Data and one to request the viewer's ID (so you can dereference the correct property from the App Data object that is returned). Here are the tags you'll want to add to the existing data pipelining <script> element:

<os:ViewerRequest key="viewer"/>
<os:PersonAppDataRequest key="giftsGiven" userId="@viewer" fields="transactions" appId="@app"/>

Now you can reference this app data in your OpenSocial templates. Here is some template code that will display the name of the user and the gift they received by looking up these values based on their ID. You can add this to your existing template for the canvas view.

Gifts you have given:
<ul>
  <li repeat="${osx:parseJson(giftsGiven[viewer.id].transactions)}" var="transaction">
    <!-- Lookup the recipient's name based on their user ID -->
    <span repeat="${viewerFriends.list}" var="person" if="${transaction.recipientId == person.id}">
      ${person.displayName}
    </span>
      received
    <!-- Lookup the gift's name based on it's user ID -->
    <span repeat="${gifts.content}" var="gift" if="${transaction.giftId == gift.id}">
      ${gift.name}
    </span>
  </li>
</ul>

Reloading the Canvas View

So far, you've added code for storing a transaction in app data, and displaying any transactions that already exist when the page is rendered. To complete the user experience, you need to refresh the app after a gift is given. That way the gift will be stored in App Data, then the app will be refreshed and have access to the most recent App Data.

Since the osapi.appdata.update function already calls a callback function when it completes, you can add the code to refresh the app in this callback. To refresh the app, you can tell the container to navigate to the canvas view. To navigate between views on some containers you'll need to include the following line in the ModulePrefs element of your gadget XML file:

<Require feature="views"/>

In addition to telling the container to reload the canvas view, you need to add a unique value as a view param or else the container may ignore the refresh, thinking that you're loading a page that is has already cached.

function giveGiftCallback(response) {
  if (response.error) {
    alert("There was an error updating App Data: " + response.error.message);
  } else {
    var canvas = gadgets.views.getSupportedViews()["canvas"];
    gadgets.views.requestNavigateTo(canvas, {unique:new Date().getTime()});
  }
}

If you've been following along, your gadget XML file should look something like this:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs title="Social Mashup Tutorial - Gifts (using app data)">
    <Require feature="opensocial-data"/>
    <Require feature="opensocial-templates"/>
    <Require feature="osapi"/>
    <Require feature="views"/>
  </ModulePrefs>
  <!-- Content section for profile view omitted -->
  <Content type="html" view="canvas">
    <![CDATA[
      <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
        <os:ViewerRequest key="viewer"/>
        <os:PeopleRequest key="viewerFriends" userId="@viewer" groupId="@friends" fields="displayName" count="100"/>
        <os:HttpRequest key="gifts" format="json" href="http://graargh.returnstrue.com/lane/opensocial/v09/gifts.json"/>
        <os:PersonAppDataRequest key="giftsGiven" userId="@viewer" fields="transactions" appId="@app"/>
      </script>

      <script type="text/os-template"  xmlns:os="http://ns.opensocial.org/2008/markup">
        <!-- The gift-giving selection form -->
        <form id='gift_form'>
          Give
          <select id="gift">
            <option repeat="${gifts.content}" var="gift" value="${gift.id}">
              ${gift.name}
            </option>
          </select>
          to
          <select id="person">
            <option repeat="${viewerFriends}" var="person" value="${person.id}">
              ${person.displayName}
            </option>
          </select>
          <a href='javascript:void(0);' onclick='giveGift();'>Give!</a>
        </form>
        <div>
          Gifts you have given:
              <ul>
                <li repeat="${osx:parseJson(giftsGiven[viewer.id].transactions)}" var="transaction">
                  <!-- Lookup the recipient's name based on their user ID -->
                  <span repeat="${viewerFriends.list}" var="person" if="${transaction.recipientId == person.id}">
                    ${person.displayName}
                  </span>
                  received
                  <!-- Lookup the gift's name based on it's user ID -->
                  <span repeat="${gifts.content}" var="gift" if="${transaction.giftId == gift.id}">
                    ${gift.name}
                  </span>
                </li>
              </ul>
        </div>
      </script>

      <script type="text/javascript">
          function giveGift() {
            // Create a transaction object based on the currently selected values in the drop down menu
            var newTransaction = {
              'recipientId': document.getElementById('person').value,
              'giftId': document.getElementById('gift').value
            }
            var transactions = [];
            transactions.push(newTransaction);

            // Create an object of key/value pairs to store in App Data
            var appdata = {
              'transactions': gadgets.json.stringify(transactions),
            }

            // Store the transactions in App Data (overwriting any previous values)
            osapi.appdata.update({data: appdata,
                                  appId: "@app",
                                  keys:['transactions'],
                                  fields:['transactions']}).execute(giveGiftCallback);
          }

          function giveGiftCallback(response) {
            if (response.error) {
              alert("There was an error updating App Data: " + response.error.message);
            } else {
              var canvas = gadgets.views.getSupportedViews()["canvas"];
              gadgets.views.requestNavigateTo(canvas, {unique:new Date().getTime()});
            }
          }
      </script>
    ]]>
  </Content>
</Module>

Next steps

In this tutorial you learned about the owner and viewer personas, navigating between views, and the container's data store. Next, try applying some of this knowledge to code up solutions to these exercises.

Out-of-the-box experience

The first time someone uses this app, they won't have given any gifts. Add some logic so that if the user has not given any gifts, the app displays an appropriate message. Remember, if the user has not given any gifts, then their translations string in App Data will be null.

Add some debug output

It always helps to see what's going on behind the scenes in your app. You can access the values returned from the data pipelining calls in your JavaScript code by referencing the data context. Output the results of the data pipelining calls in the HTML of your app, using the following API for accessing the data context:

// Get the entire set of data returned from data pipelining calls
var context = opensocial.data.getDataContext();

// Use the keys from the data pipelining tags to access individual elements
var viewer = context.getDataSet('viewer');

var html = "The viewer's name is " + viewer.displayName;
document.getElementById('debugDiv').innnerHTML = html;

Support multiple gifts

Users will want to give gifts to all their friends, so you should allow for multiple gifts. The app already uses an array to store the transactions in App Data, so to support multiple gifts you'll need to check the data context for the value returned from the giftsGiven data pipelining call. If there are existing transactions, you should append a new transaction to the array and save it back to App Data. Here's some code for accessing the existing transactions to get you started:

// Get the entire set of data returned from data pipelining calls
var context = opensocial.data.getDataContext();

// Use the keys from the data pipelining tags to access individual elements
var giftsGiven = context.getDataSet('giftsGiven');
var viewer = context.getDataSet('viewer');

// Remember that the info in app data is keyed by user ID and stored as a JSON string
var jsonString = giftsGiven[viewer.id].transactions;
var transactions = gadgets.json.parse(jsonString);

Add a profile view

Now that you've got a canvas view, try updating the profile view to show a list of the gifts the owner has sent. To get started, use the <os:OwnerRequest> tag to get the owner's information, and reuse code from the canvas view template that is appropriate for the profile view.

View source

You can find a full implementation of the gift-giving app in this tutorial (including all the exercises) at http://graargh.returnstrue.com/lane/opensocial/v09/mashup/gifts.xml.