Sunday, August 18, 2013

Creating and extending JavaScript classes with ExtJS

JavaScript is not a class-based language. It uses the prototyping approach instead. If your background is based on classical languages, this may discourage you for a moment. But wait! Don’t go away just yet! JavaScript is also a dynamic and very flexible language. Therefore, it’s pretty easy to build frameworks that simulate the classical approach on top of it.

Before checking what ExtJS has to offer, we should understand how we would create classes without the help of any framework.

 var MyNameSpace = MyNameSpace || {};  
 MyNameSpace.Person = function(config) {  
   this.name = config.name;  
 };  
 MyNameSpace.Person.prototype.sayHi = function(dude) {  
   return 'Hi ' + dude + '! My name is ' + this.name + '.';  
 };  
 var person = new MyNameSpace.Person({  
   name: 'Foo'  
 });  
 window.console.log(person.sayHi('Bar'));  

First we need to create the namespace where our class will live. Then, we create a function that works as the constructor of our class. If we wanted to create a public function, we would add it to a mysterious “prototype” property of the constructor. It seems a bit strange, doesn’t it? How would we do the same thing with ExtJS?

 Ext.define('MyNameSpace.Person', {  
   config: {  
     name: null  
   },  
   sayHi: function(dude) {  
     return Ext.String.format('Hi {0}! My name is {1}.', dude, this.name);  
   },  
   constructor: function(config) {  
     this.initConfig(config);  
   }    
 });  
 var person = Ext.create('MyNameSpace.Person', {  
   name: 'Foo'  
 });  
 window.console.log(person.sayHi('Bar'));  

That looks better. We no longer need to create the namespace because the framework takes care of it; We can easily locate our constructor; The “sayHi” function and the “name” property are part of what represents the body of our class. The recipe is simple:

 Ext.define('<name of your class>', classDefnObject, createdcallBack);  

The “classDefnObject” is a map with the definitions of our new class. The optional “createdCallback” will be executed once the class is created and is ready to be used.

Notice that we don’t use the “new” keyword. Instead, we use the “Ext.create” function. We still can use “new MyNameSpace.Person({name: ‘Foo’})”, but the create function allows the framework to dynamically load the JavaScript file that contains the “MyNameSpace.Person” source code the first time the “Person” class is needed.

You can also create anonymous classes just like you would do with Java.

 function wrapper() {  
   var Class = Ext.define(null, {  
     config: {  
       name: null  
     },  
     sayHi: function(dude) {  
       return Ext.String.format('Hi {0}! My name is {1}.', dude, this.name);  
     },  
     constructor: function(config) {  
       this.initConfig(config);  
     }    
   });  
   var person = new Class({  
     name: 'Foo'  
   });  
   window.console.log(person.sayHi('Bar'));  
 }  
 wrapper();  

In this case, you wouldn’t be able to use the “Ext.create” function. It is already loaded anyway. This feature makes possible to create classes inside functions.

Due to the dynamic nature of the JavaScript language, ExtJS is able to generate code on-the-fly. Out of the box, we have getters and a setters for all the keys defined in the “config” property.

 person.setName('Baz');  
 window.console.log(person.sayHi('Bar'));  
 window.console.log(person.getName());  

What about some validation?

 Ext.define('MyNameSpace.Person', {  
   config: {  
     name: null  
   },  
   applyName: function(name) {  
     if (Ext.isEmpty(name)) {  
       throw 'Invalid "name"';  
     }  
     return name;  
   },  
   sayHi: function(dude) {  
     return Ext.String.format('Hi {0}! My name is {1}.', dude, this.name);  
   },  
   constructor: function(config) {  
     this.initConfig(config);  
     this.setName(config.name);  
   }    
 });  
 var person = Ext.create('MyNameSpace.Person', {  
   name: 'Foo'  
 });  
 person.setName(null); // throws the validation exception  

The getter and the setter methods are not supposed to be overridden. You may want to create an “apply” method instead.

 .  
 .  
 .  
 apply<Name of the property>: function(setterParameter) {  
   // your logic here  
   return setterParameter;  
 },  
 .  
 .  
 .  

You can create class extensions by adding a “extend” property.

 Ext.define('MyNameSpace.Programmer', {  
   extend: 'MyNameSpace.Person'  
 });  
 var person = Ext.create('MyNameSpace.Programmer', {  
   name: 'Geek'  
 });  
 window.console.log(person.sayHi('Bar'));  

A programmer is a different kind of person. We say “hi” differently. We log the “date”!

 Ext.define('MyNameSpace.Programmer', {  
   extend: 'MyNameSpace.Person',  
   sayHi: function(dude) {  
     var greetings = this.callParent(arguments);  
     return Ext.String.format(  
       '{0} [Message sent at {1}]',   
       greetings, new Date()  
     );  
   }  
 });  

We call the “sayHi” function of the parent class, and then we “enhance” the resulting text. Be careful about using the “callParent” function. If you use it and the “'use strict';” statement at the same time, it will throw an exception.


The ExtJS developers needed to choose a nasty solution to make this "callParent" magic to work. A workaround for this issue is to call...

   sayHi: function(dude) {  
     var greetings = MyNameSpace.Programmer.superclass.sayHi.apply(this, arguments);  
     var greetings = this.callParent(arguments);  
     return Ext.String.format(  
       '{0} [Message sent at {1}]',   
       greetings, new Date()  
     );  
   }  

It gives you the same effect, and you can still use the strict mode.

Mixins are another way to enhance your classes. Imagine we have these classes:

 Ext.define('MyNameSpace.EnglishSpeaker', {  
   enSayHi: function(dude) {  
     return Ext.String.format('Hi {0}!', dude);  
   }  
 });  
   
 Ext.define('MyNameSpace.FrenchSpeaker', {  
   frSayHi: function(dude) {  
     return Ext.String.format('Salut {0}!', dude);  
   }  
 });  
   
 Ext.define('MyNameSpace.PortugueseSpeaker', {  
   ptSayHi: function(dude) {  
     return Ext.String.format('Oi {0}!', dude);  
   }  
 });  

They would define some linguistic abilities. You can use them by setting the "mixins" property.

 Ext.define('MyNameSpace.Person', {  
   mixins: {  
     'en': 'MyNameSpace.EnglishSpeaker',  
     'fr': 'MyNameSpace.FrenchSpeaker',  
     'pt': 'MyNameSpace.PortugueseSpeaker'  
   }  
 });  
   
 var person = Ext.create('MyNameSpace.Person', {});  
 window.console.log(person.enSayHi('Bar'));  
 window.console.log(person.frSayHi('Bar'));  
 window.console.log(person.ptSayHi('Bar'));  

Would you like to enhance one of the mixins functions? Just create a function with the same name of the one defined in the mixin.

 Ext.define('MyNameSpace.Person', {  
   mixins: {  
     'en': 'MyNameSpace.EnglishSpeaker',  
     'fr': 'MyNameSpace.FrenchSpeaker',  
     'pt': 'MyNameSpace.PortugueseSpeaker'  
   },  
   enSayHi: function() {  
     var greetings = this.mixins.en.enSayHi.apply(this, arguments);  
     return Ext.String.format('{0} [Message sent at {1}]', greetings, new Date());  
   }  
 });  

The mixins object holds the mixins prototypes. In the case above, en points to the EnglishSpeaker prototype.

Ext.define
Ext.create
What does "use strict" do in JavaScript, and what is the reasoning behind it?
ECMAScript 5 Strict Mode, JSON, and More
Mixins
callParent
Function.prototype.apply
arguments object

No comments:

Post a Comment