Voodookit

Voodookit: Test Suite

A Better <table>

At its simplest, Voodookit will silently process a table of data:

HTML

<table id="ex1">
  <tr> <th>Item</th> <th>Quantity</th> </tr>
  <tr> <td>Table</td> <td>1.0</td> </tr>
  <tr> <td>Chair</td> <td>4.0</td> </tr>
</table>

Javascript

var vkex1 = $("#ex1").voodoo();

Demos and Tests

Item Quantity
Table 1.0
Chair 4.0

Demo: vkex1.col(1).sort()

Demo: vkex1.col(1).sortReverse()

?Test: vkex1.numRows()? (expected 3)

?Test: vkex1.numCols()? (expected 2)

?Test: vkex1.numContentRows()? (expected 2)

?Test: vkex1.numHeaderRows()? (expected 1)

In addition to getting general information about the table, you can query for a specific cell by (row, col) position. Position coordinates are 0-indexed from the top left, and non-content rows are skipped. The return value is an instance of VkCell.

?Test: vkex1.cell(0,1).value()? (expected "1.0")

?Test: vkex1.cell(0,1).toString()? (expected "1.0")

?Test: vkex1.cell(1,0)? (expected "Chair")

If you want to handle an entire column at once, you can request it by index:

?Test: vkex1.col(1).cellValues().join(", ")? (expected "1.0, 4.0")

You can even calculate aggregate values for a column using the reduce function:

?Test: vkex1.col(1).reduce( function(x,y) { return parseFloat(x)+parseFloat(y); } )? (expected 5.0)

Or find matching cells using the grep function, which returns an array of VkCells for which the test function returns a true value:

?Test: vkex1.col(0).grep( function(vkcell) { return vkcell.value()=="Chair"; } )[0].row().cell(1)? (expected 4.0)

Finally, you can also get Voodookit objects based on a new or already-Voodookit-enabled <table> element, or from tr or td elements within such a table:

?Test: $("#ex1").voodoo().constructor.toString().indexOf("function VkTable") >= 0? (expected true)

On nonsucky browsers, you can do this test with nicer syntax:

?Test: $.browser.msie? "VkTable" : $("#ex1").voodoo().constructor.name? (expected "VkTable")

?Test: $.browser.msie? "VkRow" : $("#ex1 tr:last").voodoo().constructor.name? (expected "VkRow")

?Test: $.browser.msie? "VkCell" : $("#ex1 tr:last td:first").voodoo().constructor.name? (expected "VkCell")

Row Addressing

You can also look up values by row; a VkRow object exists for each row in your table, and can be used to look up a cell's neighbors.

HTML

<table id="ex1a">
  <tr> <th>Item</th>  <th>Price</th>   </tr>
  <tr> <td>Table</td> <td>100</td>     </tr>
  <tr> <td>Chair</td> <td>20.00</td>   </tr>
  <tr> <td>Sofa</td>  <td>1200.00</td> </tr>
</table>

Javascript

var vkex1a = $("#ex1a").voodoo({
    cols: [
        { name: "item" },
        { name: "price" }
    ]
});

Demos and Tests

Item Price
Table 100
Chair 20.00
Sofa 1200.00

?Test: vkex1a.cell(1, 0).row().cell("price")? (expected 20.00)

Similarly, you can get a cell's column, and query for another row's value in the same column:

?Test: vkex1a.cell(1, 0).col().cell(0)? (expected "Table")

or start with a row lookup:

?Test: vkex1a.row(1).cell("item")? (expected "Chair")

Column Data Types

Voodookit's default sorting works fine for alphabetical data. But what if you've got a bunch of actual floating-point numbers?

HTML

<table id="ex2">
  <tr> <th>Item</th>  <th>Price</th>   </tr>
  <tr> <td>Table</td> <td>100</td>     </tr>
  <tr> <td>Chair</td> <td>20.00</td>   </tr>
  <tr> <td>Sofa</td>  <td>1200.00</td> </tr>
</table>

Javascript

var vkex2 = $("#ex2").voodoo();

Demos and Tests

Item Price
Table 100
Chair 20.00
Sofa 1200.00

Demo: vkex2.col(1).sort()

As you can see, this doesn't work so well. We need to tell Voodookit that the second column contains floating-point numbers.

Column Data Types: Floating-point Numbers

HTML

<table id="ex3">
  <tr> <th>Item</th>  <th>Price</th>   </tr>
  <tr> <td>Table</td> <td>100</td>     </tr>
  <tr> <td>Chair</td> <td>20.00</td>   </tr>
  <tr> <td>Sofa</td>  <td>1200.00</td> </tr>
</table>

Javascript

var vkex3 = $("#ex3").voodoo({
    cols: [
        { type: $.voodoo.types.string }, // default
        { type: $.voodoo.types.float }
    ]
});

