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

Advertisements