Advertisement

Build a Micro-Blog With SproutCore

by
Student iconAre you a student? Get a yearly Tuts+ subscription for $45 →

SproutCore is a revolutionary JavaScript framework for creating client-side desktop-class applications that run in the browser. This tutorial will introduce you to the core concepts within SproutCore by developing a simple Twitter like micro-blog.

This tutorial contains both a screencast and a written tutorial!


Screencast



Written Tutorial


What is SproutCore?

SproutCore - unlike jQuery, which is primarily a DOM library - is a MVC framework for client-side desktop-class applications written in JavaScript and heavily influenced by Cocoa (Apple's Mac OSX development framework). It differs itself from other JavaScript libraries because it focuses on providing all the tools required to build a full desktop-like application in the browser - like routing, view controllers, UI components, data store layer, unit testing and deployment tools. Using SproutCore means moving your business logic client side which significantly decreases latency because your application only needs to reach back to the server for data (and to perform business logic you may not want client side).

Using SproutCore means moving your business logic client side which significantly decreases latency


Step 1 - Installing SproutCore

SproutCore is distributed as a gem. To install SproutCore, simply run sudo gem install sproutcore in the Terminal (other ways to install SproutCore can be found here). Despite SproutCore being a RubyGem and coming bundled with a development server, once deployed, SproutCore is nothing more than pure HTML, JavaScript and CSS. To build an application with SproutCore, you use JavaScript to define, views utilizing bundled UI elements provided by SproutCore or custom views. These UI elements render the HTML for us, and then with SproutCore's CSS framework, called Chance, we can style the UI. Using controllers, we can hook these views up to the data in the application. Lastly, using SproutCore's DataSource model we will hook up the application to CouchDB.


Step 2 - Creating Your First App

  1. Open the terminal.
  2. Run sc-init microblog creating the base SproutCore application.
  3. cd microblog
  4. sc-server which starts the SproutCore development server.
  5. Open http://localhost:4020/microblog in your browser and your should see "Welcome to SproutCore!" (Note: you won't need to restart the SC server each time you make changes to your source, just refresh the browser window).


Step 3 - Getting Acquainted With The SproutCore Files

Let's go through each file in the generated SproutCore source and explain what each part does. The source code starts in the apps/microblog folder.

  • core.js - contains your applications configuration settings and global singletons.
  • main.js - the main starting point of your application it should set the main view, initiate your primary controller and load the required data for the main view.
  • resources/loading.rhtml - this HTML is shown while the SproutCore app is loaded and initiated, it's not visible very long as SproutCore loads fairly quickly.
  • resources/main_page.js - holds the main page (and it's views) loaded by default, by the code, in main.js.

Step 4 - Setting Up The Model

Before we can start building the UI, the SproutCore application needs some data. Let's begin by defining the model. SproutCore comes with a handy model generator. Run sc-gen model Microblog.posts to generate the base model JavaScript files for the post objects. We need to tell SproutCore about the specific data the post objects will contain. Go ahead and open apps/microblog/models/posts.js and add the attributes below.

 
Microblog.Posts = SC.Record.extend( 
  /** @scope Microblog.Posts.prototype */ { 
 
  post:	SC.Record.attr(String), 
  published: SC.Record.attr(Number) 
}) ;

Microblog.Posts extends SC.Record and names the data in which the post objects should contain. We use methods provided by SproutCore to define the types of data we expect in each attribute.


Step 5 - Adding Fixture Data

In this tutorial, we will initially use Fixtures to populate our model rather than complicating ourselves with a backend server just yet. Throughout the article, if we make changes to data in the application through the UI, it will not persist if we reload the application - until we've added data persistence, via CouchDB. Open apps/microblog/fixtures/posts.js where we will specify some dummy data; insert the data as below.

 
sc_require('models/posts'); 
 
Microblog.Posts.FIXTURES = [ 
{ 
	guid: 1, 
	post: "Wow, Nettuts rocks!", 
	published: 1297986988 
}, 
{ 
	guid: 2, 
	post: "Apple's MobileMe was built using SproutCore", 
	published: 1297987009 
}, 
{ 
	guid: 3, 
	post: "Checkout this awesome JavaScript application framework called SproutCore", 
	published: 1298159746 
} 
];

To verify that our model is working correctly, we can call the SproutCore data methods from within a JavaScript console. Reload the page, open the console and execute posts = Microblog.store.find(Microblog.Posts). This should load all the posts fixtures into the posts variable. We need to iterate the posts variable with the SproutCore getEach method to view anything useful. Execute posts.getEach('post') to view all the posts.


Step 6 - Laying Out The Interface

We are going to jump ahead a bit now and build the UI. The reason for doing so is because SproutCore uses data binding to attach the values of UI elements to JavaScript objects; the result of these bindings is the values in the bound JavaScript objects are inserted into the HTML as you'd expect, but with one difference: a link is made between its value and the object. Any subsequent updates to the object in the JavaScript will be automatically reflected in the UI. Thus, we are going to build the UI first, and then implement a controller to bind the data to that UI. Let's get started: open the resources/main_page.js file, and let's give our application a top level navigation toolbar, and a list view.

 
Microblog.mainPage = SC.Page.design({ 
	mainPane: SC.MainPane.design({ 
		childViews: 'topView postView contentView'.w(), // SC helper method splits string to array. 
 
		topView: SC.ToolbarView.design({ 
			layout: { top: 0, left: 0, right: 0, height: 40 }, 
			anchorLocation: SC.ANCHOR_TOP 
		}), 
 
		postView: SC.View.design({ 
			childViews: 'form'.w(), 
			layout: { top: 40, height: 75 }, 
			backgroundColor: 'white', 
 
			form: SC.View.design({ 
					layout: { left: 200, right: 200 }, 
					backgroundColor: "black" 
			}) 
		}), 
 
		contentView: SC.ScrollView.design({ 
			hasHorizontalScroller: NO, 
			layout: { top: 115, bottom: 0, left: 0, right: 0 }, 
			contentView: SC.ListView.design({ 
 
			}) 
		}) 
	}) 
});

Your SproutCore application can have more than one page, but here you can see that we've define just the one page. Within this page, we define three child views; we've defined them both in the childViews attribute and then specified their contents within the object. The topView we define as a ToolbarView and anchor it to the top of the page. You'll notice that we haven't specified a width; this actually tells SproutCore that we want it to fill the available space and resize with the browser window.

Next, the postView we define as a standard View, which is full width, and give it a single sub view form. This will contain the microblog posting form. We don't specify a width, but we instruct the view to be 200 pixels from the right and 200 pixels from the left, making the view narrower than the browser window, but maintaining the auto resizing functionality. The main content lives in contentView, which we've defined as a ScrollView. The content of this ScrollView will be a ListView, which will contain our microblog posts.



Step 7 - Adding Controls

Now that we've got the initial layout done, let's add a form to allow a user to add posts.

 
Microblog.mainPage = SC.Page.design({ 
	mainPane: SC.MainPane.design({ 
	childViews: 'topView postView contentView'.w(), // SC helper method splits string to array. 
 
	topView: SC.ToolbarView.design({ 
		childViews: "appName".w(), 
		layout: { top: 0, left: 0, right: 0, height: 40 }, 
		anchorLocation: SC.ANCHOR_TOP, 
 
		appName: SC.LabelView.design({ 
			layout: { top: 5, left: 5, width: 100 }, 
			displayValue: "Microblog App", 
			layerId: "mb-logo" // HTML id attribute 
		}) 
	}), 
 
	postView: SC.View.design({ 
		childViews: 'form'.w(), 
		layout: { top: 40, height: 150 }, 
		backgroundColor: 'white', 
 
		form: SC.View.design({ 
			childViews: 'postContent post'.w(), 
			layout: { left: 200, right: 200 }, 
 
			postContent: SC.TextFieldView.design({ 
				layout: { top: 10, left: 10, right: 10, height: 60 }, 
				isTextArea: YES, 
				hint: "What's on your mind?" 
			}), 
 
			post: SC.ButtonView.design({ 
				layout: { top: 80, right: 10, width: 100 }, 
				title: "Post", 
				isDefault: YES 
			}) 
		}) 
	}), 
 
	contentView: SC.ScrollView.design({ 
	  hasHorizontalScroller: NO, 
	  layout: { top: 150, bottom: 0, left: 0, right: 0 }, 
	  contentView: SC.ListView.design({ 
 
	  }) 
	}) 
  }) 
});

The first thing we've added is a SC.LabelView to the toolbar with the title "Microblog App". Next, we've modified the form subview to include two child views, first postContent, which is a SC.TextFieldView and enabled isTextArea which makes it a multiline textarea. Secondly, we've added a SC.ButtonView, which we'll assign an action that will add the post when it's clicked. These both are standard SproutCore UI elements. SproutCore has a comprehensive UI catalog baked right in, take a look at the kitchen sink example to gain an idea of what's included. All of these elements can be themed with CSS using SproutCore's theme framework.



Step 8 - Binding The Posts

Let's bind the posts data in the Microblog.Posts.FIXTURES to the SC.ListView within the app so we can see the posts. First we need to create a controller. Run the SproutCore generator like below to produce a boilerplate SC.ArrayController.

 
sc-gen controller Microblog.postsController SC.ArrayController

We don't need to do anything with the generated controller files at the moment, so we are going to leave them alone. SC.ArrayController and SC.ObjectController actually act as proxies to their contents under the hood of SproutCore. You can bind directly to the controller as if you were binding to its contents. As you'll see in a moment, we'll set the content of this controller to the posts. This controllers content (the posts) will be bound to the SC.ListView, which will display them. Because the controller acts as a proxy, we can set a new array of posts to the controllers content (say posts 10 - 20 for example) and the UI will automatically update. Any changes that are also made to the underlying data set (the posts array) will be reflected by the UI automatically.

Now we need to tell the view about this controller and create the binding. Open main_page.js where all the view code lives and adjust the contentView to below.

 
contentView: SC.ScrollView.design({ 
  hasHorizontalScroller: NO, 
  layout: { top: 150, bottom: 0, left: 0, right: 0 }, 
  contentView: SC.ListView.design({ 
	contentBinding: "Microblog.postsController.arrangedObjects", 
	selectionBinding: "Microblog.postsController.selection" 
  }) 
})

Now the UI is bound to the contents of Microblog.postsController. Remember: postsController acts as a proxy to its contents. At the moment, if we run the application, we have no data set in postsController and, thus, the SC.ListView is still empty. We need to put data into the controller - we are going to do this as soon as the application starts, so open main.js and add the following to the main function.

 
var posts = Microblog.store.find(Microblog.Posts); 
Microblog.postsController.set("content", posts);

Reload the browser, and you should see something like the image below:



Step 9 - Creating The Post List View

Right now, the SC.ListView renders each post object as a string - not particularly useful for users. In this step, we are going to extend SC.View to provide our own view/html for each row in the SC.ListView.

First, open the Terminal and cd to your application directory and run:

 
	sc-gen view Microblog.PostListView

Change the generated view template to look like below:

 
Microblog.PostListView = SC.View.extend( 
  /** @scope Microblog.PostListView.prototype */ { 
  publishedDateAsString: function (timestamp) { 
	var date = SC.DateTime.create(timestamp), 
		now = SC.DateTime.create(), 
		dateFormat = "%d/%m", 
		dateStr; 
 
	if (SC.DateTime.compareDate(date, now) >= 0) { 
		dateStr = "Today"; 
	} else { 
		dateStr = date.toFormattedString(dateFormat); 
	} 
 
	return dateStr+" @ "+date.toFormattedString("%H:%M:%S"); 
  }, 
  render: function (context) { 
	var content = this.get("content"), 
		post = content.get("post"); 
 
	var context = context.begin().addClass("microblog-post"); 
	context.push("<h2>", post, "</h2>"); 
 
	context.push("<span>", this.publishedDateAsString( content.get("published") * 1000 ), "</span>"); 
	context.end(); 
 
	sc_super(); 
  } 
});

The publishedDateAsString() method is a helper that compares the published date to today's date and returns "Today @ 12:00:00" instead of "17/03 @ 12:00:00". It uses SproutCore's SC.DateTime object to format and compare the dates. Have a look at the API documentation for more information on this object. The render function is where we define our custom view and its HTML. Passed into the render function is the views render context. The render function is called initially when the view is drawn and after any subsequent updates to the data. At the top of the render function we get the content object of this list view row and the post data. Then, we begin our rendering context with context.begin(), which defines a new container div.

We're also adding a new CSS class to this div microblog-post using addClass(). Inside this context, we push two more elements, <h2> which contains the post and <span>, which uses the publishedDateAsString() to format the published date. Note: we store the date in seconds, and, before we pass the published date to publishedDateAsString(), we convert it to milliseconds. Once we've completed the intervals of the single list view row, we call context.end(), which closes the render context and sc_super() to call the super's render() method.

Next, we need to tell the SC.ListView to use the custom list view row we've defined. Open main_page.js and change the contentView to below:

 
contentView: SC.ScrollView.design({ 
  hasHorizontalScroller: NO, 
  layout: { top: 150, bottom: 0, left: 0, right: 0 }, 
  backgroundColor: "white", 
  contentView: SC.ListView.design({ 
	layout: { left: 200, right: 200 }, 
	contentBinding: "Microblog.postsController.arrangedObjects", 
	selectionBinding: "Microblog.postsController.selection", 
	exampleView: Microblog.PostListView, 
	rowHeight: 60 
  }) 
})

The exampleView attribute links to the custom view object and is used when rendering the list view rows. We've also given the SC.ListView the same right and left margin.



Step 10 - Adding a New Post

Let's hook up the form so that, when submitted a new post, object is created. Open controllers/posts.js, which contains Microblog.postsController and add another method called addPost to the controller as defined below.

 
Microblog.postsController = SC.ArrayController.create( 
/** @scope Microblog.postsController.prototype */ { 
  addPost: function () { 
	var postField = Microblog.mainPage.mainPane.get("contentField"), 
		postContent = postField.getFieldValue(), 
		published = new Date().getTime() / 1000; 
 
	var newPost = Microblog.store.createRecord(Microblog.Posts, { 
		post: postContent, 
		published: published 
	}); 
 
	postField.setFieldValue(''); // Rest the post field value. 
 
	return YES; 
  } 
}) ;

In this method, we use a SC.outlet to get the value of the SC.TextFieldView within our UI and create a new record using SproutCore's DataSource of the type Microblog.Posts. After creating the object, we reset the field's value to an empty string.

Next, open up main_page.js and hook up the "Post" button to the addPost method on the controller.

 
post: SC.ButtonView.design({ 
	layout: { top: 80, right: 10, width: 100 }, 
	title: "Post", 
	isDefault: YES, 
	target: "Microblog.postsController", 
	action: "addPost" 
})

We define the target attribute as the Microblog.postsController, and the action is the new method addPost on that controller.

Finally, to make this all work, we need to define a SC.outlet on the mainPane object so that the addPost method can access the SC.TextFieldView object within the view. Add the following property to Microblog.mainPage.mainPane, which defines an outlet called contentField.

 
contentField: SC.outlet("postView.form.postContent"),

The final code within main_page.js should appear as shown below:

 
Microblog.mainPage = SC.Page.design({ 
	mainPane: SC.MainPane.design({ 
	contentField: SC.outlet("postView.form.postContent"), 
 
	childViews: 'topView postView contentView'.w(), // SC helper method splits string to array. 
 
	topView: SC.ToolbarView.design({ 
		childViews: "appName".w(), 
		layout: { top: 0, left: 0, right: 0, height: 40 }, 
		anchorLocation: SC.ANCHOR_TOP, 
 
		appName: SC.LabelView.design({ 
			layout: { top: 5, left: 5, width: 100 }, 
			displayValue: "Microblog App", 
			layerId: "mb-logo" // HTML id attribute 
		}) 
	}), 
 
	postView: SC.View.design({ 
		childViews: 'form'.w(), 
		layout: { top: 40, height: 150 }, 
		backgroundColor: 'white', 
 
		form: SC.View.design({ 
			childViews: 'postContent post'.w(), 
			layout: { left: 200, right: 200 }, 
 
			postContent: SC.TextFieldView.design({ 
				layout: { top: 10, left: 10, right: 10, height: 60 }, 
				isTextArea: YES, 
				fieldKey: "content", 
				hint: "What's on your mind?" 
			}), 
 
			post: SC.ButtonView.design({ 
				layout: { top: 80, right: 10, width: 100 }, 
				title: "Post", 
				isDefault: YES, 
				target: "Microblog.postsController", 
				action: "addPost" 
			}) 
		}) 
	}), 
 
	contentView: SC.ScrollView.design({ 
	  hasHorizontalScroller: NO, 
	  layout: { top: 150, bottom: 0, left: 0, right: 0 }, 
	  backgroundColor: "white", 
	  contentView: SC.ListView.design({ 
		layout: { left: 200, right: 200 }, 
		contentBinding: "Microblog.postsController.arrangedObjects", 
		selectionBinding: "Microblog.postsController.selection", 
		exampleView: Microblog.PostListView, 
		rowHeight: 60 
	  }) 
	}) 
  }) 
});

After making these adjustments, reload the browser and begin adding posts.



Step 11 - Hooking Up CouchDB

At the moment, the data we load into our application comes from hardcoded Fixtures. These are perfect for testing but not suitable for production, because any changes we make don't persist. SproutCore uses SC.DataSource to interact with a server-side data store. It's configured by default to use an API following the REST convention. In this step, we are going to extend SC.DataSource and override key methods allowing us to use CouchDB. CouchDB has a RESTful API interface out of the box, so it's naturally a perfect fit for SproutCore's SC.DataSource. For more information on CouchDB have a look at this excellent CouchDB introduction on Nettuts+. We only need to make a few modifications in our SC.DataSource to allow SproutCore to handle CouchDB's revision system and slightly different URL schema. Let's get started by setting up the database.

  1. Head over to www.couchone.com/get and download CouchDB for your operating system.
  2. Open the CouchDB application.
  3. Create a database called microblog.
  4. Click the select box labeled view and select temporary view. This will allow us to create a view to display all the posts.
  5. Within the map function textarea, enter the below function.

     
    function(doc) { 
      if (doc.post && doc.published) { 
    	emit(doc._id, doc); 
      } 
    }
  6. Click "Save As" in the bottom right, and call both the design document and the view posts.

If any of this is unfamiliar, I recommend you checkout the CouchDB introduction on Nettuts+.

Now that we have our database setup, we are ready to extend SC.DataSource. Using the terminal, run the below command from within your applications root directory.

 
sc-gen data-source Microblog.PostsDataSource

Now we should have a boilerplate SC.DataSource within data_sources/posts.js. Open this file. First, we need to define a query for retrieving our data. At the top of data_sources/posts.js and outside of the Microblog.PostsDataSource construct, add the code detailed below.

 
sc_require('models/posts'); 
 
Microblog.POSTS_QUERY = SC.Query.local(Microblog.Posts, { 
  orderBy: 'published DESC' 
});

This defines a custom query that will ensure that we get all posts and order them by their published date. Next, add some helper methods to the SC.DataSource subclass as defined below.

 
// DB name 
_dbname: "microblog", 
 
getPath: function (resource) { 
	return "/"+this._dbname+"//"+resource; 
}, 
getView: function (view, queryString) { 
	return "/"+this._dbname+"/_design/"+view+"/_view/"+view+"/?"+queryString; 
},

As you can see, we store the db name in a "private" variable. You could add a closure if you want to be thorough. The getPath() and getView() functions abstract the resource to path mapping. We can call getView() with any view name and get the correct URL for that resource on the CouchDB instance.

Now, let's implement some methods to retrieve the posts from CouchDB. Amend the fetch() method and add didFetchPosts() as detailed below.

 
fetch: function(store, query) { 
if (query === Microblog.POSTS_QUERY) { 
  SC.Request.getUrl(this.getView("posts", "descending=true")).json() 
			.header("Accept", "application/json") 
			.notify(this, "didFetchPosts", store, query) 
			.send(); 
 
  return YES; 
} 
 
return NO; 
}, 
 
didFetchPosts: function(res, store, query) { 
	if (SC.ok(res)) { 
	  var couchResponse = SC.json.decode( res.get('encodedBody') ), 
		  posts = couchResponse.rows.getEach("value") 
		  ids = couchResponse.rows.getEach("key"); 
 
	  store.loadRecords(Microblog.Posts, posts, ids); 
	  store.dataSourceDidFetchQuery(query); // Notify app of win. 
	} else { 
  store.dataSourceDidErrorQuery(query, res); // Notify app of fail. 
	} 
},

The fetch() method ensures that the query is supported, and then, using SC.Request, makes a request to CouchDB for the posts view we specified earlier. Upon the request being performed, we ask that SproutCore notify the didFetchPosts() method with the result. Within didFetchPosts(), we first check that the response went ok. If it didn't, we respond with an error, accordingly. On the other hand, if the response was successful, we iterate over the results, getting all the posts and IDs and then load the records. After loading the records, we notify any listeners that the data source did fetch some results. In doing so, this automatically updates our UI, because of the data binding we initially setup.

Next, we need to handle a new post, making sure that we add the post to CouchDB. Again in Microblog.PostsDataSource, add the following methods.

 
  createRecord: function(store, storeKey) { 
	// Check we want to store the correct type of record. 
	if (SC.kindOf(store.recordTypeFor(storeKey), Microblog.Posts)) { 
	  SC.Request.postUrl(this.getPath("/")).json() 
				.header("Accept", "application/json") 
				.notify(this, this.didCreatePost, store, storeKey) 
				.send(store.readDataHash(storeKey)); 
 
	  return YES; 
	} 
 
	return NO ; // return YES if you handled the storeKey 
  }, 
 
  didCreatePost: function (res, store, key) { 
	var couchResponse = this.handleCouchResponse(res); 
	if (couchResponse.ok) { 
	  // Copy CouchDB _id and _rev into Microblog object/document. 
	  var doc = store.readDataHash(key); 
	  doc._id = couchResponse.id; 
	  doc._rev = couchResponse.rev; 
 
	  store.dataSourceDidComplete(key, doc, doc._id); 
	} else { 
	  store.dataSourceDidError(key, res); 
	} 
  }, 
 
  handleCouchResponse: function (res) { 
	if (SC.ok(res)) { 
	  var couch = SC.json.decode( res.get("encodedBody") ); 
	  if (couch.ok == YES) { 
		return { "ok": true, "id": couch.id, "rev": couch.rev }; 
	  } else { 
		return { "error": true, "response": res }; 
	  } 
	} 
 
	return { "error": true, "reponse": "Unkown error occurred." }; 
  },

createMethod() firstly ensures that we have been passed a record of the correct type before saving. Then it makes a POST request to CouchDB with the post object as its data. Once again, we attach a method to be notified when the request is over. didCreatePost() uses the helper method handleCouchResponse(), which first checks if SproutCore found the response OK, and then interrogates the returned data further to ensure that the insert was performed successfully by CouchDB. If everything went correctly, we return a object with the new document ID and revision number. didCreatePost() continues to execute if the document was created successfully. If it failed, we notify any listeners of the failure. The method then copies the ID and revision number into the local document within SproutCore and notifies any listeners of the new document. Once again, the UI will automatically update once the new document has been inserted into the posts array.

Next, we need to modify the main.js file to use the new load query as below.

 
var posts = Microblog.store.find(Microblog.POSTS_QUERY); 
Microblog.postsController.set("content", posts);

And, modify core.js to use the correct data source object.

 
Microblog = SC.Application.create( 
  /** @scope Microblog.prototype */ { 
 
  NAMESPACE: 'Microblog', 
  VERSION: '0.1.0', 
 
  store: SC.Store.create({ 
	commitRecordsAutomatically: YES 
  }).from("Microblog.PostsDataSource") 
}) ;

Lastly, we must add a proxy to the SproutCore BuildFile in the main application directory, telling SproutCore to look for CouchDB on a different port. Add the below proxy to your BuildFile

 
proxy '/microblog', :to => 'localhost:5984'

And, restart your SproutCore server.

If you reload your browser, you should see no posts. This makes sense seeing as their are no documents in the CouchDB database yet. Go ahead and make a post, reload the browser and you should still see your post. You can also check with Futon, CouchDB's administration panel to see the resulting documents in the database.



Conclusion

In this tutorial, I introduced you to some of the key concepts within SproutCore and build a simple Twitter like microblog application. Lastly we took full advantage of SproutCore's client side framework and CouchDB's RESTful interface to produce a full desktop like application, with data persistence, without a server-side application layer. Thank you so much for reading!

Advertisement