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.
Ext
.
application
({
name
:
'DemoApp'
,
launch
:
function
()
{
/* Start class definition code: */
//Create a class definition
Ext
.
define
(
'DemoApp.view.DemoComponent'
,
{
//
//
extend
:
'Ext.Component'
,
config
:
{
html
:
'Hello World'
//
}
},
function
()
{
console
.
log
(
"class is created"
);
//
});
//Create a class instance
Ext
.
create
(
'DemoApp.view.DemoComponent'
,
{
//
fullscreen
:
true
});
}
});
As you can see, I have defined a single class.
DemoApp.view.DemoComponent
maps to a single file, app/view/DemoComponent.js.This definition takes the following configurations:
extend
and aconfig
object. Don’t worry at this point whatextend
andconfig
stand for. I will discuss them in the next couple of sections.What is important for now is that a default
html
string for theDemoComponent
was set.Also, I have created an optional callback function that will log after the class definition is created.
The
DemoComponent
will be created after it is instantiated. The only thing that myDemoComponent
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.
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.
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()
.
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!)
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.
Ext
.
define
(
'VehicleApp.vehicle.Motor'
,
{
extend
:
'VehicleApp.vehicle.Vehicle'
,
config
:
{
nrOfWheels
:
2
//
},
constructor
:
function
(
config
)
{
this
.
initConfig
(
config
);
//
},
drive
:
function
(
speed
)
{
//
if
(
this
.
getNrOfWheels
()
<
3
)
{
//
console
.
log
(
this
.
$className
+
": Vrrroom, vrrroom on "
+
this
.
getNrOfWheels
()
+
" wheels."
);
}
else
{
this
.
callParent
([
60
]);
//
}
}
});
The
config
object that needs to be initialized.The initialization of the
config
object. An even better practice would be to put this constructor andinitConfig
method into the base class, which would beVehicle.js
, but for demo purposes, I’ll leave the code here.You use the same
drive()
signature so thedrive()
method will contain the override. This override will contain an additional check.When an instance passes in a
nrOfWheels
property that is smaller than three, it will populate a specific log message.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.
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.
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.
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.
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.
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.