SproutCore

SproutCore Guides

These guides are designed to help you write and perfect your code.

Core Concepts

The guide covers some of the core concepts of SproutCore. By referring to this guide, you will be able to:

  • Understand how classes work, including SC.Object.
  • Be familiar with Properties, Observers, Bindings, and the Run Loop.
  • Write your own classes based on SC.Object with full use of Observers and Bindings.
  • Know when to worry about the Run Loop.

1 SC.Object and Classes

SproutCore maintains a traditional object-oriented structure at the root of which lies SC.Object. SC.Object defines all the basic features needed by a class in SproutCore. These include Properties, Observers and Bindings, which I will go into detail on below.

1.1 Creating an SC.Object Instance

Creation of an SC.Object instance is straightforward.


var obj = SC.Object.create()

In most cases you will want to create your SC.Object with some pre-set properties. You accomplish this by providing the properties in a hash.


var person = SC.Object.create({
  firstName: 'Peter',
  lastName:  'Wagenet'
});
person.get('firstName'); // Peter
person.get('lastName');  // Wagenet

We will explain the get and set functions in more detail later. For now, it’s worth getting in the habit of using them.

1.2 Creating an SC.Object Subclass

In many cases you will want to create subclasses of SC.Object to handle similar objects. In the above example, we created a person. Normally, however, it would make sense to create a Person class, and then to create objects from it. In order to do this, we make a subclass of SC.Object.


App.Person = SC.Object.extend({
  firstName: null,
  lastName:  null,

  fullName: function(){
    return this.get('firstName')+' '+this.get('lastName');
  }
});

You could then create an instance of the Person class, and it would contain the properties firstName and lastName, and the function fullName.


var person = App.Person.create({
  firstName: 'Peter',
  lastName:  'Wagenet'
});
person.fullName(); // Peter Wagenet

In SproutCore, the fullName function would probably be defined as a computed property, to allow for things like caching and key value observing. We’ll discuss this below.

1.2.1 Calling super methods with sc_super

In some cases when you subclass an object you will want to augment a method of the parent class without completely overriding it. In this case, SproutCore provides the sc_super method which calls the original function. sc_super works very similarly to the super method in, for instance, Ruby.


App.FormalPerson = App.Person.extend({
  title: null,
  fullName: function(){
    return this.get('title') + ' ' + sc_super();
  }
});
var person = App.FormalPerson.create({
  title:     'Mr.',
  firstName: 'Peter',
  lastName:  'Wagenet'
});
person.fullName(); // Mr. Peter Wagenet

The sc_super can also pass arguments onwards.

The sc_super is one of the rare exceptions in SproutCore in that it is not actually a real JavaScript function. It’s just a pre-processor directive that gets replaced with arguments.callee.base.apply(this, arguments).

1.2.2 The init method

Whenever an instance of SC.Object is created, the init method is called. This function can be overridden when you need to perform some setup functionality when creating your object.


App.Calculation = SC.Object.extend({
  input: null,
  result: null,

  _calculate: function(){
    // Do expensive calculation
    this.set('result', this.get('input') * 2);
  },

  init: function(){
    sc_super();
    this._calculate();
  }
});

var calc = App.Calculation.create({ input: 5 });
calc.result; // 10

By convention private properties and functions are prefaced with an underscore.

When overriding the init method it is important that you call sc_super at the start of your custom init method. This will make sure that all internal object initialization takes place as expected.

1.2.3 Using Mixins

Mixins are an easy way to extend multiple classes that don’t share an inheritance tree with similar functionality. Mixins are just a hash with a series of properties that will be added to the class you create.


App.Friendly = {
  sayHello: function(){
    return 'Hello, my name is ' + this.name;
  }
};

To add one or more mixins to a hash, add them as parameters to extend:


App.Person = SC.Object.extend(App.Friendly, {
  name: null;
});

var person = App.Person.create({ name: 'Bob' });
person.sayHello(); // Hello, my name is Bob

