Chapter 4. The Class System

In the previous chapter, you learned how to instantiate components from existing component classes and reference them. Within Sencha Touch you can also define your own custom component classes by extending from Sencha Touch base classes. The next examples will show you how to instantiate a component and how to define your own classes.

In this chapter, you’ll learn:

  • How to define your own custom class
  • How to define getters and setters
  • How to define singletons and static members
  • How inheritance works (extending)
  • How multiple inheritance works (mixins)

Note

You can test the examples in this chapter from my GitHub repo or you can open a Sencha Fiddle and try it out yourself. (When using the Sencha Fiddle, make sure you choose the Sencha 2.3.x framework.)

Defining Your Own Custom Class

Now that you know how to instantiate a class definition for view components, let’s discuss how the Sencha class system works and how to define your own blueprints to define a class.

Let’s make a blueprint containing a variable, myVar, and a method, myMethod. Using Ext.define():

Ext.define('AppName.packagename.ClassName', {
    //class configuration object
    myVar: 1,
    myMethod: function(name){
       //console.log("Log: " + name);
    }
},function(){
    //optional callback
});

In the code example from Chapter 3, I instantiated a few components: a simple Hello World component and an XTemplate component. It’s important to know that components in the Sencha framework are, in fact, classes.

Officially, JavaScript is a prototype-oriented language; it has no class system. The most powerful feature of JavaScript is its flexibility. This comes at a price: JavaScript might be hard to understand, reuse, or maintain.

This makes it different from object-oriented languages like Java or C++. These come with code standards and well-known patterns. This imposes structure and forces developers to adhere to standards.

Sencha, however, allows you to create classes within JavaScript. This should allow you to take advantage of the flexibility of JavaScript and the structure that object-oriented programming provides.

Note

Object-oriented programming (OOP) is a programming paradigm that represents concepts as objects that have data fields (properties that describe the object) and associated procedures known as methods. A good book about OOP coding patterns in JavaScript is JavaScript Patterns by Stoyan Stefanov.

To create a class definition, you will use the Ext.define method. In this method, you pass in the first argument, a class name (which is a string); and as a second argument, you pass in a config object to configure and initialize the component. The third argument is optional: you can assign a callback function. This might be handy for logging, but I rarely use it.

In general, the string class name consists of the following parts: AppName.packagename.ClassName. For example, the following class maps to the file app/view/ListView.js:

Ext.define('MyDemoApp.view.ListView', {
    //class configuration object
});

In the preceding code, note the following:

  • The app name, MyDemoApp, maps to the app folder in your project root and should be written in the upper CamelCase notation (wherein each word begins with a capital letter).
  • The package name, view, maps to the package folder in the app folder; package names are always written in lowercase.
  • The class name, ListView, should also be written in the upper CamelCase notation. The class name should contain the same name as the JavaScript filename (ListView.js); therefore, it’s not possible to put multiple Sencha Touch classes in a single JavaScript file.

Because you pass the class name as a string, the namespace doesn’t need to exist before you assign the reference. This is great, because you won’t need to worry about the order of execution. The framework does this. It’s asynchronous, and dependencies will automatically load through the load mechanism (Ext.Loader). For example, if a child class requires a parent class, the Ext.Loader will make sure that class will be in memory. String class names are also handy for debugging because Sencha classes know their class name. When there is a bug in your code, it will show you a nice stack trace with readable class names to help you easily find your bug.

The second argument takes a class configuration object. This is where you can set properties and methods. This makes total sense when you are defining a custom component, like in Example 4-1.

Example 4-1. Class definition for a custom component
Ext.application({
    name: 'DemoApp',
    launch: function() {


        /* Start class definition code: */

        //Create a class definition
        Ext.define('DemoApp.view.DemoComponent', { //1
            //2
            extend: 'Ext.Component',
            config: {
                html: 'Hello World' //3
            }

        }, function() {
            console.log("class is created"); //4
        });


        //Create a class instance
        Ext.create('DemoApp.view.DemoComponent', { //5
            fullscreen: true
        });


    }
});
1

As you can see, I have defined a single class. DemoApp.view.DemoComponent maps to a single file, app/view/DemoComponent.js.

2

This definition takes the following configurations: extend and a config object. Don’t worry at this point what extend and config stand for. I will discuss them in the next couple of sections.

3

What is important for now is that a default html string for the DemoComponent was set.

