Click here to Skip to main content
15,868,016 members
Articles / Web Development / HTML

JavaScript UI Control Suite using TypeScript

Rate me:
Please Sign up or sign in to vote.
3.20/5 (4 votes)
7 Dec 2014CPOL12 min read 40.2K   387   32   2
A usable suite of JavaScript UI controls written with TypeScript.

Introduction

Attached is a 'suite' of JavaScript UI controls that I developed. The documentation, including how to use them, for each control can be found in each of the .ts files. Currently, there are four controls: a listbox, a checkbox list, a calendar, and a grid. All of them support some base functionality such as a popup option, scroll bars, and writing the selection(s) to an 'output' HTML element. In addition, all of these controls work in three major browsers: Internet Explorer 8 and above, Firefox, and Chrome.

The primary goal of these controls is to make client-side development easier by shielding the developer from the intricacies of HTML and the compatibility issues between browsers. As it turned out, it also became my first foray into TypeScript and when I finished I thought, since TypeScript is so new, my experience with this project may help other developers new to TypeScript.

I have included both the TypeScript code as well as the corresponding JavaScript files built upon compilation.

Background

A few months ago, I started developing this project entirely in pure JavaScript (i.e., no frameworks or libraries like jQuery). I was rolling along just fine when all of a sudden my world was turned upside down. It was at this time that I opened my email box and discovered that Microsoft had just released a new 'language' called TypeScript.

Whatever you want to call it and whether you would consider it to be new, I knew immediately that my life had just gotten easier.

Normally, when a new language that looks promising comes along, I put on my decision hat and analyze whether it will be worth my time to dig into it. Among the factors that influence this decision include how easy it is to use, how dynamic it is (meaning, of course, a dynamic runtime), what development tools are available (for type-checking, debugging, etc.), and whether it has the potential to become popular and stay around for a while. Fortunately, TypeScript passes all of these requirements, as described below:

  • Since the primary goal of TypeScript is to make application-scale development with JavaScript easier and I already knew JavaScript, that was sufficient enough reason to believe TypeScript would be easier to use.
  • JavaScript is a dynamic language at heart and since, per Microsoft, any JavaScript code is considered to be TypeScript code (i.e., it can be compiled along with the new TypeScript constructs) it was obvious that I could make TypeScript code as dynamic as necessary.
  • Being a Microsoft product, there was a good chance that the necessary development tools would be available (even though your wallet may be a bit lighter at the end of the day). Sure enough, on the day TypeScript was made available to the public, I could download a TypeScript plug-in for Visual Studio that provides IntelliSense, code navigation, static error messages, and refactoring. (Of course, this requires that you DO have Visual Studio and since I do and my main concern is ME, the requirement passed.)
  • Finally, and perhaps most important if you have a manager that has a clue, the question becomes about longevity. i.e., if I decide to retire at 43 can my boss find another TypeScript developer? While it is impossible to predict the future (and possible that I don't know what I'm talking about) it is evident that JavaScript is becoming increasingly popular on top of its already stellar popularity. Consider that JavaScript is one of the standard languages for development in Windows 8 and that JavaScript is starting to become recognized as a valuable language for server-side development (e.g. Node.js).

For me, the decision was easy and straightforward. Even my Microsoft-hating boss would have a hard time rebutting this logic.

So, now that I am almost two months into developing with TypeScript and have found it to be as good as I had hoped I thought I would share some of my experiences and what I feel are the most beneficial aspects of this new language.

It is important to realize that since TypeScript is just a facade, so-to-speak, over JavaScript and that the compiled output of TypeScript is just runnable JavaScript, any TypeScript functionality described here can be written in pure JavaScript. However, unless you are an expert-level JavaScript programmer, it would be much more difficult to write much of this functionality in pure JavaScript. This is particularly true for developers already familiar with almost any mainstream language like C#, Java, and PHP because TypeScript has, for example, object-oriented constructs that are similar to those languages. This is really the point... make JavaScript easier to code!

Using the Code

The documentation, including how to use them, for each control can be found in each of the .ts files. Here is an example of using one of the controls but they are all similar.

var cbList = new Zenith.CheckBoxList('baseElement');
 
cbList.NumColumns = 2;
cbList.ColumnSpace = 15;
cbList.MaximumHeight = 100;
cbList.PopUpControlId = 'testPopup';
cbList.PopUpPosition = 'right';
cbList.PopUpDirection = 'down';
cbList.OutputElementId = 'output1';
cbList.addZenithEventListener(Zenith.ZenithEvent.EventType.Selected, 
  function (value, text, checked) { alert(text + ' ' + (checked ? 'selected' : 'unselected')); });
cbList.addZenithEventListener(Zenith.ZenithEvent.EventType.Close, function () { });
 
