Archives for posts with tag: SC.CollectionView

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

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:

  1. 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
  2. The collection view needs to externalize specific information about the item views that is not directly captured in the content objects
  3. 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:

Cool!

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:

  1. Does the list view need to have control over how a property is applied to an item view?
  2. Can I simply control the way a specific list item view is displayed through a content object that won’t impact other item views?
  3. 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

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 exampleView property
  • 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, and isEnabled

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 createItemView method

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

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.Page the 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

In many cases when you are building an application for the desktop or the web, the need arises to display a list of content to a user in which the user can scroll through, sort, search, select and modify content among other things depending on the needs of a given feature. A good framework that is designed to help build desktop or web-based applications will usually come stock with a list component that is both easy to use and easy to extended for each application’s unique needs. SproutCore is one such framework that is ready to provide you with such a component — the SC.ListView.

If you go to the official SproutCore site and follow the todo tutorial you’ll get a good initial sense of how simple the list view is to use. Right out of the box you can give the list view an array of objects and set it up pretty quickly for users to browse through content and interactive with. And most of what you need to program can be done through the list view’s exposed properties. However, as you work with the list view you may come to a point where the the way the content is listed and how you interact with it in the view can only go so far. You may feel like you want to come up with a unique way to list content that makes more sense for your particular needs. Does the list view allow you to create unique items in the list? Is it difficult to do? For the first question: Yes. For the second question: No.

In order to display each item in the list view, the list view makes use of a default list item called SC.ListItemView (located in the list_item.js file in the frameworks/desktop/view directory). If you look through the JS file you’ll notice a lot of code. This is because the list item view was designed to be very flexible so that you can do a lot with it out of the box. At first this might seem intimidating because it makes you think you will also have to write as much code yourself. Don’t be concerned. A good chunk of the code is devoted to rendering the list item’s many features and getting it set up so that you can edit content inline. What I’m here to do it get you introduced with the basics, and it really doesn’t take much. So with that being said, let’s dig in!

Part 1.1: Getting Ourselves Setup

Before we start writing a custom list item view, we need to get some stuff set up. First I want you to start with a fresh application by running sc-init and calling your application MyApp. Now that we have a foundation, I want you to go ahead and create a controller by doing the following:

sc-gen controller MyApp.testController SC.ArrayController

Open the newly created test.js file in the controllers directory and replace the content of the file with the following:


MyApp.testController = SC.ArrayController.create(
{
  selection: null,
  
  listEnabled: YES,
  
  listView: null,
  
  /**
    Note: This is a hack. Currently the SC.CollectionView does not correctly respond to 
    changes made to its isEnabled property. Therefore we have to force the collection 
    view to reload to make sure the view and its list item views correctly receive the 
    changes. 
    
    In addition, the SC.CollectionView does not handle changes to its isSelectable 
    property either. Sigh.
    
    Such is life working with a beta release. [Sept 6, 2009]  
  */
  listEnabledChanged: function() {
    var list = this.get('listView');
    list.set('isEnabled', this.get('listEnabled'));
    list.reload();
  }.observes('listEnabled')
}) ;

Now, open the main_page.js file in the english.lproj 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'
      })
    }),
    
    toolbar: SC.View.design({
      layout: { bottom: 0, left: 0, right: 0, height: 30 },
      classNames: ['toolbar'],
      childViews: 'enableList'.w(),
      
      enableList: SC.RadioView.design({
        layout: { height: 20, width: 200, right: 10, centerY: 0 },
        items: [{ title: "Enable", 
                  value: YES },
                { title: "Disable", 
                  value: NO }],
        valueBinding: 'MyApp.testController.listEnabled',
        itemTitleKey: 'title',
        itemValueKey: 'value',
        layoutDirection: SC.LAYOUT_HORIZONTAL
      })
    })
  })

});

Next, create a new CSS file in the english.lproj resources directory called style.css and copy the following code into it:


div.custom-list-item-view div.top {
  position: absolute;
  top: 0px;
  left: 0px;
  right: 0px;
  height: 35px;
  border-width: 0px;
}

div.custom-list-item-view div.top.standard {
  background-color: #c0c0c0;
}

div.custom-list-item-view p.name {
  float: left;
  padding: 0px;
  margin: 5px 0px 0px 5px;
  font-family: Georgia;
  font-size: 2em;
  font-weight: bold;
  line-height: normal;
}

div.custom-list-item-view div.bottom {
  position: absolute;
  top: 35px;
  left: 0px;
  right: 0px;
  height: 15px;
}