4

Also, I have created an optional callback function that will log after the class definition is created.

5

The DemoComponent will be created after it is instantiated. The only thing that my DemoComponent does is display an HTML string.

Do you like the Sencha class system? There is a lot more to it—autogeneration of getters and setters (magic methods), and multiple inheritance. Read on to find out about more nice features of the Sencha class system.

Tip

Are you looking for more information about the Sencha class system? Check out the SenchaCon presentation by Jacky Nguyen or read the book JavaScript Patterns about OOP design patterns in native JavaScript. Another great book about native JavaScript design patterns is Addy Osmani’s ebook Learning Javascript Design Patterns.

Defining Getters and Setters

In object-oriented programming, properties aren’t accessed directly very often; it’s better to use accessors and mutators (get and set methods) instead. One benefit of this approach is that if you have a set method, you can add a business rule or fire an event as the mutator is run.

You probably won’t be very happy when you have to create a get method and a set method for every property. Luckily, the Sencha class system can automatically create accessors and mutators (known as magic methods) for you.

It is very easy to create getter and setter methods to access or mutate a class property; they will be automatically generated for you. That is why some people call them magic.

To autocreate magic getter and setter methods, set a property in the class config:

config: {
  myProperty: "some value"
}

The config object autocreates the getter and setter methods as follows:

getMyProperty: function(){
  return this.myProperty; //returns "some value"
}
setMyProperty: function(x){
  this.myProperty = x;
}

You don’t need to add these functions by yourself. This reduces repetitive code.

Besides getter and setter methods, it also automatically creates apply and update methods. These methods are handy to change the process, before and after you set a value:

applyMyProperty: function(x){
  //runs before this.myProperty changes.
  //for example validation
}
updateMyProperty: function(x){
  //runs after this.myProperty was changed.
}

Validation is an area where these methods can be implemented. Figure 4-1 shows the entire process of getting and setting values. Let’s say we have a class config, driver, set to the default value "John Doe" (config: { driver: "John Doe" }). You can retrieve the driver with the getter getDriver(), and it returns the current value John Doe. You can set the driver to a new value, setDriver("Lee Boonstra"), and before the value gets changed it will run applyDriver(), at which point you can run a validator check. If all goes well, it will change the value and afterward will run updateDriver(); you can add some additional logics or logging at this time.

Example 4-2 provides the accompanying code to Figure 4-1. It defines a Cab class with configs. Note that getDriver(), setDriver(), applyDriver(), and updateDriver() are automatically generated. To create some validation, you would override the applyDriver() function; and to add some functionality after changing the driver, you could override the updateDriver() function.

Process of getting and setting values
Figure 4-1. Process of getting and setting values
Example 4-2. Configs in a class that generate magic methods
Ext.define('VehicleApp.vehicle.Cab', {
    // The default config
    config: {
        driver: 'John Doe',
        driver2: {
          firstName: 'John',
          lastName: 'Doe'
        }
    },

    constructor: function(config) {
        this.initConfig(config);
    },

    applyDriver: function(newVal){
      if(newVal === 'The Pope') {
        console.log(newVal + " is an invalid taxi driver.");
        return;
      }
      return newVal;
    },
    updateDriver: function(newVal, oldVal){
      console.log('The owner has been changed from ' + oldVal + ' to ' + newVal);
    }
});

Because the previous code example does not extend from a Sencha component, I had to initialize the config settings in my constructor:

  constructor: function(config) {
      this.initConfig(config);
  },

The config object sets some default values. When you are not extending from an Ext.Component, you have to call the initConfig(config) method once by yourself (e.g., in the base class), which will initialize the configuration for the class that was passed in while creating the objects.

After you instantiate the class, you have access to the getters and setters in the prototype. They have been magically generated:

var taxi = Ext.create("VehicleApp.vehicle.Cab", {
    driver: "John Doe"
});
alert(taxi.getDriver()); //alerts 'John Doe';

taxi.setDriver('The Pope');
alert(taxi.getDriver()); //alerts 'John Doe' because 'The Pope' is invalid.

//changes the driver from 'John Doe' to 'Lee Boonstra'
taxi.setDriver('Lee Boonstra');
alert(taxi.getDriver());

You can even use magic getter and setter methods to access complex objects. For example, let’s change the code in Example 4-2 and define a config with a complex object:

