Client JavaScript Components

This section discusses the creation of a client-side JavaScript component and matching rendering/synchronization peer. To walk through this process, we'll create an example component, a "SpinButton". Our SpinButton implementation will consist on an integer-entry text field, with increment and decrement buttons that will adjust its value.

Note: Even if you're only interested in creating server side components that render custom HTML and JavaScript, you will still want to read this section. Creating a server-side component with a custom renderer requires creating a client-side component and synchronization peer. Developing the server-side wrapper for the component will be discussed later.

To create a new client-side component, we'll need to create two classes, an Echo.Component-derivative representing the component itself, and an Echo.Render.ComponentSync implementation to render it.

Namespaces: As is a best practice in client-side JavaScript programming, we'll want to place our spin button implementation to reside in a namespace. For this tutorial we'll call that namespace "Example". The following line of code will define this namespace.

Example = { };

Creating the Component Object

To create a new client-side Echo component, the first step is to create the object which extends the component base class, i.e., Echo.Component. This step is fairly trivial in most cases:

Example.SpinButton = Core.extend(Echo.Component, {

    $load: function() {
        Echo.ComponentFactory.registerType("Example.SpinButton", this);
    },

    componentType: "Example.SpinButton"
});

The componentType property is abstract in Echo.Component and thus must be defined in all component implementations. The property is a string which represents a unique name for the component. Generally speaking, the property should be equivalent to the fully-qualified object name, as shown in the example above.

Creating the Synchronization Peer

A Synchronization Peer is used to render the state of the component to the DOM and process raw user input. Synchronization peers are derived from the Echo.Render.ComponentSync base class. An implementation of this class will need to be created for each Echo.Component you develop. The peer will be registered with the rendering engine, and every component instance that needs to be rendered will be provided with a new instance of the custom peer. The peer instance will exist for the duration that the component is displayed on-screen, and will automatically be destroyed when it is removed.

Provided Properties: Whenever an Echo.Render.ComponentSync is instantiated the following properties are automatically set:

  • component: a reference to the Echo.Component which it supports.
  • client: a reference to the EchoClient that is rendering the component. The client may be an EchoFreeClient in the case of a stand-alone client application, an EchoRemoteClient in the case of a server-hosted application, or any other derivative of the EchoClient class.

Required Methods: The abstract Echo.Render.ComponentSync class has three methods which must be implemented by the developer. The first parameter, update, provided to each of these methods is the Echo.Update.ComponentUpdate that triggered the update. Note that in the case of methods other than renderUpdate, the provided update likely will refer to an ancestor of the component being worked on.

  • renderAdd(update, parentElement): Invoked to render the component to the DOM. The parentElement parameter specifies the DOM element to which the component's rendered state should be added.
  • renderDispose(update): Invoked to inform the rendered component that it is being removed from the DOM, such that it may clean up any resources in use (e.g., unregistering event listeners). The component's rendered state will automatically be removed from the DOM, so only resource cleanup is performed here.
  • renderUpdate(update): Invoked to inform the rendered component that its state has changed, either by its properties being updated, or child components being added, removed, or having their layout data information updated. An Echo. Update. ComponentUpdate object is provided to describe what specific updates have occurred.

Below is a first go at a synchronize peer implementation for our "spin button." This is just to get the framework of the peer laid out. It'll run, but all it does is add an empty <DIV> element to the DOM. Though it doesn't yet do anything for the user, the following code is a good start to developing a component peer:

Example.SpinButton.Sync = Core.extend(Echo.Render.ComponentSync, { 

    $load: function() {
        Echo.Render.registerPeer("Example.SpinButton", this);
    },

    _div: null,

    renderAdd: function(update, parentElement) {
        this._div = document.createElement("div");
        this._div.id = this.component.renderId;
        parentElement.appendChild(this._div);
    },

    renderDispose: function(update) {
        this._div = null;
    },

    renderUpdate: function(update) {
        var element = this._div;
        var containerElement = element.parentNode;
        Echo.Render.renderComponentDispose(update, update.parent);
        containerElement.removeChild(element);
        this.renderAdd(update, containerElement);
        return true;
    }
});

