Abstraction layer between JavaScript logic code and the actual HTML/jQuery runtime so that you can TDD your JavaScript with ease!
JavaScript
Latest commit 6b4889d May 6, 2011 @JonKruger more readme updates
Permalink
Failed to load latest commit information.
jQueryView adding/removing/loading lists of items May 4, 2011
.gitignore removed .idea folder Mar 10, 2010
README.markdown more readme updates May 6, 2011

README.markdown

JSView

JSView is an abstraction layer that sits between your JavaScript code and jQuery/HTML so that you can write tests for your JavaScript code without having the actual page and the DOM available in a test. Instead of calling jQuery methods directly, you will call methods that will interact with jQuery in your app, but will interact with a fake replacement for jQuery in your tests. Not only that, JSView will generate methods for you so that you can easily access values and events in a readable fashion. This will allow you to truly TDD your object-oriented JavaScript before the actual page even exists!

A Simple Example

I am going to have an HTML page that will have three textboxes and an Add button. When the Add button is clicked, the value in the first two textboxes will be added together and the result will be placed in the third textbox.

First, let's write some tests! (I'm using QUnit, but you can use any test framework that you would like.)

module('When the Add button is clicked',
{
	setup: function()
	{
		calculator = new Calculator(null, new ViewTestDouble());
		calculator.setFirstValue(2);
		calculator.setSecondValue(3.5);
		calculator.clickAddButton();
	}
});

	test('Then it should add the values in the first two textboxes and put the result in the third textbox', function()
{
	equals(calculator.getResult(), 5.5);
});

Notice how readable these tests are, and that I have methods like getResult() and setFirstValue() and clickAddButton(). The ViewTestDouble class is our fake version of jQuery and the DOM which has limited functionality and will fire certain events when certain methods are called.

Here's the implementation code:

function Calculator(element, view)
{
	if (view == null)
		view = new jQueryView('Calculator', element);

	registerObjectProperties(this, view, ['FirstValue', 'SecondValue', 'Result', 'AddButton']);

	this.whenAddButtonIsClicked(function()
	{
		this.addValues();
	});

	this.addValues = function()
	{
		this.setResult(Number(this.getFirstValue()) + Number(this.getSecondValue()));
	}
}

When you call registerObjectProperties, the third parameter is an array of names of fields on your screen. When you register a field (for example, an input field named Amount), you get the following methods on your object:

  • getAmount
  • setAmount
  • whenAmountChanges
  • whenAmountClicked
  • whenAmountIsClicked
  • whenAmountGainsFocus
  • whenAmountLosesFocus
  • whenKeyIsPressedInAmount
  • whenKeyDownInAmount
  • showAmount
  • hideAmount
  • clickAmount
  • pressKeyInAmount
  • keyDownInAmount
  • enableAmount
  • disableAmount

All of the "when" methods are hooked up to the appropriate jQuery events, depending on the type of the HTML element that is actually being wired up. The "get" and "set" methods will call text() or val() depending on the type of the HTML element (I could never remember whether to call text() or val() before this!).

Now that we've written our tests and made them pass, we have to wire it up in HTML. We will use the "class" attribute to help link things up (this way you don't have to worry about other frameworks that want to use the "id" attribute for various reasons).

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
	<head>
		<title>Example</title>
		<script type="text/javascript" src="../testobjects/jquery-1.3.2.min.js"></script>
		<script type="text/javascript" src="../testobjects/qunit.js"></script>
		<script type="text/javascript" src="../scripts/jQueryView.js"></script>
		<script type="text/javascript" src="../scripts/ViewTestDouble.js"></script>
		<script type="text/javascript" src="../testobjects/Calculator.js"></script>
		<script type="text/javascript" src="../testobjects/CalculatorTests.js"></script>
		<link rel="Stylesheet" href="../testobjects/QUnit.css" />
	
		<script language="javascript">
			$(document).ready(function()
			{
				calculator = new Calculator($('#Example'));
			});
		</script>
	</head>
	<body>
		<h1 id="qunit-header">Example</h1>
		<h2 id="qunit-banner"></h2>
		<h2 id="qunit-userAgent"></h2>
		<ol id="qunit-tests"></ol>

		<div id="Example">
			<table>
				<tr>
					<td>First value:</td>
					<td><input type="textbox" class="Calculator-FirstValue" /></td>
				</tr>
				<tr>
					<td>Second value:</td>
					<td><input type="textbox" class="Calculator-SecondValue" /></td>
				</tr>
				<tr>
					<td>Result:</td>
					<td><input type="textbox" class="Calculator-Result" /></td>
				</tr>
			</table>
			<input type="button" class="Calculator-AddButton" value="Add" />
		</div>
	</body>
</html>

Notice the class attributes on the elements. The format is "<object friendly name>-<field name>". The "object friendly name" must match the first parameter to the jQueryView constructor in our JavaScript object. This doesn't necessarily have to be the same as the class name, although I'm finding that it makes sense to do that.

Dealing with lists

Sometimes you want have lists of items, and in your tests you want to be able to do things like add items to the array and that sort of thing. We can handle this too thanks to jQuery templates and some more conventions.

Let's say we have some classes that look like this:

function ListObject(element, view)
{
	if (view == null)
		view = new jQueryView("ListObject", element);

	registerObjectProperties(this, view, ["Name"]);
}

function List(element, view)
{
	if (view == null)
		view = new jQueryView("List", element);

	// 3rd parameter = the name of the list element (so the CSS class will be List-ListItems)
	// 4th parameter = the class name of the list item
	registerList(this, view, "ListItems", ListObject);
}

Here in our List class, notice that we call registerList() instead of registerObjectProperties(). This tells us the type of our list items as well as how to create them.

When we call registerList() here, the following methods are added to the List class:

  • appendToListItems
  • prependToListItems
  • getListItems

(Yeah, someday you probably will want "insertIntoListItems" or "removeListItem". I'm planning on adding that at some point, or maybe that's where you come in!)

I can now do the following:

list = new List($('#Main'));
var obj = new ListObject(null, new UnboundView());
obj.setName("object 1");
list.appendToListItems(obj1);

Notice that I passed on UnboundView() to the ListObject. That's because I want to be able to assign values to the ListObject, but I don't have an actual jQueryView that I can use because the HTML hasn't been created yet.

When I call appendToListItems(), it will automatically create the HTML for the list item and create a new ListObject that is tied to the HTML view.

Remember I said that we needed jQuery templates for this... here's how that works.

On your page, you have to define a template, which looks like this:

<script language="javascript">
	$(document).ready(function()
	{
		$.template("ListItems", "<div class='ListItems-Object'><b>Name: </b><span class='ListObject-Name'>${Name}</span></div>");
	});
</script>

There are some important points to mention:

  • The name of the template must match the name of the list class (not the name of the list item class), so in this case, "ListItems"
  • The outermost element of the template must has the "ListItems-Object" class, where ListItems is the name of the list class
  • Elements inside the outermost template follow the normal class convention (e.g. ListObject-Name is the Name element for the ListObject class)
  • The template tokens (e.g. ${Name}) must match the name of the property that you would like to be mapped to the token

What's Next

This project is called JSView, but the class I've been referencing is jQueryView. I did this intentionally to leave it open for someone to implement a PrototypeView, YUIView, or whatever other JavaScript framework you're using.

Did you find something I missed, or is there a better way to do something I've done? Please feel free to contribute!