Demos and Tests

Item Price
Table 100
Chair 20.00
Sofa 1200.00

Demo: vkex3.col(1).sort()

Demo: vkex3.col(1).sortReverse()

Here we're passing a little bit of information to Voodookit about the columns in our table. The $.voodoo.types.float object is a Voodookit column type, whose main purpose is to provide a parseValue method, converting the table cell's HTML contents into a Javascript value. For this type, the parseValue method is simply a call to Javascript's built-in parseFloat function.

Declaring types ensures that any cell's value() function returns a Javascript object of the appropriate type. It also lets you use the type-correct operators for things like arithmetic, and the convenient built-in column arithmetic function:

?Test: vkex3.col(1).sum()? (expected 1320.00)

Column Names

You can also give columns alphanumeric names, and refer to them using these names instead of their integer identifiers.

HTML

<table id="ex3a">
  <tr> <th>Item</th>  <th>Price</th>   </tr>
  <tr> <td>Table</td> <td>100</td>     </tr>
  <tr> <td>Chair</td> <td>20.00</td>   </tr>
  <tr> <td>Sofa</td>  <td>1200.00</td> </tr>
</table>

Javascript

var vkex3a = $("#ex3a").voodoo({
    cols: [
        { name: "item" },
        { name: "price",
          type: $.voodoo.types.float }
    ]
});

Demos and Tests

Item Price
Table 100
Chair 20.00
Sofa 1200.00

?Test: vkex3a.col("price").reduce( function(x,y) { return parseFloat(x)+parseFloat(y); } )? (expected 1320.0)

Demo: vkex3a.col("price").sort()

Demo: vkex3a.col("price").sortReverse()

As an added bonus, when you provide a column name, Voodookit adds that name as a class to all td elements in that column:

?Test: $("#ex3a tr:eq(2) td:eq(1)").hasClass("price")? (expected true)

Convenience Method for Sorting

We often want to allow users to sort the content of a table by clicking on th elements in the first row. Voodookit provides a convenience method, makeHeadersSort(), which easily activates this behavior.

HTML

    <table id="ex3b">
      <tr> <th>Name</th> <th>Age</th> <th>IQ</th> </tr>
      <tr> <td>Alice</td> <td>2</td> <td>100</td> </tr>
      <tr> <td>Bob</td> <td>1</td> <td>110</td> </tr>
      <tr> <td>Charlie</td> <td>3</td> <td>90</td> </tr>
    </table>

    <style>#ex3b th { cursor: pointer; }</style>

Javascript

    var vkex3b = $("#ex3b").voodoo();
    vkex3b.makeHeadersSort();

    // synthesize a click event on the second th, so we can test that
    // the table has been sorted by age.
    $("#ex3b tr:first th:eq(1)").trigger("click");

Demos and Tests

Name Age IQ
Alice 2 100
Bob 1 110
Charlie 3 90

?Test: vkex3b.cell(1,1).value() ? (expected 2)

Date and Time Data

Voodookit also supports date and date+time data types: DateTime, for a date and time, and Date, which is a date only. One way to express the values for cells of these types is in milliseconds since epoch. An advantage of this approach is that times can be localized on the client side, so you don't have to care what time zone your user is in.

HTML

<table id="ex4">
  <tr> <th>Task</th>                          <th>Deadline (raw)</th> <th>Deadline (date)</th> <th>Deadline (d+t)</th></tr>
  <tr> <td>(3) get approval from client</td>  <td>1245729600000</td>  <td>1245729600000</td>   <td>1245729600000</td> </tr>
  <tr> <td>(1) sketch UI for floobulator</td> <td>1239163200000</td>  <td>1239163200000</td>   <td>1239163200000</td> </tr>
  <tr> <td>(2) send example links to Tim</td> <td>1239336009000</td>  <td>1239336009000</td>   <td>1239336009000</td> </tr>
  <tr> <td>(4) watch a bunch of TV</td>       <td></td>               <td></td>                <td></td>              </tr>
</table>

Javascript

    var vkex4 = $("#ex4").voodoo({
            cols: [
            { },
            { },
            { type: $.voodoo.types.date },
            { type: $.voodoo.types.dateTime }
        ]
});

    vkex4.col(2).sort();

Demos and Tests

Task Deadline (raw) Deadline (date) Deadline (d+t)
(3) get approval from client 1245729600000 1245729600000 1245729600000
(1) sketch UI for floobulator 1239163200000 1239163200000 1239163200000
(2) send example links to Tim 1239336009000 1239336009000 1239336009000
(4) watch a bunch of TV

?Test: vkex4.cell(0,2).value().constructor == Date? (expected true)

?Test: vkex4.cell(0,2).value().getTime() < vkex4.cell(1,2).value().getTime() ? (expected true)

