Raw Sugar Tutorial: Building a JavaScript table grid application

What you'll be working on

TaffyDB extracts the "hard part" of working with data in JavaScript. It provides methods to insert, update, remove, sort, and filter a data collection in much the same way you can with SQL. But how do you take advantage of this when building a data intensive application? How can you incorporate what TaffyDB does well to minimize and simplify your code?

In this tutorial you'll go step by step and build a simple data grid application that uses TaffyDB as the engine. The application will render a table and expose options to customize the table to your liking.
  1. Prerequisites
  2. A simple grid app (in under 25 lines)
  3. Adding column names
  4. Custom column names
  5. Add column sorting with TaffyDB
  6. Custom columns/record level interaction
  7. Imagine the possibilities
  8. Questions/Comments?

1. Prerequisites

For these examples you'll need just a couple of snippets of code:

2. A simple grid app (in under 25 lines)

Taking your fruit collection above, you can easily turn it into a table like this:


That grid was created by using a very simple grid "printer" app. The gridPrinter() function takes the TaffyDB fruit collection and adds a method to "print" the collection to a div on the page. Take a look at how it works:


var gridPrinter = function (config,taffyCollection) {
	var app = {
		// the print method to render the table
		print: function(){
			// get the column names from the first record in the TaffyDB Collection
			config.columns = TAFFY.getObjectKeys(this.first());
			var thebody = document.createElement("tbody");
			// loop over each record using TaffyDB's forEach, add new row
			this.forEach(function(r, c){
				var newRow = document.createElement("tr");
				// loop over each value in the record, add to row
				for (var x = 0; x < config.columns.length; x++) {
					var newCell = document.createElement("td");
					newCell.appendChild(document.createTextNode(r[config.columns[x]]));
					newRow.appendChild(newCell);
				}
				// add row to body of table
				thebody.appendChild(newRow);
			});
			// create table and append body
			var thetable = document.createElement("table");
			thetable.appendChild(thebody);
			thetable.border=1;
			
			// clear div and append new table
			document.getElementById(config.containerID).innerHTML = "";
			document.getElementById(config.containerID).appendChild(thetable);
		}
	}
	app = TAFFY.mergeObj(app,taffyCollection);
	return app;
}
Note: forEach is a TaffyDB method that loops over every record in the collection and applies a function. To learn more about how to use forEach and filter to a spesific record set click here.

Note: The TAFFY.getObjectKeys() is a TaffyDB utlity function. It gets the key names from a JavaScript object. There are a number of these functions which you can use within your applications. To see them all click here.

To call the gridPrinter() you use an init() function that is called onload:


var init = function () {
	// invoke your gridPrinter() and pass in the div ID and the new taffy collection
	fruit = gridPrinter({
		containerID: "displayGridDiv"
	},TAFFY(fruits));
	// call print
	fruit.print();
}

3. Adding column names

You want your data to be understandable and that requires column names. To add column names you'll need to modify gridPrinter() and add a new row at the beginning of your grid. Changes are prefixed with // #NEW# comment:

var gridPrinter = function (config,taffyCollection) {
	var app = {
		print: function(){
			var thebody = document.createElement("tbody");
			config.columns = TAFFY.getObjectKeys(this.first());
			var newRow = document.createElement("tr");
			// #NEW# loop over the column names and add cells to a new row with each name
			for (var x = 0; x < config.columns.length; x++) {
				var newCell = document.createElement("td");
				newCell.appendChild(document.createTextNode(config.columns[x]));
				newRow.appendChild(newCell);
			}
			// #NEW# add the column names to the first row of the grid
			thebody.appendChild(newRow);
			
			this.forEach(function(r, c){
				var newRow = document.createElement("tr");
				for (var x = 0; x < config.columns.length; x++) {
					var newCell = document.createElement("td");
					newCell.appendChild(document.createTextNode(r[config.columns[x]]));
					newRow.appendChild(newCell);
				}
				thebody.appendChild(newRow);
			});
			var thetable = document.createElement("table");
			thetable.appendChild(thebody);
			thetable.border=1;
			document.getElementById(config.containerID).innerHTML = "";
			document.getElementById(config.containerID).appendChild(thetable);
		}
	}
	app = TAFFY.mergeObj(app,taffyCollection);
	return app;
}
Here is your new grid:


You'll use the very same init() function as the last example: :


var init = function () {
	fruit = gridPrinter({
		containerID: "displayGridDiv"
	},TAFFY(fruits));
	fruit.print();
}

4. Custom column names

Having names is great, but in order to make them useful you really need to be able to control the order of the columns and apply custom names. Here is an output using custom names and the code examples for how it was done. You're going to start leaning more heavily on the config object defined in init() from here on out.

The new grid:


Notice that the food column is now called Fruit? That is done by defining a custom object with a name and and a display value within your columns array. The gridPrinter() takes that and uses the display value for the column name. Here is how your gridPrinter() has evolved:

var gridPrinter = function (config,taffyCollection) {
	var app = {
		print: function(){
			var thebody = document.createElement("tbody");
			
			// #NEW# use getObjectKeys only if config.columns wasn't defined
			config.columns = config.columns || TAFFY.getObjectKeys(this.first());
			var newRow = document.createElement("tr");
			for (var x = 0; x < config.columns.length; x++) {
				var newCell = document.createElement("td");
				// #NEW# use the display value if given in the columns config
				newCell.appendChild(document.createTextNode(
					TAFFY.isObject(config.columns[x]) ? 
					config.columns[x]["display"] : 
					config.columns[x]
				));
				newRow.appendChild(newCell);
			}
			thebody.appendChild(newRow);
			this.forEach(function(r, c){
				var newRow = document.createElement("tr");
				for (var x = 0; x < config.columns.length; x++) {
					var newCell = document.createElement("td");
					// #NEW# use the name value if given in the columns config
					newCell.appendChild(
						(TAFFY.isObject(config.columns[x]) && 
						!TAFFY.isUndefined(config.columns[x].name)) ? 
						document.createTextNode(r[config.columns[x].name]) : 
							TAFFY.isString(config.columns[x]) ? 
							document.createTextNode(r[config.columns[x]]) : 
							document.createTextNode("")
					);
					newRow.appendChild(newCell);
				}
				thebody.appendChild(newRow);
			});
			var thetable = document.createElement("table");
			thetable.appendChild(thebody);
			thetable.border=1;
			document.getElementById(config.containerID).innerHTML = "";
			document.getElementById(config.containerID).appendChild(thetable);
		}
	}
	app = TAFFY.mergeObj(app,taffyCollection);
	return app;
}
Note: JavaScript's || (sometimes called logical or) operator can be used to provide variable defaults. This is done with config.columns where we use config.columns if it is defined, else we get the column names from the TaffyDB collection.


Note: JavaScript's ternary operator is used often in these examples. This makes it easy to evaluate an expression and choose between two options based on if the expression is true or false. This is often a good alternative to an if statement. The format is: expression ? true_option : false_option.

This time you'll need to pass some more information from init(). For a custom column you need a name and a display value to identify which column to use and what name you want to display.

var init = function () {
	fruit = gridPrinter({
		containerID: "displayGridDiv",
		// #NEW# pass in a custom array of columns with one custom column name
		columns: [{
					name: "food",
					display: "Fruit"
				}, "water", "fiber", "fat", "protein", "sugar"]
	},TAFFY(fruits));
	fruit.print();
}

5. Add column sorting with TaffyDB

Augmenting the gridPrinter() app to add sorting is a breeze with TaffyDB. In this example you can sort by Fruit. Try clicking on it to sort descending and clicking again to sort ascending:


This example is uses the TaffyDB orderBy method as a shortcut to add sorting. To do this you add a new method to your app object inside of gridPrinter() called sortColumn(). This is called when the Fruit column is clicked.

	
var gridPrinter = function (config,taffyCollection) {
	var app = {
		sortColumn:function (col) {
				var s = {};
				// #NEW# check to see what was last sorted
				// #NEW# if nothing was sorted or this column was last sorted, sort desc
				if (((this.lastSort) ? this.lastSort == col : true)) {
					s[col] = "logicaldesc";
					// #NEW# call TaffyDB's orderBy
					this.orderBy([s]);
					this.lastSort = col + 'desc'
				} else {
					s[col] = "logical";
					// #NEW# call TaffyDB's orderBy
					this.orderBy([col]);
					this.lastSort = col;
				}
				this.print();
			},
		print: function(){
			var thebody = document.createElement("tbody");
			config.columns = config.columns || TAFFY.getObjectKeys(this.first());
			var newRow = document.createElement("tr");
			for (var x = 0; x < config.columns.length; x++) {
				var newCell = document.createElement("td");
				newCell.appendChild(document.createTextNode(
					TAFFY.isObject(config.columns[x]) ? 
					config.columns[x]["display"] : 
					config.columns[x]
				));
				// #NEW# if sortable is true make column sortable
				if (TAFFY.isObject(config.columns[x]) && 
					!TAFFY.isUndefined(config.columns[x].sortable) && 
					config.columns[x].sortable) {
						// #NEW# add the colName variable to the table cell
						newCell.colName = config.columns[x]["name"];
						newCell.onclick = function () {
							// #NEW# call app.sortColumn since the functions "this" 
							// #NEW# now points to the cell
							app.sortColumn(this.colName);
					}
			}
				newRow.appendChild(newCell);
			}
			
			thebody.appendChild(newRow);

			this.forEach(function(r, c){
				var newRow = document.createElement("tr");
				for (var x = 0; x < config.columns.length; x++) {
					var newCell = document.createElement("td");
					newCell.appendChild(
						(TAFFY.isObject(config.columns[x]) && 
						!TAFFY.isUndefined(config.columns[x].name)) ? 
						document.createTextNode(r[config.columns[x].name]) : 
							TAFFY.isString(config.columns[x]) ? 
							document.createTextNode(r[config.columns[x]]) : 
							document.createTextNode("")
					);
					newRow.appendChild(newCell);
				}
				thebody.appendChild(newRow);
			});
			var thetable = document.createElement("table");
			thetable.appendChild(thebody);
			thetable.border=1;
			document.getElementById(config.containerID).innerHTML = "";
			document.getElementById(config.containerID).appendChild(thetable);
		}
	}
	app = TAFFY.mergeObj(app,taffyCollection);
	return app;
}
Note: app.sortColumn() uses TaffyDB's logical sorting algorithm. There are other options, but this sorting approach produces the most human usable result with values containing letters and numbers sorted the way you would expect from using a desktop OS.


Note: When adding an onclick event within the gridPrinter() print method you'll need to use app."methodName()" to call a TaffyDB method. This is because the JavaScript "this" object points to the click event and not TaffyDB.


You'll now need to add a sortable:true flag to your Fruit column to identify it as a column you want sortable:

var init = function () {
	fruit = gridPrinter({
		containerID: "displayGridDiv",
		// #NEW# add sortable:true to your custom fruit column
		columns: [{
					name: "food",
					display: "Fruit",
					sortable: true
				}, "water", "fiber", "fat", "protein", "sugar"]
	},TAFFY(fruits));
	fruit.print();
}

6. Custom columns/record level interaction

With surprising ease you can add support for custom columns and record level interactions. Once you add support for special callme() methods within gridPrinter() it is easy to add new columns inside your init() function directly. This keeps gridPrinter() generic while giving you full control of your output.

In this example a row number column and a delete button has been added. Try clicking delete to remove a record. You can also resort by clicking on Fruit column.


To make this work you need to add support for callme() methods in the config columns array. These methods will be called each time a row is rendered and the results will be inserted into that row and column's cell.

Here is your new grid printer:

var gridPrinter = function (config,taffyCollection) {
	var app = {
		sortColumn:function (col) {
				var s = {};
				if (((this.lastSort) ? this.lastSort == col : true)) {
					s[col] = "logicaldesc";
					this.orderBy([s]);
					this.lastSort = col + 'desc'
				} else {
					s[col] = "logical";
					this.orderBy([col]);
					this.lastSort = col;
				}
				this.print();
			},
		print: function(){
			var thebody = document.createElement("tbody");
			config.columns = config.columns || TAFFY.getObjectKeys(this.first());
			var newRow = document.createElement("tr");
			for (var x = 0; x < config.columns.length; x++) {
				var newCell = document.createElement("td");
				newCell.appendChild(document.createTextNode(
					TAFFY.isObject(config.columns[x]) ? 
					config.columns[x]["display"] : 
					config.columns[x]
				));
				if (TAFFY.isObject(config.columns[x]) && 
					!TAFFY.isUndefined(config.columns[x].sortable) && 
					config.columns[x].sortable) {
						newCell.colName = config.columns[x]["name"];
						newCell.onclick = function () {
							app.sortColumn(this.colName);
					}
			}
				newRow.appendChild(newCell);
			}
			thebody.appendChild(newRow);
			this.forEach(function(r, c){
				var newRow = document.createElement("tr");
				for (var x = 0; x < config.columns.length; x++) {
					var newCell = document.createElement("td");
					newCell.appendChild(
						(TAFFY.isObject(config.columns[x]) && 
						!TAFFY.isUndefined(config.columns[x].name)) ? 
						document.createTextNode(r[config.columns[x].name]) : 
							// #NEW# add condition for custom columns with callme methods
							(TAFFY.isObject(config.columns[x]) && 
							!TAFFY.isUndefined(config.columns[x].callme)) ?
							config.columns[x].callme(r,c) :
							// #NEW# otherwise add column as normal
							TAFFY.isString(config.columns[x]) ? 
							document.createTextNode(r[config.columns[x]]) : 
							document.createTextNode("")
					);
					newRow.appendChild(newCell);
				}
				thebody.appendChild(newRow);
			});
			var thetable = document.createElement("table");
			thetable.appendChild(thebody);
			thetable.border=1;
			document.getElementById(config.containerID).innerHTML = "";
			document.getElementById(config.containerID).appendChild(thetable);
		}
	}
	app = TAFFY.mergeObj(app,taffyCollection);
	return app;
}
Now you need to define two new columns with their callme() methods. Remember that in order to display anything in the cell you'll need to return what it is you want to display.

var init = function () {
	fruit = gridPrinter({
		containerID: "displayGridDiv",
		columns: [
				// #NEW# create new column with a callme function to render row number
				{
					display: "#",
					callme: function(r, c){
						// #NEW# create text node to display row number
						return document.createTextNode((c+1));
					}
				},
				{
					name: "food",
					display: "Fruit",
					sortable: true
				}, "water", "fiber", "fat", "protein", "sugar",
				// #NEW# create new column with a callme function to render delete
				{
					display: "Delete",
					callme: function(r, c){
						// #NEW# create a tag to return to the print function
						var op = document.createElement("strong");
						op.onclick = function(){
							// #NEW# on click of the strong tag remove record and reprint grid
							fruit.remove(c);
							fruit.print();
						};
						op.appendChild(document.createTextNode("Delete"));
						return op;
					}
				}]
	},TAFFY(fruits));
	fruit.print();
}
Note: The r and c values passed into callme() are provided by TaffyDB. r is the record object for the row and c is the row number.

7. Imagine the possibilities

You could easily go on and keep adding any amount of new funtionality to your gridPrinter(). With this approach it is possible to build about as complex an application as you can imagine and neatly separate your data management from your interface components.

To learn more check out TaffyDB.com. There you will find a getting started article, a faq, and a mailing list.

8. Questions/Comments?

This article was authored by Ian Smith (http://blog.joesgoals.com/). This article uses the highlight.js library for code formating.

Got a comment or spot a bug? Please use the TaffyDB contact form to reach out and tell us what is on your mind.

Find this useful? Please pass it on.