Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Tree: ccd5dd8556
Fetching contributors…

Cannot retrieve contributors at this time

478 lines (407 sloc) 16.539 kB

Core Concepts

The guide covers some of the core concepts of views in SproutCore. By
referring to this guide, you will be able to:

  • Layout your views relative to their parent
  • Make your views dynamic with bindings
  • Change how your views render and update themselves
  • Handle events that occur over your view like a mouse click

endprologue.

Introduction

After you’ve created your first SproutCore project, you’ll notice the file
main_page.js in your resources folder. This is the main page that your app
appends to the DOM in your main function. Let’s start by looking at what
every SproutCore project starts with as it’s main page:

MyApp.mainPage = SC.Page.design({
mainPane: SC.MainPane.design({
childViews: ‘labelView’.w(),
labelView: SC.LabelView.design({
layout: { centerX: 0, centerY: 0, width: 200, height: 18 },
textAlign: SC.ALIGN_CENTER,
tagName: “h1”,
value: “Welcome to SproutCore!”
})
})
});

To understand what all of this means, we’ll have to look at and understand
what this boilerplate code does. MyApp.mainPage is our main application
page. Your application may end up having several “pages” or you may choose to
swap content into and out of a single page. MyApp.mainPage is an SC.Page
which works by lazily configuring itself. The views within a page are only
awakened when you get the page in the main.js file:

MyApp.getPath(‘mainPage.mainPane’).append();

MyApp.mainPage.mainPane is an SC.MainPane which is itself an SC.Pane.
There will probably be many panes in your app because a pane is just like a
regular view except that it doesn’t need to live within a parent view. These
can be anything from a pallette to a popup to a menu. In this case, our pane
is an SC.MainPane which automatically makes itself the main pane as soon as
it’s appended to the doument.

Our main pane is configured with only one childView, an SC.LabelView.
SproutCore uses absolute positioning to layout it’s views, and this label view
is no different. Giving it a width and height allows us to center the label
directly into the center of the page using centerX and centerY which
specify how many pixels offset from the center we’d like the view.

NOTE: The w function splits a string by spaces and turns it into an array.
We could have accomplished the same thing by using
childViews:[‘labelView’].

Laying Out Views on the Page

Let’s say we wanted to make an address book. This address book would have a
list of all your contacts that you could scroll through and see contact
details such as phone numbers and addresses. Let’s start the address book
by starting an SC.WorkspaceView which allows us to have a toolbar on the top
and bottom and a view between those two toolbars:

MyApp.mainPage = SC.Page.design({
mainPane: SC.MainPane.design({
childViews: ‘workspaceView’.w(),
workspaceView: SC.WorkspaceView.design({
contentView: SC.View
})
})
});

NOTE: design performs like extend and may register with SproutCore’s
designer framework, which is the basis of the interface builder, Greenhouse.
You should never use design outside of a page definition.

We now have a nice toolbar on the top of the page, but we still don’t have a
list of any contacts. If we want a list on the right and details on the left,
it sounds like SC.SplitView is the perfect view to do that, so let’s make
that our contentView:

SC.WorkspaceView.design({
contentView: SC.SplitView.design({
dividerThickness: 1,
defaultThickness: 300,
topLeftView: SC.View,
bottomRightView: SC.View
})
})

Our UI is starting to come together. We can see where we’re going to have our
contacts listed and where their details will be displayed. The
dividerThickness is just a aesthetic property since the default really fat/
defaultThickness will make our fixed view—in this case the
topLeftView—300 pixels wide.

NOTE: When you use values less than 1 for dimensions in SproutCore they’re
interpreted as percentages. E.g. width: 0.5 tells SproutCore that you want
it to be 50% wide.

We’re going to need to list our contacts, so let’s do that to the left, with
our details view to the right. For this, we could use SC.ListView which is
used for generic lists of stacked information where you know each list item’s
height. Fortunately, SproutCore includes SC.SourceListView which not only
looks great out of the box, but also provides the default behaviors of a
source list:

topLeftView: SC.SourceListView.design({
content: [“Devin Torres”, “Charles Jolley”, “Peter Wagenet”]
})

We can then move on to the details of a contact, such as his name, phone
phone number, and address. We can use SC.LabelView for all three in our
bottomRightView:

bottomRightView: SC.View.design({
childViews: ‘contactDetails’.w(),
contactDetails: SC.View.design({
layout: { top: 50, left: 50, bottom: 50, right: 50 },
childViews: ‘nameLabel phoneLabel addressLabel’.w(),
nameLabel: SC.LabelView.design({
layout: { width: 500, height: 18 },
value: “Contact name”
}),
phoneLabel: SC.LabelView.design({
layout: { top: 40, width: 500, height: 18 },
value: “Contact phone number”
}),
addressLabel: SC.LabelView.design({
layout: { top: 80, width: 500, height: 500 },
value: “Contact address”
})
})
})