config: {
  driver: {
    firstName: "John",
    lastName: "Doe"
  }
}

I can get access to its properties with the line taxi.getDriver().firstName.

The config object in a class definition is very useful for class instances. Sometimes you don’t want to instantiate a class; for example, you may just want to run some default common utility functions. Singletons and static members would do the trick. We’ll discuss them in the next section.

Defining Singletons and Static Members

In software engineering, the singleton pattern is a design pattern that restricts the instantiation of a class to one object. This is useful when exactly one (global) instance of a class is needed to coordinate actions across the system.

To get access to a function or a property of the class itself (without instantiating an object), you would need to define a class as a singleton or static member—for example, a common utility function such as converting the speed of a car from miles per hour to kilometers per hour, or a static property that tells me the version number of the application. You don’t need an object for that; you just want to run a generic used function or retrieve a common used constant from anywhere in your app.

It is pretty simple to define a class as a singleton—just set the config singleton to true:

Ext.define('Utils.common.Functions', {
    singleton: true,
    //key value pairs here
});

A singleton class definition can’t create objects (technically it can’t create more than one object, because the singleton itself gets instantiated once), but you can get access to all the methods and properties in the class itself. This is very handy for when you want to get access to generic functions or properties used as constants:

Ext.define('Utils.common.Logger', {
    singleton: true,

    version: "1.02",
    log: function(msg) {
        console.log(msg);
    }
});

You can call the log() function by invoking Utils.common.Logger.log() directly from the class, and retrieve the version property by calling Utils.common.Logger.version from the class.

Singletons also can contain config objects, and therefore generate magic getters and setters from properties. For example:

Ext.define('Utils.common.Version', {
    singleton: true,

    config: {
      version: "1.03",
    }
});

The previous code will generate a getter: getVersion(). Now, from anywhere in my application I can get access to this property with Utils.common.Version.getVersion().

A nice alternative for singletons are classes with a statics object defined. To set up a class with a statics object, you only need to define a statics object with key/value pairs. (Note that you can’t set a config object within a statics object.)

Here we define statics with the VehicleApp.utils.Commons class:

Ext.define('VehicleApp.utils.Commons', {
        statics: {
                YELP_API: 'http://api.yelp.com/business_review_search?',
                YELP_KEY: 'ftPpQUCgfSA3yV98-uJn9g',
                YELP_TERM: 'Taxi',
                LOCATION: 'Amsterdam NL',

                getUrl: function() {
                        return this.YELP_API + "term=" + this.YELP_TERM +
                                "&ywsid=" + this.YELP_KEY
                                + "&location=" + this.LOCATION;
                },
        }
});

You can create objects of a class that has statics defined, but these objects cannot get access to its properties and methods without invoking it from the class itself with the dot notation. In other words, requesting properties and methods from an instance via this will not work, but calling the full namespace (i.e., VehicleApp.utils.Commons.LOCATION) will:

var mySettings = Ext.create('VehicleApp.utils.Commons');
//It is possible to create an instance of a class with static members:
console.log(mySettings);
//But getting access to a static member from an object fails:
mySettings.getUrl();
  //Uncaught TypeError: Object [object Object] has no method 'getUrl'

Inherit from a Single Class

Inheritance is when a child class receives functionality from a parent class. Inheritance is known as extending in Sencha Touch. To create single class inheritance, you extend from a parent class by setting the extend config:

Ext.define('AppName.packagename.ClassName', {
   extend: 'AppName.packagename.SomeClassName'
});

The following code examples illustrate the concept of single class inheritance. Example 4-3 defines the base class, Vehicle, while Example 4-4 defines the class Car, which inherits behavior from the parent. Example 4-5 creates instances of both classes.

Example 4-3. Define a parent class
Ext.define('VehicleApp.vehicle.Vehicle', {
    unit: "mph",
    drive: function(speed) {
        console.log(this.$className + ": Vrrroom: " + speed + " " + this.unit);
    }
});

In Example 4-3, the VehicleApp.vehicle.Vehicle class is the parent class. It has a unit property set and a method, drive().

Example 4-4. Define a child class and implement inheritance
Ext.define('VehicleApp.vehicle.Car', {
    extend: 'VehicleApp.vehicle.Vehicle',
    drive: function(speed) {
        console.log(this.$className + ": Vrrroom, vrrroom: " + speed + this.unit);
    }
});

