Working with SproutCore's controllers

Just as most of the objects in the View layer are views, we call the primary objects in the Controller layer as controllers. The base class for these controllers is the SC.Controller class, but it does essentially nothing more than the SC.Object class it extends, so we will always use one of its subclasses such as SC.ObjectController, SC.ArrayController or SC.TreeController. The role of these three types of controllers should be fairly self-evident. The SC.ObjectController subclass proxies a single object, SC.ArrayController proxies an array or array-like object and SC.TreeController proxies a tree-like object.

To use any of these controllers is a matter of creating the controller and setting its content property to the appropriate type of object. Once the content of the controller is set, we can use the controller as a proxy to the original object. In this way, the controller can translate between mismatched endpoints if necessary. Let's look at a couple of simple examples.

First, if we had a MyApp.Person model such as the following code:

MyApp.Person = SC.Record.extend({

  firstName: SC.Record.attr(String),

  lastName: SC.Record.attr(String)

}); 

we could create an object controller that would back a person's record shown as follows:

// Our person controller singleton.
MyApp.personController = SC.ObjectController.create();

The best part of this is that it gives us a single controller that the entire application can use to access the current person's record. Each time that we retrieve a person's record from the store and wish to display or edit it, we would set it as the content of this controller and anything bound to MyApp.personController.content would update accordingly. This same concept applies for the array and tree controllers as well.

We have already seen a few controller examples in Chapter 1, Introducing SproutCore, but before we move on, we should look at one more example to help clarify what mediating between the Model and View layers really means.

Using the preceding example code, let's play with our controller in the browser console. To prepare this, I will first push a sample MyApp.Person record to the store that we can use. Have a look at the following screenshot:

Working with SproutCore's controllers

Now we can re-enact the simple pattern that I have described earlier, that is, to retrieve an object and set it as the content of a controller. For example, have a look at the following screenshot:

Working with SproutCore's controllers

As you can see, once I've set the content of the controller, I can access the properties of the content directly on the controller. The way this works is that if the property doesn't exist on the controller itself, the controller will attempt to retrieve it from its content. This is the behavior we employ to provide more appropriate properties for use elsewhere in the app.

For example, in Chapter 1, Introducing SproutCore, we had a similar model to our MyApp.Person model called Contacts.Contact. If you recall, that model had a computed property, fullName that generated the full name of the contact. While having the model, providing the full name is not a bad idea; if you think about it, the full name of the person is actually a display property. For an instance, depending on the language or the user preference, full name may be firstName lastName or it may be lastName firstName or something slightly different. Does it make sense for the model to be connected directly to a global display property determining the name order? No, it doesn't. It would actually cause us a lot of trouble if we had to update all our records each time the user changed the way they wanted the full name to be displayed.

Instead, this is a place where our mediating controller can step in to provide a suitable property without modifying the underlying data layer to do so. Here's an updated personController that makes a proper fullName property available to any view or any other that wants it:

MyApp.personController = SC.ObjectController.create({
  /** Determine the display order of the full name. */
  displayOrder: 'lastName',

  fullName: function () {
    var displayOrder = this.get('displayOrder'),
      firstName = this.get('firstName'),
      lastName = this.get('lastName'),

    if (displayOrder === 'lastName') {
      return [lastName + ',', firstName].compact().join(' '),
    } else {
      return [firstName, lastName].compact().join(' '),
    }
  }.property('firstName', 'lastName', 'displayOrder').cacheable()

});

And, here's our new controller in action once more:

Working with SproutCore's controllers

See how easily we can modify the display order using the global person controller. You can imagine how having several views bound to the fullName property would allow you to toggle the displayOrder value to magically update all the views.

Tip

Be careful when setting or binding properties on an object controller. Just as get goes to the controller's content object, if the property doesn't exist on the controller, set will also set the value directly on the content object if the property isn't defined on the controller. To avoid accidentally dirtying the content object with a property meant only for the controller, be sure to always define the property on the controller.

SC.ArrayController

This controller is used to house a collection of objects such as an array or a set and is a very important and widely used controller in most SproutCore apps. The reason SC.ArrayController is so useful is because it not only proxies a collection of objects, but also automatically observes the collection for membership changes. This allows us to easily bind to arrays and other enumerables, simply by setting them as the content of an array controller and binding to that array controller.

Let's have a look at a basic array controller setup. We begin with a collection of items such as those returned by a query on the store.

var people = MyApp.store.find(MyApp.Person);

Which we simply assign as the content of a controller shown as follows:

MyApp.peopleController = SC.ArrayController.create({
  content: people
});

Once we've assigned the content, we can use the array controller much like any other enumerable. For example, have a look at the following screenshot:

SC.ArrayController

But, what we typically want to do with the controller is to bind it to a collection view so that we can display the items and have that display update automatically when the items change. For instance, this is exactly what we did in the Connecting it all together section from Chapter 1, Introducing SproutCore.