div.custom-list-item-view div.bottom.standard {
  background-color: #e3e3e3;
  border-bottom-color: black;
  border-bottom-width: 3px;
  border-bottom-style: solid;
}

div.custom-list-item-view p.item {
  float: left;
  padding: 0px;
  font-family: Helvetica;
  font-size: 1em;
  line-height: normal;
}

div.custom-list-item-view p.company {
  margin: 0px 0px 0px 5px;
}

div.custom-list-item-view p.title {
  margin: 0px 0px 0px 10px;
}

div.custom-list-item-view span.label {
  font-weight: bold;
}

div.custom-list-item-view span.value {
  margin-left: 5px;
  font-style: italic;
}

.toolbar .sc-radio-button {
  margin: 0px 10px 0px 0px;
}

Finally, let’s updated the main.js file located in the app’s root directory. Replace the file with the following code:


MyApp.main = function main() {

  
  MyApp.getPath('mainPage.mainPane').append() ;

  var content = [
    SC.Object.create({ 
      fname: 'John', 
      lname: 'Doe', 
      company: 'Google',
      title: 'Senior Manager' 
    }),

    SC.Object.create({ 
      fname: 'Bob', 
      lname: 'Smith', 
      company: 'Microsoft', 
      title: 'Sales'
    }),
    
    SC.Object.create({ 
      fname: 'Fred', 
      lname: 'MacDoogle', 
      company: 'Apple', 
      title: 'Developer'
    })
  ];

  var controller = MyApp.testController;
  controller.set('content', content);
  
  var listView = MyApp.mainPage.mainPane.childViews[0].contentView;
  controller.set('listView', listView);

} ;

function main() { MyApp.main(); }

Awesomeness. If you run the app you won’t see much even though we have wired the list view to the controller. That’ll change soon. Just remember that we are getting things set up quickly for the purposes of this tutorial, not because we are trying to make a real, professional application :).

Here’s what you should see so far:

custom-list-item-p1-default

Part 1.2: Creating Our Custom List Item View

To start making a simple custom list view item we have to create a new view, which we’ll do by running the following command:

sc-gen view MyApp.CustomListItemView

To get us started we need two things: 1) a content property; and 2) a way to render the content. (The objects making up the content are located in the main.js file). Hmm. If you’ve read my prior posting on this blog, those two things seem familiar… Familiar like… like… like creating a custom view! This means we can leverage what we’ve learned before. (If you’re new to this blog or haven’t had a chance to read up on how to create a simple custom view, start by reading posts here and here).

For out custom list item view, we want it to look like the following:

custom-list-item-view

As you can see above, our view will display a person’s first name, last name, the company the person works for, and the title the person holds at the company. Let’s update the custom list item view’s code to be the following:


MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {

  classNames: ['custom-list-item-view'],

  contentDisplayProperties: 'fname lname company title'.w(),
  
  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');

    context = context.begin().addClass('top');
    context = context.begin('p').addClass('name').push('%@, %@'.fmt(lname, fname)).end();
    context = context.end(); // div.top
    
    context = context.begin().addClass('bottom');
    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.item.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.item.title
    context = context.end() // div.bottom
    
    sc_super();
  }

});

This is enough to get us to the point where we can set up our list view to show our custom list view item. But how do we get our list view to actually display the custom list view item? Hmm. Well, it turns out that SC.ListView extends another view called SC.CollectionView. SC.CollectionView is the root view to display a collection of views. In SC.CollectionView it has a property called exampleView (collection.js, line 286) that when given a view, it will use it to display all the content objects in a given array. And as you’ll notice, the default setting for exampleView is SC.ListItemView — Ta-da!

Looking at the comments for the exampleView property, you’ll see that the three most important properties the example view should have are the following:

  • content – The content object from the content array your view should display
  • isSelected – True if the view should appear selected
  • isEnabled – True if the view should appear enabled

So it would appear that the list view has a bit of a contract with the view used to display its array of content. We’ll start with the content property and work our way to using the isSelected and isEnabled property. As a side note, the list view, or actually the collection view, also supplies the example view with some additional attributes, but I’ll get to that when I post part two of this tutorial… I just had to toss that in there ;).

In the main_page.js file, update the list view so that we set the exampleView property 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,
    rowHeight: 54,
    rowSpacing: 0
  })
})

That’s all ya need to do. (In addition to setting the exampleView property, we also set the list view’s rowHeight and rowSpacing properties). Now go ahead and refresh your browser to see the result. You should get something that looks like the following:

custom-list-item-p1-custview