?Test: vkex4.cell(1,2).value().getTime() < vkex4.cell(2,2).value().getTime() ? (expected true)

?Test: vkex4.cell(0,2).value().getTime() < vkex4.cell(2,2).value().getTime() ? (expected true)

Demo: vkex4.col(2).sort()

Demo: vkex4.col(2).sortReverse()

Demo: vkex4.col(2).sortReverseBlanksLast()

Events

Want to add an odd class to the odd rows? Or respond to changes in the table's sort order? Voodookit generates events you can register for:
  • vkRowScan — generated when a content row (a tr element that contains one or more td elements) is handled by Voo2do: on first review of the table content, and on subsequent changes to row position (sorting). The event originates from the row tr element. In this event's data hash:
    • rowIndex: an integer indicating this row's position among all content rows in the table, starting from 0.
    • vkRow: the VkRow object for this table row.
  • vkChange — generated when the value of a cell is changed. The event originates from the changed td element, and bubbles up the document so you can also bind to a table and get this event. Because you may be interested in changes within a column, the vkChange event is also fired by the corresponding column, which provides a bind method for this purpose. In this event's data hash:
    • oldValue: the cell's previous value.
    • cell: the VkCell object for the affected cell. You can get the current (new) value by calling cell.value().
  • vkRowAppend — generated when a new row is added to the table after the table's initial loading, e.g. with append or appendRow. Originates with the new tr element and bubbles up.
  • vkRowDelete — generated before a row is deleted from the table using the deleterow() method. Originates with the delete tr element and bubbles up. Note that deleting a row will also trigger vkRowScan events to be triggered for all remaining rows; these are triggered after the vkRowDelete event.
  • vkRowsReordered — generated once after a table's rows have been re-ordered, such as in a sort operation. Originates on the table object.
These events are triggered on the underlying tr elements, so you can listen for and react to them in the typical jQuery way.

HTML

<table id="ex5">
  <tr> <th>First Name</th>  <th>Last Name</th> </tr>
  <tr> <td>Bob</td>         <td>Jones</td>     </tr>
  <tr> <td>Alice</td>       <td>Smith</td>     </tr>
  <tr> <td>Charlie</td>     <td>Williams</td>  </tr>
</table>

Javascript

    // we use the live() event delegation system so that even events on
    // newly-created rows trigger the handler.
    $("#ex5 tr").live("vkRowScan", function(e, data) {
        var isOdd = data["rowIndex"] % 2;
        $(this).css({ "background-color": isOdd? "skyblue": "lightgreen",
                      "font-weight": isOdd? "bold": "normal"
                    });
    });
    var vkex5 = $("#ex5").voodoo();

    var vkex5_got_rowAppend = 0;
    var vkex5_got_rowsReordered = 0;
    $("#ex5").bind("vkRowAppend", function() {
        vkex5_got_rowAppend += 1;
    });
    $("#ex5").bind("vkRowsReordered", function() {
        vkex5_got_rowsReordered += 1;
    });
    vkex5.appendRow(["Dave","Sawyer"]);
    vkex5.col(1).sort();

Demos and Tests

First Name Last Name
Bob Jones
Alice Smith
Charlie Williams

Demo: vkex5.col(0).sort()

Demo: vkex5.col(1).sort()

Did that bolding work correctly? We have to tolerate either a textual value ("bold") or a numeric one (700) for the font-weight property, depending on the browser used.

?Test: $("#ex5 tr:eq(2)").css("font-weight")=="bold" || $("#ex5 tr:eq(2)").css("font-weight")==700? (expected true)

?Test: $("#ex5 tr:eq(1)").css("font-weight")=="normal" || $("#ex5 tr:eq(1)").css("font-weight")==400? (expected true)

?Test: vkex5_got_rowAppend? (expected 1)

?Test: vkex5_got_rowsReordered? (expected 1)

Renderers

In the examples so far, the values displayed in our HTML tables have simply been the default, stringified version of the cell's value. For a $.voodoo.types.date column, we see something like "06/23/2009". But what if you want your cell displayed in different format?

That's where renderers come in. Renderers determine how to display the values of cells in HTML. The renderer has access to the cell's VkCell object, allowing the rendering behavior to be based on the cell's value as well as its row, column, and table.

Voodookit includes the following built-in renderers. These are classes and should be instantiated using new. You can also build your own renderer—more on that later.

  • $.voodoo.render.String
  • $.voodoo.render.HtmlString
  • $.voodoo.render.FloatHours *TODO*
  • $.voodoo.render.Currency *TODO*
  • $.voodoo.render.LocaleDate
  • $.voodoo.render.LocateDateTime