The labels are wrapped in contactDetails so they can be cushioned from the
edges of the parent view to see them better. phoneLabel and addressLabel
are given a top to clear each other so they don’t overlap, and
addressLabel is given a large height so a long address can wrap within the
label. Right now these labels are only placeholders until we hook their values
to a binding.

Binding Views to Controllers

Instead of having our source list content and our label values in our
views, let’s bind them to some controllers. We’ll start by creating an
SC.ArrayController for our contacts and an SC.ObjectController that
proxies individual contact objects:

MyApp.contactsController = SC.ArrayController.create({
content: [
SC.Object.create({
name: “Devin Torres”,
phone: “(555) 391-1419”,
address: “214 12th St. Austin, TX 78701”,
website: ‘http://www.linkedin.com/in/devintorres’
}),
SC.Object.create({
name: “Charles Jolley”,
phone: “(555) 749-1585”,
address: “378 16th St. Austin, TX 78701”,
website: ‘http://www.linkedin.com/in/charlesjolley’
}),
SC.Object.create({
name: “Peter Wagenet”,
phone: “(555) 856-3750”,
address: “935 2nd St. Austin, TX 78701”,
website: ‘http://www.linkedin.com/in/wagenet’
})
]
});

MyApp.contactController = SC.ObjectController.create({
contentBinding: SC.Binding.from(‘MyApp.contactsController.selection’).single()
});

Now we can bind our source list to the SC.ArrayController to get a dynamic
list of contacts and individual contact selection support:

SC.SourceListView.design({
contentValueKey: ‘name’,
contentBinding: ‘MyApp.contactsController.content’,
selectionBinding: ‘MyApp.contactsController.selection’
})

With the contacts selectable, the only thing missing is dynamic label values
for the contact’s name, phone, and address:

SC.View.design({
layout: { top: 50, left: 50, bottom: 50, right: 20 },
childViews: ‘nameLabel phoneLabel addressLabel’.w(),
nameLabel: SC.LabelView.design({
layout: { width: 500, height: 18 },
valueBinding: SC.Binding.oneWay(‘MyApp.contactController.name’)
}),
phoneLabel: SC.LabelView.design({
layout: { top: 40, width: 500, height: 18 },
valueBinding: SC.Binding.oneWay(‘MyApp.contactController.phone’)
}),
addressLabel: SC.LabelView.design({
layout: { top: 80, width: 500, height: 500 },
valueBinding: SC.Binding.oneWay(‘MyApp.contactController.address’)
})
})

NOTE: Using SC.Binding.oneWay is not necessary here, but using one way
bindings when you don’t need changes from the view reflected back to the
controller—which is the case for these label views—is more performant.

The contact information will now change automatically whenever a new contact
is selected. Our bottomRightView should now look like this:

SC.View.design({
childViews: ‘usageHint contactDetails’.w(),
usageHint: SC.View.design({
classNames: ‘myapp-usage-hint’,
isVisibleBinding: SC.Binding.from(‘MyApp.contactsController.hasSelection’).not(),
childViews: [SC.LabelView.design({
layout: { width: 300, height: 22, centerX: 0, centerY: 0 },
tagName: ‘h1’,
textAlign: SC.ALIGN_CENTER,
value: “Select a contact”
})]
}),
contactDetails: SC.View.design({
layout: { top: 50, left: 50, bottom: 50, right: 20 },
childViews: ‘nameLabel phoneLabel addressLabel’.w(),
nameLabel: SC.LabelView.design({
layout: { width: 500, height: 18 },
valueBinding: ‘MyApp.contactController.name’
}),
phoneLabel: SC.LabelView.design({
layout: { top: 40, width: 500, height: 18 },
valueBinding: ‘MyApp.contactController.phone’
}),
addressLabel: SC.LabelView.design({
layout: { top: 80, width: 500, height: 500 },
valueBinding: ‘MyApp.contactController.address’
})
})
})

Custom View Styling and Rendering

SproutCore provides three ways to customize how your view looks. You can
simply give it a class name and add it to the view’s classNames array
property to style it using CSS, override it’s render method to use it’s own
custom HTML, or use an SC.RenderDelegate to render and update all views of
the view type.

Using CSS to Style a View

Let’s make the interface a bit more visually appealing and user friendly by
giving a usage hint when no contact has been selected yet. We ca create a
generic view and add the class name myapp-usage-hint to the classNames
array, and then a label view as a child to display the hint:

SC.View.design({
classNames: ‘myapp-usage-hint’,
isVisibleBinding: SC.Binding.from(‘MyApp.contactsController.hasSelection’).not(),
childViews: [SC.LabelView.design({
layout: { width: 300, height: 22, centerX: 0, centerY: 0 },
tagName: ‘h1’,
textAlign: SC.ALIGN_CENTER,
value: “Select a contact”
})]
})

Right now it looks rather small, so let’s add some style to it. Create a file
in the resources directory and name it theme.css:

.myapp-usage-hint {
background-color: #eee;
}

.myapp-usage-hint h1 {
font-family: “Helvetica Neue Light”, “HelveticaNeue-Light”, inherit;
font-size: 28px !important;
line-height: 20px !important;
color: #999;
}