Here's some code from that tutorial that bound the content of an SC.ListView to the arrangedObjects property of an array controller:

contentView: SC.ListView.design({
    // The content for this list is contained in Contacts.groupsController.
    contentBinding: 'Contacts.groupsController.arrangedObjects'

The key thing to remember is that we should always access the array controller's content via the special arrangedObjects property. This is because we want to get the proxied version of the content, not the content itself, in case the array controller has transformed the content somehow.

Here's an example that shows this better and introduces one more special property of SC.ArrayController that we can use to order the content, called orderBy. In the first screenshot, we see that arrangedObjects and content appear to be the same.

SC.ArrayController

However, once we set an orderBy value, the original content and the content returned by arrangedObjects are different. This is because the controller is doing a simple sort transform on the content without actually modifying the original content.

SC.ArrayController

In this manner, the array controller can be used to provide data that is modified to meet the needs of whichever views are consuming it, including returning placeholder data for an empty array.

SC.TreeController

The final controller in SproutCore is used for managing tree-structured collections. Although SC.TreeController is not nearly as simple to use as SC.ObjectController or SC.ArrayController, because of its extreme power, you will definitely want to use a tree controller for any hierarchical data. Attempting to manage tree data on your own any other way would be a difficult and time-consuming task. What SC.TreeController does for us is that it provides an arrangedObjects property like SC.ArrayController, so that we can bind and display tree data in a collection view. Most importantly, it observes the tree structure for changes and automatically updates arrangedObjects as well.

The key to understanding tree controllers is really just to understand which properties must exist in the content to make the entire tree work. Once you know what the controller is looking for, it becomes much easier to use it without having to worry about the magic it's doing behind the scenes to transform your tree into displayable data.

Essentially, there are only two properties required by the controller: treeItemChildren and treeItemIsExpanded. By default, each object in the tree will be inspected for these two properties, which the controller will then use and observe for changes. If treeItemChildren of an object returns an array of other objects, that parent object will become a branch in the tree and if treeItemChildren returns null, that object will become a leaf in the tree.

To best include these properties to your model objects, we will mix in SC.TreeItemContent to the class, which defines these properties as well as some additional methods used by the controller and any list views displaying the content. For example, to be able to display an employee hierarchy, we would first mix in SC.TreeItemContent into the MyApp.Employee model. Have a look at the following code:

MyApp.Employee = SC.Record.extend(SC.TreeItemContent, {

  employees: SC.Record.toMany('MyApp.Employee'),

  name: SC.Record.attr(String)

});

We'll leave the value of treeItemIsExpanded as its default of true, so all we still need to do is provide the treeItemChildren, which in this case is the value of employees. One option would be to rename the employees attribute to treeItemChildren, but we may want to work with employees in different contexts and may not want to have its name be so ambiguous, so instead we should simply add a computed property for treeItemChildren. Have a look at the following code:

// …  

  treeItemChildren: function () {
    var employees = this.get('employees'),

    // Return null so this employee is a leaf in the tree.
    if (SC.empty(employees)) { return null; }
    else { return employees; }
  }.property('employees')

// …

Tip

We could also use different property names for treeItemChildren and treeItemIsExpanded by setting the values of treeItemChildrenKey and treeItemIsExpandedKey on the controller to some other names. However, as of Version 1.10, SC.TreeItemContent only respects using treeItemChildren and treeItemIsExpanded, so it's better if we use these property names.

Finally, we simply need to set the root object as the content of a tree controller so that we can start using it. For example, the root of the employee tree could be the president or CEO record.

// Create a tree controller to back the employees display.
MyApp.employeesTreeController = SC.TreeController.create();

// Retrieve the CEO record in some manner.
var ceo = MyApp.store.find(MyApp.Employee, 1);
 
// Assign the CEO as root of the tree controller.
MyApp.employeesTreeController.set('content', ceo);

Once our controller is set up, we use it like any other array controller and can bind its arrangedObjects property to a list view. There are some additional nuances to using tree controllers, but this should be enough to get you started. As well, all of these controllers appear in a number of SproutCore demos and so you can refer to the source code of the demos at http://showcase.sproutcore.com for more examples when you're ready.

Tip

The root object of the tree controller does not appear in arrangedObjects. Therefore, if we wanted to include the CEO from the previous example in the list, we would simply create a wrapper root object like,

rootObject = SC.Object.create(SC.TreeItemContent, {
  treeItemChildren: [ceo] 
});

So there you have it, working with the array controller and tree controller is very similar to object controller, only using their respective content types and all in all it's pretty simple. However, that's not to gloss over the subtlety of using controllers with views and models appropriately. Good SproutCore development often doesn't mean writing a lot of code. Instead it means writing very little code, but only the right code. Knowing what code to write and where to put it takes some experience, but by this point you are well on your way. In fact, there is only one more major area to cover before we wrap up.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.235.219