As you may have guessed, SC.Object.extend just takes a series of hashes and mixes them all in to a newly created class.

1.2.4 Class methods

You can also define class methods on your custom classes. If you need to add a single class method to a class, you can do it in the traditional JavaScript manner.


App.Person.quickCreate = function(firstName, lastName){
  return App.Person.create({
    firstName: firstName,
    lastName: lastName
  });
};

var person = App.Person.quickCreate('Peter', 'Wagenet');
person.fullName(); // Peter Wagenet

However, sometimes you may want to add a series of class methods or properties. In this case you can use the mixin method.

Note that the mixin method is different from the Mixins described above. The mixin method will add the properties and methods within the block as class properties and methods. Mixins themselves add instance variables and methods.


App.Person.mixin({
  quickCreate: function(firstName, lastName){
    return App.Person.create({
      firstName: firstName,
      lastName: lastName
    });
  }),
});

person = App.Person.quickCreate('Mary', 'Shelley');
person.fullName(); // Mary Shelley

2 Properties, Bindings and Observers

Properties, Bindings and Observers all make up what is known as the Key-Value Observing (KVO) system of SproutCore.

2.1 Getters and Setters

For KVO to work properly, SproutCore implements getters and setters to track changes to objects. This is why it’s important to use get and set for any properties that might use observers, bindings, or computed properties. Failure to do so will quickly cause your app to get out of sync. I know this may sound like a bit of a pain, but don’t worry, you’ll quickly get used to using get and set and you’ll forget you ever had to worry about it.


var obj = SC.Object.create({ name: 'Jim' });
obj.get('name'); // Jim
obj.set('name', 'Bob');
obj.get('name'); // Bob

Those of you who have worked in languages like Java may be familiar with having to write property accessors. Instead of having to define accessors for every property, get and set serve as universal accessors.

2.2 Computed Properties

Sometimes you may have properties that depend on other properties. These are known as computed properties. Computed properties are defined as functions with a call to property and a list of the dependent properties.


