And welcome back on how to create a simple custom list item view, part 3. This part of the tutorial continues our discussion in part 2 when I ended with the idea about the collection view’s createItemView
method. Using the method allows you to apply properties to specific items instead of all of them.
- Part 3.1: Useful Guidelines For
createItemView
- Part 3.2: Now to Get Our Hands Dirty
- Part 3.3: Improving What We’ve Got
- Part 3.4: To Conclude
Part 3.1: Useful Guidelines For createItemView
Before we just jump into how to use this createItemView
method, we should first look at when it is a good idea to use it. Why? Because if not used property, createItemView
can lead to performance issues and unnecessary complexity in your code.
As your are aware by now, the collection view is supplied an array of content, and, in turn, there is an item view created for each of the objects in the array. In addition, also knowing that an item view is just like a plain old SproutCore view with some extra stuff, the view can react to changes made to the given content object by including the SC.ContentDisplay
mixin. Therefore, we can target changes to a specific item view through the content object without having to go through the collection view. Okay, fine. So this then begs the question that if we can go through the content object then why ever bother thinking about passing properties through the collection view to target specific item views? Some reasons why you may want to consider this alternative path are the following:
- The collection view (or some inherited view thereof) needs precise control of how properties get applied to the item views that are not part of a content object
- The collection view needs to externalize specific information about the item views that is not directly captured in the content objects
- The change to one item view effects other item views outside of a content object’s control
The reasons above are not absolute, but, rather, are some good guidelines to follow. But what do each of the points really mean? Let’s start with the first point about precise control of how properties are applied.
The collection view directly handles how items are selected. (Yes, as well as with optional use of a collection view delegate). When you mouse down/up on an item view, the collection view picks up the event and handles what was targeted and will do all the necessary logic to eventually tell some item view that it was selected or unselected. The selection information is (usually) not part of the content objects and therefor needs to be handled by the collection view. Now, we could externalize all of the selection logic to a controller or mixin, but this is a common feature that all types of collection views have, so it is added to the view directly. But, in any case, to route selection information, the collection view (mostly) requires direct control of how selection works.
Let’s move on to the second point: externalizing information. For information that the collection view manages but is not part of the array of content, there could be cases when that information is useful for other components in an application. Again, we can witness this idea with the collection view’s selection information. Simply by allowing the collection view to control how items are selected is good, but it is also beneficial to externalize what items are selected so that other parts of your app can also react to the selection. This is why the collection view has the selection
property. So if you had a controller that has a binding to the selection, it can perhaps route that information to other views to display some additional selection information, as an example. In turn, the selection can be equally programmatically written to to inform the collection view of what should be selected.
Finally, the last point: the change to one item view can effect other item views outside of a content object’s control. A primary example of this can be seen when you allow for variable heights of the list item views in a list view. In SproutCore, to modify a specific list item’s height, you need to make use of the SC.CollectionRowDelegate
and assign the delegate to a list view. The list view will in turn use the delegate to check what the height of a specific list item should be. Therefore if there is one list item who’s height has been determined to be, say, 50 pixels, it will cause the list item below it to shift down by at least 50 pixels, and so on. The heights themselves are not directly part of the content objects.
Awesome. But are these the only guidelines? No, but they are the main guidelines that I’ve used while building SproutCore applications.
Part 3.2: Now to Get Our Hands Dirty
With the guidelines under our belts, let’s finally give this createItemView
a try. Back in part 2 of this tutorial I had you add a property to our custom list item view that toggles the visibility of a user’s title. However, unlike before where all the list item views were effected, we are now going to target specific list item views.
As a side note, I know that there might be a hand or two raised about the approach I’m taking for this tutorial. Toggling the visibility of the title could equality be done through the content object using controllers, such as a controller dynamically attaching a “isTitleVisible” property to content objects. Yes, true, that can be done and is admittedly preferable, but since we’ve been building on our custom list item for the last two tutorials, the approach we are going to use just makes it easier for everyone to follow. And now back to the rest of our regularly scheduled tutorial… :-).
The idea this time is that for the item selected in the list view there will be a check box a user can click on that will toggle the visibility of the title on the selected item. Pretty straight forward, right? Cool.
Part 3.2.1: Getting the Application Set Up
First things first. Let’s update the code of our custom list item view in the customer_list_item_view.js
so that it has a isTitleVisible
property:
MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {
classNames: ['custom-list-item-view'],
isTitleVisible: YES,
displayProperties: 'isSelected isEnabled isTitleVisible'.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 isCompanyVisible = this.get('isCompanyVisible');
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();
}
});
Great. Now let’s change our test controller in the test.js
file under the controllers
directory so that it is the following:
MyApp.testController = SC.ArrayController.create(
{
selection: null,
visibleTitleItems: [],
itemTitleVisible: function(key, value) {
var sel = this.getPath('selection.firstObject');
if (!sel) return NO;
var visibleTitles = this.get('visibleTitleItems');
if (value !== undefined) {
if (value === YES) {
if (visibleTitles.indexOf(sel) === -1) visibleTitles.pushObject(sel);
return YES;
}
else if (value === NO) {
if (visibleTitles.indexOf(sel) > -1) visibleTitles.removeObject(sel);
return NO;
}
}
return visibleTitles.indexOf(sel) > -1 ? YES : NO;
}.property('selection').cacheable()
});
Above, the selection
property informs the controller of what items have been selected in the list view. The visibleTitleItems
property is used to keep track of what item views in the list view will have a visible title. The itemTitleVisible
computed property is what will actually update the visibleTitleItems
property based on what has been selected. We’ll next bind all these properties to views.
Open the main_page.js
file in the resources
directory and replace all the code with the following:
MyApp.mainPage = SC.Page.design({
mainPane: SC.MainPane.design({
childViews: 'list toolbar'.w(),
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',
selectionBinding: 'MyApp.testController.selection',
exampleView: MyApp.CustomListItemView,
rowHeight: 54,
rowSpacing: 0,
allowDeselectAll: YES
})
}),
toolbar: SC.View.design({
layout: { bottom: 0, left: 0, right: 0, height: 30 },
classNames: ['toolbar'],
childViews: 'titleVisibleCheckbox'.w(),
titleVisibleCheckbox: SC.CheckboxView.design({
layout: { height: 20, width: 200, left: 10, centerY: 0 },
title: 'Title Visible',
isEnabled: NO,
isEnabledBinding: SC.Binding.oneWay().bool('MyApp.mainPage.list*selection.firstObject'),
valueBinding: 'MyApp.testController.itemTitleVisible'
})
})
}),
list: SC.outlet('mainPane.list.contentView')
});
In the main page, we changed the code so that we first of all removed the enable and disable radio buttons in the toolbar and replaced them with a single check box used to toggle a selected item’s title visibility. Notice how the check box is only enabled when something is selected. As well, the value of the check box is bound with the test controller’s itemTitleVisible
computed property. We also updated the list view so that the test controller’s and list view’s selection properties are bound together.
If you start up the application (sc-server
) you should see the following:
Go ahead and try selecting list items and clicking the check box. Notice how after checking the check box on a selected item the check box will then remain checked for that item until unchecked. However, at this point the list item views are obviously not changing the visibility of their title when you click on the check box. That comes next.
Part 3.2.2: Toggling Those Titles
In order to make use of the createItemView
method we’ll have to extend the list view, which means we need to make a new view. We’ll call our view CustomListView
and it’ll go into the views
directory; the same place where out custom list item view is located.
Basically our custom list view will not only need to override the createItemView
method but will also need a property that can be bound to in order for the view to know what list item views should actually display their title. Let’s call this property visibleTitleItems
. We’ll also need to observe changes to the property. Let’s create a skeleton of what our list view should be:
MyApp.CustomListView = SC.ListView.extend({
visibleTitleItems: null,
visibleTitleItemsBindingDefault: SC.Binding.oneWay(),
visibleTitleItemsDidChange: function() {
}.observes('*visibleTitleItems.[]'),
createItemView: function(exampleClass, idx, attrs) {
var view = exampleClass.create(attrs);
return view;
}
});
Good start. Hmm. So what else do we need to do? Let’s start with the createItemView
method. In the method we need to inform the view whether it should or should not display its title, and we’ll do this using the visibleTitleItems
property like so:
createItemView: function(exampleClass, idx, attrs) {
var visibleTitleItems = this.get('visibleTitleItems');
if (visibleTitleItems) {
attrs.isTitleVisible = visibleTitleItems.indexOf(attrs.content) > -1 ? YES : NO;
}
var view = exampleClass.create(attrs);
delete attrs.isTitleVisible;
return view;
}
We updated the method so that we check the view’s visibleTitleItems
property and determine if the content object is in the array. The value returned is then added to the attrs
object via the isTitleVisible
property that our custom list item view will pick up on. Finally we clean up by removing the property from the attrs
object as is required by the collection view’s createItemView
method (see line 971 in collection.js
file).
Okay, we’re almost there, but we have one more thing to do in our list view which is to react to changes made to the view’s visibleTitleItems
. We have an empty observer in place but just how do we tell the view to update the views so that the createItemView
will eventually be invoked? It turns out the collection view has a handy method called reload
.
When you call reload
you tell the collection view what item views to reload via an index set. If you supply a null value then all the item views will be reloaded. (Well, that’s not entirely true. By default the collection view will reload all the views for each object in the content array, but the list view that inherits the colleciton view has optimization so that only the items that are considered now showing are actually reloaded. So if you pass an content array of thousands of objects but the list view can only display at most ten at a time, then those considered now showing are reloaded, plus a few more that are not directly visible to the user).
Let’s go ahead an update the observer in our list view:
visibleTitleItemsDidChange: function() {
this.reload(null);
}.observes('*visibleTitleItems.[]'),
Perfect. Now when the property is modified the view will be notified to update all of its list item views. One more thing left to do which is to go back and update our main page. In the main_page.js
file, replace the list view with our custom list view like so:
list: SC.ScrollView.design({
layout: { top: 10, bottom: 30, left: 10, right: 10 },
contentView: MyApp.CustomListView.design({
layout: { top: 0, bottom: 0, left: 0, right: 0 },
contentBinding: 'MyApp.testController',
selectionBinding: 'MyApp.testController.selection',
visibleTitleItemsBinding: 'MyApp.testController.visibleTitleItems',
exampleView: MyApp.CustomListItemView,
rowHeight: 54,
rowSpacing: 0,
allowDeselectAll: YES
})
}),
Now we are making use of the custom list view and we have its visibleTitleItems
bound to the test controller. So once again, go ahead and restart the application. Once loaded, try selecting list items and then checking and unchecking the check box. If everything goes okay you’ll now see the selected item view’s title display changing. It should look something like the following:
Part 3.3: Improving What We’ve Got
Great stuff! We’ve finally got to make use of the createItemView
method in order to control how we effect specific list item views. That being said, does the code in the custom list item view feel, well, sub-optimal? It should. Why? Simply because every time the visibleTitleItems
property changes the observer is always calling reload
to update all of the list items views that are considered now showing. Calling reload
can potentially be an expensive operation and should be called both correctly and, more importantly, sparingly.
By “correctly” I mean that we shouldn’t be simply passing null to the reload
method by default. Rather, you should be telling the reload
method what specific item views really need to be updated (read: rebuilt) via an index set. You can do this by determining the difference between what was targeted before what is being targeted now, which is what the collection view does when the selection set changes. It makes use of the SC.SelectionSet
object to make calculating the difference between sets fairly easy. (see the collection view’s _cv_selectionContentDidChange
private method and the selection_set.js
file in the runtime/system
directory to see how this is done).
By “sparingly” I mean that you should call reload
only when absolutely necessary. In the case of our view, we can instead determine the difference of what was targeted before and now and then access the list item view directly via the itemViewForContentIndex
method (making sure the rebuild
parameter is NO
). Once you have the list item view you can then directly set the item view’s isTitleVisible
property. Again, this is the approach the collection view follows when the selection set changes.
So to make a point, its usually not enough to simply override the createItemView
. You have to factor in performance and added complexity too. This in turn also raises the point that if you feel that you need to override the createItemView
method, stop and and think if that’s really the case. Ask yourself the following questions before moving ahead:
- Does the list view need to have control over how a property is applied to an item view?
- Can I simply control the way a specific list item view is displayed through a content object that won’t impact other item views?
- If the content object does not have the property needed by the item view, is there a way I can dynamically add the property via a controller?
If you answer “no” to the first question and “yes” to the second and third question, then you probably won’t have to override createItemView
.
Part 3.4: To Conclude
And that all folks! Hopefully your journey through the three (admittedly long) parts of this tutorial allowed you have a better understanding of how to create your very own simple custom list item view and get a better understanding of just how the collection view manages those item views. As well, when, and more importantly, when not to override the collection view’s createItemView
method.
Have fun programming with SproutCore!
-FC