Many of these renderers accept options that configure how they behave. Options can be passed in as an associative array parameter. Two options are supported by all renderers: default, which specifies the result to return when rendering an empty cell, and defaultValue which does something slightly different: it treats empty cells as having the given value — useful if you want to apply the same logic to generate the HTML for empty cells.

Note: The word default is a reserved word in Javascript. This option should probably be renamed; in any usage, be sure to quote the word default. For example, the object literal { default: "none" } will work in Firefox but cause a syntax error in Google Chrome, whereas { "default": "none" } works in all browsers.

Let's see how the default setting works using the String renderer.

HTML

    <table id="ex6">
      <tr> <th>Name</th>    <th>Pizza Choices</th> </tr>
      <tr> <td>Bob</td>     <td>Pepperoni</td>     </tr>
      <tr> <td>Alice</td>   <td> </td>             </tr>
      <tr> <td>Charlie</td> <td></td>              </tr>
    </table>

    <p>Part 2:</p>

    <table id="ex6_2">
      <tr> <th>Name</th>    <th>Pizza Choices</th> </tr>
      <tr> <td>David</td>   <td></td>              </tr>
      <tr> <td>Eliza</td>   <td><b>peppers</b>--this is bad, should just be HTML</td>              </tr>
    </table>

Javascript

    var vkex6 = $("#ex6").voodoo({
        cols: [
            {},
            { render: new $.voodoo.render.String({"default":"<i>unknown</i>"}) }
        ]
    });

    // on the second table, let's use the defaultValue option instead:
    var vkex6_2 = $("#ex6_2").voodoo({
        cols: [
            {},
            { render: new $.voodoo.render.String({"defaultValue":"<i>unknown</i>"}) }
        ]
    });

Demos and Tests

Name Pizza Choices
Bob Pepperoni
Alice
Charlie

Part 2:

Name Pizza Choices
David
Eliza peppers--this is bad, should just be HTML

?Test: $("#ex6 tr:last td:last").html().toLowerCase()? (expected "<i>unknown</i>")

Note that in our HTML, the value for Alice's pizza choices is a single space character— not considered empty by Voodookit. Hence it is rendered as a space:

?Test: "["+$("#ex6 tr:eq(2) td:last").html().toLowerCase()+"]"? (expected "[ ]")

In the second table, we used the defaultValue option instead of default. What does this mean?

?Test: $("#ex6_2 tr:eq(1) td:last").html()? (expected "&lt;i&gt;unknown&lt;/i&gt;")

Renderers with Editing

Often, you'll want to let users not only view the data in a table cell, but also edit that data. Voodookit's editable renderers make this easy, by providing a representation of the cell value as an editable HTML form widget, whose change event triggers a value change on the corresponding vkCell. That means the changes are more than just skin deep—you can instantly re-sort, or use event listeners to react to vkValueChange events.

HTML

<table id="ex7">
  <tr> <th>Name</th>    <th>Pizza Choices</th> <th>Paid?</th> </tr>
  <tr> <td>Alice</td>   <td>Artichokes</td>    <td>1</td>  </tr>
  <tr> <td>Bob</td>     <td>Pepperoni</td>     <td>0</td> </tr>
  <tr> <td>Charlie</td> <td></td>              <td></td>      </tr>
</table>

Javascript

var vkex7 = $("#ex7").voodoo({
    cols: [
        {},
        { render: new $.voodoo.render.TextField() },
        { type: $.voodoo.types.boolean,
          render: new $.voodoo.render.CheckboxField() }
    ]
});

Demos and Tests

Name Pizza Choices Paid?
Alice Artichokes 1
Bob Pepperoni 0
Charlie

Demo: vkex7.col(0).sort()

Demo: vkex7.col(0).sortReverse()

Demo: vkex7.col(1).sort()

Demo: vkex7.col(1).sortReverse()

Demo: vkex7.col(2).sort()

Demo: vkex7.col(2).sortReverse()

Dealing with Edits

When a cell's value is changed, the vkChange event fires. This event bubbles up from the changed td element, so you can listen for it on a particular cell or on the whole table.

HTML

<table id="ex8">
  <tr> <th>Name</th>    <th>Pizza Choices</th> <th>Slices Eaten</th> </tr>
  <tr> <td>Alice</td>   <td>Artichokes</td>    <td>1</td>  </tr>
  <tr> <td>Bob</td>     <td>Pepperoni</td>     <td>0</td> </tr>
  <tr> <td>Charlie</td> <td></td>              <td>0</td>      </tr>
</table>