Now we have nice looking instructions for when a user first starts using the
app. You can add as many class names to classNames as you want for any view.

The render and update Methods

The render method is used to generate the view’s HTML and is called whenever
the view first renders itself and any subsequent updates to the view can be
handled by an update method after that.

NOTE: As soon as you start overriding methods such as render and update,
your views really need to be in their own files under the views directory.
A good rule of thumb is that main_page.js should never have anything longer
than a one line function.

The update method is called whenever a property that is defined on the view’s
displayProperties array is changed on the view. The render method is
passed a context variable which is an SC.RenderContext. SC.RenderContext
is used to queue up and build HTML for a view. You build HTML by calling
context.push() and passing it HTML:

CreepyApp.CharlesJolleyView = SC.View.extend({
render: function (context) {
context.push(“

Charles Jolley is 31 years young…

”);
}
});

You can also use context.begin() and context.end() to begin building a tag
and push arguments that are “stringable” (have a toString) into it:

CreepyApp.CharlesJolleyView = SC.View.extend({
displayProperties: [‘name’, ‘age’],
name: ‘Charles Jolley’,
age: 31,
render: function (context) {
var name = this.get(‘name’), age = this.get(‘age’);
var context = context.begin(‘h1’);
context.push(name, ’ is going to be ‘, age+1, ’ years young soon…’);
context.end();
}
});

The update method is passed a jQuery handle of the view’s layer, the DOM
element that belongs to the view. You can use it and all of jQuery’s API to
manipulate the DOM and update your view:

CreepyApp.CharlesJolleyView = SC.View.extend({
displayProperties: [‘name’, ‘age’],
name: ‘Charles Jolley’,
age: 31,
render: function (context) {
var name = this.get(‘name’), age = this.get(‘age’);
var context = context.begin(‘h1’);
context.push(‘’, name, ‘’);
context.push(’ is going to be ‘);
context.push(’‘, age+1, ’’);
context.push(’ years young soon…‘);
context.end();
},
update: function (jquery) {
var h1 = jquery.find(’h1’);
h1.find(‘.name’).text(this.get(‘name’));
h1.find(‘.age’).text(this.get(‘age’));
}
})

Say we wanted to make the name of the contact bigger in the details. To do
this, let’s create a custom view called MyApp.ContactNameView that will put
the contact name in an h1 tag:

MyApp.ContactNameView = SC.View.extend({
displayProperties: [‘value’],
render: function (context) {
context.push(‘

’, this.get(‘value’), ‘

’);
},
update: function (jquery) {
jquery.find(‘h1’).text(value);
}
});

Render Delegates

Render delegates are an easy way to manage theming your apps. You can create
a render delegate to manage the rendering of your views for you and add it to
the theme you’re using. Render delegates also have a render and update
method, but their first argument is the dataSource, or the view delegating
to them. Let’s recreate the last scenario using an SC.RenderDelegate that
we’re going to create in render_delegates/contact_name.js:

SC.AceTheme.contactNameRenderDelegate = SC.RenderDelegate.create({
render: function (dataSource, context) {
context.push(‘

’, dataSource.get(‘value’), ‘

’);
},
update: function (dataSource, jquery) {
jquery.find(‘h1’).text(dataSource.get(‘value’));
}
});

Now we have to tell our view to use our new render delegate to draw itself:

MyApp.ContactNameView = SC.View.extend({
displayProperties: [‘value’],
renderDelegateName: ‘contactNameRenderDelegate’
});

View Events

A SproutCore view hierarchy can be viewed like a pond, if the water is touched
it ripples outwards affecting all the water around it, or in our case, all the
parent views. SC.View extends from SC.Responder, inheriting the ability to
react to DOM events such as mouseDown and mouseEntered. Let’s highlight the
contact name when it’s being hovered over and take the user to the contact’s
website if it’s clicked:

MyApp.ContactNameView = SC.View.extend({
displayProperties: [‘value’],
renderDelegateName: ‘contactNameRenderDelegate’,
mouseEntered: function () {
var jquery = this.$();
jquery.css(‘cursor’, ‘pointer’);
jquery.css(‘color’, ‘#356aa0’);
return YES;
},
mouseExited: function () {
var jquery = this.$();
jquery.css(‘cursor’, ‘normal’);
jquery.css(‘color’, ‘#000’);
return YES;
},
mouseDown: function (evt) {
MyApp.openWebsite();
return YES;
}
});

And in core.js:

openWebsite: function () {
var website = MyApp.contactController.get(‘website’);
window.open(website);
}

You should return YES when you’ve handled the event, otherwise it would
propagate to all of it’s parent views.

WARNING: Actions such as these would usually be handled by a statechart and
are handled here for the purposes of demonstration.

SproutCore views are powerful, and when coupled with bindings, controllers, a
few models, and a data source make developing applications fast and easy.

Changelog

Jump to Line
Something went wrong with that request. Please try again.