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:
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:
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:
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.
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.
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:
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.
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.
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.
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') // …
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.
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.
3.143.235.219