Javascript

    var vkex8 = $("#ex8").voodoo({
        cols: [
            {},
            { render: new $.voodoo.render.TextField(),
              name: "pizza_choices"
            },
            { type: $.voodoo.types.integer,
              name: "numslices",
              render: new $.voodoo.render.TextField() }
        ]
    });

    var vkex8_gotTableEvent = false;
    var vkex8_gotRowEvent = false;
    var vkex8_gotColumnEvent = false;

    $("#ex8").bind("vkChange", function() {
        vkex8_gotTableEvent = true;
    });

    $("#ex8 tr:eq(2)").bind("vkChange", function() {
        vkex8_gotRowEvent = true;
    });

    vkex8.col("pizza_choices").bind("vkChange", function() {
        vkex8_gotColumnEvent = true;
    });

    vkex8.cell(1,1).value("Sausage");

Demos and Tests

Name Pizza Choices Slices Eaten
Alice Artichokes 1
Bob Pepperoni 0
Charlie 0

?Test: vkex8_gotTableEvent? (expected true)

?Test: vkex8_gotRowEvent? (expected true)

?Test: vkex8_gotColumnEvent? (expected true)

The vkChange event is also used by renderers to update the rendered value when a cell's value is changed; try this increment operation to see how that works:

Demo: vkex8.cell(1,2).value( 1+vkex8.cell(1,2).value() )

Adding Rows

In addition to changing data in existing rows, you can add rows to a Voodookit-enabled table. You can do this in two ways:
  1. use a standard jQuery method on the table, like $("#mytable").append("...") followed by a call to $("#mytable").voodoo.findNewRows(), or
  2. use the built-in $("#mytable").voodoo.append() method, which does the same thing.
When content is added this way, any new rows are processed and rendered based on the parameters first sent to Voodookit on initialization of the parent table. A vkRowScan event is also generated for each new row.

HTML

<table id="ex9">
  <tr> <th>Name</th>    <th>Pizza Choices</th> <th>Slices Eaten</th> </tr>
  <tr> <td>Alice</td>   <td>Artichokes</td>    <td>1</td>  </tr>
  <tr> <td>Bob</td>     <td>Pepperoni</td>     <td>0</td> </tr>
  <tr> <td>Charlie</td> <td>Chicken</td>       <td>0</td>      </tr>
</table>

Javascript

    var vkex9_newRowText = null;
    var vkex9_newRowIndex = null;

    $("#ex9").bind("vkRowScan", function(e,data) {
        vkex9_newRowText = data.vkRow.cell(0).value().toString();
        vkex9_newRowIndex = data.rowIndex;
    });

    var vkex9 = $("#ex9").voodoo({
        cols: [
            {},
            { render: new $.voodoo.render.TextField() },
            { render: new $.voodoo.render.TextField() }
        ]
    });

    $("#ex9").voodoo().append("<tr><td>Dave</td><td>Olives</td><td>1</td></tr>");

Demos and Tests

Name Pizza Choices Slices Eaten
Alice Artichokes 1
Bob Pepperoni 0
Charlie Chicken 0

?Test: vkex9_newRowIndex? (expected 3)

?Test: vkex9_newRowText? (expected "Dave")

So to recap, there's the two-step method: add table content however you want and then call findNewRows:

Demo: $("#ex9").append("<tr><td>Ed</td><td>Spinach</td><td>2</td></tr>")

Demo: $("#ex9").voodoo().findNewRows()

Or the append method built into Voodookit's VkTable class, which returns the VkTable object so it's chainable:

Demo: $("#ex9").voodoo().append("<tr style=display:none><td>Ed</td><td>Spinach</td><td>2</td></tr>").$table.find("tr:last").fadeIn("slow") && false

But there's a shortcut...

Often, you just want to append a blank row, or a row with certain values pre-filled. Voodookit provides a convenience method, appendRow, that does this based on its parameters:
  • no arguments: appends a row with as many empty td elements as there are columns in the table so far.
  • integer: appends a row with as many empty td elements as specified in the parameter.
  • COMING SOON BUT NOT YET...
  • array: appends a row with values as given in order in the parameter.
  • object (hash): appends a row with as many columns as in the table so far, with some values filled in using column names or indices if available in the parameter.

HTML

<table id="ex9b">
  <tr> <th>Name</th>    <th>Pizza Choices</th> <th>Slices Eaten</th> </tr>
  <tr> <td>Alice</td>   <td>Artichokes</td>    <td>1</td>  </tr>
  <tr> <td>Bob</td>     <td>Pepperoni</td>     <td>0</td> </tr>
  <tr> <td>Charlie</td> <td>Chicken</td>       <td>0</td>      </tr>
</table>

Javascript

    var vkex9b_newRowTexts = [];

    $("#ex9b").bind("vkRowScan", function(e,data) {
        vkex9b_newRowTexts.push( data.vkRow.cell(0).value().toString() );
    });

    var vkex9b = $("#ex9b").voodoo({
        cols: [
            { name: "name" },
            { render: new $.voodoo.render.TextField(), name: "pizza_choices" },
            { render: new $.voodoo.render.TextField(), name: "num_slices" }
        ]
    });

    $("#ex9b").voodoo().appendRow();
    $("#ex9b").voodoo().appendRow(3);
    $("#ex9b").voodoo().appendRow(["Eustace", "Broccoli", 2]);