In Example 4-4, the VehicleApp.vehicle.Car class inherits both the unit property and the drive() method. However, it has its own drive() method, and therefore this method will be overridden. (Although it still has access to the unit property!)

Example 4-5. Create instances
var vehicle = Ext.create("VehicleApp.vehicle.Vehicle");
vehicle.drive(40); //alerts "Vrrroom: 40 mph"

var car = Ext.create("VehicleApp.vehicle.Car");
car.drive(60); //alerts "Vrrroom, vrrroom: 60 mph"

As with any object-oriented language, if you need to do further initializations upon creation, you code a constructor. It makes sense to code an initConfig(config) method in a constructor.

The initConfig(config) method initializes the configuration for this particular class. Whether you initialize default config values or pass in config values as an argument while creating an object, this method will override and merge them all together and create an instance with these default settings. When you are inheriting from other classes, you don’t need to rewrite the initConfig method. It’s inherited, so the functionality is already there, but it does need to exist. Typically, the best place to include it would be in your base class.

Another powerful method is callParent([arguments]). It also makes sense to write this call in the constructor, although you don’t have to. You can run this from any other method, as shown in Example 4-6, which I will discuss shortly.

The callParent(arguments) method calls the ancestor method, in this case the drive() function in the parent VehicleApp.vehicle.Motor class. When you invoke this method from the constructor, it will call the parent’s constructor. Object-oriented languages used to force developers to write this call in the constructor. It’s important to have this call in your custom classes, because you always want to initialize the config settings from every parent. Maybe in the future you will change some base class config properties, in which case you will want your child classes to have access to them.

But you are free to call the parent from whatever method you are in. This can be handy for overriding functionality. In Example 4-6, I want to override the drive() function that is inherited from the Vehicle class in order to customize it specifically for a Motor class.

Example 4-6. Class inheritance
Ext.define('VehicleApp.vehicle.Motor', {
    extend: 'VehicleApp.vehicle.Vehicle',

    config: {
        nrOfWheels: 2 //1
    },

    constructor: function (config) {
        this.initConfig(config); //2
    },

    drive: function(speed) { //3

        if(this.getNrOfWheels() < 3) { //4
            console.log(this.$className +
                ": Vrrroom, vrrroom on " + this.getNrOfWheels() +
                     " wheels.");
        } else {
            this.callParent([60]); //5
        }
    }
});
1

The config object that needs to be initialized.

2

The initialization of the config object. An even better practice would be to put this constructor and initConfig method into the base class, which would be Vehicle.js, but for demo purposes, I’ll leave the code here.

3

You use the same drive() signature so the drive() method will contain the override. This override will contain an additional check.

4

When an instance passes in a nrOfWheels property that is smaller than three, it will populate a specific log message.

5

When an instance does not pass in the nrOfWheels property, or the property is larger than four, then it will display the default behavior, which we can retrieve by calling the parent (Vehicle) class.

I can run this code by creating an instance of the Motor class.

var motor = Ext.create('VehicleApp.vehicle.Motor', {
    //nrOfWheels: 4
});
motor.drive();

