Home / AJAX

Design Standards

RSS
Modified on 2010/05/10 16:32 by Stephen Walther Categorized as Uncategorized
The design guidelines in this section are intended to encourage good practices that lead to maintainable, supportable, well performing script.

4.1 Design Approach

Two common approaches to JavaScript development exist, sometimes referred to as “static” and “dynamic”. A static approach consists of defining script components as encapsulated JavaScript classes and their associated functions, organized into namespaces. A dynamic approach leans more towards creating small highly specialized functions, often combined to have the complete desired effect.

Both of these approaches are completely valid, and this document does not aim to prefer one over the other. However, a static approach is likely to contain more code elements (such as namespaces and classes), and therefore more of this document's content is applicable.

4.2 Progressive Enhancement

When building a Web Site consider using Progressive Enhancement. This results in a page that is fully functional without script; the functionality and user experience are enhanced when script is enabled. This is particularly well complimented by the principles of Unobtrusive JavaScript. Web pages created in this manner do not have a mandatory dependency on JavaScript, and therefore are more likely to be compatible with a wider range of clients and accessibility tools, and to be considered Search Engine Optimized (SEO).

However, requiring no dependency on JavaScript can be unrealistic for some applications, and adds significant development effort. Developers should therefore weigh the requirements for each project against the effort involved in following this practice. For further guidance on Progressive Enhancement refer to the Web Client Guidance project from patterns & practices .

4.3 Types

4.3.1 Namespace

Do use namespaces to organize types into a hierarchy of related feature areas.

Avoid deep namespace hierarchies. Such hierarchies can make it difficult for a developer to locate the functionality they require, and lead to unnecessary increased script size and hence page weight.

Avoid empty namespaces or extending a namespace without adding value. Avoid having too many namespaces.

Types that are used in the same scenarios should be in the same namespaces when possible. Do not define types without specifying their namespaces. Types that are not assigned a namespace are placed in the global namespace. This can cause name collisions with other scripts or frameworks, therefore preventing some libraries from being used concurrently on the same page.

4.3.2 Component, Behavior and Control

Select the correct base class for script classes. Use the Type.registerClass method to define the base class for a script when inheritance functionality is required, such as implementing an interface, overriding base class members, or creating Components, Controls or Behaviors:

    MyNamespace.MyClass.registerClass('MyNamespace.MyClass',
        Sys.IComponent);

Use Sys.Component as the base class for script that does not have a visual element, but may manage references between other components or DOM elements. Use events and the dispose method to ensure that relationships are managed correctly and released in a timely fashion. Use Sys.UI.Behavior as the base class for script that enhances an existing DOM element. When using ASP.NET Web Forms, associate the script Behavior with a server-side Extender. Behaviors are ideal for progressively enhancing a web page.

Use Sys.UI.Control as the base class for script that fundamentally changes a single DOM element’s behavior to provide new functionality. When using ASP.NET Web Forms, associate the script Control with a server control.

When selecting a base class, prefer Sys.Component where possible. If the control has a visual representation through a relationship with a DOM element, prefer Sys.UI.Behavior where possible. This means that multiple Behaviors may be applied to a single DOM element, in contrast to a Control which takes exclusive ownership of a single DOM element at a time.

4.3.3 IDisposable

Implement Sys.IDisposable for script classes that manage references to DOM elements, other script elements, timers, or more.

    MyNamespace.MyClass.registerClass('MyNamespace.MyClass', 
        null, 
        Sys.IDisposable);

Prefer inheriting from Sys.Component over directly implementing Sys.IDisposable. Component automatically implements IDisposable, but also provides further infrastructure and lifetime management for script elements. When directly implementing IDisposable without inheriting from Component, ensure that Sys.Application.registerDisposableObject is called when instances of the class are created.

When implementing the dispose method, ensure that the final task is to call the dispose method on the base class:

    MyNamespace.MyClass.callBaseMethod(this, 'dispose');

The dispose method should never cause errors, and must allow for being called multiple times. Ensure that references are released early, event handlers are cleared for the DOM element the current class refers to, event handlers placed on other DOM elements are removed, and timers are stopped. An example dispose implementation follows:

dispose: function dispose() {
    $clearHandlers(this.get_element());
    this._buttonReference.removeHandler('click',
        this._clickHandler);
    window.clearInterval(this._tick);
    MyNamespace.MyClass.callBaseMethod(this, 'dispose');
}

Note that this example addresses a Control, which has full ownership of a DOM element and therefore can call $clearHandlers. Behaviors should not call this method, and instead should use $removeHandler.

Finally, ensure member variables that hold references to DOM elements or browser plug-ins are set to null. You may also consider using the delete keyword. For example, use one of the following:

this._events = null; OR delete this._events;

Dispose methods should also support being called multiple times without causing negative side effects.

4.3.4 Class Design