Demos and Tests

Name Pizza Choices Slices Eaten
Alice Artichokes 1
Bob Pepperoni 0
Charlie Chicken 0

?Test: $("#ex9b").voodoo().appendRow({ name: "Frank", 2: 7 }).is("tr")? (expected true)

?Test: vkex9b_newRowTexts[3]? (expected "")

?Test: vkex9b_newRowTexts[4]? (expected "")

?Test: vkex9b_newRowTexts[5]? (expected "Eustace")

?Test: vkex9b_newRowTexts[6]? (expected "Frank")

One more thing: the appendRow returns a jQuery object for the row, so you can do further manipulation of the row if you wish:

Demo: $("#ex9b").voodoo().appendRow().find("input:first").focus() && false;

Deleting Rows

Add too many rows? Maybe you want to delete some. You can do this with standard jQuery/DOM methods if you wish: just .remove() a tr and all VoodooKit methods (row counts, positional cell coordinates, etc.) will keep working just fine, because VoodooKit treats the DOM (plus jQuery .data) as the primary underlying data structure whenever possible.

However, in most cases it is preferable to use the VkRow.deleterow() method, which will not only remove the appropriate tr from the table, but also generate some useful events:

  • vkRowDelete on the row about to be deleted, while the VkRow object is still accessible.
  • vkRowScan on all remaining rows in the table.

HTML

<table id="ex10">
  <tr> <th>Name</th>    <th>Slices Eaten</th>  <th>Delete?</th> </tr>
  <tr> <td>Alice</td>   <td>1</td>             <td></td>        </tr>
  <tr> <td>Bob</td>     <td>0</td>             <td></td>        </tr>
  <tr> <td>Charlie</td> <td>0</td>             <td></td>        </tr>
</table>

Javascript

    var vkex10_name_deleted;
    var vkex10_num_scans_per_name = {};

    $("#ex10").bind("vkRowDelete", function(e) {
        vkex10_name_deleted = $(e.target).voodoo().cell(0);
    });

    $("#ex10 tr").bind("vkRowScan", function(e, data) {
        var r = data.vkRow;
        var name = r.cell(0).value();

        if(!vkex10_num_scans_per_name[ name ]) {
            vkex10_num_scans_per_name[ name ] = 0;
        }

        vkex10_num_scans_per_name[ name ]++;
    });

    var vkex10 = $("#ex10").voodoo({
        cols: [
            {},
            { render: new $.voodoo.render.TextField() },
            { render: new $.voodoo.render.HtmlString({"default":"<a href='#' class='del'>del</a>"}) }
        ]
    });

    $("#ex10 a.del").live("click", function() {
        $(this).parents("tr").voodoo().deleterow();
        return false;
    });

    $("#ex10 a.del:first").trigger("click");

Demos and Tests

Name Slices Eaten Delete?
Alice 1
Bob 0
Charlie 0

?Test: vkex10.numContentRows()? (expected 2)

?Test: vkex10_name_deleted? (expected "Alice")

The row we deleted should have 1 row scan, from the initial load:

?Test: vkex10_num_scans_per_name["Alice"]? (expected 1)

Remaining rows should have 2 row scans, one from initial load and one after the deletion of the Alice row:

?Test: vkex10_num_scans_per_name["Bob"]? (expected 2)

?Test: vkex10_num_scans_per_name["Charlie"]? (expected 2)

In some cases, you might not want to immediately remove the deleted tr from the document. For example, you might want to animate the removal effect. You can do this by calling prepToDelete instead of deleteprepToDelete will ready the row for deletion and send the appropriate events, and like delete will return a jQuery object wrapping the relevant tr. But unlike delete, prepToDelete counts on you to make a final call to remove the row when you're done. (It is important that you actually do remove the row from its containing table; until you complete that step, the VkTable object's state, such as the result of numRows(), will be inconsistent with the events that have been generated.)

Demo: $("#ex10 tr:eq(1)").voodoo().prepDelete().fadeOut(function(){ $(this).remove(); }) && false;

Computed Columns

Sometimes you want to calculate a value for a row. For example, if you are tracking tasks you might have estimated time and a time elapsed columns entered by the user. What if you want to also display a time remaining column?

Voodookit makes this easy by letting you define a computed column. Computed columns are like regular columns in your table, but instead of reading their value from the content of a <td> element, their value is calculated according to a formula you specify.

