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 () { });
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)
{
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:
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:
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:
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.
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:
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).