Hey all. Well I must admit that it has been a while since I got around to writing a SproutCore tutorial. The last one I wrote was back on September 6th, and during that time the SproutCore framework has continued to mature and stabilize. So what was it that I last wrote about anyway? Oh, right. It was the first part of how to create a simple custom list item view. I figure since I left you all hanging with just the first part it would be helpful if I actually, you know, wrote the second part. Well overdue.
Before I even begin the second part of how to create a simple custom list item view, it’s probably best we do a quick re-cap of what we went over in the first part. Even I forgot!
In part 1 of how to create a simple list item view I wanted to start you off with a foundation that consisted of the following:
- Building a list item view from scratch that can be rendered in a list view (
SC.ListView) - That SproutCore already comes stock with a default list item view (
SC.ListItemView) that you can extend yourself - A list view can make use of a custom list item view by assigning the item to the list view’s
exampleViewproperty - That there is a contract between the list item view you create and the collection view (
SC.CollectionView) that ultimately manages all the list item views. - How to use the most fundamental properties provided to a list item view:
content,isSelected, andisEnabled
Great. Now that I remember… er, I mean, rather that we remember, let’s move forward and see how we can do some more interesting things when creating a custom list item view. I’ll be going over the following with you:
- Know what other properties are provided to a list item view by the collection view
- How to add addition properties to a list item view through a collection view
- How to go the extra mile and override the collection view’s
createItemViewmethod
Oh, and for those of you playing along at home (or in the office), we’ll be doing some code spelunking in SC.CollectionView. So you’ll probably want to open up the file collection.js that you can find in the SproutCore framework under the frameworks/desktop/views directory.
- Part 2.1: Getting to Know Your Default Properties
- Part 2.2: But I Want to Use My Own Properties
- Part 2.3: Assigning Properties to a Specific List Item
Part 2.1: Getting to Know Your Default Properties
As noted earlier, the collection view provides an item view with a suit of properties that the view can use as it sees fit. We’ve already learned about the basic properties, content, isSelected, and isEnabled, so let’s see what other default properties are provided. And to take out any mystery, let’s go to the source of where these properties are actually being assigned.
All item views are created by the SC.CollectionView via the itemViewForContentIndex method starting on line 945. I won’t go into all the details of the method for this tutorial, but we’ll focus our energy starting on line 977 where an attrs object is created as follows:
// collect some other state
var attrs = this._TMP_ATTRS;
attrs.contentIndex = idx;
attrs.content = item ;
attrs.owner = attrs.displayDelegate = this;
attrs.parentView = this.get('containerView') || this ;
attrs.page = this.page ;
attrs.layerId = this.layerIdFor(idx, item);
attrs.isEnabled = del.contentIndexIsEnabled(this, content, idx);
attrs.isSelected = del.contentIndexIsSelected(this, content, idx);
attrs.outlineLevel = del.contentIndexOutlineLevel(this, content, idx);
attrs.disclosureState = del.contentIndexDisclosureState(this, content, idx);
attrs.isGroupView = isGroupView;
attrs.isVisibleInWindow = this.isVisibleInWindow;
if (isGroupView) attrs.classNames = this._GROUP_COLLECTION_CLASS_NAMES;
else attrs.classNames = this._COLLECTION_CLASS_NAMES;
layout = this.layoutForContentIndex(idx);
if (layout) {
attrs.layout = layout;
} else {
delete attrs.layout ;
}
ret = this.createItemView(E, idx, attrs);
The attrs object is what will be provided to the item view class to construct an instance of it. In the code above you’ll notice three familiar properties being assigned to attrs: content, isSelected, isEnabled. The other properties being assigned to attrs that are worth noting are:
- contentIndex – The ordered index of the content provided to the item view
- page – The
SC.Pagethe collection view may belong to - parentView – The parent view of the item view
- outlineLevel – The outline level at which the content belongs too. Used for tree like structures
- disclosureState – Indicates if the item view is disclosing any item views that are children of it
- isGroupView – Indicates if the item view groups other item views
Once all the properties are assigned to attrs the object is then passed to the createItemView method to finally construct the item view. (We’ll return to the createItemView method a bit later). How about we give one of the properties a try? Let’s use the contentIndex property. (The outlineLevel, disclosureState, and isGroupView are for more complex scenarios, but if you’d like to see how they are used then be sure to check out the code for SC.ListItemView).
If you recall in part 1 of this tutorial, I made you create a custom list item view to display in a list view that ended up looking like the following:
Notice that, aside from the text displayed, the look of each list item is the same. I’d like to change that so that the background colors alternate just to make them look a little more distinct and easier for the user to look at. Hmm. So how can we do that? Since the collection view provides our item view with the contentIndex property we can use it to our advantage. By using the modulus operator on the contentIndex we can determine if the index is odd or even, like so:
var isOdd = this.get('contentIndex') % 2
Very simple. So if the content index divides evenly by 2 then we’re even, otherwise the index is odd. Now let’s put our new found knowledge to practice and update our custom list item view’s render method like so (new lines in bold):
MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {
classNames: ['custom-list-item-view'],
displayProperties: 'isSelected isEnabled'.w(),
contentDisplayProperties: 'fname lname company title',
render: function(context, firstTime) {
var content = this.get('content');
var fname = content.get('fname');
var lname = content.get('lname');
var company = content.get('company');
var title = content.get('title');
var isSelected = this.get('isSelected');
var isEnabled = this.get('isEnabled');
var contentIndex = this.get('contentIndex');
var isEven = contentIndex % 2 ? YES : NO;
context = context.setClass({ 'odd': !isEven, 'even': isEven });
var standard = isEnabled && !isSelected;
var selected = isEnabled && isSelected;
var disabled = !isEnabled;
var classes = { 'standard': standard, 'selected': selected, 'disabled': disabled };
context = context.begin().addClass('top').setClass(classes);
context = context.begin('p').addClass('name').push('%@, %@'.fmt(lname, fname)).end();
context = context.end(); // div.top
context = context.begin().addClass('bottom').setClass(classes);
context = context.begin('p').addClass('item').addClass('company');
context = context.begin('span').addClass('label').push('Company:').end();
context = context.begin('span').addClass('value').push(company).end();
context = context.end(); // p.label.company
context = context.begin('p').addClass('item').addClass('title');
context = context.begin('span').addClass('label').push('Title:').end();
context = context.begin('span').addClass('value').push(title).end();
context = context.end(); // p.label.title
context = context.end(); // div.bottom
sc_super();
}
});
That’s it! By just adding three simple lines we have now modified how the item views should be displayed. Now, if you’ve been reading any of my tutorials I’m sure you realize there’s still something left we have to do… which is? Update the CSS to take advantage of the added class tags. So in your style.css file located in the resources directory (Remember: There is no more english.lproj directory), add the following:
div.custom-list-item-view.even div.top.standard {
background-color: #808080;
}
div.custom-list-item-view.even div.bottom.standard {
background-color: #a3a3a3;
border-bottom-color: black;
border-bottom-width: 3px;
border-bottom-style: solid;
}
Now if you start up your app using sc-server, you should get something like the following:
Good job! You’ve now taken advantage of another property supplied to you by the SC.CollectionView.
Part 2.2: But I Want to Use My Own Properties
We’ve taken advantage of the properties provided to our list item view by the SC.CollectionView. Great. But since we’ve gone out of our way to create our custom list item view, wouldn’t it be nice if we could assign some of our own properties to the list item view? Sure would! But how do we do it? One way, as I’m sure some of you will note, is to simply assign properties to the content like you can do with a typical custom view. We can already witness this with the given content’s firstName and lastName properties. But what about properties that are not part of the content? Well, this means we have to add properties to the list item view itself, just like the isSelected property. The question then becomes just how do you assign those properties that the collection view does not know about? To understand let’s first go look at the code in our application that creates the list view in the main_page.js file:
list: SC.ScrollView.design({
layout: { top: 10, bottom: 30, left: 10, right: 10 },
contentView: SC.ListView.design({
layout: { top: 0, bottom: 0, left: 0, right: 0 },
contentBinding: 'MyApp.testController',
exampleView: MyApp.CustomListItemView,
rowHeight: 54,
rowSpacing: 0
})
}),
From the code above we have two options to assign additional properties to the custom list item view. The first option is to simply extend the list item view with your specific properties, like so:
list: SC.ScrollView.design({
layout: { top: 10, bottom: 30, left: 10, right: 10 },
contentView: SC.ListView.design({
layout: { top: 0, bottom: 0, left: 0, right: 0 },
contentBinding: 'MyApp.testController',
exampleView: MyApp.CustomListItemView.extend({
foo: 100,
bar: 200
}),
rowHeight: 54,
rowSpacing: 0
})
}),
The second approach is to do the following:
list: SC.ScrollView.design({
layout: { top: 10, bottom: 30, left: 10, right: 10 },
contentView: SC.ListView.design({
layout: { top: 0, bottom: 0, left: 0, right: 0 },
contentBinding: 'MyApp.testController',
exampleView: MyApp.CustomListItemView,
rowHeight: 54,
rowSpacing: 0,
foo: 100,
bar: 200
})
}),
The first approach seems the most obvious since we just extend the custom list item view and assign the values directly. We could even use binding if we so choose. The second approach seems, well, odd. It’s looks like you’re assigning the properties foo and bar directly to the list view itself. But the list view, and by extension the collection view, don’t understand what foo and bar are. So how do they make their way over to the list item view? For that we go back to the attrs object in the collection view’s itemViewForContentIndex method.
Two properties that I didn’t mention earlier that are also assigned to the attrs object are the owner and displayDelegate properties, both of which happen to reference the collection view object:
attrs.owner = attrs.displayDelegate = this;
As you may suspect, in order for our custom list item view to acquire the values assigned to foo and bar the item view has to access either the given owner or displayDelegate properties first, like so:
var foo = this.getPath('owner.foo');
Not too bad. The limitation with going through either the owner or displayDelegate properties is that you then lose the ability to use bindings on those properties, which may or may not be of concern depending on what you are trying to accomplish. It depends on if you know the values will be statically assigned and not change or will change during the course of the application’s execution. In either case, the approach you choose will dictate how to code your custom list item view, so make sure to comment your code as such. Also make note that the properties you assign to your item views will not effect the list view or its parent collection view. They both remain oblivious to what is going on.
So now that we have an idea of how to add our own properties to the list item views, let take them for a spin. For both approaches we want to add additional property that will simply toggle the visibility of the person’s title. We’ll start with the first approach by simply extending our custom list item view. First update your list item view to be the following:
MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {
classNames: ['custom-list-item-view'],
isTitleVisible: YES,
displayProperties: 'isSelected isEnabled'.w(),
contentDisplayProperties: 'fname lname company title',
render: function(context, firstTime) {
var content = this.get('content');
var fname = content.get('fname');
var lname = content.get('lname');
var company = content.get('company');
var title = content.get('title');
var isSelected = this.get('isSelected');
var isEnabled = this.get('isEnabled');
var contentIndex = this.get('contentIndex');
var isTitleVisible = this.get('isTitleVisible');
var isEven = contentIndex % 2 ? YES : NO;
context = context.setClass({ 'odd': !isEven, 'even': isEven });
var standard = isEnabled && !isSelected;
var selected = isEnabled && isSelected;
var disabled = !isEnabled;
var classes = { 'standard': standard, 'selected': selected, 'disabled': disabled };
context = context.begin().addClass('top').setClass(classes);
context = context.begin('p').addClass('name').push('%@, %@'.fmt(lname, fname)).end();
context = context.end(); // div.top
context = context.begin().addClass('bottom').setClass(classes);
context = context.begin('p').addClass('item').addClass('company');
context = context.begin('span').addClass('label').push('Company:').end();
context = context.begin('span').addClass('value').push(company).end();
context = context.end(); // p.label.company
if (isTitleVisible) {
context = context.begin('p').addClass('item').addClass('title');
context = context.begin('span').addClass('label').push('Title:').end();
context = context.begin('span').addClass('value').push(title).end();
context = context.end(); // p.label.title
}
context = context.end(); // div.bottom
sc_super();
}
});
If your app is started and loaded into a browser, go ahead and refresh. Everything should appear as it did before. Good. So now we want to change the list items so that they don’t show the person’s title. We’ll do this be updating the list item view supplied to the list view in the main_page.js file, like so:
list: SC.ScrollView.design({
layout: { top: 10, bottom: 30, left: 10, right: 10 },
contentView: SC.ListView.design({
layout: { top: 0, bottom: 0, left: 0, right: 0 },
contentBinding: 'MyApp.testController',
exampleView: MyApp.CustomListItemView.extend({
isTitleVisible: NO
}),
rowHeight: 54,
rowSpacing: 0
})
}),
Like before, refresh your browser. You should see the following:
Awesome! Now the title of the people listed is no longer visible. Again, notice how the changes to the view above is just the same thing you would do if making a typical custom view.
Alright. Now let’s try the second approach by passing the properties through the displayDelegate provided by the collection view. Let’s go back and change the code in the custom list item view:
MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {
classNames: ['custom-list-item-view'],
displayProperties: 'isSelected isEnabled'.w(),
contentDisplayProperties: 'fname lname company title',
render: function(context, firstTime) {
..
var isTitleVisible = this.getPath('displayDelegate.isTitleVisible');
...
if (isTitleVisible) {
context = context.begin('p').addClass('item').addClass('title');
context = context.begin('span').addClass('label').push('Title:').end();
context = context.begin('span').addClass('value').push(title).end();
context = context.end(); // p.label.title
}
...
}
});
In the code above we removed the isTitleVisible property from the view. In addition, we updated the render method to acquire the property's value through the displayDelegate. Now let's update the list view again in the main_page.js file:
list: SC.ScrollView.design({
layout: { top: 10, bottom: 30, left: 10, right: 10 },
contentView: SC.ListView.design({
layout: { top: 0, bottom: 0, left: 0, right: 0 },
contentBinding: 'MyApp.testController',
exampleView: MyApp.CustomListItemView,
rowHeight: 54,
rowSpacing: 0,
isTitleVisible: YES
})
}),
The isTitleVisible property is now applied directly to the list view instead of the custom list item view. Go ahead and refresh your browser. If all checks out you should see the title being displayed for all the people listed. If you change the value of isTitleVisible to NO and refresh you will no longer see the titles being displayed. So the display delegate worked! Cool.
Part 2.3: Assigning Properties to a Specific List Item
Going over the last part of this tutorial, you were able to apply your own custom properties to the list items. However, did you feel something... lacking? In the example you went through, when you change a property what gets those changes? All of the items, not just a specific one. So what if you have the scenario where you don't want to show the title of a specific item in the list? Hmm. This is where things get admittedly a bit more complicated, but its still achievable. Remember how I mentioned a method in the collection view called createItemView? Well it comes back to that method. Here is what the method looks like in the collection view (collection.js) on line 971:
createItemView: function(exampleClass, idx, attrs) {
return exampleClass.create(attrs);
}
Very trivial by default. The collection view just uses it to create an instance of the example view by providing it with the attrs hash.
I'll come back to how to target properties against specific item in part 3 of this tutorial. I know, I know: boo-erns to me ;-). But it'll be worth it as you get a lot more bang for the buck, and then you can tell people about it at parties you attend... very, very nerdy parties.
Till then, have fun programming in SproutCore
-FC