Computed cell values can be based on any non-computed columns in the current row, as well arbitrary Javascript. Values based on other columns will automatically change if their parameters are changed. And of course, cells in computed columns still generate the same events as other cells.

HTML

<table id="ex11">
  <tr> <th>Name</th>    <th>Slices Eaten</th>  <th>% of 8-slice Pizza</th> </tr>
  <tr> <td>Alice</td>   <td>1</td>             <td>?</td>        </tr>
  <tr> <td>Bob</td>     <td>0</td>             <td>.15</td>        </tr>
  <tr> <td>Charlie</td> <td>0</td>             <td></td>        </tr>
</table>

Javascript

var vkex11 = $("#ex11").voodoo({
    cols: [
        { name: "person" },
        { name: "num_slices",
          render: new $.voodoo.render.TextField(),
          type: $.voodoo.types.integer
        },
        { name: "pct_of_za",
          type: $.voodoo.types.float,
          compute_from_cols: [ "num_slices" ],
          compute_function: function(num_slices) {
               return num_slices / 8;
          }
        },
    ]
});

Demos and Tests

Name Slices Eaten % of 8-slice Pizza
Alice 1 ?
Bob 0 .15
Charlie 0

?Test: vkex11.col("num_slices").is_computed()? (expected false)

?Test: vkex11.col("pct_of_za").is_computed()? (expected true)

?Test: vkex11.cell(0,2).value()? (expected 0.125)

But wait, there's more! Computed columns listen for vkChange events on their neighbor cells specified in the compute_from_cols parameter. So if we change one of the num_slices cells:

?Test: vkex11.cell(1,1).value(3)? (expected 3)

?Test: vkex11.cell(1,2).value()? (expected 0.375)

Try it yourself... just edit one of the central column values and watch the right column change in response!

HTML Templating Renderers

Sometimes it's simpler to specify how a column might look by writing an HTML template, instead of a renderer function. Voodookit includes a simple template processor that, given a HTML-like template string, compiles a Renderer class that executes the given template. The template can refer to other cells in the same row as variables, and can execute some basic javascript code.

HTML

<table id="ex12">
  <tr> <th>Name</th>    <th>Slices Eaten</th>  <th>Story</th> </tr>
  <tr> <td>Alice</td>   <td>2</td>             <td></td>      </tr>
  <tr> <td>Bob</td>     <td>3</td>             <td></td>      </tr>
  <tr> <td>Charlie</td> <td>0</td>             <td></td>      </tr>
</table>

Javascript

var vkex12 = $("#ex12").voodoo({
    cols: [
        { name: "person" },
        { name: "num_slices",
          render: new $.voodoo.render.TextField(),
          type: $.voodoo.types.integer
        },
        { name: "story",
          render: $.voodoo.makeRen.template("<<person>> <<if num_slices>>ate <<num_slices>> slice(s).<<else>>is STARVING!<</if>>")
        },
    ]
});

Demos and Tests

Name Slices Eaten Story
Alice 2
Bob 3
Charlie 0

?Test: vkex12.cell(0,2).$td.text()? (expected "Alice ate 2 slice(s).")

?Test: vkex12.cell(2,2).$td.text()? (expected "Charlie is STARVING!")

Note that currently, templates will only automatically re-render when their cell's value changes, but not when other values that may have been used in the template change.

Alternate Representations of the Table Data

Voodookit includes a flexible facility to generate an alternate representation of the data in your table. This allows you to take a VkTable in your document and, based on its current state including any recent user modifications, get a representation of that table. You can use it to quickly get a JSON serialization of your table, or a custom representation specific to your needs.

HTML

    <table id="ex13">
      <tr> <th>Name</th>    <th>Slices Eaten</th> </tr>
      <tr> <td>Alice</td>   <td>2</td>            </tr>
      <tr> <td>Bob</td>     <td>3</td>            </tr>
      <tr> <td>Charlie</td> <td>0</td>            </tr>
    </table>

    <h4>Rendering output:</h4>
    <div style="background:linen; border: 1px solid blue;" id="ex13_altrender"> </div>

Javascript

var vkex13 = $("#ex13").voodoo({
    cols: [
        { name: "person",
          render: new $.voodoo.render.TextField() 
        },
        { name: "num_slices",
          render: new $.voodoo.render.TextField(),
          type: $.voodoo.types.integer
        }
    ]
});

Demos and Tests

Name Slices Eaten
Alice 2
Bob 3
Charlie 0

Rendering output:

Demo: $("#ex13_altrender").text(vkex13.altRender("json")) && false

?Test: vkex13.altRender("json").indexOf("\"Charlie\",\"0\"") > 0? (expected true)

?Test: vkex13.altRender("json").charAt(0)? (expected "[")

Custom AltRendering