The following breaks down the above code to describe what each property of our synchronize peer definition is doing:

Static Initializer ($load): The initializer registers the synchronize peer with the Echo.Render object, such that it will know to render components with a componentType of Example.SpinButton with this synchronization peer.

_div: This variable will hold a reference to the main element of our spin button.

renderAdd(): Create a new <DIV> element which will (eventually) contain the text field and increment/decrement buttons that make up our spin button.

renderDispose(): Release the reference to the DOM element. Technically garbage collection should take care of this, but it's critical to forcibly null any references to DOM elements to eliminate any potential for client-side memory leaks in certain browsers, i.e., IE.

renderUpdate(): This method will update the rendered state of the component any time the component is changed. The above example is just about the simplest implementation possible: any time the component is updated, it will removed from the DOM and then re-rendered. For simple components like our spin button, this is not a bad strategy. For more advanced components that might contain children or large hierarchies of descendants, it can be quite inefficient. Better strategies for renderUpdate() will be discussed later, but for the moment, the above will suffice. The following describes each step of this simplest-form renderUpdate implementation:

  • var element = this._div;
    Create a local variable referencing the current element, as the instance variable this._div will soon be deleted.
  • var containerElement = element.parentNode;
    Create a local variable referencing the parent element. This is necessary because element will be removed from its parent in the near future, and we'll want to add the updated, re-rendered version of the component to this parent element.
  • Echo.Render.renderComponentDispose(update, update.parent);
    Invoking Echo.Render.renderComponentDispose() causes the renderDispose() method to be invoked on this synchronization peer as well as on the synchronization peers of any descendant components. This will allow each synchronization peer to dispose of any resources it required to support its rendered state (e.g., unregistering event listeners from the DOM). This step is very important as we are about to destroy our own DOM element and all its descendants. In the particular case of our example spin button, child components are not supported. It is nevertheless required that you invoke Echo.Render.renderComponentDispose() in all cases out of convention.
  • containerElement.removeChild(element);
    Remove the element from its container.
  • this.renderAdd(update, containerElement);
    Invoke our renderAdd() implementation to add a current version of the component back to the DOM beneath its container element.

Rendering the Component

renderAdd(): Our renderAdd implementation for the spin button will render a clickable less-than symbol ("<") to decrement the value, followed by a text field, followed by a clickable greater-than symbol (">") to allow incrementing. To do this, we can simply replace our renderAdd() implementation with the following:

    renderAdd: function(update, parentElement) {
        this._div = document.createElement("div");
        this._div.id = this.component.renderId;

        this._decSpan = document.createElement("span");
        this._decSpan.style.cursor = "pointer";
        this._decSpan.appendChild(document.createTextNode("<"));
        this._div.appendChild(this._decSpan);

        var value = this.component.get("value");
        this._input = document.createElement("input");
        this._input.type = "text";
        this._input.value = value == null ? "0" : parseInt(value);
        this._input.style.textAlign = "right";
        this._div.appendChild(this._input);

        this._incSpan = document.createElement("span");
        this._incSpan.appendChild(document.createTextNode(">"));
        this._incSpan.style.cursor = "pointer";
        this._div.appendChild(this._incSpan);

        parentElement.appendChild(this._div);
    },

We'll additionally want to dereference these new elements when the component is disposed of, so renderDispose() will be modified as well:

    renderDispose: function(update) {
        this._decSpan = null;
        this._input = null;
        this._incSpan = null;
        this._div = null;
    },

With these modifications, the component will appear properly on-screen. The rendered value of the text input field will be pulled from this.component, specificially retrieving the value of the value property. At this point though, our renderer will not actually write any data back to the underlying Echo.Component, nor will the increment or decrement buttons do anything.

Registering DOM Event Handlers

In order to make this component process user input, the peer will need to register some event handlers on the DOM. Specifically, the synchronize peer needs to be notified when the value of the text field changes, and when the increment or decrement buttons are pressed.