Thanks for a great tutorial!
I’m new to SproutCore, but I have chosen to use this framework to a application I’m building on my spare time. It is an application where a person with diabetes (my daughter has diabetes) can enter information about glucose values, food, activity… The application will have a log (ListView) with all entered information and I have started to implement at custom List Item View like this one (but with other information). One property each item has is a date, and I would like to group the list by date. I.e. have a Group Item View like: “Sunday the 3 jan” with all List Item View rows for that date below. It should be possible to set a filter for the size of the date window that should be displayed.
What is the best way to implement a grouped list view like this?
Regards,
Jonny
@Jonny:
When getting involved with grouping items in a list view, or just a collection view in general, there are several pieces that come into play.
For starters, the content supplied to the list view typically needs to come from a tree controller (SC.TreeController) that can arrange the content into groups. This is done using the controller’s arrangedObjects computed property. Mind you, when grouping content, you have to set up some keys on the controller so it knows how identify children and if a particular node is expanded or collapsed (ie. to show or hide child list items).
In additional to the controller, you’ll need to update your custom list item view to handle the outlineLevel, disclosureState and isGroupView properties. These properties can either be done in the normal example view or through a special group example view when you set the groupExampleView property on the list view.
Finally, if you are going to expand and collapse a group item to show and hide child item views, you need to handle the disclosure action in your custom list item view so that it knows when to call the given display delegate’s expand() and collapse() methods.
As you can see, there are a bunch of moving parts when grouping content in a collection view, which is why I skipped past how to do it for this tutorial. It really is a tutorial in and of itself. However, although I have yet to write such a tutorial, do take a look at the code for the SC.ListItemView since it shows the mechanics of how you’ll need to code up your custom list item view. Because of the amount of work involved, most people simply extend the SC.ListItemView so you can leverage the grouping capability with a lot less work.
This tutorial is awesome, thank you!!
If I wanted to add a checkbox “Show Titles” to the application, that would switch the display of titles on and off for all list items at once… how would that work?
- Would I need to extend the exampleView in main_page.js? Because in part 2.2 (“But I Want to Use My Own Properties”), you wrote: The limitation with going through either the owner or displayDelegate properties is that you then lose the ability to use bindings on those properties, which may or may not be of concern depending on what you are trying to accomplish. It depends on if you know the values will be statically assigned and not change or will change during the course of the application’s execution.
So how are things meant to work if the values should change during the course of the application’s execution? E.g. let’s say I want to add a checkbox “Show Title” to the bottom toolbar.
- What would be the SC.CheckboxView’s valueBinding?
- How would the CustomListItemView be notified of value changes? (using properties? by registering as an observer? observing MyApp.mainPage.mainPane.list.contentView.exampleView.prototype.isTitleVisible?)
I have been fiddling about for a while but I can’t get it to work. Could you provide hints (maybe even some sourcecode) as to how this could be accomplished?
Thank you so much, Johannes
@Johannes:
Ah, good question. Because you want the item views to either all display their titles or not, you have a few options.
First, yes, your custom item view will have to have some kind of property, such as isTitleVisible, that the item view can react to when it’s value changes.
Now as to how you make that property update, you can could bind the isTitleVisible property to a controller’s property so when something changes the controller’s property all the item views will immediately react, like so:
exampleView: MyCustomItemView.extend({
isTitleVisibleBinding: ‘myController.showTitle’
})
To update showTitle, you could then bind that property to the value of a SC.CheckboxView.
Another approach is to extend the SC.ListView and provide it with a isTitleVisible property. In your custom list view, when isTitleVisible changes you have an observer that reacts to the change and will in turn update all the visible item views’ isTitleVisible property (see part three of this tutorial for an idea of how that could be done). Mind you, this approach is more work and means you have to create another view.
Finally, you could always update each item view’s corresponding content object to dynamically add a isTitleVisible property. In such a case your controller instead updates all the content objects after, say, a checkbox is selected or deselected. Now instead of your custom item view having an explicit isTitleVisible property, it instead monitors its given content object’s itTitleVisible property.
This final option means you don’t have to extend your custom list item view or even extend a list view like in the first and second approach I discussed, but it does mean the controller has to do some more work.