(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.
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.
|
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.
|
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.
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.
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:
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
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>
|
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
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
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
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 && 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
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
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 && 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
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:
makeRequest
call can be made.makeRequest
call to finish and return data so that social data can be requested.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
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:
requestNavigateTo
to navigate back to the "home" viewgetProxyUrl
to cache imagesDesigning 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!
#opensocial
room on Freenode IRC will let you talk to developers in realtime.