SocialSprout: Using SproutCore in OpenSocial Apps Step by Step

Posted on 06 July 2008 by Johannes Fahrenkrug. Tags: JavaScript Programming SproutCore OpenSocial

I have been playing around with using SproutCore for developing OpenSocial applications. I want to share my insights about this. You should only read this if you know something about OpenSocial and at least a little something about SproutCore. Trust me, if you're doing anything halfway serious with web development you should know these two technologies, especially the latter. 
Enough of my yappin' already! We will build a super simple SproutCore OpenSocial application that will display the thumbnail pictures of your friends and give you a slider to alter their display size. Let's get started:

  1. Install the SproutCore gem (at least 0.9.13):
    sudo gem install sproutcore
  2. Create a new SproutCore application (I'll call it socialsprout):
    sproutcore socialsprout
  3. Change into that new directory.
  4. Create a new model called "friend_icon":
    sc-gen model socialsprout/friend_icon
  5. Create a new controller called "master":
    sc-gen controller socialsprout/master
  6. Open the project in Textmate (or any other editor... Just kidding: open it in TEXTMATE).
  7. Open the file socialsprout/clients/socialsprout/fixtures/friend_icon.js.
  8. With fixtures you can create some test data for your models, just like in Rails. So let's do that, put this in your friend_icon.js file (some of my WWDC08 pictures):
    // ==========================================================================
    // Socialsprout.FriendIcon Fixtures
    // ==========================================================================

    require('core') ;

    Socialsprout.FIXTURES = Socialsprout.FIXTURES.concat([

    { guid: 1,
    type: 'FriendIcon',
    url: 'http://farm4.static.flickr.com/3267/2591718129_04945449fb_m.jpg'
    },

    { guid: 2,
    type: 'FriendIcon',
    url: "http://farm4.static.flickr.com/3194/2591717747_c4b0fa9690_m.jpg"
    },

    { guid: 3,
    type: 'FriendIcon',
    url: "http://farm4.static.flickr.com/3157/2563126707_79db7ea0b3_m.jpg"
    },

    { guid: 4,
    type: 'FriendIcon',
    url: "http://farm4.static.flickr.com/3008/2563069825_7090454788_m.jpg"
    },

    { guid: 5,
    type: 'FriendIcon',
    url: "http://farm4.static.flickr.com/3101/2563894088_2b83235b3c_m.jpg"
    }

    ]);
  9. Now open the body.rhtml file:
  10. Replace it with this code (it creates a grid view for displaying images and a slider):
    <% content_for('body') do %>

    <% view :main do %>
    <div style="margin-top: 20px">
    <h1>Social Friends are Really Just Social Sprouts</h1>
    </div>
    <% scroll_view :grid_scroll_view, :outlet => true do %>
    <%= grid_view :grid_view, :outlet => true,
    :example_view => 'SC.ImageCellView',
    :content_value_key => 'url',
    :can_reorder_content => true,
    :row_height => 120,
    :column_width => 160,
    :bind => {
    :content => 'Socialsprout.masterController.arrangedObjects',
    :selection => 'Socialsprout.masterController.selection'
    } %>
    <% end %>

    50
    <%= slider_view :outlet => true,
    :minimum => 50,
    :maximum => 320,
    :step => 10,
    :bind => { :value => 'Socialsprout.masterController.friendIconSize' }
    %>
    320
    <% end %>
    by <a href="http://springenwerk.com">Johannes Fahrenkrug</a> in the Year of the Sprout

    <% end %>
    Ok, let me just explain this code a little bit: We create a view called "main". Within that view we create a scroll view called "grid_scroll_view" and within THAT we create a grid_view called - yup - "grid_view". Each cell will be a SC.ImageCellView and it's bound to the arrangedObjects property of our master controller and also to it's selection property. Our master controller will be an ArrayController, so whenever we add items to it or remove items from it, the grid_view will reflect those changes. And underneath we create a slider_view without a name that will be bound to the friendIconSize property of our master controller. Please note that this is only an example and claims in no way to follow any SproutCore best practices. I'm still learning this stuff just as you are!
  11. Next we'll add two items to the end of the body.css file in the same directory:
    .grid_scroll_view {
    height: 200px;
    }

    .main {
    height: 300px;
    }
  12. Next we'll edit our master controller in "master.js":
  13. Replace it with this code:
    // ==========================================================================
    // Socialsprout.MasterController
    // ==========================================================================

    require('core');


    Socialsprout.masterController = SC.ArrayController.create(
    /** @scope Socialsprout.masterController */ {

    friendIconSize: 150,

    friendIconSizeObserver: function() {
    if (this._friendIconSizeDidChangeTimer && this._friendIconSizeDidChangeTimer.get('isValid')) return ;

    this._friendIconSizeDidChangeTimer = this.invokeLater('friendIconSizeDidChange') ;
    }.observes('friendIconSize'),

    /**
    This is the actual method that updates the grid view with the new
    icon size. It computes an ideal row and column width based on the
    friendIconSize property and sets them. Then it calls updateChildren(true),
    which will force the collection view to relayout all of its children.

    Most of the time collection views will notice anytime a property affecting
    their content has changed and update accordingly. However if you change
    some shared properties such as this one that effect all layout, you may
    sometimes need to call updateChildren() manually.
    */
    friendIconSizeDidChange: function() {
    // clear the timer..
    this._friendIconSizeDidChangeTimer = null ;

    // get the friendIconSize and compute the new row and colum width
    var friendIconSize = this.get('friendIconSize') ;
    var columnWidth = friendIconSize ;
    var rowHeight = Math.floor((friendIconSize * 120) / 160) ; // ratio

    var view = SC.page.getPath('main.gridScrollView.gridView') ;
    if (view && ((view.get('columnWidth') != columnWidth) || (view.get('rowHeight') != rowHeight))) {
    view.beginPropertyChanges() ;
    view.set('rowHeight', rowHeight) ;
    view.set('columnWidth', columnWidth) ;
    view.endPropertyChanges() ;
    view.updateChildren(true) ;
    }
    },

    }) ;
    This sets up an ArrayController which will hold all of our friendIcons. It also has a property called friendIconSize which is observed and the grid layout is updated whenever it changes. This code was stolen from the SproutCore photo example. If you don't know what observers are, you should get this book. You should get it even if you know what they are.
  14. Now we have to tie it all together, so open the main.js file:
  15. Replace it with this code:
    // ==========================================================================
    // Socialsprout
    // ==========================================================================

    // This is the function that will start your app running. The default
    // implementation will load any fixtures you have created then instantiate
    // your controllers and awake the elements on your page.
    //
    // As you develop your application you will probably want to override this.
    // See comments for some pointers on what to do next.
    //
    function main() {

    Socialsprout.server.preload(Socialsprout.FIXTURES) ;
    Socialsprout.awakeApp();
    //Socialsprout.loadFriends();

    } ;

    Socialsprout.loadFriends = function() {
    var req=opensocial.newDataRequest();

    var friends_params = {};
    //friends_params[opensocial.DataRequest.PeopleRequestFields.FILTER] = opensocial.DataRequest.FilterType.HAS_APP;
    friends_params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS]=[opensocial.Person.Field.GENDER, opensocial.Person.Field.PROFILE_URL];
    //TODO make sure we get all the friends, there might be a 20 friends limit
    //params[opensocial.DataRequest.PeopleRequestFields.MAX]=10;

    req.add(req.newFetchPeopleRequest(opensocial.DataRequest.Group.OWNER_FRIENDS, friends_params), 'owner_friends');

    req.send(Socialsprout.loadFriendsCallback);
    }


    Socialsprout.loadFriendsCallback = function(data) {
    if (data.hadError()) {
    //I know this is ugly, but it's just a PoC, so c'mon...
    alert('error getting opensocial friends');
    } else {
    var owner_friends = data.get('owner_friends').getData();

    if (owner_friends.size() > 0) {
    friends_array = owner_friends.asArray();
    for (i = 0; i < friends_array.length; i += 1) {
    // create CS records
    var thumb_url = friends_array[i].getField(opensocial.Person.Field.THUMBNAIL_URL);
    console.log(thumb_url);
    SC.Store.addRecord( Socialsprout.FriendIcon.create({ 'url': thumb_url }) );
    }
    }
    }

    Socialsprout.awakeApp();
    }

    Socialsprout.awakeApp = function() {
    SC.page.awake() ;

    var icons = Socialsprout.FriendIcon.findAll();
    Socialsprout.masterController.set('content',icons);

    SC.page.get('main');
    }

  16. Alright, it's time to take it for a test drive. On your terminal inside the "socialsprout" project directory, run this:
    sc-server
  17. Open your browser at http://localhost:4020/socialsprout and you should see this:
    When you move the slider, the images should resize. That shows how great and powerful bindings are.
  18. All this was pure SproutCore so far (except for some methods in main.js as the observant reader might have noticed. We haven't used them yet, though). So to make this thing social (open-social, that is.... c'mon you saw that one coming) we have to simple put this file into your clients/socialsprout directory and name it "index.rhtml":
    <?xml version="1.0" encoding="UTF-8"?>
    <Module>
    <ModulePrefs title="SocialSprout"
    description="Test to see if sproutcore and opensocial work together"
    author="Johannes Fahrenkrug"
    author_link="http://springenwerk.com"
    height="300">
    <Require feature="opensocial-0.7"/>
    </ModulePrefs>
    <Content type="html">
    <![CDATA[

    <% #
    # This line should appear in your head area to include the stylesheets
    # generated by your client. If you need to include your own
    # stylesheets, you don't need to change it here. Instead, use the
    # :requires option in routes.rb.
    -%>
    <%= stylesheets_for_client(nil, :include_method => :import) %>
    <%= @content_for_page_styles %>


    <div class="sc-theme focus">
    <!-- Main Page Body -->
    <%= @content_for_body %>
    </div>

    <% #
    # This is where the resources you delcare will appear. You must
    # include the following three lines verbatim for the resources you
    # declare to be properly used.
    -%>
    <!-- Resources to be removed from DOM on page load -->
    <div id="resources" style="display:none; visibility: hidden;">
    <%= @content_for_resources -%>
    </div>

    <% #
    # This line should appear at the bottom of your page to include your
    # generated JavaScript and any libraries you reference. If you need
    # to include other javascripts, add them to the :requires option of
    # your client in routes.rb instead of changing it here.
    -%>
    <!-- Include Site Javascript -->
    <%= javascripts_for_client %>
    <%= @content_for_page_javascript %>

    <% #
    # The following lines to the closing body tag must be included at the
    # very end of your file. This will actually setup the JavaScript
    # views on your page and register SproutCore to start on page load.
    -%>
    <!-- Render Page Views -->
    <%= render_page_views %>

    <!-- Start SproutCore on Page Load -->
    <script type="text/javascript">gadgets.util.registerOnLoadHandler(SC.didLoad);</script>
    <%= @content_for_final %>
    ]]>
    </Content>
    </Module>

    Obviously you might want to edit some values in the ModulePrefs section.
  19. And now open sc-config:
  20. Insert these three lines after "c[:index_at] = ''"
      c[:resources_relative] = true
    c[:url_prefix] = 'http://socialsprout.googlecode.com/svn/trunk/tmp/build'
    c[:layout] = 'index.rhtml'
    The url_prefix value is important, you'll need to change it to the absolute value at which your app will be hosted. It has to point to the directory that will contain the sproutcore, socialsprout, and prototype sub directories. This is needed for OpenSocial because within OpenSocial you can't work with relative paths.

  21. We have to make SproutCore use our OpenSocial friends instead of my goofy WWDC pictures, so just comment one and uncomment two other lines in main.js so they look like this:
    function main() {

    //Socialsprout.server.preload(Socialsprout.FIXTURES) ;
    //Socialsprout.awakeApp();
    Socialsprout.loadFriends();

    } ;

  22. Now go back to the terminal and build your application:
    sc-build sproutcore
    sc-build prototype
    sc-build socialsprout
    Inside tmp/build you'll now find the prototype, sproutcore, and socialsprout subdirectories.
  23. Upload those three directories to your server, making sure that the actual URL matches the one you set in url_prefix.
  24. You'll find the OpenSocial gadget specification in [url_prefix]/socialsprout/en/index.html
  25. Now you can load your spiffy new SproutCore OpenSocial application in the OpenSocial container of your choice. Here are two screenshots from Orkut and MySpace:
    (I know, I know, my Orkut friend list is pretty pathetic)
  26. That's it!


