Although SproutCore comes with a great set of general views for you to use right out of the box, you are probably going to still come to a point where you realize you need to create your own custom views to suit your needs. Creating a custom view in SproutCore can seem like an intimidating task for a newbie — it was for me. So in the aim of helping out all those who want to start dipping their toes into creating custom views, here’s one place to get started.
We are going to create a simple custom view that will be used to summarize a user. We’ll call the view User Summary and it will display the user’s name, a description of the user and the user’s age (just because we can). This seems okay to start off with. I often find that if you are going to create any custom view, it is good practice to start by working with plain old HTML to build the visible structure of what we want. Here is a preview of what we want the view to look like:
Okay, so to get started let’s first create a new SproutCore application called MyApp by running the following command:
sc-init MyApp
Now go into the new directory and run the following command:
sc-gen view MyApp.UserSummaryView
If everything went well you should now have some new files in your project. sc-gen
helped us create a template for us to build our view on. You will see a file called user_summary.js
in the my_app/apps/my_app/views
folder. Open up user_summary.js
file. You should see something like the following:
MyApp.UserSummaryView = SC.View.extend(
/** @scope MyApp.UserSummaryView.prototype */ {
// TODO: Add your own code here.
});
The view looks pretty empty. Not to worry. We’ll be adding code to this baby soon enough! But before we jump into the view, we first need to do a few quick things.
First, we need to create a CSS file that will be used to make our view look nice. create a new CSS file in the english.lproj
directory and call it style.css
. Copy and paste the following into your CSS file:
.user-summary-view {
position: relative;
height: 50px;
background-color: #c0c0c0;
}
.user-summary-view-name {
position: absolute;
top: 0px;
left: 0px;
font-family: georgia;
font-size: 22px;
}
.user-summary-view-desc {
position: absolute;
left: 0px;
bottom: 0px;
font-family: arial;
font-size: 12px;
font-style: italic;
}
.user-summary-view-age {
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
width: 50px;
background-color: #008000;
}
.user-summary-view-age-value {
padding: 0px;
margin: 0px 0px 5px 0px;
font-family: georgia;
font-size: 22px;
color: white;
text-align: center;
}
.user-summary-view-age-capt {
padding: 0px;
margin: 0px;
font-family: arial;
font-size: 12px;
text-align: center;
color: white;
}
Great. Now we are going to update the main_page.js
file located in the english.lproj
directory. Replace the code in the file to be the following:
MyApp.mainPage = SC.Page.design({
mainPane: SC.MainPane.design({
childViews: 'userSummaryView'.w(),
userSummaryView: MyApp.UserSummaryView.design({
layout: { top: 0, left: 0, height: 50 },
})
})
});
If you try running the application using sc-server
you won’t see much, but you will soon.
Now that we got the house cleaning done, let’s get to the good stuff! First and foremost: We always want to keep our views independent of everything else because: 1) it’s just good programming practice; 2) it makes it easier to test our view; and 3) maximizes the view’s reuse. Moving on.
For every view you are going to be dealing with two fundamental concepts. The first is to decide what properties the view will react to to update itself. The second is how to actually render the view using those properties. Let’s focus on the properties first.
We know that our view will display a user’s name, description and age, so those look like good properties for our view to have and others to bind to. Update your user_summary.js
file to be the following:
MyApp.UserSummaryView = SC.View.extend({
name: '',
description: '',
age: 0,
displayProperties: ['name', 'description', 'age']
});
In the code you see that we have added three basic properties. Pretty easy. Also, we added another interesting property to our view called displayProperties
. This is used to inform the view to watch for changes to some or all of its properties in order to re-render the view. SproutCore hides a lot of the complexity so that we can focus on building our view and not get tied up in the plumbing.
Now for the second part — getting the view to render. We are going to add a render()
function to our view, as so:
MyApp.UserSummaryView = SC.View.extend({
name: '',
description: '',
age: 0,
displayProperties: ['name', 'description', 'age'],
render: function(context, firstTime) {
sc_super();
}
});
By adding this function to our view, we have just overridden the parent view’s render function. This is the function where the real meat will be added to give us our end result. To start, we will get the values of the properties we will use to help render our view. Update the render method to be the following:
render: function(context, firstTime) {
var name = this.get('name');
var description = this.get('description');
var age = this.get('age');
sc_super();
}
Remember that in SproutCore we always want to get the value of a object’s property through the get()
method instead of directly, like as in myObject.foo
.
We’re almost there. Now we are going to make use of the argument context
to create our HTML that SproutCore will eventually spit out to the browser. context
is a SC.RenderContext
object that is used to build the HTML. Think of the render context as a fancy string builder that queues up changes to an element. With the context in hand, let’s once more update the render()
method to be the following:
render: function(context, firstTime) {
var name = this.get('name');
var description = this.get('description');
var age = this.get('age');
context = context.begin('div').addClass('user-summary-view');
context = context.begin('div').addClass('user-summary-view-name').push(name).end();
context = context.begin('div').addClass('user-summary-view-desc').push(description).end();
context = context.begin('div').addClass('user-summary-view-age');
context = context.begin('div').addClass('user-summary-view-age-value').push(age).end();
context = context.begin('div').addClass('user-summary-view-age-capt').push('age').end();
context = context.end();
context = context.end();
sc_super();
}
Because I know you’re eager to see something on the screen, let’s go ahead and update the browser (remember to have the sc-server
running). You should see the following:
Yay! We’re finally getting something. It looks close to what we want. In fact, the view is ready to be used. But before we go any further, let’s go back and study what we added to the render()
method.
With the context object, we make calls to its methods, such as calling begin()
, end()
, addClass()
, push()
. All these methods make it convenient to construct our view. begin()
and end()
create the start and end of a tag, such as div
. addClass()
adds a value to the tag’s class attribute. And push()
is used to push any old string to build up your HTML. To see the reset of the context’s methods, go to the render_context.js
file located in the SproutCore framework’s frameworks/foundation/system
.
Now that we got the render context object out of the way, let’s get back to making our custom view do cool stuff. We are going to modify our view’s properties programmatically but we’ll do it through the use of a interactive JavaScript console. If you are using the Firefox browser then go ahead and use the JavaScript console that comes with the Firebug add-on. If you are using Apple Safari or Google Chrome (read: Webkit) then you can just use the built-in JavaScript debugger. I’ll be using the latest Safari.
To access our custom view in our application we are going to have to traverse the application’s object graph. Remember that when we initially created the application, SproutCore set up a root object called MyApp
. So in our JavaScript console, we’ll acquire our view as follows:
var view = MyApp.mainPage.mainPane.userSummaryView
You should now have a reference to the view object. With view in hand let’s change its properties and see what happens. First, we’ll modify the view’s name property by entering the following into the console:
view.set('name', 'Skippy McGaven')
Now, if like me, you happen to hit enter and didn’t see anything happen, don’t fret. Just move your mouse over the browser’s window and the name will appear. I’m not sure why this is, but it appears you have to trigger an event to invoke SproutCore to update the view. No matter. You should now see our view with “Skippy McGaven” brightly displayed for all to behold, such as the following:
Again, remember that in order to set a SproutCore object’s properties you use the set()
method (at least for the object’s public properties anyway).
Things are looking up! So let go the next mile and modify the view’s description property. Enter the following into the console:
view.set('description', 'Skippy always said that the answer to everything in life is 42')
Hit the enter button and remember to move your mouse over the browser’s window to see the changes if nothing happened. You should see the following:
We are on a role now! So let’s modify the view’s final property, age. Again, in the JavaScript console, enter the following:
view.set('age', 17)
If all goes well you should see the final result:
Great work! Now its time to do the funky chicken dance in the end-zone.
Hopefully this tutorial showed you the beginnings of how to make a custom view. There is still more to making a full-blown custom view, but this is a good first step. In part 2, I’ll take this custom view and do a bit more advanced stuff by hooking up the view to a model object.
You can find the complete source for the custom view below:
MyApp.UserSummaryView = SC.View.extend({
name: '',
description: '',
age: 0,
displayProperties: ['name', 'description', 'age'],
render: function(context, firstTime) {
var name = this.get('name');
var description = this.get('description');
var age = this.get('age');
context = context.begin('div').addClass('user-summary-view');
context = context.begin('div').addClass('user-summary-view-name').push(name).end();
context = context.begin('div').addClass('user-summary-view-desc').push(description).end();
context = context.begin('div').addClass('user-summary-view-age');
context = context.begin('div').addClass('user-summary-view-age-value').push(age).end();
context = context.begin('div').addClass('user-summary-view-age-capt').push('age').end();
context = context.end();
context = context.end();
sc_super();
}
});
[Update (Aug 16, 2009): Be sure to check out my post discussing some updates to this tutorial here based on some of the feedback below.]
[Update (Aug 16, 2009): Part 2 of how to create a simple custom view has been added.]
Another cool thing to point out is that the view you just built is already to go to support bindings. Say you have a controller called “MyApp.currentUserController”, in your main_page.js you could put:
userSummaryView: MyApp.UserSummaryView.design({
layout: { top: 0, left: 0, height: 50 },
nameBinding: "MyApp.currentUserController.name",
ageBinding: "MyApp.currentUserController.age"
})
etc.
Great work!
Awesome article! This method seems geared towards reusable views, but what is SC best practice on creating and rendering views that aren’t as generic?
eg: you have a person’s details displayed as static text and a button ‘edit’ to turn the view into a form…Should one be creating customs views for each of these view states?
In rails one might use partials, what is SCs approach for less generic views?
Excellent post!
Great post!
One correction or clarification. The context that is given to the render() function is for the actual view so you don’t need to do the following:
context = context.begin(‘div’).addClass(‘user-summary-view’);
/* MORE STUFF */
context = context.end();
this is an extra unnecessary ‘div’, you can just call:
context = content.addClass(‘user-summary-view’)
Even better, for a base-level class on the view just use:
classNames: [‘user-summary-view’]
For decorators on the main view, use the addClass() directly on the context for things like ‘selection’ and stuff…
Thanks for the feedback!
@Charles: Good addition to my post on how to bind a view to a controller using the xxxBinding pattern. For those confused about binding a view to a model, check out my other post http://bit.ly/kmqd2.
@Gary: I hear you about views that are less generic. I’ll be getting around to posting about more advanced stuff with views soon. Keep comin’ back :)
@Evin: Nice! Great clarification. Just to be sure, where you say “context = content.addClass(‘user-summary-view’)”, I assume “content” should be “context”. In any case good to know :).
Another tip. The reason your values only update on mouse move is because when you .set them from the console, they are occuring outside the sproutcore run loop. You cam wrap multiple calls in SC.runLoop.begin() and SC.runLoop.end() and the changes will propagate immediately. There’s also a convenience method that may be easier — I think it’s just SC.run(function(){/*your code*/}) — I’m blanking on whether that’s the right method but you get the idea.
Much kudos! Looking forward the second part.
@Joshua: ah, yes. Thanks for the info on the run loop. I thought I was missing something.
For all those curious, Joshua is referring to the SC.RunLoop object in SproutCore. You can find the code for the run loop in the run_loop.js file located in the app/frameworks/runtime/system directory.
Hi,
I can’t get your example working. Safari keeps telling me:
TypeError: Result of expression MyApp.MyView’ [undefined] is not an object.
Unfortunately I am completely new to JavaScript and I’m not sure what’s wrong with my code (basically, I just copied yours). Do you have any Idea?
Cheers,
Michael
there is a good chance you’re missing a comma after the line with the displayProperties in the user_summary.js
@Michael: If you are getting an error message about some object being undefined it means that the object does not exist either because it hasn’t been created or there is an error in your object’s code. For this tutorial, there is only a view called MyApp.UserSummaryView.
As a tip, in your browser’s JavaScript console you can type in the name of your object during runtime to see if the object exists. It’s definitely a handy debugging device when building your JavaScript. Hopefully this helps you out.
Re-run this on the latest safari (Version 4.0.4 (6531.21.10)) on SnowLeopard and see if it renders properly. I’m not getting the background color for the main part of the view, though I am for the age section. I’m guessing that the CSS is causing the element with the background (the outer div) to be 0 pixels high, since all child elements are absolutely positioned. But if that’s not how it renders for you, I may have a problem with my Safari…
The render function creates a couple of new divs everytime it is run. A good example would be to reuse the created divs and replace the name value only through bindings.
Sproutcore lets you do that.
thank you master for tutorial. awesome..