Pretty sweet! That really didn’t take much effort. Now, bare in mind that we are just rendering the content and not doing much else. So our custom list item view is okay but we can do better. Let’s update our custom list item view so that when the user selects it in the list view, the list item view will update to visually indicate it is currently selected. How do we actually go about doing this? Remember that contract that the list view has with its example view? Well we’re going to take advantage of the isSelected property that the list view adds to our custom list item view.

Part 1.3: Becoming Selected

When a user selects an item in the list view, the list view will set the selected list item view’s isSelected property. We can take advantage of this by updating our view to be the following:


MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {

  classNames: ['custom-list-item-view'],

  displayProperties: 'isSelected'.w(),

  contentDisplayProperties: 'fname lname company title'.w(),
  
  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 standard = !isSelected;
    var selected = isSelected;
    var classes = { 'standard': standard, 'selected': selected };
  
    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();
  }

});

What did we just do? Basically we updated the code so that the view will re-render when the isSelected property is updated by the list view. As well, we updated the render method so that parts of the view will either have “standard” or “selected” applied to the class attribute. If isSelected is true then “selected” will be added to an element’s class attribute. If isSelected is false then only “standard” will be added to an element’s class attribute. (Notice how we took advantage of the render context object’s setClass method). Easy, right? Go ahead and refresh your browser and select the items in your list view. What did you get? Nothing changed? Uh-oh. Something’s not right. We’re missing something. Remember the CSS file I had you create earlier on? Well it needs to be updated to take advantage of the “selected” class that gets added to the HTML elements. Add the following to your CSS file:


div.custom-list-item-view div.top.selected {
  background-color: #2222FF;
}

div.custom-list-item-view div.bottom.selected {
  background-color: #9999FF;
  border-bottom-color: black;
  border-bottom-width: 3px;
  border-bottom-style: solid;
}

Great. With the CSS file updated to make use of the “selected” class, let’s go back to our browser and refresh it. Select the list item views. You should get something like the following:

custom-list-item-p1-selected

Success! Now we got our custom list view to be interactive and it didn’t take much work to do it. Woot! Okay, so that’s the isSelected property down. We’re almost done.

Part 1.4: Enabling and Disabling

How about we wrap up and make use of the isEnabled property. Just as a heads up, as of this writing, Sept 6, 2009, the SC.CollectionView doesn’t properly update when the isEnabled property is changed. So I had to do a bit of a hack that you can see if you look at the code in the test controller. I was working on fixing the problem, but I decided to just go ahead and shoot out this post. SproutCore is still in beta after all. With that being said, let’s go ahead and update your custom list item view’s code one more time:


MyApp.CustomListItemView = SC.View.extend(SC.ContentDisplay, {

  classNames: ['custom-list-item-view'],

  displayProperties: 'isSelected isEnabled'.w(),

  contentDisplayProperties: 'fname lname company title'.w(),
  
  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 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.item.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.item.title
    context = context.end() // div.bottom
    
    sc_super();
  }

});

Like the isSelected property, we are just doing the same thing with the isEnabled property where we set the “disabled” class to parts of our view. Nothin’ to it. We’re going to have to update our CSS file one more time by adding the following to the file:


div.custom-list-item-view div.top.disabled {
  background-color: #f0f0f0;
  color: #d0d0d0;
}

div.custom-list-item-view div.bottom.disabled {
  background-color: #ffffff;
  color: #d0d0d0;
  border-bottom-color: #c0c0c0;
  border-bottom-width: 3px;
  border-bottom-style: solid;
}

Okay. Now you can go ahead and refresh your browser. Click on the disable radio button that is down in the bottom-right of the window. If everything went as planned your view should look like the following:

custom-list-item-p1-disabled

You did it! The custom list items now look disabled. If you click on the enabled radio button the list items will return to their standard look. Again, there really wasn’t much effort to get this working.

As another one of those beta issues, you can still click on the items in the list view and the list view’s selection property will still be updated. D’oh. As the comments say in the collection.js file for the isEnabled property, nothing should be selectable. There is the same issue with the collection view’s isSelectable property. Again, beta. I’m not sure if anyone else in the SproutCore community has been working on fixing this, but I am in the middle of trying to correct the problem.

Part 1.5: Until We Meet Again

There ya go. You’ve successfully created your own custom list item view making use of the three properties: content, isSelected and isEnabled. Now I should stress that this custom list item view is not nearly as feature complete as the default SC.ListItemView, but that’s okay. If you rather make use of the SC.ListItemView to take advantage of all it has to offer you can by simply extend it and overriding its various methods.

In second part of this tutorial, I will be going into some more advanced things you can do when creating a custom list item view. But until then, have fun programming in SproutCore!