This is really cool, since development with SproutCore is a lot of fun, like JavaScript on Rails. This also really just scratches the surface, but if you check out the methods in main.js, you can see that you can seamlessly integrate OpenSocial calls with SproutCore.
If you just want to try it out or download the code, go to the SocialSprout Google Code Project. And if you just want to try out the SocialSprout OpenSocial application within your favorite container, here's the link to the gadget spec: http://socialsprout.googlecode.com/svn/trunk/tmp/build/socialsprout/en/index.html.

If you have questions about this, please leave a comment. If you have pure OpenSocial questions, go to the OpenSocial Google Group and if you have pure SproutCore questions, go to the SproutCore Google Group.


Comments

Johannes Fahrenkrug said...

Hi Mark,

I haven't kept up with SproutCore for a while. You might want to ask you question on their mailing list. Sorry :(

- Johannes

March 01, 2010 06:16 AM

Mark Jaffe said...

Johannes,

Thanks for all the effort here. I tried this today and got a negative experience :-(

NoMethodError at /socialsprout
undefined method `view' for #

Ruby /Library/Ruby/Gems/1.8/gems/sproutcore-1.0.1046/lib/sproutcore/helpers/capture_helper.rb: in content_for, line 24
Web GET localhost/socialsprout
Jump to:
GET POST Cookies ENV
Traceback (innermost first)

/Library/Ruby/Gems/1.8/gems/sproutcore-1.0.1046/lib/sproutcore/helpers/capture_helper.rb: in content_for
eval "@content_for_#{name} = (@content_for_#{name} || '') + (capture(&block) || '')"...
When I installed sproutcore, I got version 1.0.1046 and my disk layout is different from what you show after doing sc-init step. For example, there is no english.lproj directory.

January 24, 2010 02:43 AM

Johannes Fahrenkrug said...

Thank you, man! I'm glad you find the site useful :)

December 25, 2009 07:21 AM

Dr Boolean said...

Hi Johannes,

I hate to be "that guy", but i'm really tossing and turning over the decision to learn objective-c(j) and use cappuccino or try to crack this sproutcore nut.

I see you work with cappuccino. Is there a reason you chose this over sproutcore?

I feel like i have to learn cocoa to grasp sproutcore anyway.

One other thing, have you found a rest implementation for cappuccino yet? Since sproutcore does it for you i thought i'd save tons of code in the long run. I might fork jester to work with json as a solution.

Anyway, great tutorial! I can't find hardly any others out there.

October 10, 2009 06:42 PM

Johannes Fahrenkrug said...

Hi Ashish,

Thank you for your comment! I'm not sure how to render files from your local disk. That will definitely not work when you try to run your OS app on Orkut or MySpace.

- Johannes

March 17, 2009 06:40 AM

Ashish Rai said...

Hi Johannes,

I tried with you this example step by step, its very nice example. I am also trying to access images on my local system, i gave the file URL in friend_icon.js but its not working. can you suggest me some way to render a images from my local system?

Thanks
Ashish

March 17, 2009 06:29 AM

Johannes Fahrenkrug said...

Hey william,

sorry for the late reply. Well, I'm not exactly sure what you mean. If you are talking about running sproutcore within facebook: I doubt that's possible since facebook took the "our own markup language" path instead of allowing full javascript applications. If you are talking about building a SproutCore app that communicates with facebook, then yes, the REST API would be the way to go, imho.

- Johannes

August 06, 2008 12:33 PM

william said...

what would the easiest way to implement facebook calls? The rest API? Thanks in advance.

August 03, 2008 06:45 AM

Enric said...

One problem I discovered is that for master.js:

Socialsprout.masterController = SC.ArrayController.create

there's a extra comma (,) at the end of the object properties. This works fine in FireFox and Safari. But fails in IE. So the extra comma at the end should be removed.

July 11, 2008 06:56 AM

Danny Brewer said...

Nice job.

I'm also trying to help the cause, by putting together a forum at www.sproutcoredev.com.

Feel free to come by and chip in.

July 09, 2008 11:03 PM

Comments

Please keep it clean, everybody. Comments with profanity will be deleted.

blog comments powered by Disqus