Javascript Classes: Design Patterns, MVC and Ext 2.0
maanantaina, tammikuuta 14, 2008
Few would argue that the advancements in web application design over the five years are anything short of staggering. Five years ago almost all the serious work was done on a server which would churn out all the html, application behavior, storage and marginally improve the user’s life with some javascript that may bring up a few tool tips or validate a form. Now we can write the entire UI layer entirely in Javascript, use the server to serve the application up and act as storage and supplement the server with browser side storage.
The challenge in modern web application design is that browser-side web applications get very complex very quickly so there needs to be a way to organize readable and serviceable code. Javascript’s class functionality has been extended through a number of novel implementations to the point where it is relatively easy to implement the proven design patterns of traditional desktop applications. We are going to take a look at that functionality. Actually, we are going to take a look at a lot of different things.
The Example
Here we see a UI with three entire fields. The idea is you type in one and then press enter. Then other two fields update with the converted value. Its no more difficult than that.
The original example was going to be some simple thing like the mammal-cat-lion, and the rest of the barnyard, class hierarchy that bored everyone with a CS degree to tears. We will look at this temperature conversion utility using several different patterns and implementations to demonstrate how things can work in small and large Javascript applications. There is no argument that this conversion utility could have been done far more simply without classes and many of the other components, but it was just big enough to demonstrate all the major points:
- MVC Pattern
- Observer Pattern
- Composite Pattern
- Class creation and extension
- Event handling
- Factory Pattern
I’ve implemented this using Ext 2.0 and the following components:
- Ext.extend
- Events - existing and new events, chains
- “superclass.constructor.call”
- Ext.form.FormPanel
- Ext.form.NumberField
- Ext.util.Observable
With that said, lets take a look at the basics of MVC and its pattern elements. I’ll explain why I made many of the design decisions and the sometimes conflicting implementation decisions. Code examples are throughout, but the complete example is near the bottom. I’ve annotated some of the dumb and not-so-dumb mistakes I made too. Finally, there is a bibliography at the end, because there was a lot of material worth referencing.
Figure 1: An example diagram of an MVC.
The model-view-controller (MVC) pattern has pretty much been clubbed into the brains of any developer in the MFC days. I’ve been in Java land doing web applications for 8 years and I haven’t followed if .Net has abstracted that model away. The idea is that the model governs the data and its various transformations. You sometimes see it referred to as the document model. The model does much of the logic for loading, saving and sometimes validating data. The model emits events indicating that a given data item has changed or is no longer valid.
The view is the user interface and it performs queries against the model for the value of data items. The view typically registers for change notifications from the model too. The UI updates upon receiving a change notification.
The controller is responsible for performing updates from the UI to the model. It can also determine which view should be active for the current application context. It can also be responsible for loading and saving data contained within the model.
Note that I wrote that both or either the model and the controller can be responsible for loading and saving data. There are a lot of different implementations of this pattern. Apache Struts has almost completely abstracted out the controller. The user only works with the model and the view. The model is responsible for connecting to the a service, EJB, or directly to the database for its data. MFC had the developer load the document model from the controller. Further, elements of the model and the controller can be delineated at the method level, rather than at the class level, in some designs.
The Model
GenericDocument = function() {
this.elementAssocArray = new Array();
this.elementArray = new Array();
this.getAll = function() {
return this.elementArray;
};
this.put = function(name, value, fireEvent) {
if ( typeof(fireEvent) == 'undefined' ) {
fireEvent = true;
}
var documentItem = null;
if ( name ) {
documentItem = this.get(name);
if ( !documentItem) {
documentItem = new DocumentItem( name, value );
this.elementAssocArray[name] = documentItem;
this.elementArray.push(documentItem);
} else {
documentItem.setValue(value, fireEvent);
}
}
return documentItem;
};
this.get = function(name, createNew) {
var documentItem = null;
if ( name ) {
documentItem = this.elementAssocArray[name];
if ( !documentItem && createNew ) {
documentItem = this.put(name);
}
}
return documentItem;
};
this.getValue = function(name) {
var value = null;
var documentItem = this.get(name);
if ( documentItem ) {
documentItem.getValue();
}
return value;
};
this.addHandler = function(name, object, handler) {
var documentItem = this.get(name);
if ( documentItem ) {
documentItem.register(object, handler);
}
};
}
The implementation of the example’s document model is based on two patterns:Observer and Composite. The Composite pattern is a tree-like construct that contains a series of different objects that are either leaves or other composites. The main document class, GenericDocument, is a Composite implementation. The leaves are DocumentItem objects which expose methods for the Observer pattern.
The GenericDocument class is little more than a factory and repository ofDocumentItem objects. However, this class is very useful as we will see later. The biggest contribution is that the GenericDocument stores the master data items from which all other things refer. The construct eliminates the need for lots of global variables and centralizes access- more later.
DocumentItem = function(name, value, object, handler) {
this.setValue = function(value, fireEvent) {
if ( value != this.value ) {
this.value = value;
if ( fireEvent ) {
this.fireEvent( 'valueChange', value );
}
}
};
this.register = function(object, handler) {
if ( object && handler ) {
this.addListener( 'valueChange', handler, object);
}
};
this.getValue = function() {
return this.value;
};
this.addEvents( {'valueChange': true} );
this.name = name;
this.setValue(value);
};
Ext.extend(DocumentItem, Ext.util.Observable);
An Observer pattern implementation relies upon a Subject, theDocumentItem in this case, which exposes one or more methods to allow Observerobjects to register for events. These events indicate that the subject has changed in some way. The observer can observe those state changes rather than visit to see if the subject’s state has changed.
You may ask “What!?” at this point. Another way to say it is that the observer registers for events indicating that the subject has changed in some way. The subject then emits a message, or event, to the observer saying “Hey, I changed. Wake up and do something.” The observer can then visit the subject and look at the value. The observer is not required to do anything beyond that. A slightly different implementation is that the subject notifies the observer and includes the new value. This is fine when the object can be passed by reference, which minimizes memory requirements. The downside is that the observer is then able to modify the value of the subject which can be unwise in some scenarios. We are converting temperatures, so we are sending the observer the new value along with the notification.
Implementing the observer pattern is a snap with Ext. I’m going to describe the operations in the order of writing the code, not the order they appear in the file.
- Create your Subject class.
- Call Ext.extend with the name of you Subject and derive fromExt.util.Observable. “Ext.extend(DocumentItem, Ext.util.Observable);”
- Call addEvents() during the construction of the new class so thatExt.util.Observable will dispatch your new events and let objects register for it.
- Add a “register”,”attach”,”bind” or whatever you want to call it, method for registering the objects interested in receiving events about the change in state of your class. Within this method call addListener with the name of the event, the handler function and the scope you want the function to run in. Usually the scope is the object that contains the event handler function.
- Call fireEvent with the event name defined in step 3 and the value that changed. Ideally you would fire the event from the same place that governs changes to the value object. You should only fire an event if the value actually changes from one setoperation to the next to avoid dispatching unnecessary updates to the observers.
About half the code is for implementing the Observer pattern, while the rest is for storing and updating the value of the Subject. The Ext.util.Observable object lets you register as many observers as you like. This is great because you can have applications where there are more than one user interface element bound to a given data object. I’ll get into this a little later when I describe why I chose this specific implementation.
The code is very simple because all of the difficult work is being done by theExt.util.Observer base class. The setValue() method just checks to make sure the value is different from the one stored and performs an assignment. Then it callsfireEvent() to notify the observers that the value has changed.
Testing to see if the value is different is meant to limit the number of unnecessary updates. We don’t know the logic of any observer when implementing a document model. The observer’s logic could be very complex or very simple. Consequently, we try to minimize dispatching pointless events.
That if statement is also too simple for anything beyond string and number tests. A complex application may associate more complex types, like an Ext.data.Storeobject or an array, and that test would not work effectively.
There is also a check to see if we should fire the event at all. Sometimes you don’t want to update the value depending upon who is doing the update. It is possible to create a recursive series of events where modifying value A causes an event that modifies of value B which causes another event that modifies the value A until the stack runs out of memory and the application or browser crashes. I successfully accomplished this feat during some late night coding.
The next code related item is the call to addEvents(). We can’t dispatch a new kind of event until we register the event with the Ext.util.Observable base class. Ext will throw a number of errors if we fail to do this when we fire the event.
The last line is critical to all of this where we associate the Ext.util.Observableas the parent class of DocumentItem. The extend() method adds the properties of the parent to the child class.
The behavior of Javascript classes in relation to inheritance and behavior is very different from more traditional languages in part because Javascript never defined a real class structure. Instead some very inventive developers figured out that they could copy the properties of one object into another. Consequently, don’t look for the usual modifiers (constants, access, etc.) in these class definitions nor expect the class to behave exactly like a Java or C++ class.
These DocumentItem instances are managed by the GenericDocument class. The GenericDocument class is an example of a very simple Composite. First, the calling code works through the Composite to manipulate the individual data items. Second, the Composite treats each data item as a leaf. Third, the leaves can be of broader types than just an instance of DocumentItem. We could have several child classes off DocumetItem or we could remove the factory functionality of the putmethod entirely and let the user just add name/value pairs where the value may vary from the DocumentItem class. The Composite could contain other Compositeinstances though this specific implementation does not.
Let’s go into why the document model was done in this particular way. It seems like a lot of work to implement all that code so that we can convert temperatures. Sure, it is a lot of work and it could be written far simpler. Let’s say that you had to develop a real application in Javascript, like maybe Excel or PowerPoint. You can have literally a dozen UI components all sharing a single data item. One way to code this would be through a global variable, or maybe a really simple class with a bunch of member fields.
The first problem is how do you modify the object and have all those dependent components be aware of the change? You could have the changing UI component go around and update the other dependent controls. That would be a lot of work, and the UI control’s event handler would have to have a list of all the other controls. That means you have to maintain some code that populates and manages that list. Then a developer on your team removes one control. Now you have to remove that control from the list. Its a maintenance issue and it can be a convoluted one.
Another way to update those controls is to have each control visit the data item and then the visiting UI control can update its value. That could result in a lot of controls having to visit that data item often. You can poll, but polling can be very inefficient, especially when you have say a dozen controls all polling the same object every 2 or 3 seconds. Your CPU utilization will spike and your application will run more slowly.
Yet another way to do it is through a global variable that is anExt.util.Observable child. This would give you the same event notification strategy but without having the document. Global variables certainly work, but they have a high degree of effort with fewer benefits. For example, global factory and management functions will probably be necessary to manage all these globals in a common way since some of them will be based on the Observer patten. Some of the globals will not be an observable object because they may be a UI control or some other data item. Consequently you will have heterogeneous data objects, making the code more difficult to read and determine which object is of what type.
These scenarios are based on having just one data item being monitored by several UI controls. A complex application will have many data items being shared across many UI controls. Using the Observer pattern ensures that you UI controls only update when data actually changes, thus keeping them always synchronized to the data in the document and keeping CPU utilization low. Associating theDocumentItem objects with a GenericDocument object makes the code more manageable.
This pattern is the basis for another the Publisher-Subscriber pattern too. The publisher-subscriber pattern is usually a little bigger and more distributed, with multiple publishers and multiple listeners for the same event and data. Let’s move on and look at the View.
The View
View = function() {
this.getField = function (label) {
var field = new Ext.form.NumberField( {
allowBlank: true,
allowDecimals: true,
fieldLabel: label,
hideLabel: false
});
field.documentItemName = label;
field.addListener('specialkey', function(field, eventObject) {
if ( eventObject.getKey() == null || eventObject.getKey() == Ext.EventObject.ENTER) {
var value = this.getValue();
documentModel.setTemp( this.documentItemName, value, true);
}},
field
);
documentModel.get(label, true).register(field, function(value) {
this.setValue(value);
} );
return field;
};
this.getForm = function(){
var fahrenheitField = this.getField(documentModel.getFahrenheitLabel());
var celsiusField = this.getField(documentModel.getCelsiusLabel());
var kelvinField = this.getField(documentModel.getKelvinLabel());
var panel = new Ext.FormPanel( {
bodyStyle:'padding:5px 5px 0',
border: false,
frame: false,
items: [
fahrenheitField,
celsiusField,
kelvinField
],
width: 300
});
return panel;
};
}
The view portion of the MVC is governed by, originally enough, the View class. The example application has a really simple implementation. Its job is to create anExt.form.FormPanel with three Ext.form.Numberfield objects that differ by label and the model’s data item. This class has one Factory implementation where the the field creation is performed by only the View.getField() method.
There are a few interesting lines worth looking at. In the getField() method, we see a call to field.addListener() which is registering for keyboard events like the tab,enter and various other keys. We are registering with this method to get enter key events. The handler calls documentModel.setTemp() with the name of the data item, the updated value and to dispatch an event about the value’s change. ThesetTemp() method is defined a little later when I talk more about class derivation.The effect of this code is that any change to a field plus the enter key will cause the update of a DocumentItem object.
The next interesting line is a call to “documentModel.get(label, true).register(” which creates a new DocumentItem, adds it to the document, and then registers the field object to receive events about changes to that DocumentItem. This line simply allows the UI control to be notified of a change to the DocumentItem.
A quick recap may be helpful. The field.addListener() call enables the UI control to notify the DocumentItem that a change has occurred. Thedocument.get().register() call enables the DocumentItem to update the UI control. I mentioned earlier that it was possible to great recursive, or circular, events. I give you another avenue I drove down for crashing your application unless you are careful.
The Controller
If you haven’t guessed yet, there is almost no explicit controller in this application. The GenericDocument, the event handling and dispatching, and the UI initialization all mixes in controller type functionality together. It is not clean but you can’t always make it clean either without rewriting much more code, like down into the Ext library which is exactly where we don’t want to go.
var documentModel = new TemperatureDocument();
function main() {
var view = new View();
view.getForm().render(Ext.getDom('formDiv'));
}
The above code is about the closest thing to a distinct controller as it initializes the and serves the UI, which is an explicit feature of the controller.
Class Derivation
We now get to the last chunk of code worth mentioning. It is generally useful to have a specific document implementation for a given application. TheGenericDocument object is handy for storing data, but we need something to encapsulate the operations that do temperature conversions. TheTemperatureDocument class is a child class of GenericDocument that is application specific. The TemperatureDocument class contains the data item name constants, temperature conversion functions and functions that save the updated values that cause the UI controls to update.
You can do class derivations a couple of different ways. First, you can use theprototype keyword. Second, you can define all your class methods in the constructor and then call Ext.extend to bind a parent class. You saw this in the DocumentItemdeclaration. Third, you can call Ext.extend with your new class, the base class and an object defining the new behaviors to add. You’ll see this in the following snippet. Fourth, you can use the Ext.apply and Ext.applyIf methods to bind properties to arbitrary objects.
TemperatureDocument = function() {
TemperatureDocument.superclass.constructor.call(this);
}
Ext.extend(TemperatureDocument, GenericDocument, {
getFahrenheitLabel : function() {
return 'Fahrenheit';
},
getCelsiusLabel : function() {
return 'Celsius';
},
getKelvinLabel : function() {
return 'Kelvin';
},
setTemp : function(label, value ) {
var validationValue = '0' + value;
if ( validationValue.length > 1) {
if ( label == this.getFahrenheitLabel() ) {
this.fahrenheitConversion(value)
} else if ( label == this.getCelsiusLabel() ) {
this.celsiusConversion(value);
} else if ( label == this.getKelvinLabel() ) {
this.kelvinConversion(value);
}
}
},
round: function(value, decimalPlaces) {
var rounder = Math.pow(10, decimalPlaces);
return Math.round(value * rounder)/rounder;
},
fahrenhietToCelsius: function(value) {
return this.round(( value - 32 ) * 5/9, 3);
},
fahrenheitToKelvin : function(value) {
return this.round((value + 459.67) * 5/9,3);
},
celsiusToFahrenheit : function(value) {
return this.round((value * 9/5) + 32,3);
},
kelvinToFahrenheit : function(value) {
return this.round(( value * 9/5) -459.67,3);
},
fahrenheitConversion : function(value) {
celsius = this.fahrenhietToCelsius(value);
kelvin = this.fahrenheitToKelvin(value);
this.put(this.getCelsiusLabel(), celsius);
this.put(this.getKelvinLabel(), kelvin);
this.put(this.getFahrenheitLabel(), value, false );
},
celsiusConversion : function(value) {
fahrenhiet = this.celsiusToFahrenheit(value);
kelvin = this.fahrenheitToKelvin(fahrenhiet);
this.put(this.getFahrenheitLabel(), fahrenhiet );
this.put(this.getKelvinLabel(), kelvin);
this.put(this.getCelsiusLabel(), value, false);
},
kelvinConversion : function(value) {
fahrenhiet = this.kelvinToFahrenheit(value);
celsius = this.fahrenhietToCelsius(fahrenhiet);
this.put(this.getFahrenheitLabel(), fahrenhiet);
this.put(this.getCelsiusLabel(), celsius);
this.put(this.getKelvinLabel(), value, false);
}
});
I’m going to skip explaining the bulk of the code in favor of talking aboutTemperatureDocument(), setTemp() and fahrenheitConversion(). The only line in TemperatureDocument() calls the GenericDocument() method. This is how you initialize your base classes.
The setTemp() method figures out which conversion method to call. There is an odd line in that method I will talk about later that has to do with zero and null being treated the same.
The fahrenheitConversion() method is where we end our tour to theTemperatureDocument(). It is a short tour. Two straightforward calls convert fahrenheit into celsius and kelvin and then we update the document’s master values. Note that we want to fire events when we update the celsius and kelvin values because we want the UI controls to update. We don’t want the fahrenheit UI control to update because it is the source of the change.
Now look at the code a second time and see the glaring flaw. Our goal was to allow multiple UI controls to all share a common data item. However, we don’t want the source UI control, the one that caused the value to change, to be updated too. We accomplished this by turning off the firing of the change event back to the UI control. That’s the problem. If we turn off the firing of the event then we end up disabling the event dispatching for all the other dependent UI controls. Other controls displaying the fahrenheit value would not update either. It works here because we only have one dependent control. Well, it turns out that you can enable passing the event back to the source UI control with no problems in this application because we are not dispatching events from the UI control as a result of calling the control’s setValue() method. If we were dispatching events caused by setValue(), then we would risk the circular event crash I mentioned, and experienced, earlier.
There isn’t a method that explicitly deals with the change of a value except for “valid” event. Consequently, most applications are probably safe. The only way to find out is to dig deeper into your own application.
This brings us to the end of the MVC pattern. Now lets see the complete application and talk about some of the interesting things that happened while writing it. We’ll also go into the motivations for some of my decisions.
The Code
There’s a lot of code for a temperature conversion application. It could have been done more simply if I wasn’t trying to make an example of design patterns and MVC. I’ve labeled and highlighted the interesting components where I ran into bugs, crashes and strange little error messages. They are talked about a little later.
First, why go MVC? The idea is that I don’t really know how much the application will grow by. I know, its an example application - it isn’t going to grow. Let’s say that it will and that we’ll have historical data, charts, data stores, probes, and lots of UI controls. The MVC pattern lets us add new data items and UI controls in a central store. Its easy to look them up and share them across many different components.
Why have a GenericDocument and then an application specificTemperatureDocument? We have a lot of conversion functions, constants and the need for other helper operations that are specific to the application and the document, but not the UI. The MVC pattern works best when you put these operations in the document itself. In our case, the TemperatureDocument adds helper functions and simplifies the code. All the important data management stays in theGenericDocument while all the application specific operations were in theTemperatureDocument.
Why define the entire class in the constructor of DocumentItem and then defineTemperatureDocument differently? I wanted to show there are different ways of doing pretty much the same thing. Of course, it really isn’t the same thing. These operations occur at completely different times. The DocumentItem got all of its methods assigned when the DocumentItem instance is created. TheTemperatureDocument got all of its methods bound when the document was loaded. The latter method is probably better since you generally want your user to wait for everything to initialize at the start rather than during the normal operation of the application. The better performing TemperatureDocument initialization would probably become more noticeable if you had to create several hundredTemepratureDocument instances and DocumentItem instances. TheDocumentItem objects would take longer to initialize even though they are otherwise simpler.
Why are there two arrays in GenericDocument? One is so that I can quickly get all the items in one pass. The other is so that I can look them up by name. The by-name lookup is speedy and ideal for most operations. The other version is handy if I need to perform some kind of server side automatic updates and such as I can just iterate through them more easily than an associative map. That’s just my opinion though and others will differ on this. The world is a diverse place.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Simple Ext Class Example</title>
<link rel="stylesheet" type="text/css" href="./ext-2.0/resources/css/ext-all.css"/>
<script type="text/javascript" src="./ext-2.0/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="./ext-2.0/ext-all-debug.js"></script>
<script>
GenericDocument = function() {
this.elementAssocArray = new Array();
this.elementArray = new Array();
this.getAll = function() {
return this.elementArray;
};
this.put = function(name, value, fireEvent) {
if ( typeof(fireEvent) == 'undefined' ) {
fireEvent = true;
}
var documentItem = null;
if ( name ) {
documentItem = this.get(name);
if ( !documentItem) {
documentItem = new DocumentItem( name, value );
this.elementAssocArray[name] = documentItem;
this.elementArray.push(documentItem);
} else {
documentItem.setValue(value, fireEvent);
}
}
return documentItem;
};
this.get = function(name, createNew) {
var documentItem = null;
if ( name ) {
documentItem = this.elementAssocArray[name];
if ( !documentItem && createNew ) {
documentItem = this.put(name);
}
}
return documentItem;
};
this.getValue = function(name) {
var value = null;
var documentItem = this.get(name);
if ( documentItem ) {
documentItem.getValue();
}
return value;
};
this.addHandler = function(name, object, handler) {
var documentItem = this.get(name);
if ( documentItem ) {
documentItem.register(object, handler);
}
};
}
DocumentItem = function(name, value, object, handler) {
this.setValue = function(value, fireEvent) {
if ( value != this.value ) { // 1
this.value = value;
if ( fireEvent ) {
this.fireEvent( 'valueChange', value );
}
}
};
this.register = function(object, handler) {
if ( object && handler ) {
this.addListener( 'valueChange', handler, object);
}
};
this.getValue = function() {
return this.value;
};
this.addEvents( {'valueChange': true} ); // 2
this.name = name;
this.setValue(value);
};
Ext.extend(DocumentItem, Ext.util.Observable);
TemperatureDocument = function() {
TemperatureDocument.superclass.constructor.call(this);
}
Ext.extend(TemperatureDocument, GenericDocument, {
getFahrenheitLabel : function() {
return 'Fahrenheit';
},
getCelsiusLabel : function() {
return 'Celsius';
},
getKelvinLabel : function() {
return 'Kelvin';
},
setTemp : function(label, value ) {
var validationValue = '0' + value; // 3
if ( validationValue.length > 1) {
if ( label == this.getFahrenheitLabel() ) {
this.fahrenheitConversion(value)
} else if ( label == this.getCelsiusLabel() ) {
this.celsiusConversion(value);
} else if ( label == this.getKelvinLabel() ) {
this.kelvinConversion(value);
}
}
},
round: function(value, decimalPlaces) {
var rounder = Math.pow(10, decimalPlaces);
return Math.round(value * rounder)/rounder;
},
fahrenhietToCelsius: function(value) {
return this.round(( value - 32 ) * 5/9, 3);
},
fahrenheitToKelvin : function(value) {
return this.round((value + 459.67) * 5/9,3);
},
celsiusToFahrenheit : function(value) {
return this.round((value * 9/5) + 32,3);
},
kelvinToFahrenheit : function(value) {
return this.round(( value * 9/5) -459.67,3);
},
fahrenheitConversion : function(value) {
celsius = this.fahrenhietToCelsius(value);
kelvin = this.fahrenheitToKelvin(value);
this.put(this.getCelsiusLabel(), celsius);
this.put(this.getKelvinLabel(), kelvin);
this.put(this.getFahrenheitLabel(), value, false );
},
celsiusConversion : function(value) {
fahrenhiet = this.celsiusToFahrenheit(value);
kelvin = this.fahrenheitToKelvin(fahrenhiet);
this.put(this.getFahrenheitLabel(), fahrenhiet );
this.put(this.getKelvinLabel(), kelvin);
this.put(this.getCelsiusLabel(), value, false);
},
kelvinConversion : function(value) {
fahrenhiet = this.kelvinToFahrenheit(value);
celsius = this.fahrenhietToCelsius(fahrenhiet);
this.put(this.getFahrenheitLabel(), fahrenhiet);
this.put(this.getCelsiusLabel(), celsius);
this.put(this.getKelvinLabel(), value, false);
}
});
View = function() {
this.getField = function (label) {
var field = new Ext.form.NumberField( {
allowBlank: true,
allowDecimals: true,
fieldLabel: label,
hideLabel: false
});
field.documentItemName = label;
field.addListener('specialkey', function(field, eventObject) { // 4
if ( eventObject.getKey() == null || eventObject.getKey() == Ext.EventObject.ENTER) {
var value = this.getValue();
documentModel.setTemp( this.documentItemName, value, true);
}},
field
);
documentModel.get(label, true).register(field, function(value) {
this.setValue(value);
} );
return field;
};
this.getForm = function(){
var fahrenheitField = this.getField(documentModel.getFahrenheitLabel());
var celsiusField = this.getField(documentModel.getCelsiusLabel());
var kelvinField = this.getField(documentModel.getKelvinLabel());
var panel = new Ext.FormPanel( {
bodyStyle:'padding:5px 5px 0',
border: false,
frame: false,
items: [
fahrenheitField,
celsiusField,
kelvinField
],
width: 300 // 5
});
return panel;
};
}
var documentModel = new TemperatureDocument();
function main() {
var view = new View();
view.getForm().render(Ext.getDom('formDiv'));
}
</script>
</head>
<body>
<div style="margin: 10px 10px;">
<div style="font: normal normal 110% helvetica">
Pick a field and type in a temperature and press enter to convert.
</div>
<div id="formDiv"/>
</div>
<script>
Ext.onReady( main ); // 6
</script>
</body>
</Html>
Now on to some of the interesting things that happened while writing this. Look for red code and comments to match them to item numbers.
- This was interesting. Originally I checked for null too. Turns out that null and zero are the same, big surprise. That holds true in pretty much every language too. Well, I thought I would get the string “0” rather than an actual zero. Had to remove the value so 0 degrees would be valid.
- I had this line commented out to figure out a different bug and started to get this error message in FireBug “this.events has no properties: var ce = this.events[eventName] || true;”. Always add your events before you try to use them!
- I had a similar problem as in item 1. I had to prepend 0 to the beginning of the value to ensure that it got processed, but only if the value was 00 rather than a completely empty field. So a user could type 0 and it would get processed and a blank field won’t.
- This was really annoying. Again, part 1 strikes in yet another way. Turns out that a value of zero gets processed slightly differently when you hit the enter key or any other special key. The key code is null! Who would have thought that so much work went into handling zero?
- Ext in Internet Explorer 6 is a little odd with width calculation at times. I often find that I have to define a hardcoded width or the control will take the entire screen.
- I accidentally had this instead “Ext.onReady( main() );”. Note that this is now a function call inside of onReady which means that main() was getting called and passing its null result into Ext.onReady. Firebug gave me this error message “l.fireFn has no properties: if(l.fireFn.apply(l.scope||this.obj||window, arguments) === ...”. The bad bit is that everything worked except I got this error message. Seems like a T-shirt title.
Summary
The goal was to show how you can build an MVC application using Ext 2.0. We used a number of different patterns and components to accomplish something that can be used effectively in much larger applications than this little thing. It wasn’t necessary to use Ext to demonstrate this though. Many of the Javascript libraries use some kind of OO contraption and consequently, you can take advantage of some of the well proven patterns that have built a lot of very succesfull applications.
Eckstein, Robert. “Java SE Application Design with MVC.” java.sun.com. March 2007.<http://java.sun.com/developer/technicalArticles/javase/mvc/>
Gamma, Erich., Helm, Richard., Johnson, Ralph., and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Reading: Addison-Wesley, 1995
Jacobs, Kurt. “Subscribe now for rapid prototyping.” Javaworld.com, Septemer 2001.<http://www.javaworld.com/javaworld/jw-11-2001/jw-1109-subscriber.html>
0 comments