App.Person = SC.Object.extend({
  firstName: null,
  lastName:  null,

  fullName: function(){
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName')
});

var person = App.Person.create({
  firstName: 'Peter',
  lastName:  'Wagenet'
});

person.get('fullName'); // Peter Wagenet
person.set('lastName', 'Smith');
person.get('fullName'); // Peter Smith

As you can see, you are able to use get with computed properties in the same way you would use it with normal ones.

Setting computed properties is slightly more complicated, but still easy to grasp. The key and value are passed in as the first two arguments to the property function. The get function will call a computed property with this value set to undefined, where the set function will set value to the property given in the parameters to set.


App.Capitalizer = SC.Object.extend({
  capitalizedValue: function(key, value){
    if (value !== undefined) {
      this._capitalizedValue = value.toUpperCase();
    }
    return this._capitalizedValue;
  }.property()
});

var cap = App.Capitalizer.create();
cap.set('capitalizedValue', 'abc'); // Calls capitalizedValue with value set to 'abc'. Sets the value, then returns ABC.
cap.get('capitalizedValue'); // Calls capitalizedValue with value set to undefined. Returns ABC.

In most cases you will find you can just ignore the keyName parameter.

2.3 Property Paths

SproutCore also introduces the concept of property paths. A property path is a string that points to a nested property. For instance "App.userController.firstName" refers to the firstName property on the App.userController object. You can also have relative paths which refer paths relative to the current object. For instance, within the context of App, you could use ".userController.firstName" to point to the same location. As you can see, the relative property begins with a period. This will always be true of relative properties.

To work with relative property paths you can use the getPath and setPath functions. These work identically to get and set except they expect a path for the first argument.

In addition to getPath and setPath you will also see property paths used for observers and bindings, which we will explain below.

3 Observers

Closely related to the concept of properties is that of observers. Observers do exactly what their name suggests, they observe properties, watching for changes. The most basic observer looks like this:


var obj = SC.Object.create({
  value: null,
  valueDidUpdate: function(){
    alert('New Value: ' + this.get('value'));
  }.observes('value')
});

obj.set('value', 'Test'); // alert('New Value: Test');

As you can see observes is called on a function and when the property is updated the function is called. You can also pass multiple properties into an observer.


App.Person = SC.Object.extend({
  firstName: null,
  lastName: null,

  nameDidChange: function() {
    alert('The Name changed!');
  }.observes('firstName','lastName');
});

person = App.Person.create({
  firstName: 'Emma',
  lastName: 'Goldman'
});

person.set('lastName','Berkman'); // alert('The Name changed!')
person.set('firstName','Alexander'); //alert('The Name changed!')

Observers will also accept property paths to observe.

3.1 Observer Notification

It is important to realize that observers have to be notified of property changes. The set method handles this for us automatically – this is one of the reasons get and set should be used to access properties. However, there are cases where using set doesn’t work. In these cases you can use notifyPropertyChange to tell observers that the property has been updated, i.e. this.notifyPropertyChange('value').

In some cases you may find that you are updating a number of properties at once. If you have a lot of observers that depend on these properties, you may find them getting called more often than necessary. In this case, you can use beginPropertyChanges and endPropertyChanges to wrap your property change calls. This will cause all change notifications to happen once endPropertyChanges is called and will prevent unnecessary duplicate notifications.

3.2 Chained Property Paths

Observers and bindings also use a special type of property path called a chained property path. When using an observer or binding, usually the actual observer is only added to the the second to last object in the property path. So, if your path is "App.usersController.mainUser.name" then SproutCore finds the object at "App.usersController.mainUser" and then adds an observer for its name property. However, nothing is observing App.usersController to see if mainUser changes to a different user. For example:


App.usersController = SC.ArrayController.create({
  mainUser: SC.Object.create({
    name: 'Joe'
  })
});

App.observerObject = SC.View.create({
  userNameDidChange: function() {
    alert(this.getPath('App.usersController.mainUser.name'));
  }.observes('App.usersController.mainUser.name')
});

App.usersController.setPath('mainUser.name', 'Jim'); // alert('Jim')
App.usersController.set('mainUser', SC.Object.create({ name: 'Bob' })); // No alert

As you can see, when we replace mainUser the observer does not fire. This is because we only have an observer on the the original user object. We are not watching for changes to usersController.

What we want to do is watch for changes to usersController.mainUser and for changes to mainUser.name. This is where chained property paths come in. To let SproutCore knows that we want to observe for both changes, we use App.usersController*mainUser.name.

The asterisk (*) in the property path indicates that we want SproutCore to observe changes to all properties following the asterisk. In this case, that is both mainUser and name. This is known as a chained observer.

Here’s an updated version of the previous example with a chained observer:


App.observerObject = SC.View.create({
  userNameDidChange: function() {
    alert(this.getPath('App.usersController.mainUser.name'));
  }.observes('App.usersController*mainUser.name') // We have made this a chained observer
});

App.usersController.setPath('mainUser.name', 'Jim'); // alert(Jim)
App.usersController.set('mainUser', SC.Object.create({ name: 'Bob' })); // alert('Bob')

The observer will now fire if App.usersController.mainUser.name changes, or the App.usersController.mainUser object itself is changed.

So why don’t we always use chained observers? Observers are expensive and often times we don’t care about changes in all levels. In the previous example, we don’t care about changes to App.usersController. The controller is set once and won’t ever change through the lifetime of the application. Had we defaulted to chained observers we would also be observing this, which isn’t necessary. It is advisable in practice to use chained observers as little as possible, in order to protect performance.

4 Bindings

When you put together Properties and Observers, you end up with Bindings. Bindings serve to link two properties together. For example:


App.userController = SC.Object.create({
  name: null
});

App.mainView = SC.View.create({
  userBinding: SC.Binding.from('App.userController.name')
});

App.userController.set('name', 'Joe');

// The runloop must run

console.log(App.mainView.get('user')); // Joe
App.mainView.set('user', 'Jim');

// The runloop must run

console.log(App.userController.get('name')); // Jim

One very important difference between bindings and observers is that observers update almost immediately (see Section 3.1: Observer Notification), while bindings update only at the end of each run loop. This offers considerable improvements in performance, but can be confusing if you are acting outside the normal SproutCore context – for instance, when using a console. See ‘The Run Loop’ for more details.

In this example, we used the absolute path to the property. Bindings support relative properties and chained properties as well.

As you can also see, when you update the value in one location, it is updated in the other. By default bindings are bi-directional but you can also set them to only go in one direction by using SC.Binding.oneWay instead. In this case the local property (where the binding is defined) will be updated when the remote property changes, but changes to the local property will not propagate to the remote property.

4.1 Bindings Shorthand

Since simple bindings are used so often, there’s also a shortcut to creating them. By default, if you only specify a string for the binding it is created as if you had done SC.Binding.from(STRING). In the above example, you could instead do:


userBinding: 'App.userController.name'

4.2 Bindings and Chained Property Paths

Since bindings make use of observers behind the scenes, the same principles of chained observers applies. This means that if you want to observe a chained property, you will want to use asterisks in your path as appropriate.


App.usersController = SC.ArrayController.create({
  mainUser: SC.Object.create({
    name: 'Joe'
  })
});

App.userNameView = SC.LabelView.create({
  valueBinding: 'App.usersController*mainUser.name'
});

App.usersController.setPath('mainUser.name', 'Harriet');

// The run loop must run

App.userNameView.get('value'); // 'Harriet'

App.usersController.set('mainUser', SC.Object.create({ name: 'Eunice' }));

// The run loop must run

App.userNameView.get('value'); // 'Eunice'

Since we used an asterisk in the valueBinding property path, the binding will be updated if either mainUser or mainUser.name changes.

5 The Run Loop

You will rarely have to reference the Run Loop in your app itself. However, you may find it useful in debugging and will likely have to use it in your unit tests. It’s also worth gaining an understanding of it to understand more about how your application works.

The run loop coordinates all the events within your application. This includes primarily observers and timers. This helps make sure that events stay synchronized and run at the proper times. One of the main ways this will affect your application is that bindings do not fire until the Run Loop has fired.

SproutCore manages the run loop for you automatically triggering when it receives any browser events or user input. The only case in which you will have to manage the run loop from within your app is if you have a callback from an external library that is not managed by SproutCore. In this circumstance, you will want to trigger a new run loop by wrapping the code in SC.RunLoop.begin() and SC.RunLoop.end(). This will create a new run loop for that bit of code and will make sure all events are fired upon completion.

When working with unit tests, you will also have situations where you need to force bindings and observers to update so you can check for the correct results. In these cases, it is completely legitimate to manually invoke the run loop. Normally user events would trigger the run loop, but since your tests are automated, there are no user events taking place.

You may also find that when you are working in your browser’s JavaScript console that, when you set a variable, your application’s interface doesn’t update as you expect. This may be caused by the run loop not firing (since you aren’t interacting directly with the application no events are being triggered). In this case you should try invoking the run loop manually, or just move your mouse over the application to trigger a new event.

Remember, you rarely need to manage the run loop manually from within your application. If you aren’t certain of why you are using it, then you are probably doing something wrong. The average user should use the run loop only in Unit Tests or in the console.

6 Changelog

  • January 12, 2011: initial partial version by Peter Wagenet
  • January 19, 2011: further updates by Peter Wagenet
  • January 20, 2011: corrections to “The init Method” and “The Run Loop” by Peter Wagenet
  • January 24, 2011: added section on “Bindings and Chained Property Paths” by Peter Wagenet
  • March 2, 2011: fixed paragraph formmatting by Topher Fangio
  • March 2, 2011: fixed grammar, clarified phrasing, added examples to the Observer section by Jason Gignac