It has been a while since I posted the InPlaceEditing sample which features an Atlas extender control and script behavior. The goal was to show how the Atlas platform makes it really possible to build and incorporate rich Web experiences. This post will focus on building the script behavior. Rather than covering things line-by-line, I'll touch upon the key points. I'll cover the extender control in the next post.
One fundamental aspect of Atlas both in terms of the technology and in terms of practice is the approach of encapsulating logic and data into classes, or components (if you also want to make it declaratively usable). This approach makes script less ad-hoc, and makes larger apps more manageable and realistic. Atlas behaviors are script components that take this encapsulation to the next level. Essentially they are a reusable encapsulation of event handling logic to package some useful pattern or functionality that can be attached to HTML elements or script controls (another core Atlas concept). Behaviors can be used to extend a fairly compact or minimal control with additional useful semantics, without resorting to building kitchen-sink style controls (which do not scale in the DHTML/scripting world). For example, this post is all about the InPlaceEditBehavior. I could also implement a MaskedEditBehavior and attach it to the same textbox to get both masked editing, and in-place editing if that is desirable.
What does InPlaceEditBehavior enable specifically? It's a behavior that can be attached to a TextBox. When the textbox does not contain focus, it is replaced with a label containing the same text, thereby helping the form look less cluttered (esp. if editing in that textbox isn't the mainline scenario). The label itself can present a different look to indicate its active, can be clicked or tabbed into to switch it into edit mode.
Step 1: A Basic Behavior
Now, that I've covered what behaviors are, and the motivation behind them, and the specific behavior, lets get started with the implementation. To start, I am going to define a skeleton behavior class:
Type.registerNamespace('nStuff.Samples');
nStuff.Samples.InPlaceEditBehavior = function() {
nStuff.Samples.InPlaceEditBehavior.initializeBase(this);
}
Type.registerClass('nStuff.Samples.InPlaceEditBehavior', Web.UI.Behavior);
Essentially I have defined a class called InPlaceEditBehavior in the nStuff.Samples namespace that derives from Web.UI.Behavior. And it calls the base class ctor in its own ctor.
Step 2: Add Properties
The next thing to do is make it more useful. I am going to add some properties. This behavior needs CSS class properties it uses to change the look and feel of labels as the mouse hovers.
nStuff.Samples.InPlaceEditBehavior = function() {
...
var _labelCssClass;
this.get_labelCssClass = function() {
return _labelCssClass;
}
this.set_labelCssClass = function(value) {
_labelCssClass = value;
}
}
JavaScript doesn't have true properties, but as you can see, I have used the Atlas pattern that bears resemblance to properties in C# and managed code to define a read/write labelCssClass property. I can do the same for the other properties needed to customize this behavior's working.
Step 3: Support for XML-Script
This behavior needs to usable in a declarative manner. The declarative model makes it simpler to associate behaviors with controls and to customize them with property values. I need to do two things for this to happen: define the properties and associated metadata, and secondly register a tag that maps to my class type. The XML-script parser uses these to do its job.
nStuff.Samples.InPlaceEditBehavior = function() {
...
this.getDescriptor = function() {
var td = nStuff.Samples.InPlaceEditBehavior.callBaseMethod(this, 'getDescriptor');
td.addProperty('labelCssClass', String);
return td;
}
}
...
Web.TypeDescriptor.addType('nk', 'inPlaceEdit', nStuff.Samples.InPlaceEditBehavior);
Basically, I am overriding getDescriptor method, calling the base implementation first so the type descriptor I hand out will contain information about all the properties (such as the bindings collection) that I am inheriting from Behavior and Component, and then adding the information about properties such as labelCssClass. I specify the type as well, so that the parser can appropriately create typed values from their string representation in the form of XML attributes. Secondly, I am declaring the tag name for this type to be "nk:inPlaceEdit".
As you can see, XML-script is completely metadata driven. The object model of the component describes the set of valid tags, and attributes. Note I can also describe events and methods in the type descriptor. Event information is used to associated event handlers and actions in markup. Method information is used to declaratively invoke methods in response to an event via invokeMethodAction.
Step 4: Add Core Functionality
OK, enough setup. Now, onto to some real work. Like most behaviors, this one needs to attach event handlers during initialization by overriding the initialize method. Do not listen to window onload, because that may have happened in the past by the time your behavior is initialized.
nStuff.Samples.InPlaceEditBehavior = function() {
var _labelElement;
var _labelMouseOverHandler;
var _isEditing;
...
this.get_isEditing = function() {
return _isEditing;
}
this.initialize = function() {
nStuff.Samples.InPlaceEditBehavior.callBaseMethod(this, 'initialize');
var textBoxElement = this.control.element;
_labelElement = document.createElement('SPAN');
...
_labelMouseOverHandler = Function.createDelegate(this, this._onLabelMouseOver);
_labelElement.attachEvent('onmouseout', _labelMouseOutHandler);
...
if (Web.UI.InputControl.isInstanceOfType(this.control)) {
_validatedHandler = Function.createDelegate(this, this._onValidated);
this.control.validated.add(_validatedHandler);
}
}
this._onLabelMouseOut = function() {
Web.UI.Control.removeCssClass(_labelElement, _labelHoverCssClass);
}
this._onLabelMouseOver = function() {
Web.UI.Control.addCssClass(_labelElement, _labelHoverCssClass);
}
this._onTextBoxBlur = function() {
this.endEdit();
}
this._onValidated = function(sender, eventArgs) {
if (this.control.get_isInvalid()) {
this.beginEdit();
}
}
this.beginEdit = function() {
var textBoxElement = this.control.element;
textBoxElement.style.display = '';
_labelElement.style.display = 'none';
textBoxElement.focus();
_isEditing = true;
this.raisePropertyChanged('isEditing');
}
this.endEdit = function() {
if (_isInputControl && this.control.get_isInvalid()) {
return;
}
var textBoxElement = this.control.element;
_labelElement.innerHTML = textBoxElement.value;
_labelElement.style.display = 'block';
textBoxElement.style.display = 'none';
_isEditing = false;
this.raisePropertyChanged('isEditing');
}
}
During initialization, the behavior creates a label and adds it to the DOM such that it covers the textbox. In addition it starts monitoring for user actions such as hovering over the label. A Behavior can not only listen to DOM events; it can also listen to events raised by the script control associated with the DOM event. If the control associated with the Behavior is an InputControl (which TextBox is), then the behavior sinks to the validated event and forces the textbox to remain visible even after the user has moved away from it if it is in an invalid state. You don't want the label to replace the TextBox in this case. In addition it also listens to the blur event on the textbox, and focus event of the label (to enable keyboard interaction and accessibility).
Notice the use of delegates, which are part of the OOP extensions to JavaScript provided by Atlas. These allow usage of "this" naturally within the event handlers. Also notice the usage of addCssClass and removeCssClass - These are some of the helper methods on Web.UI.Control to provide a more friendly interface to the underlying DHTML APIs provided by Atlas.
One other thing to notice is the isEditing property, and the change notification being raised in the beginEdit and endEdit methods. These change notifications form the basis of data-binding in Atlas. Any other component can now bind to this property, and the binding engine can now automatically move data around as required. Coupled with declarative markup, this mechanism immensely simplifies code (to contrast, think event handlers you'd have to write that move data around manually in imperative code.) When you write components, think about what are the meaningful properties and events from a binding perspective, so that page and application developers can start incorporating the declarative model.
Step 5: Add Cleanup Logic
One important piece of logic you do not want to forget about is dispose. While JavaScript itself does have GC, the combination of Script + DHTML DOM can lead to memory leaks as a result of circular references. The Atlas framework will take care of calling your dispose implementation at the right times. What you need to do is detach your event handlers from the DOM elements, thereby breaking the circular references.
this.dispose = function() {
if (_labelElement) {
_labelElement.detachEvent('onmouseover', _labelMouseOverHandler);
_labelElement.detachEvent('onmouseout', _labelMouseOutHandler);
_labelElement = null;
_labelMouseOverHandler = null;
_labelMouseOutHandler = null;
}
...
if (_validatedHandler) {
this.control.validated.remove(_validatedHandler);
_validatedHandler = null;
}
nStuff.Samples.InPlaceEdit.InPlaceEditBehavior.callBaseMethod(this, 'dispose');
}
That's pretty much it for a simple behavior. Using these concepts, you can build larger, more involved and interesting components.
Should I write a behavior or a control?
While I haven't talked about authoring controls, there is a lot of overlap. There is one technical difference: Only a single control can be associated with a given HTML element. However one of more behaviors can be associated. A behavior does require a control to exist, since it needs to be added into the behaviors collection to work. There is a subtle distinction between the two that will determine what is appropriate. You should write a control, when it is the answer to a question "What is this widget" - For example, the answer to this question for <input type="text" /> is a "TextBox". You should write a behavior when it describes one optional and independent functional aspect of a widget. Hence InPlaceEditBehavior is a behavior and not a derived TextBox.
What control should the behavior attach to?
This is a key design decision to make. My initial thought was to have a label, and to display a textbox as needed. However, I chose the opposite for the implementation. I thought I'd share some of the thoughts that led me to do this. Some will carry over to other behaviors.
If extended a label, I'd have to create a textbox dynamically. A textbox requires many properties to fully customize (too expensive to implement in script), while a label can be easily customized via a single CSS class. Furthermore, the Atlas model is to allow UI to be fully specified via HTML/CSS, providing full flexibility to the designer. The script should focus on the behavioral aspects of the application. This is a key difference from ASP.NET server controls that tend to abstract UI rendering and behavior into a single entity.
Atlas provides a templating mechanism, so perhaps the textbox could be specified in a template. But this is simply too complex in terms of the markup the page developer would have create for such a simple scenario. Templates are powerful, but use them judiciously (they are inherently complex).
At the same time, allowing the developer to specify both label and textbox in the UI would lead to designer issues, and implementation issues around overall layout of the page. Finally I decided it would be simplest if the TextBox pre-existed in the page somehow, and the relatively simpler label was dynamically created.
Usually the simpler design is also the better design and this became apparent as I proceeded with the implementation. With the textbox on the page, the functionality of the page is preserved in the absence of script. It also allows association of validation rules, and ensures the entered value is present in the form post-back. All of these are bits of goodness.