Define a class when it encapsulates some behavior or data, providing functionality to other script or DOM elements found on a page.

Prefer using a prototype approach over using closures to define class members. Brief examples of closure and prototype-based approaches are as follows.

4.3.4.1 Closure Approach

MyNamespace.MyClass = function MyClass () {
    this._interval = 1000;
    this._enabled;
    this._timer = null;
    this._tick = function() {
        alert('Ticked...');
    }
}

The definition of the “tick” method is local to the constructor of “MyClass”. This approach requires more memory and processing when multiple instances of MyClass are created in comparison with a prototype approach, shown in the next section. This can be proven with the following simple script;

MyClass = function MyClass() {
    this._tick = function() { alert('tick'); };
}
var a = new MyClass();
var b = new MyClass();

// functions are not the same alert(a._tick === b._tick);

MyClass2 = function MyClass2() { }; MyClass2.prototype = { _tick : function tick() { alert('tick'); } } var c = new MyClass2(); var d = new MyClass2();

// functions are the same alert(c._tick === d._tick);

4.3.4.2 Prototype Approach

MyNamespace.MyClass = function MyClass () {
    this._interval = 1000;
    this._enabled;
    this._timer = null;
}

MyNamespace.MyClass.prototype = { _tick: function tick() { alert('Ticked...'); } }

MyNamespace.MyClass.registerClass('MyNamespace.MyClass');

Ensure that functions are separated by commas to avoid parsing errors. For example:

MyNamespace.MyClass.prototype = { 
    _tick: function tick() {
        alert('Ticked...');
    },
    _tock: function tock() {
        alert('Tocked...');
    }
}

The prototype approach does have a small performance cost in requiring members to be looked up through the prototype chain, but this is seen as a worthwhile trade-off.

4.3.5 Interface Design

It is preferable to create classes rather than interfaces. Classes can have new members added in later versions of a framework without breaking compatibility, but interfaces cannot. Do not add members to an interface that has previously been shipped.

Use interfaces when classes that are otherwise unrelated need to share some common functionality, and when classes that share functionality already have logical base classes. Avoid using marker interfaces (interfaces with no members). Instead consider using a property or status field.

4.3.6 Enumerations

Enumerations provide sets of constant values that are useful for strongly typing members and improving readability of code.

Use an enumeration to strongly type parameters, properties, and return values that represent sets of values. Favor using an enumeration instead of static constants. Do not define reserved enumeration values that are intended for future use.

Avoid publicly exposing enumerations with only one value.

Do provide a default for simple enumerations. If possible, name this value none. If none is not appropriate, use a logical term.

Define enumerations using a prototype approach, and register them using the registerEnum method. This is demonstrated by the system Sys.UI.Key enumeration:

Sys.UI.Key = function Key() {
}

Sys.UI.Key.prototype = { backspace: 8, tab: 9, enter: 13, esc: 27, space: 32, pageUp: 33, pageDown: 34, end: 35, home: 36, left: 37, up: 38, right: 39, down: 40, del: 127 }

Sys.UI.Key.registerEnum('Sys.UI.Key');

4.4 Members

4.4.1 Function overloading

Functions in JavaScript are defined by their name, not by their signature as in some other languages, such as C#. Therefore there is no way provided by the core language to define function overloads.

Instead of providing overloads, consider taking advantage of JavaScript's optional parameters; any parameter can be omitted from a call, for example:

MyNameSpace.MyClass.prototype.foo = function foo(p1, p2) {
    // other code removed
    if (p2) {
        // use param2 parameter
    }
    // other code removed
}

This demonstrates the use of an “if” statement to determine whether a parameter has been provided. See section 5.5 for more information on undefined and null.

To communicate this behavior to a developer, an XML comment can be used to indicate that a parameter is optional, as follows:

/// <param name="p2" type="Type" 
    optional="true" mayBeNull="true"></param>

It is good practice to permit optional parameters to be supplied as null, especially if they are not the last parameter in the list. When there are a large number of optional parameters consider packaging the parameters in an "options" object to improve code readability. For example;

// call function
myobject.applySettings(this, { 
    width: 10,
    height: 100,
    color: 'red',
    message: 'Do not click here'
});

4.4.2 Overriding base class behavior

When overriding methods defined on a base class, it is usually desirable to call the base class implementation too, for example:



function initialize() { Sys.UI.Behavior.callBaseMethod(this, 'initialize'); var name = this.get_name(); if (name) this._element[name] = this; }

The call to Type.callBaseMethod ensures this happens as expected.

4.4.3 Properties vs Methods

Class library designers often must decide between implementing a class member as a property or a method. In general, methods represent actions and properties represent data. Use the following guidelines to help you choose between these options.

Use a property when the member is a logical attribute of a type, such as a “Name” property. Use a property if the value it returns is simply a data item stored within a class, and the property would just provide access to the value.