First, we'll need to create event handlers for increment-click, decrement-click, and text-field-change events. These event handlers can be added to our synchronization peer object definition:

    _processDecrement: function(e) {
        var value = parseInt(this._input.value);
        value--;
        this._input.value = isNaN(value) ? 0 : value;
        this.component.set("value", value);
    },
    
    _processIncrement: function(e) {
        var value = parseInt(this._input.value);
        value++;
        this._input.value = isNaN(value) ? 0 : value;
        this.component.set("value", value);
    },
    
    _processTextChange: function(e) {
        var value = parseInt(this._input.value);
        this._input.value = isNaN(value) ? 0 : value;
        this.component.set("value", value);
    },

Each of these event handlers receives an argument, e which represents the DOM event.

The renderAdd() method will then need to modified so that it registers the event handlers with their respective event-producing DOM elements. To register the event handlers, the Core.Web.Event object should be used. (See the Event Processor Documentation for more information on how this works and why it is used). The following lines of code are added to renderAdd():

        Core.Web.Event.add(this._decSpan, "click",
                Core.method(this, this._processDecrement), false);
        Core.Web.Event.add(this._incSpan, "click",
                Core.method(this, this._processIncrement), false);
        Core.Web.Event.add(this._input, "change",
                Core.method(this, this._processTextChange), false);

The event handlers will need to be unregistered when the component is disposed. To do this, the following code is added to renderDispose():

        Core.Web.Event.removeAll(this._decSpan);
        Core.Web.Event.removeAll(this._incSpan);
        Core.Web.Event.removeAll(this._input);

The safest way to remove event handlers is simply to invoke removeAll() on all elements for which events were registered. It's not any less efficient than removing each one specifically, and it eliminates the requirement of having to retain a reference to the "method" instances returned by Core.method() in order to remove them.

The Final Code

At this point the Example.SpinButtonSync object should read as follows:

Example.SpinButton.Sync = Core.extend(Echo.Render.ComponentSync, { 

    $load: function() {
        Echo.Render.registerPeer("Example.SpinButton", this);
    },

    _div: null,

    _processDecrement: function(e) {
        var value = parseInt(this._input.value);
        value--;
        this._input.value = isNaN(value) ? 0 : value;
        this.component.set("value", value);
    },
    
    _processIncrement: function(e) {
        var value = parseInt(this._input.value);
        value++;
        this._input.value = isNaN(value) ? 0 : value;
        this.component.set("value", value);
    },
    
    _processTextChange: function(e) {
        var value = parseInt(this._input.value);
        this._input.value = isNaN(value) ? 0 : value;
        this.component.set("value", value);
    },

    renderAdd: function(update, parentElement) {
        this._div = document.createElement("div");
        this._div.id = this.component.renderId;
    
        this._decSpan = document.createElement("span");
        this._decSpan.style.cursor = "pointer";
        this._decSpan.appendChild(document.createTextNode("<"));
        this._div.appendChild(this._decSpan);
        
        var value = this.component.get("value");
        this._input = document.createElement("input");
        this._input.type = "text";
        this._input.value = value == null ? "0" : parseInt(value);
        this._input.style.textAlign = "right";
        this._div.appendChild(this._input);
    
        this._incSpan = document.createElement("span");
        this._incSpan.appendChild(document.createTextNode(">"));
        this._incSpan.style.cursor = "pointer";
        this._div.appendChild(this._incSpan);
        
        Core.Web.Event.add(this._decSpan, "click",
                Core.method(this, this._processDecrement), false);
        Core.Web.Event.add(this._incSpan, "click",
                Core.method(this, this._processIncrement), false);
        Core.Web.Event.add(this._input, "change",
                Core.method(this, this._processTextChange), false);
    
        parentElement.appendChild(this._div);
    },

    renderDispose: function(update) {
        Core.Web.Event.removeAll(this._decSpan);
        Core.Web.Event.removeAll(this._incSpan);
        Core.Web.Event.removeAll(this._input);
        this._decSpan = null;
        this._input = null;
        this._incSpan = null;
        this._div = null;
    },

    renderUpdate: function(update) {
        var element = this._div;
        var containerElement = element.parentNode;
        Echo.Render.renderComponentDispose(update, update.parent);
        containerElement.removeChild(element);
        this.renderAdd(update, containerElement);
        return true;
    }
});