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!

Advertisements