Component inheritance works the same way, because at the end a component is a Sencha class. Use the extend property within the configuration of the class definition. You will pass in the string name of the parent Sencha component (e.g., Ext.Component. This maps to the <sencha-touch-framework-folder>/src/Component.js component. But you can also find the component class names (as well as the xtype names) in the API docs.

Constructors aren’t used with components. If you subclass Ext.Component, you probably won’t use a constructor. Instead, components are initialized in a method named initialize().

Inheritance is a very powerful concept. What about multiple inheritance? Sometimes you need to inherit functionality from multiple classes. Let’s check out the next section.

Inherit from Multiple Classes

When would you want to use multiple inheritance? In some cases, you might want a class to inherit features from more than one superclass. When you want to implement multiple inheritance of classes, you need mixins. A mixin object does exactly what the name implies: it mixes in functionality. “Take a little bit of this, use a little bit of that…” For example, take a method of class X, take a method of class Y, implement it in class Z.

Let’s say we have two vehicle classes, a normal car and a monster 4-wheeler. Both vehicles can inherit the methods to brake and to drive. Only the monster 4-wheeler can also jump, however; the normal car can’t.

Take a look at Figure 4-2. The code corresponding to this diagram is written in Example 4-7.

Inheriting multiple classes
Figure 4-2. Inheriting multiple classes

Now, let’s define three classes, each with its own functionality to share drive(), brake(), and jump(). Later you will define two vehicle classes that inherit from these classes and mix in those methods.

Example 4-7. Define classes with the methods you will mix in
Ext.define('VehicleApp.mixins.Drive', {
  drive: function(){ //the method to share
    console.log(this.$className + ": Vrrrrooom");
  }
});
Ext.define('VehicleApp.mixins.Brake', {
  brake: function(){
    console.log(this.$className + ": Eeeeekk");
  }
});
Ext.define('VehicleApp.mixins.Jump', {
  jump: function(){
    console.log(this.$className + ": Bump");
  }
});

Finally, you can define the two Vehicle classes with the mixin implementations. Again, these are just normal class definitions, but with a mixins object. You can list all the mixins underneath one another. They are used from one place without copying code over.

Example 4-8 shows the Car class with mixins to inherit the drive() and brake() functionalities.

Example 4-8. Define a Car class with mixin implementations
Ext.define('VehicleApp.vehicle.Car', {
  mixins: {
    canBrake: 'VehicleApp.mixins.Brake',
    canDrive: 'VehicleApp.mixins.Drive'
  }
});

Example 4-9 shows the monster FourWheeler class with mixins to inherit the drive(), brake(), and jump() functionalities.

Example 4-9. Define a FourWheeler class with mixin implementations
Ext.define('VehicleApp.vehicle.FourWheeler', {
  mixins: {
    canBrake: 'VehicleApp.mixins.Brake',
    canDrive: 'VehicleApp.mixins.Drive',
    canJump: 'VehicleApp.mixins.Jump'
  }
});

With the implementation of Examples 4-8 and 4-9, the drive() and brake() methods are available to the Car class, and the drive(), brake(), and jump() methods are available to the FourWheeler class. You can just execute those methods with the following code:

var mercedes = Ext.create('VehicleApp.vehicle.Car');
var honda = Ext.create('VehicleApp.vehicle.FourWheeler');

mercedes.drive();
mercedes.brake();

honda.drive();
honda.jump();
honda.brake();

The mixin identifier canBrake matches the prototype, and therefore you can run the brake() method on the FourWheeler and Car classes.

Summary

Vanilla JavaScript by its nature has no class system, as JavaScript is a prototype-based language. To mimic the ideas of object-oriented programming, you can write your JavaScript functions in an object-oriented way:

function Cab(driver, passenger) {
  this.driver = driver;
  this.passenger = passenger;
}

To create an instance of this Cab class, you can create a new object with the new operator:

var mercedes = new Cab('John Doe', 'The president');

For every object that you create, you need to make sure the class definition is loaded in the memory. It is possible to create inheritance or define singletons—the functionality’s just not there out of the box.

Sencha Touch has a built-in class system that ships with inheritance, magic methods, and singleton strategies. Example 4-10 is the Sencha version of the previous Cab class.

Example 4-10. app/view/Cab.js
Ext.define('TaxiApp.view.Cab', {
  extend: 'Ext.Component',
  config: {
    driver: '',
    passenger: ''
  }
});

Sencha has strict naming conventions. The class that you define needs to contain an application name in upper CamelCase notation (TaxiApp) that maps to the app folder. Packages are subfolders in the app folder and are written in lowercase. The class name should contain the same name as the filename, and should also be written in CamelCase notation (Cab). The class TaxiApp.view.Cab maps to app/view/Cab.js and it can contain only a single class definition. These naming conventions are required by the Sencha loading mechanism. The Ext.Loader will make sure that every class definition is loaded in the right order.

With the extend class configuration, it is possible to inherit from a single class. In this example we will need it, so we do not need to write a constructor.

To create an instance of this Sencha Cab class, you can create a new object, but without the new operator:

var mercedes = Ext.create('TaxiApp.view.Cab', {
  driver: 'John Doe',
  passenger: 'The president'
});

When you run mercedes.getDriver(), it will return the name John Doe. This is because the config object in Example 4-10 automatically generates magic methods, like getDriver() and setDriver()—as well as applyDriver() and updateDriver().

Now that you know everything about the Sencha class system and the Sencha fundamentals, we are almost ready to build our application.

There is just one topic I would like to discuss first: how to create layouts for your Sencha applications and components. In the next chapter, we’ll explore the layout system.

Get Hands-On Sencha Touch 2 now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.