English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Testcode

3. Ember.Object

At the heart of Ember are several primitives that enhance JavaScript's prototype-based object model. Ember.Object is one important primitive, providing a cornerstone for implementing classes and utilizing mixins. These tools are used to manage complexity in large codebases, and allow you to use patterns like delegation or decoration much more easily than with native JavaScript objects.

In this chapter, we’ll cover Ember’s object model and its approach to classes, inheritance, and mixins. In addition, we will cover object events, observers, computed properties, and aliases.

In this and other chapters, we will provide code snippets that should execute successfully on emberjs.jsbin.com (with Ember v1.2 at time of writing). Alternatively there are JSFiddle and JSBin snippets running Ember-latest. Experimenting with Ember in fiddles is a fantastic way to learn the framework.

Classes and Inheritance

JavaScript is an object-oriented language, and features inheritance of properties and methods. Unlike Java, C#, Python, or Ruby, which all employ “classical” inheritance, JavaScript implements “prototypal” inheritance. The difference lies in where objects look to find property and method definitions.

When a method is called on an object with classical inheritance, that object looks to a class for the method's definition. If the class does not provide the method, the object will look through a chain of superclasses for a fitting method. In prototypal systems classes do not exist. Instead, an object will look for a method defined on itself, and if it cannot be found, look to its attached prototype. The prototype is just another object, which can in turn have its own prototype. Thus, a chain of object instances is traversed when looking for a method instead of classes.

In JavaScript, an object's prototype is set as a property on its constructor. This constructor must be called with the new keyword to properly initialize the object.

var ParentObjectConstructor = function(){};
ParentObjectConstructor.prototype = {
foo: function() { return 'bar'; }
};

var MyObjectConstructor = function(){};
MyConstructor.prototype = new ParentObjectConstructor();

var myObject = new MyObjectConstructor();
myObject.foo() // => 'bar'

JavaScript's prototypal inheritance has some limitations. There is no concept of calling "super": MyObjectConstructor cannot have its own definition of foo that delegates to the parent. There are no mixins, only parent objects. For each parent, an object must be instantiated.

Ember.Object provides a more classical and flexible JavaScript object.

  • Ember.Object and its descendants behave like classes.
  • The init property becomes the object constructor.
  • Subclasses can be created by calling extend on the parent class.
  • Instances can be created with new MyClass() if you wish to pass arguments to init, or with MyClass.create({ newProp: 'foo' }); if you want to add properties at initialization time.
  • Available methods can be added or overridden on an existing class (aka “monkey patching”) by calling MyClass.reopen({ addedProp: 'foo' }).
  • Class methods can be added with MyClass.reopenClass({ classMethod: function(){} });.

Let's explore these features. First, open Chrome. Then, navigate to the starter kit and open the JavaScript console.

Create an instance of an Ember.Object:

var myInstance = new Ember.Object();

Confirm that the variable is an Ember.Object:

myInstance instanceof Ember.Object;  // => true

Set a property on an Ember object:

myInstance.set('name', 'Tom Dale');

Get a property on an Ember object:

myInstance.get('name', 'Tom Dale');

You can also define properties of the object upon creation:


var fido = Ember.Object.create({ animal: 'dog' });
fido.get('animal'); // => 'dog'

Now, define a class that inherits from Ember.Object: When calling extend, the properties you want present on instances of that class (instance properties and methods) are passed as an object. Let’s try it out:


var Animal = Ember.Object.extend();
var Dog = Animal.extend({ sound: 'woof' });

var spot = new Dog();
spot.get('sound'); // => 'woof'

var Beagle = Dog.extend({ sound: 'howl' });
var snoopy = new Beagle();
snoopy.get('sound'); // => 'howl'

Reopen a class and add a property. What do you expect to happen?

Beagle.reopen({ sound: 'hoooooowl' });

Existing instances are not affected:

snoopy.get('sound');  // => 'howl'

But new instances are affected:


var rover = new Beagle();
rover.get('sound'); // => 'hoooooowl'

The object of properties passed to extend is really just a mixin. You can create named mixins and pass them to extend in the same manner as an object.


var Adoptable = Ember.Mixin.create({
hasOwner: function() {
return !!this.get('owner');
}.property('owner')
});

var GoldenRetriever = Dog.extend(Adoptable, {
sound: 'ruff'
});