Use a method when:
  • The operation is a conversion, such as Object.toString.
  • The operation is expensive enough that you want to communicate to the user that they should consider caching the result.
  • Obtaining a property value using the get accessor would have an observable side effect.
  • Calling the member twice in succession produces different results.
  • The order of execution is important. Note that a type's properties should be able to be set and retrieved in any order.
  • The member is static but returns a value that can be changed.
  • The member returns an array. Properties that return arrays can be very misleading. Usually it is necessary to return a copy of the internal array so that the user cannot change internal state. This, coupled with the fact that a user can easily assume it is an indexed property, leads to inefficient code.

4.4.4 Properties vs Fields

Properties are used in static languages to encourage a class' interface to remain static, and to adhere to behaviour encapsulation guidelines. In JavaScript the penalties of the additional execution time and script download size can outweigh the benefits of strictly using Properties.

Consider using a field when;
  • The declaring Type is only used for passing data between methods (such as an Event Args class)
  • There is no behaviour or validation required when the value is changed.
  • Change notifications are not required.

4.4.5 Property Design

Use a read-only property when the user cannot change the property's logical data member. Do not use write-only properties – use a method instead.

Provide sensible default values for all properties, ensuring that these defaults result in an efficient design. Use null as a default if there is no logical default value. undefined should never be the return value of a property.

Avoid throwing exceptions from property getters. Preserve the previous value if a property setter throws an exception.

Do allow properties to be set in any order even if this results in a temporary invalid object state. Indicate this state using a status, or disable behavior until the class is correctly configured.

Classes should raise property-changed events if consumers should be notified when the component's property changes programmatically. The Sys.Component class implements the Sys.INotifyPropertyChange interface so classes inheriting from Sys.Component can implement change notification simply by calling raisePropertyChanged from property setters. For example:

    set_text: function set_text(value) {
        if (this._text !== value) {
            this._text = value;
            this.raisePropertyChanged('text');
        }
    }

4.4.6 Constructor Design

Consider providing simple, ideally default, constructors. A simple constructor has a very small number of parameters, and all parameters are primitive types, enumerations, or a reference to a DOM element.

Use constructor parameters as shortcuts for setting main properties. Consider allowing parameters to be optional as a substitute for constructor overloads, which are of course not possible in JavaScript.

Define and set default values for member variables that are not set by constructor parameters.

For example:

MyNamespace.Sample = function Sample(error, dataItems) {
    // call base constructor
    MyNamespace.Sample.initializeBase(this);
    // initialize member variables not set by constructor params
    this._handled = false;
    // initialize main properties, allowing for optional params
    this._error = error;
    this._dataItems = dataItems || {};
}

This approach helps to ensure that all class members are initialized correctly, and therefore reduces errors from confusing undefined and null (see section 5.5).

4.4.7 Event Design

The recommended mechanism for implementing Events is different depending upon the Ajax framework in use, due to the availability of the Sys.Observer class. Sys.Observer became available in the Ajax Control Toolkit. Both approaches are documented below; the Sys.Observer approach is preferred where available.

4.4.7.1 Manual Events

Events should be implemented in five stages; 1. Ensure the class has a member variable of type Sys.EventHandlerList. this._events = new Sys.EventHandlerList(); This member variable is accessed via the method get_events() if your class derives from Sys.Component. 2. Create public methods to add and remove an event handler.

    function add_amountChanged(handler) {
        this._events.addHandler("amountChanged", handler);
    }
    function remove_amountChanged(handler) {
        this._events.removeHandler("amountChanged", handler);
    }

These handlers should expect two parameters – “sender” and “args”.

3. Optionally create a class derived from Sys.EventArgs to carry any data payload.

    MyNamespace.AmountChangedEventArgs = 
        function AmountChangedEventArgs(amount) {

MyNamespace.AmountChangedEventArgs.initializeBase(this); this._amount = amount; }

MyNamespace.AmountChangedEventArgs.prototype = { get_amount: function get_amount () { return this._amount; } }

MyNamespace.AmountChangedEventArgs.registerClass( 'MyNamespace.AmountChangedEventArgs', Sys.EventArgs);

4. Create a private method (prefixed by an underscore as per convention, see section 2.4.1) to raise each event. Use the raise terminology for events rather than fire or trigger. Allow the EventArgs derived class to be passed as a parameter, if required.

    function _raiseAmountChanged(args) {
        if (!this._events) return;
        var handler = this._events.getHandler("amountChanged");
        if (handler) {
            handler(this, args);
        }
    }

5. Call the raise method when the event should be fired.
    if (someCondition){
        this._raiseAmountChanged(
            new MyNamespace.AmountChangedEventArgs(someParameter));
    }