// There are multiple ways to add the data to this control.  This is the simplest way:

cbList.AddItem(1, "Blue");
cbList.AddItem(2, "Yellow");
cbList.AddItem(3, "Red");
cbList.AddItem(4, "Green");
cbList.AddItem(5, "Turqoise");
cbList.AddItem(6, "Orange");
cbList.AddItem(7, "Black");
cbList.AddItem(8, "White");
cbList.AddItem(9, "Aqua");
cbList.AddItem(10, "Gray");
cbList.AddItem(11, "Purple");
cbList.Build();

How It Works

Here, we will analyze just one of the controls, the CheckBoxList control. Each of the other controls were built similarly.

All of the controls derive (inherit) from a base class named ControlBase which handles most of the logic that is common among them such as the popup logic, adding the scroll bar, executing common events, and some 'protected' methods needed by the derived classes.

This is what the class definition looks like:

export class CheckBoxList extends Zenith.ControlBase

'Export' essentially indicates that the class should be made available outside of the enclosing module and 'Zenith.ControlBase' references the ControlBase class in the 'Zenith' module. Note that all of the controls are within the 'Zenith' module even though each control is in its own file.

Each control class constructor accepts the id of an HTML DIV element and this element is used as the 'base' element of the control, meaning that all of the UI elements that make up the control are inside this DIV element. Each of these constructors calls the constructor of the base class and it is here where the element is retrieved from the document and assigned to a class attribute and a border is created around this DIV element.

The following is the base class constructor implementation:

constructor (baseDivElementId: string)
{
    if (baseDivElementId.length <= 0)
        throw Error("The id of a 'div' HTML element must be passed to the Zenith control when 
            creating.");

    this.BaseElement = document.getElementById(baseDivElementId);

    if (!this.BaseElement)
        throw Error("The id of the 'div' HTML element passed in is not valid.");

    if (!(this.BaseElement instanceof HTMLDivElement))
        throw Error("The element associated with the Zenith control must be a div element.");
   
    this.BaseElement.style.borderColor = '#B6B8BA';
    this.BaseElement.style.borderWidth = '1px';
    this.BaseElement.style.borderStyle = 'solid';
}

Each control also has a Build method that should be called after the control is constructed and the appropriate attributes have been set on the created object (see the example above). The Build method, possibly with other private methods, will actually create and position all of the elements that makeup the control. The placement of the elements is handled with an HTML table element; i.e. the 'parent' element under the 'base' element (DIV) is a <table> element. Below is the implementation of the Build method for the CheckBoxList control:

public Build(): void
{
    if (this.ItemList.Count() <= 0)
        throw new Error("The item list is empty.");

    this.Clear();

    var table: HTMLTableElement = <HTMLTableElement>document.createElement('table');
    this.BaseElement.appendChild(table);
    table.className = 'ZenithCheckBoxTable';

    var tbody: HTMLElement = document.createElement('tbody');
    table.appendChild(tbody);

    var trow: HTMLTableRowElement, tcell: HTMLTableCellElement;
    var colIndex: number = 0;

    for (var index = 0; index < this.ItemList.Count(); index++)
    {
        if (!trow || colIndex >= this.NumColumns)
        {
            trow = <HTMLTableRowElement>document.createElement('tr');
            tbody.appendChild(trow);
            colIndex = 0;
        }

        tcell = <HTMLTableCellElement>document.createElement('td');
        trow.appendChild(tcell);
        if (colIndex > 0)
            tcell.style.paddingLeft = this.ColumnSpace + "px";

        this.addEventListener(tcell, 'click', (event) => { this.selectedEventHandler(event); });

        var itemCheckbox: HTMLInputElement = <HTMLInputElement>document.createElement('input');
        itemCheckbox.type = 'checkbox';
        itemCheckbox.name = 'ZenithControlCheckBox';
        itemCheckbox.value = this.ItemList.ElementAt(index).Value;
        itemCheckbox.id = 'chk_' + this.ItemList.ElementAt(index).Value;
        tcell.appendChild(itemCheckbox);

        var label:HTMLLabelElement = <HTMLLabelElement>document.createElement('label');
        label.htmlFor = 'chk_' + this.ItemList.ElementAt(index).Value;
        label.className = 'ZenithCheckBoxLabel_Unselected';
        label.textContent = this.ItemList.ElementAt(index).Text;
        label.innerHTML = this.ItemList.ElementAt(index).Text;
        tcell.appendChild(label);

        colIndex++;
    }

    this.ParentElement = table;

    if (this.IsPopup())
        super.SetPopup();

    super.Build();
} 

Notice that after the UI is built, we check whether this control should be a popup control and, if so, call the SetPopup method in the base class. These two lines need to be included in the Build method of each control in order to provide popup functionality. The SetPopup method includes all the logic needed to handle the popup functionality such as placement of the control relative to the assigned 'popup' control and event handling to 'open' (display) and 'close' (hide) the control. The control is displayed when the associated popup control is 'clicked' and is close on any of the following events: the mouse moves out of the control client area, the user presses a mouse button when outside of the control client area, or the 'ctrl' key is pressed.

Event handling is where the arrow function construct of TypeScript really comes in handy. Look at the following code in the SetPopup method:

this.addEventListener(this.BaseElement, 'mouseout', (event) =>
{
    var mouseEvent: MouseEvent = <MouseEvent>event;
    var targetElement: HTMLElement = <HTMLElement>mouseEvent.toElement;
    if (!targetElement)
        targetElement = <HTMLElement>document.elementFromPoint(mouseEvent.clientX, 
            mouseEvent.clientY);
    if (targetElement)
    {
        // The onmouseout event will happen event when leaving an element inside the element 
        // the event is tied to.
        while (targetElement && targetElement != this.BaseElement)
            targetElement = targetElement.parentElement;
        if (targetElement != this.BaseElement)
            this.Close();
    }
}); 

By using an arrow function as the listener for the mouseout event, we can call the Close method of the class that this code is inside of, using the 'this' keyword, in order to hide this control.

The primary purpose of the Build method in the base class is to set the size of the base DIV element to the size of the parent TABLE element otherwise the width of the DIV element will expand to the width of the page client width. This is why the Build base class method needs to be called at the end of the Build method of the derived class, so that the UI has been built before resizing the outer DIV element.

Each control also has its own custom events and custom event handling. The only custom events currently
supported are 'Selected' and 'Close' and can be found in the ZenithEvent class. Here is the full implementation of the ZenithEvent class:

export class ZenithEvent
{
    public static EventType = { Selected: 1, Close: 2 };

    public eventType: number;
    public listener: Function;

    constructor (eventType: number, listener: Function)
    {
        this.eventType = eventType; this.listener = listener;
    }
} 

The EventType object literal acts as a kind of enum so that the type of event can be identified with pre-defined
text as in the following code which is used to add a function handler to the 'cbList' CheckBoxList:

cbList.addZenithEventListener(Zenith.ZenithEvent.EventType.Selected, 
    function (value, text, checked)
    { 
        alert(text + ' ' + (checked ? 'selected' : 'unselected')); 
    }
);  

The 'Selected' custom event supports a different set of listener parameters depending on the control. For example, for the CheckBoxList control like in the example above three parameters are passed to the listener function: the value of the selected item, the text of the selected item, and whether the item was checked or unchecked. The 'Close' event does not pass any parameters to the associate listener for all of the controls.

A few other goodies are worth noting.

The Calendar control uses a custom DateHelper class that can be found in the Calendar code file ZenithCalendar.ts. This small class provides some of the 'missing' features of the JavaScript Date type such as long and short day of week and month names and a function that returns the number of days in any month. The entire code for this is below.

class DateHelper
{
    public static MonthNames: string[] = ['January', 'February', 'March', 'April', 'May', 
        'June', 'July', 'August', 'September', 'October', 'November', 'December'];
    public static MonthShortNames: string[] = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    public static DayOfWeekNames: string[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 
        'Thursday', 'Friday', 'Saturday'];
    public static DayOfWeekShortNames: string[] = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
    
    public static DaysInMonth(iMonth, iYear): number
    {
        return 32 - new Date(iYear, iMonth, 32).getDate();
    }

    public static toShortDate(date: Date): string
    {
        return date.getFullYear().toString() + '-' + (date.getMonth() + 1).toString() + '-' +
            date.getDate().toString();
    }

    public static toLongDate(date: Date): string
    {
        return MonthNames[date.getMonth()] + ' ' + date.getDate().toString() + ', ' + 
            date.getFullYear().toString();
    }

    public static toShortDisplayDate(date: Date): string
    {
        return (date.getMonth() + 1).toString() + '/' + date.getDate().toString() + '/' + 
            date.getFullYear().toString();
    }
} 

The CheckBoxList class uses a custom class called List which can be found in the ZenithList.ts code file. This is a class I created in JavaScript earlier this year and ported to TypeScript. Since the CheckBoxList control doesn't really use much of the functionality available in this class, I won't go into detail about it here other than to say that the intent of it was to mimic the .NET List collection class with some LINQ-like functionality built-in.

Points of Interest

The following topics are not necessarily in any order; however, they are generally in the order of what I feel are the most important benefits (starting at the top) even though this is a partially subjective topic. Also, this is obviously not intended to be a full-scale tutorial or reference on TypeScript.

Namespaces

Even though the word 'namespace' is barely used in the TypeScript specification, the 'module' construct in TypeScript essentially provides this ability.

Example:

C#
module Zenith 
{ 
     export class ListBox extends Zenith.ControlBase 
     { 
         public NumColumns: number = 1; 
     } 
}

In this example, the class ListBox is not part of the global namespace but is a type in the Zenith namespace. So, referencing this class outside of this module requires 'Zenith' to be prepended to the type as in the following:

C#
var lstList = new Zenith.ListBox('baseElement'); 

Casting

In order to provide complete type-checking and IntelliSense, a language must have some form of type casting. Indeed TypeScript does provide this using the <> deliminators as in the following:

C#
var table: HTMLTableElement = <HTMLTableElement>document.createElement('table'); 

Now, of course, IntelliSense can provide the available set of attributes and functions for an HTML table.

Referencing Other Files

Another facet of a language that is necessary to provide type-checking and IntelliSense is a way to 'include' or 'reference' another file containing the types, variables, methods, etc. used by your code. This is provided with the 'reference' declaration.

C#
///<reference path='ZenithControlBase.ts'/> 

OOP

At this point, it is probably a good idea to reiterate the disclaimer made above in relation to object-orientation: any OOP functionality described here can be written in pure JavaScript. However, it is here where most developers (not JavaScript experts) get confused, and with good reason, as the way JavaScript provides object-oriented functionality is so unlike any other language.

Example:

C#
class ListBox extends Zenith.ControlBase 
{ 
     static x: string = 'Test'; 
     public NumColumns: number = 1; 
     public ColumnSpace: number = 10; 
     private ItemList = new Zenith.List(); 
     constructor (baseDivElementId: string) 
     { 
           super(baseDivElementId); 
     } 
}

Most developers familiar with OOP should be able to easily understand this code and the OOP constructs in it. The 'class' keyword identifies a class, 'extends' provides inheritance, 'constructor' identifies the constructor, 'super' references the base class, 'static' provides static class members, and the 'public' and 'private' keywords provide encapsulation.

A few things are worth nothing here:

  • Only one constructor is permitted.
  • There is no way to 'protect' members of a base class. This is one thing I truly miss but I would imagine this will be the first thing implemented once version 6 of the ECMAScript language specification is ratified.

Function Scope

One very handy feature of TypeScript involves function scope and the 'this' keyword. Normal function declarations that are part of a class behave as expected where the 'this' keyword references the object instance of the class. More unexpectedly, TypeScript provides what is termed 'arrow' functions where the meaning of 'this' is changed to reference the enclosing script. You may be asking yourself how is this different than using 'this' in a normal class function. The answer becomes clear when using callback functions in a class; for example, when declaring a callback function for an event. Take this example from within a class function.

In JavaScript, the second 'this' would reference the HTML element that the event was assigned to, in this case 'tcell', and there would be no easy way to get a reference to the object instance, which is a more valuable reference when using OOP (the event object itself has a reference to the source HTML element).

History

  • Initial version uploaded - 11/26/2012
  • Added 'How It Works' section - 11/30/2012
  • New Controls - 02/11/2013

I have added three new controls to this suite:

  • Date Selector - allows the user to pick a date using month, day, and year drop down lists
  • Menu - a (horizontal only for now) hierarchical menu with unlimited levels
  • Context Menu - a hierarchical context menu with unlimited levels that is initiated with a right mouse button click

Both menu controls use the same menu class with the default being a normal menu but you can tell it to be a context menu using the second parameter of the constructor.

Just like all of the other controls in this suite, these new ones inherit from the same base class and are used similarly to all the other controls (instantiate, set appropriate properties, and call the Build method).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
For over 25 years I have worked in the Information Systems field as both a full-time employee and an independent contractor for a variety of companies.

I have extensive professional experience with numerous programming languages and technologies including C#, JavaScript, SQL, VB.NET, and ASP.NET as well as a working knowledge of a great variety of others. I also have an advanced understanding of the concepts behind these technologies including Object-Oriented Programming, Relational Data, Functional Programming, MVC and MVVM.

Some of my more recent work has been designing and building web applications primarily with JavaScript in conjunction with many of the JavaScript libraries/frameworks including jQuery, KnockoutJS and Bootstrap and consuming both JSON and REST services.

In nearly all of the work I have been involved with in the past ten years I have played a lead role in the design as well as the development of the work. More recently I have managed a team of software developers at a local mid-size company.

Comments and Discussions

 
QuestionCouldn't find a download link for this suite Pin
michaelprabhu21-Feb-16 18:20
michaelprabhu21-Feb-16 18:20 
QuestionGood one Pin
Moim Hossain6-Dec-12 3:48
Moim Hossain6-Dec-12 3:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.