var dog = new GoldenRetriever();
dog.get('hasOwner'); // => false
dog.set('owner', 'Troy');
dog.get('hasOwner'); // => true
dog.get('sound'); // => 'ruff'

Mixins are a familiar pattern to many developers using object-oriented programming. They patch a class with methods or properties without providing a class themselves. A mixin cannot be instantiated; it can only extend a class or instance. Applied mixin methods can call methods applied before themselves with _super, and in fact the object passed to extend( is treated just like a mixin.

Ember comes packed with some powerful OO programming features. The Ruby programming language is an obvious influence, but Ember is far from being a clone of Ruby's functionality. In ambitious applications and large codebases, you gain a quick appreciation for how Ember fundamentals help you compose objects and interfaces.

Object Event Handlers

Ember objects can respond to events, a tool often used for lifecycle hooks. An example:

var ClickTracker = Ember.Object.extend({
clickCount: 0,
trackClick: function(){
this.incrementProperty('clickCount');
}.on('click'),
logClick: function(){
console.log('User had clicked.');
}.on('click')
});

var tracker = new ClickTracker();
Ember.sendEvent(tracker, 'click')

Multiple functions can handle an event, and a handler can listen for multiple events if you pass multiple arguments to on.

Constructors and Events

Ember objects define constructors with the init method:


var Logger = Ember.Object.extend({
init: function(prefix){
this.set('prefix', prefix);
},
log: function(message){
console.log(this.get('prefix'), message);
}
});

var logger = new Logger('info');
logger.log('I want to teach the world to sing');

The constructor interface is a better way to deal with initializing properties than create. Internal properties are encapsulated within the class, not shared with the code creating the object.

When init is overwriting the init of a parent mixin or class, the parent's init must be called with _superinit is a life-cycle hook, and not calling the code in a life-cycle hook can easily introduce difficult-to-diagnose bugs.

To avoid this, life-cycle hooks on Ember objects are often implemented with object events. init is one such event:

var Logger = Ember.Object.extend({
logInitialization: function(){
console.log('Started logger for '+this.get('prefix'));
}.on('init'),
log: function(message){
console.log(this.get('prefix'), message);
}
});

var InfoLogger = Logger.extend({
init: function(singer){
this.set('singer', singer);
},
log: function(message){
this._super(this.get('singer')+': '+message);
},
prefix: 'info'
});

var logger = new InfoLogger('michael');

// => Started logger for info
logger.log('I want to teach the world to sing');
// => info, michael: I want to teach the world to sing

In this example, InfoLogger defines an init method, but logInitialization still fires. Other events use this same system, most notably didInsertElement and willDestroyElement on Ember.View objects.

Observers

In the earlier examples, we jumped into using get and set for reading and writing properties. ES5 (the version of JavaScript in most browsers) does not provide a way to override getters and setters, or to observe when a property changes. For several of Ember's features to work consistently and in a performant manner, those hooks are needed. get and set fill a gap in JavaScript features.

Observing a property for change is among the simpler features provided by get and set. As with many of Ember's features, there is an explicit syntax and a convenient syntax. First, the explicit syntax:


var dog = Ember.Object.create({
name: 'Toby',
isGrowling: false
});
Ember.addObserver(dog, 'isGrowling', function(){
if (dog.get('isGrowling')) {
console.log('Bad dog '+dog.get('name')+'!');
}
});

dog.set('isGrowling', true); // => triggers the observer

For convenience, an observer can also be written with a class definition:


var Dog = Ember.Object.extend({
name: 'Toby',
isGrowling: false,
disciplineWhenGrowling: function(){
if (this.get('isGrowling')) {
console.log('Bad dog '+this.get('name')+'!');
}
}.observes('isGrowling')
});

var dog = Dog.create({ name: 'Toby' });
dog.set('isGrowling', true); // => triggers the observer

Observers fire any time a property changes. For observers written into class definitions, be aware that properties passed to create will be set before the observer is attached. If isGrowling was passed into create with a value of true, that would not trigger the observer. This is a subtle but important point: observers fire upon property changes, not for initial property values on an object.

Observing arrays requires a slightly different syntax.


var dog = Ember.Object.create({ tags: [] });

Ember.addObserver(dog, 'tags', function(){
console.log('tags changed!');
});
Ember.addObserver(dog, 'tags.@each', function(){
console.log('tags.@each changed!');
});

dog.set('tags', []); // Both observers will fire.
dog.get('tags').pushObject('Fido'); // Only the tags.@each observer will fire.

When items are added or removed from an array, the array itself does not change. An observer of the array itself (as with the first observer) only fires if a new array is set as the property's value. To observe changes to the contents of an array, the special propertyName.@each path is observed.

Nested objects present a similar challenge. Changing a property of an object does not trigger an observer of that object. Instead, you must observe a specific property.


var tag = Ember.Object.create({ name: 'Fido' });
var dog = Ember.Object.create({ tag: tag });

Ember.addObserver(dog, 'tag', function(){
console.log('tag changed!');
});
Ember.addObserver(dog, 'tag.name', function(){
console.log('tag.name changed!');
});

tag.set('name', 'Marmaduke'); // Only the tag.name observer will fire.

These dot-separated strings are commonly called "paths" or "property chains." Paths can be observed, get, or set.

Property observation is key to much of Ember's other low-level functionality. Though get and set are required for observation today, in ES6 (the upcoming version of JavaScript) a similar feature will be provided by Object.observe.

Computed Properties

Most of the properties we've demonstrated hold simple values. They can be get or set, but have no logic for determining their value. Computed properties are Ember's equivalent to overriding a getter and setter.

First, consider this class:


var User = Ember.Object.extend({
firstName: 'Tomhuda',
lastName: 'Kazdale',
combineForName: function(){
var name = [this.get('firstName'), this.get('lastName')].join(' ');
this.set('name', name);
}.observes('firstName', 'lastName').on('init')
});

The name property is a combination of firstName and lastName. When either property changes, or when an object is initialized, the name will be computed and set. This code works, but is difficult to imagine working with on a regular basis. It is also eager: name is being constantly recomputed regardless of whether any other code asks for name.

Writing the same logic as a computed property simplifies the code, and gives us lazy execution of the logic.


var User = Ember.Object.extend({
firstName: 'Tomhuda',
lastName: 'Kazdale',
name: function(){
var name = [this.get('firstName'), this.get('lastName')].join(' ');
return name;
}.property('firstName', 'lastName')
});

The behavior of this code is almost identical to that of the observer-based example. The property call specifies that a computed property is being created. When other code gets the name property, the value will be computed and cached. When either dependent key changes, the name will invalidate and notify any observers that it has changed.

The name example is one of a computed property as a getter. The name property has logic, but in all other respects behaves like a static property. Computed properties can also provide setter logic:


var User = Ember.Object.extend({
firstName: 'Tomhuda',
lastName: 'Kazdale',
name: function(key, value){
if (arguments.length > 1) {
var nameFragments = value.split(' ');
this.setProperties({
firstName: nameFragments[0],
lastName: nameFragments[1]
});
return value;
} else {
var name = [this.get('firstName'), this.get('lastName')].join(' ');
return name;
}
}.property('firstName', 'lastName')
});

var user = new User();
user.get('name'); // => "Tomhuda Kazdale"
user.set('name', 'Yehom Datz');
user.get('firstName'); // => "Yehom"

Ember's computed property system is aggressively lazy in the interest of performance. Computed properties will not be computed until other code asks them to compute. Their values are also cached, and unless a dependent key changes or a new value is set, they will keep their value.

Aliases and Bindings

Aliases and bindings are two tools Ember provides to share information between objects.

An alias is simply a computed property shorthand. It delegates that a property at one path be available to get or set at another path. For an example, the band U2 might have a guitar technician and drum technician decided by the guitarist and drummer, but the band should still be able to show that information.


var Rockstar = Ember.Object.extend({
init: function(name, attributes){
this.set('name', name);
this.setProperties(attributes || {});
}
});

var Band = Ember.Object.extend({
guitarTechnician: Ember.computed.alias('guitarist.technician'),
drumTechnician: Ember.computed.alias('drummer.technician')
});

var sting = new Rockstar('Sting');
var andy = new Rockstar('Andy Summers', {technician: 'Danny Quatrochi'});
var stewart = new Rockstar('Stewart Copeland', {technician: 'Jeff Seitz'});

var thePolice = Band.create({
bassist: sting,
guitarist: andy,
drummer: stewart
});

The alias for drumTechnician allows the tech to be read directly off of the Band instance:


thePolice.get('drumTechnician'); // => "Jeff Seitz"
stewart.set('technician', 'Harry Cowell');
thePolice.get('drumTechnician'); // => "Harry Cowell"

Aliases are two-way. The value can be changed from either object (thePolice or from stewart) and updates are synchronized. Here is an example updating in the opposite direction:


stewart.get('technician'); // => "Jeff Seitz"
thePolice.set('drumTechnician', 'Harry Cowell');
stewart.get('technician'); // => "Harry Cowell"

This feature might seem a little contrived at first glance, but in complex applications a simple way to synchronize data across objects is invaluable. In the chapters on templates and controllers the value of aliases will become clearer. For now, it is safe to think of them as an elegant implementation of delegation.

Ember provides similar but not identical functionality in a feature called bindings. Bindings are built using observers, and update asynchronously. If you set on a bound property, the target of the binding will not immediately update. Most of your interaction with bindings will be in templates, where this alternative behavior is useful to internals of the rendering pipeline. It is good to know that this alternative exists, but it is unlikely you will want to use bindings explicitly.

Computed Property Macros

In the previous section we created an alias with the Ember.computed.alias function, and learned that aliases are themselves implemented with computed properties. When building a real-world application, you quickly find yourself writing similar boilerplate for many computed properties. Ember has a flock of these common functions already built-in. Let’s work through a few examples and see how they can be improved with computed property macros.

Ember.computed.(and|or)


var Order = Ember.Object.extend({
isComplete: function() {
return this.get('isReady') && this.get('isDelivered');
}.property('isReady', isDelivered'),

isReady: function() {
return this.get('isPaid') || this.get('isComplementary');
}.property('isPaid', 'isComplementary')
});

Becomes:


var Order = Ember.Object.extend({
isComplete: Ember.computed.and('isReady', 'isDelivered'),
isReady: Ember.computed.or('isPaid', 'isComplementary')
});

Ember.computed.(equal|not|match)


var Book = Ember.Object.extend({
isFiction: function() {
return this.get('genre') === 'Fiction';
}.property('genre'),

nonFiction: function() {
return this.get('genre') !== 'Fiction';
}.property('genre'),

isByIainBanks: function() {
return this.get('author').match(/Iain( M.)? Banks/);
}.property('author')
});

Becomes:


var Book = Ember.Object.extend({
isFiction: Ember.computed.equal('genre', 'Fiction'),
nonFiction: Ember.computed.not('genre', 'Fiction'),
isByIainBanks: Ember.computed.match('author', /Iain( M.)? Banks/)
});

Ember.computed.(gt|gte|lt|lte)


var Person = Ember.Object.extend({
canBuyAlcohol: function() {
return this.get('age') >= 21;
}.property('age')
});

Becomes:


var Person = Ember.Object.extend({
canBuyAlcohol: Ember.computed.gte('age', 21)
});

computed.defaultTo


var City = Ember.Object.extend({
population: function() {
return this.get('census.population')
|| this.get('defaultPopulation');
}.get('census.population'),

defaultPopulation: 'Unknown'
});

Becomes:


var City = Ember.Object.extend({
population: Ember.computed.defaultTo('census.population', 'defaultPopulation'),
defaultPopulation: 'Unknown'
});

Ember.computed.map


var Song = Ember.Object.extend({
info: function() {
return [
this.get('name'),
this.get('length'),
this.get('artist'),
this.get('album')
];
}.property('name', 'length', 'artist', 'album')
});

Becomes:


var Song = Ember.Object.extend({
info: Ember.computed.map('name', 'length', 'artist', 'album')
});

Plenty More Where These Came From

A complete list of computed property macros can be found in Ember’s API Docs.

The above examples are contrived to demonstrate the convenience and terseness of macros as well as the flexibility of computed properties. Additionally, they show how macros can clearly communicate an object’s dependencies and the transformations it performs on those dependencies. These are all reasons to embrace macros.

Wrapping Up

In this chapter, we have learned about Ember's object-oriented extensions to JavaScript, about event handlers, observers, computed properties, aliases, and macros. These primitives power Ember's views, controllers, components, and routes, but beyond that they offer a powerful, flexible and performant platform to write your own application architecture.

In the next chapter we will look at simple Ember applications, and at how Ember works with the DOM via views.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.