To create a custom rendering, you supply methods for the VkTable, VkRow, VkColumn, and/or VkCell classes that return data in the desired format. You can do this by directly adding methods to the classes:
  • $.voodoo.VkTable
  • $.voodoo.VkColumn
  • $.voodoo.VkRow
  • $.voodoo.VkCell
Here's an example where we'll write custom handlers to generate a printable HTML rendering of the table.

HTML

    <table id="ex14">
      <tr> <th>Name</th>    <th>Slices Eaten</th> </tr>
      <tr> <td>Alice</td>   <td>2</td>            </tr>
      <tr> <td>Bob</td>     <td>3</td>            </tr>
      <tr> <td>Charlie</td> <td>0</td>            </tr>
    </table>

    <h4>Rendering output:</h4>
    <div style="background:linen; border: 1px solid blue;" id="ex14_altrender"> </div>

Javascript

    var vkex14 = $("#ex14").voodoo({
        cols: [
            { name: "person",
              render: new $.voodoo.render.TextField() 
            },
            { name: "num_slices",
              render: new $.voodoo.render.TextField(),
              type: $.voodoo.types.integer
            }
        ]
    });

    $.voodoo.VkTable.prototype.as_printable = function() {
        var result = $("<table>");
        result.append(this.$table.find("tr:has(th):eq(0)").clone());

        this.rows().map(function() { result.append(this.as_printable()); });

        return result;
    };

    $.voodoo.VkRow.prototype.as_printable = function() {
        var result = $("<tr>");
        
        this.cells().map(function() { result.append(this.as_printable()); });

        return result;
    };

    $.voodoo.VkCell.prototype.as_printable = function() {
        var result = $("<td>").text(this.isEmpty? "" : this.value() );
        return result;
    };

    $("#ex14_altrender").html(vkex14.altRender("printable"));

Demos and Tests

Name Slices Eaten
Alice 2
Bob 3
Charlie 0

Rendering output:

This code follows a typical rendering pattern: the table's as_printable method calls a method of the same name on each of its rows, which in turn call a method of the same name on each of their cells. This approach works well in many cases and yields generally readable code, but there's no reason you need to stick with it. The only requirement is that, for myvktable.altRender(format) to work, there must be an as_format method on the VkTable object.

?Test: $("#ex14_altrender td:eq(2)").text()? (expected "Bob")

Demo: $("#ex14_altrender").html(vkex14.altRender("printable")) && false

Demo: $("#ex14_altrender").text(vkex14.altRender("json")) && false

Paging

Voodookit's paging support works on a simple principle: loading the contents of another HTML table into the current table. You can use the same mechanism to dynamically load a table's contents, or to grow a table, or to turn pages back and forth.

HTML

    <table id="ex15">
      <tr> <th>Page</th>    <th>Item Number</th>  </tr>
    </table>

    <table id="ex15_staticpage" style="display:none">
      <tr> <td>0</td>    <td>0.1</td>  </tr>
      <tr> <td>0</td>    <td>0.2</td>  </tr>
      <tr> <td>0</td>    <td>0.3</td>  </tr>
    </table>

Javascript

    var vkex15 = $("#ex15").voodoo();

    // first, let's load rows from a hidden table that's in the document.
    vkex15.loadRowsFromTable($("#ex15_staticpage"));

    // in between pages, we'll want to clear the table:
    vkex15.clear();

    // now we can load multiple pages, even:
    vkex15.loadRowsFromTable($("#ex15_staticpage"));
    vkex15.loadRowsFromTable($("#ex15_staticpage"));

Demos and Tests

Page Item Number

?Test: vkex15.cell(0,0).$td.text()? (expected "0")

?Test: vkex15.cell(4,1).$td.text()? (expected "0.2")

This loading should be non-destructive to the original table:

?Test: $("#ex15_staticpage td:eq(3)").text()? (expected "0.2")

Try it yourself:

Demo: vkex15.loadRowsFromTable($("#ex15_staticpage"));

Demo: vkex15.clear();

Paging with Ajax

Of course, paging is much more useful when you can load data that's not already in your document!

HTML

    <table id="ex16">
      <tr> <th>Page</th>    <th>Item Number</th>  </tr>
    </table>

    <a id="ex16_pagelink" href="/voodookit/tests/example_page_data/">load next page</a>

Javascript

    var vkex16 = $("#ex16").voodoo();
    var pageNum = 0;
    var pageUrl = $("#ex16_pagelink").attr("href");

    $("#ex16_pagelink").bind("click", function() {
        vkex16.loadRowsFromUrl(pageUrl+"?page="+pageNum, {}, function(data, textStatus) { vkex16.clear(); pageNum++; });
        return false;
    });

Demos and Tests

Page Item Number
load next page

Demo: vkex16.loadRowsFromUrl($("#ex16_pagelink").attr("href"));


Tests loading...