Use a derived class of Sys.EventArgs as the event argument if the event needs to send data to the event handler, or Sys.EventArgs.Empty if none is required.

Do not pass null as the sender or EventArgs parameter. By default use this and EventArgs.Empty respectively.

Be prepared for arbitrary code executing in the event-handling method.

4.4.7.2 Sys.Observer Events

To implement events using Sys.Observer there is significantly less work involved;

1. Create public methods to add and remove an event handler.
    function add_amountChanged(handler) {
        Sys.Observer.addEventHandler(
            this, "amountChanged", handler);
    }
    function remove_amountChanged(handler) {
        Sys.Observer.removeEventHandler(
            this, "amountChanged", handler);
    }

These handlers should expect two parameters – “sender” and “args”.

2. Optionally create a class derived from Sys.EventArgs to carry any data payload, as described above.

3. When the class is disposed, ensure that all event handlers are removed;

    dispose: function() {
        Sys.Observer.clearEventHandlers(this);
    }

4. To raise an event, use the following syntax (passing in the optional EventArgs instance);

Sys.Observer.raiseEvent(this, “foo”, args);

4.4.8 Event Handler Design

Define event handlers to expect two parameters;

    function _handleAmountChanged(sender, args) {
        // code removed
    }

When attaching a handler to an event from within a class instance, you may use the createDelegate method to ensure that the scope of the this keyword is as expected;

    var handler = Function.createDelegate(this,
                      this._handleAmountChanged);
    component.add_amountChanged(handler);

This ensures that “this” refers to the current class instance in the handleAmountChanged method.

It is recommended that event handlers are detached when no longer required.

4.4.9 Field Design

Prefix instance fields intended to be private with an underscore (see section 2.4.1). For simple data items that do not require logic to be executed during information retrieval or updating (e.g. notifying of data changes) use fields to keep download size small (see section 4.4.4). For more complex data items create properties that provide access to private data. This enables the implementation to be changed without affecting calling code.

4.5 Extensibility

Designing for extensibility leads to more flexible, reusable components, but can also lead to unnecessary overheads and complexity that are more obvious in JavaScript than other platforms. Therefore raw performance and simplicity must be balanced against extensibility and reuse.

When designing a library or reusable control, enhance the priority of extensibility. The primary mechanisms for extensibility in the Ajax Control Toolkit are;

  • Events; define events that other components can subscribe and react to.
  • Base classes; define base classes to provide partial implementations, to simplify customization of behavior.

Interfaces; use interfaces to indicate commonality between components, when base classes may already be chosen or there is no shared behavior.

4.6 Accessibility

An “accessible” web site means ensuring that it can be used by accessibility tools (such as Screen Readers), that it is device independent, and that it makes minimal assumptions about the users setup. This encompasses many topics, which are covered by the W3C Web Accessibility Initiative . The ARIA specification is the latest authority on writing accessible internet applications .

There are a number of approaches that can assist with creating an accessible web site.

1. If your site must function without JavaScript, consider using Progressive Enhancement and Unobtrusive JavaScript. See section 4.2 for more information.

2. Ensure that event handlers do not assume the presence of a mouse. Consider using corresponding keyboard events in partnership with mouse events.

3. Provide non-script based alternatives for functionality. This can range from plain HTML downloads to telephone based services.

4. Test with Accessibility tools to ensure that functionality works well with them. Further information is available from the W3C and WebAIM .

4.7 Localization

Messages, image locations, and other content displayed to a user via script may need to be localized as with any other web content.

Localized content should be placed in a separate script, named with a culture embedded in the filename. For example;

  • MyScript.js
  • MyScript.en-US.js
  • MyScript.en-GB.js
  • MyScript.es-CO.js

Further information on localization is available online .

4.8 Exceptions

Script errors should be defined using a function that uses the Error.create method to create an error instance, as follows;

    MyNamespace.Errors.accountClosed = 
        function accountClosed(sourceId) {

var displayMessage = "MyNamespace.AccountClosedException: " + MyNamespace.Res.accountClosed; var err = Error.create(displayMessage, { name: "MyNamespace.AccountClosedException", source: sourceId }); err.popStackFrame(); return err; }

When an AccountClosedException is required, this method may then be called as follows:

    throw MyNamespace.Errors.accountClosed(this._id);

This implementation demonstrates;

  • Ensuring Exceptions are created consistently using a function.
  • Storing Exception messages using a localizable constant;

    MyNamespace.Res = {
        accountClosed: 'The account is already closed'
    }

  • Passing the Exception’s type name as a “name” parameter to Error.create;

    var err = Error.create(displayMessage, {
        name: "MyNamespace.AccountClosedException", 
        source: sourceId
    });

  • Using a call to popStackFrame to ensure the error creation function is not included in the reported stack.
  • Optionally defining parameters specific to the Exception type. In this example, “sourceId” is specific to the AccountClosedException.