Minimally functional sort-able table

(edit - 5/28) I've discovered a couple of issues already with this component, so don't use this component except for reference purposes. I promise to post a more complete version soon.

Here's a Table component I've been working on. It is not at all finished, but does have a pretty solid minimal level of functionality.

The table was designed for the following:
- you have a collection of data objects (ex: List<MyDataObject> )
- you want to display one object per row
- allow the user to select a table row and have an action be fired on the server side delivering the Object that was selected, so if user selects row #0, we should get List.get( 0 )
- table sorts by column on the client side
- allow the the server side to determine the table's sort order
- support Firefox 3 and IE8

The code posted here does the above. It also handles larger data sets well (2000 rows, 30 columns).

Though, this table component has many problems:
- Too much of the formatting information is set and or is only available in the javascript code.
- does not use stylesheets
- major display bug in Opera (does not load initially for some reason, requires the screen to be resized before it renders)
- the table does not support client side paging
- the table does not sort Date columns correctly (sorts them alphabetically which is not correct for Jan 13, 2009 and Dec 14, 1999)
- the table header row really should stay fixed, so it is always visible.
- table is limited by Integer.MAX_VALUE
- Does not support paging, but paging could be done on the server side. (The table performs really well when it is small, I see myself adding client side paging support in the near future)
- in Firefox, the left hand side table border does not show up.
- You can't specify column widths (<colgroup> tag is ignored for some reason)
- you can not specify column text alignment, or specify text fonts on a per column nor row basis.
- no support for editing table data

Here's some sample code, I've attached a screenshot, source code for the table is after the sample code:

--------------------------------
SAMPLE CODE
--------------------------------

    public DataTable<SampleAccount> getTable() {
    	
       // create some data for the table
    	List<SampleAccount> dataForTable = new ArrayList<SampleAccount>();
    	for( int i = 0; i < 10; i ++ ) {
    		String acctName = "account" + i;
    		double amountOwed = genRandNumber();
    		double amountPayed = genRandNumber();
    		boolean active =  ((i %3) == 0);
    		dataForTable.add( new SampleAccount( acctName, active, amountOwed, amountPayed ) );
    	}

       // define the columns for the table, specifying the data type, either: text, boolean, decimal, integer, or date (but date does not sort
      // correctly, sorts as alpha-numeric/text)
    	List<DataColumn<SampleAccount>> columns = new ArrayList<DataColumn<SampleAccount>>();
    	columns.add( new AbstractTextColumn<SampleAccount>( "Account Name") {
    		@Override public String getDisplayValue(SampleAccount data) {
    			return data.getAccountName();
			}
    		
    	});
    	
    	columns.add( new AbstractBooleanColumn<SampleAccount>( "Original Balance") {
			@Override public String getDisplayValue(SampleAccount data) {
				return (new DecimalFormat("#.##")).format( data.getAmountOwed() );
			}
    		
    	});
    		
    	columns.add( new AbstractDecimalColumn<SampleAccount>( "Amount Payed") {
			@Override public String getDisplayValue(SampleAccount data) {
				return (new DecimalFormat("#.##")).format( data.getAmountPayed() );
			}
	
    	});
    	
    	columns.add( new AbstractDecimalColumn<SampleAccount>("Outstanding Balance") {
			@Override public String getDisplayValue(SampleAccount data) {
				return (new DecimalFormat("#.##")).format( data.getBalance() );
			}
    		
    	});

    	columns.add( new AbstractBooleanColumn<SampleAccount>( "At risk account") {
			@Override
			public String getDisplayValue(SampleAccount data) {
				return String.valueOf( data.getBalance() > 10.00 );
			}
    	});
    	
    	
    	DataTable<SampleAccount> table = new DataTable<SampleAccount>( dataForTable , columns );
    	return table;
    }

   private class SampleAccount {
    	private String accountName = "hello";
    	private boolean active = true;
    	private double amountOwed = 252.00;
    	private double amountPayed = 242.00;
    	
    	
    	public SampleAccount( String name, boolean active, double amountOwed, double amountPayed ) {
    		this.accountName = name;
    		this.active = active;
    		this.amountOwed = amountOwed;
    		this.amountPayed = amountPayed;
    	}
    	
    	public boolean isAccountActive() {
    		return active;
    	}
    	public double getAmountPayed() {
    		return amountPayed;
    	}
    	
    	public double getAmountOwed() {
    		return amountOwed;
    	}
    	
    	public String getAccountName() {
    		return accountName;
    	}
    	
    	public double getBalance() {
    		return amountOwed - amountPayed;
    	}
    }


   private double genRandNumber() {
    	return Math.random() * 1000;
    }



--------------------------------
SOURCE CODE
--------------------------------

I've the following folder structure (when deployed to tomcat),

classes = tomcat-root/webapps/myapp/WEB-INF/classes

classes/Application.DataTable.js
classes/Sync.DataTable.js
classes/META-INF/nextapp/echo/SynchronizePeerBindings.properties
classes/echo/uicomponent/AbstractBooleanColumn.class
classes/echo/uicomponent/AbstractColumn.class
classes/echo/uicomponent/AbstractDateColumn.class
classes/echo/uicomponent/AbstractIntegerColumn.class
classes/echo/uicomponent/DataColumn.class
classes/echo/uicomponent/DataColumntype.class
classes/echo/uicomponent/DataTable.class
classes/echo/uicomponent/DataTablePeer.class

--------------------------------
AbstractBooleanColumn.java

package echo.uicomponent.table;

public abstract class AbstractBooleanColumn<T> extends AbstractColumn<T> {
	
	public AbstractBooleanColumn( String columnHeader ) {
		super( columnHeader );
	}

	
	
	public DataColumnType getColumnType() {
		return DataColumnType.TEXT;
	}
	
	
	
	public String format( String value ) {
		if( value.equals( String.valueOf( Boolean.TRUE ))) {
			return "Yes";
		} else if( value.equals( String.valueOf( Boolean.FALSE ))) {
			return "No";
		} else {
			return value;
		}
	}
	

}


-------------------------------------------
 AbstractColumn.java 

package echo.uicomponent.table;

public abstract class AbstractColumn<T> implements DataColumn<T> {

	
	private String columnHeader;
	
	public AbstractColumn( String columnHeader ) {
		if( columnHeader == null || columnHeader.trim().equals( "" ) ) {
			throw new IllegalArgumentException("Illegal constructor argument to AbstractColumn, parameter: columnHeader, must be non-null and non-empty.");
		}
		this.columnHeader = columnHeader;
	}

	@Override
	public String getColumnHeaderLabel() {
		return columnHeader;
	}

	
	public String format( String value ) {
		return value;
	}
}


----------------------------------------------
 Abstract DateColumn.java 

package echo.uicomponent.table;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;


public abstract class AbstractDateColumn<T> extends AbstractColumn<T> {

	
	
	public AbstractDateColumn( String columnHeader ) {
		super( columnHeader );
	}

	
	
	public DataColumnType getColumnType() {
		return DataColumnType.TEXT;
//		return HtmlColumnType.DATE;
	}
	
	
	/*
	public String format( String value ) {

		DateFormat format = new SimpleDateFormat(  "MMM d, yyyy");
//		DateFormat format = DateFormat.SHORT;
		format.setLenient( true );
		try {
			Date d = format.parse( value );
			return format.format( d );
		} catch( ParseException e ) {
			throw new RuntimeException( e );
		}
		this.getDisplayValue( )
	}
	*/
	public abstract Date getDateValue( T value );
	
	
	public String getDisplayValue( T value ) {
		DateFormat format = new SimpleDateFormat(  "MMM d, yyyy");
		Date d = getDateValue( value );
		if( d == null ) {
			return "-";
		} else {
			return format.format( getDateValue( value ) );
		}
	}
	
	
}


------------------------------
 AbstractDecimalcolumn.java 

package echo.uicomponent.table;

public abstract class AbstractDecimalColumn<T> extends AbstractColumn<T> {

	
	public AbstractDecimalColumn( String columnHeader ) {
		super( columnHeader );
	}
	
	public DataColumnType getColumnType() {
		return DataColumnType.DECIMAL;
	}
	
}


-------------------------------
 AbstractIntegerColumn.java 
package echo.uicomponent.table;


public abstract class AbstractIntegerColumn<T> extends AbstractColumn<T> {

	public AbstractIntegerColumn( String columnHeader ) {
		super( columnHeader );
	}
	
	public DataColumnType getColumnType() {
		return DataColumnType.INTEGER;
	}

}

--------------------------------
 AbstractTextColumn.java 

package echo.uicomponent.table;

public abstract class AbstractTextColumn<T> extends AbstractColumn<T> {

	
	public AbstractTextColumn( String columnHeader ) {
		super( columnHeader );
	}
	
	public DataColumnType getColumnType() {
		return DataColumnType.TEXT;
	}

}


--------------------------------------
 DataColumn.java 

package echo.uicomponent.table;



/**
 * Class that defines the data that will be used to populate a single column for a single row.
 * Use a list of these to define how a list of data objects will be rendered into a table.
 * The interface methods are defined for a generic single row, which is then replicated for
 * each row in the table.
 * 
 * @author DVanatta
 * @param <T> The data type to be rendered. This should match the data type of the HtmlTable.
 * @see HtmlTable
 * @see DataColumnType
 */
public interface DataColumn <T> {
	 
	/**
	 * @return  The text value that will be displayed at the top of the column as a header. 
	 */
	 abstract String getColumnHeaderLabel( );		// the table header row label
	 
	 /**
	  * @return The sort key that classifies the column data type, for use with sorting.
	  */
	 abstract DataColumnType getColumnType( );
	 
	 /**
	  * A method to extract the cell value for this column for a particular data object.
	  * 
	  * @param data A reference to the data object that is displayed on the current table row.
	  * @return A String value that will be displayed in the cell
	  * 	corresponding to this column, corresponding to the row that is currently
	  * 	displaying the parameter data object.
	  */
	 abstract String getDisplayValue( T data );	// the text to disply in the table cell
	 
	 
	 abstract String format( String  value );
	 
}


------------------------------
 DataColumnType.java 

package echo.uicomponent.table;


/**
 * Java style enumeration for column type labels to be used by HtmlTable's javascript sort function.
 * @author DVanatta
 */
public final class DataColumnType {

	/**
	 * Internal id string, used to differentiate between instances.
	 */
	private String id;
	
	/**
	 * Text type, use this type for columns that should be alpha numerical sorted. Using this
	 * type, the list "10", "11", "100" will be sorted as "10, 100, 11", and "a" "c" "b", would
	 * be sorted to "a,b,c"
	 */
	public static final DataColumnType TEXT = new DataColumnType("TEXT");
	
	/**
	 * Use this type for numerical values that do not contain fractional parts.
	 */
	public static final DataColumnType INTEGER = new DataColumnType("INTEGER");
	
	/**
	 * Use this type for numerical values that will not contain a decimal or fractional part.
	 */
	public static final DataColumnType DECIMAL = new DataColumnType("DECIMAL");

//	public static final HtmlColumnType DATE = new HtmlColumnType("DATE");
	
	/**
	 * Private constructor to disallow inheritance and external construction of this class.
	 * @param id
	 */
	private DataColumnType( String id ) {
		this.id = id;
	}
	
	
	/**
	 * Returns the data type label for this instance.
	 */
	public String toString() {
		return id;
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((id == null) ? 0 : id.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		DataColumnType other = (DataColumnType) obj;
		if (id == null) {
			if (other.id != null)
				return false;
		} else if (!id.equals(other.id))
			return false;
		return true;
	}
}

-------------------------------
 DataTable.java 

package echo.uicomponent.table;

import java.util.ArrayList;
import java.util.EventListener;
import java.util.List;

import nextapp.echo.app.Border;
import nextapp.echo.app.Color;
import nextapp.echo.app.Component;
import nextapp.echo.app.event.ActionEvent;
import nextapp.echo.app.event.ActionListener;



public class DataTable<T> extends Component {
	private static final long serialVersionUID = 2015476399313418076L;
	public static final String SELECTED_ROW_CLIENT_PROPERTY = "selectedRowProperty";
	public static final String TABLE_SORT_ORDER_CLIENT_PROPERTY = "tableSortOrder";
	public static final String ARRAY_ELEMENT_DELIM_TOKEN_CLIENT_PROPERTY = "#";

	public static final String ROW_SELECTED_ACTION = "rowSelectedAction";

	/**
	 * The split token between rows.
	 */
	private static final String ROW_SPLIT_TOKEN = "<!R!>";

	/**
	 * The split token between cells
	 */
	private static final String CELL_SPLIT_TOKEN = "<!C!>";

	/**
	 * Property key for a columns sort type. This property should be defined for
	 * each column, the property name should be appended with the column number
	 * (starting with zero)
	 * 
	 * @see addColumnSortType
	 */
	private static final String COLUMN_SORT_TYPE_PROPERTY = "COL_SORT_TYPE_"; 
	// then the column number is appended to this arg

	private Integer selectedRowNumber = null;
	private List<Integer> clientTableSortOrder = null;

	public static final int STYLE_BORDER_COLLAPSE = 4231;
	public static final int STYLE_BORDER_SEPERATE = 3261;

		
		
	
	 public static final String ACTION_LISTENERS_CHANGED_PROPERTY = "actionListeners";
	    public static final String ACTION_COMMAND_CHANGED_PROPERTY = "actionCommand";

	    private String actionCommand;

	private final List<T> data;

	
		@Override
	public void processInput(String inputName, Object inputValue) {
		super.processInput(inputName, inputValue);
		if (inputName.equals(ROW_SELECTED_ACTION)) {
			fireAction();
		} else if (inputName.equals(SELECTED_ROW_CLIENT_PROPERTY)) {
			Integer value = (Integer) inputValue;
			this.setSelectedRowNumber(value);
		} else if (inputName.equals(TABLE_SORT_ORDER_CLIENT_PROPERTY)) {
			String value = (String) inputValue;
			this.setClientTableSortOrder(value);

		}
	}
 
		
		private void setClientTableSortOrder( String encodedValue ) {
			
			if( encodedValue == null ) {
				throw new IllegalStateException("enocded VALUE IS NULL>..");
			}
			String delimToken =ARRAY_ELEMENT_DELIM_TOKEN_CLIENT_PROPERTY;
			String[] split = encodedValue.split( delimToken );
			this.clientTableSortOrder = new ArrayList<Integer>();

			for( int i = 0; i < split.length; i ++ ) {
				clientTableSortOrder.add( Integer.valueOf( split[i] ) );
			}
			
		}
		
		public List<Integer> getClientTableSortOrder() {
			if( clientTableSortOrder == null ) {
				throw new IllegalStateException("clienTableSortOrder has yet to be set by the client (and is still null).");
			}
			return clientTableSortOrder;
		}
		
		public List<T> getSortedDataOrder() {
			List<T> sortedData = new ArrayList<T>();
			List<Integer> sortKey = getClientTableSortOrder();
			for( int i = 0, n = sortKey.size(); i < n ; i ++ ) {
				sortedData.add( data.get( sortKey.get( i )));
			}
			return sortedData;
		}
	   
	private void setSelectedRowNumber(Integer value) {
		this.selectedRowNumber = value;
	}

	public Integer getSelectedRowNumber() {
		return selectedRowNumber;
	}
	    
	   
	
	
	public void setBorderCollapseStyle( int styleCode ) {
		if( styleCode != STYLE_BORDER_COLLAPSE && styleCode != STYLE_BORDER_SEPERATE ) {
			throw new IllegalArgumentException("Invalid style code, use one of public static final int fields of this class, either STYLE_BORDER_COLLAPSE, or STYLE_BORDER_SEPERATE.");
		}
		
		if( styleCode == STYLE_BORDER_COLLAPSE ) {
			this.set("borderCollapse", "collapse" );
		} else {	// styleCode == STYLE_BORDER_SEPERATE ) {
			this.set("borderCollapse", "seperate" );
		}
	}
	
	
	
    
	public List<T> getTableData() {
		return data;
	}
	
	public DataTable( List<T> data, List<DataColumn<T>> columnData ) {
		super();
		this.data = data;
		
    	int colWidth = columnData.size();
    	String[][] tableData = new String[data.size() + 1 ][ colWidth ];
		
    	// build the first/header row
    	String[] firstRow = new String[ colWidth ];
    	for( int i = 0; i < colWidth; i ++ ) {
    		DataColumn<T> colData = columnData.get( i );
    		firstRow[i] = colData.getColumnHeaderLabel();
    		
    		addColumnSortType( i , colData.getColumnType() );
    	};
    	tableData[0] = firstRow;

    	// build the rest of the table
    	for( int i = 1; i < tableData.length; i ++ ) {
    		tableData[i] = buildRow( data.get( i -1 ) , columnData );
    	}
    	
    	
    	this.set( "tableData", encode( tableData ));
    	
    	
		Border b = new Border( 1, Color.BLACK, Border.STYLE_SOLID );		
		this.set( "border", b );
		
	}
	
	public void addActionListener(ActionListener l) {
		getEventListenerList().addListener(ActionListener.class, l);
		firePropertyChange(ACTION_LISTENERS_CHANGED_PROPERTY, null, l);
	}

	public void removeActionListener(ActionListener l) {
		getEventListenerList().removeListener(ActionListener.class, l);
		firePropertyChange(ACTION_LISTENERS_CHANGED_PROPERTY, l, null);
	}

	public boolean hasActionListeners() {
		return hasEventListenerList()
				&& getEventListenerList()
						.getListenerCount(ActionListener.class) > 0;
	}

	    
   public String getActionCommand() {
       return actionCommand;
   }

   public void setActionCommand(String newValue) {
       String oldValue = actionCommand;
       actionCommand = newValue;
       firePropertyChange(ACTION_COMMAND_CHANGED_PROPERTY, oldValue, newValue);
   }
   private void fireAction() {
       EventListener[] actionListeners 
               = getEventListenerList().getListeners(ActionListener.class);
       ActionEvent e = new ActionEvent(this, getActionCommand());
       for (int i = 0; i < actionListeners.length; ++i) {
           ((ActionListener) actionListeners[i]).actionPerformed(e);
       }
   }
	
    /**
     * Constructs a single row in the table.
     * @param data The data to be drawn in the row.
     * @param colData A list of column descriptors to render the row.
     * @return An array of Strings, each element corresponds to a table cell value for the row,
     * in order from left to right starting at zero.
     */
    private String[] buildRow( T data, List<DataColumn<T>> colData ) {
    	String[] row = new String[ colData.size() ];
    	for( int i = 0; i < row.length; i ++ ) {
    		row[i] = colData.get( i ).getDisplayValue( data );
    		row[i] = colData.get( i ).format( row[i] );
    		if( row[i] == null || row[i].trim().equals("null") ) {
    			row[i] = " - ";
    		}
    	}
    	return row;
    }
	
	

    
    /**
     * Converts the table's data from a two dimensional String array into a flat String delimited by split tokens.
     * @param dataGrid A 2 dimensional array representing the entirety of the text content of the table 
     * 	(so include the header row as the first array, ie: dataGrid[0] = header row array).
     * @return A specially formated String representing the table's data.
     */
    private String encode( String[][] dataGrid ) {
    	StringBuilder sb = new StringBuilder();

    	for( int i = 0 ; i < dataGrid.length; i ++ ) {
    		for( int j = 0; j < dataGrid[i].length; j ++ ) {
    			sb.append( dataGrid[i][j] );
    			if( j != dataGrid[i].length -1 ) {
    				sb.append( CELL_SPLIT_TOKEN );
    			}
    		}
    		if( i != dataGrid.length -1 ) {
    			sb.append( ROW_SPLIT_TOKEN );
    		}
    	}
    	return sb.toString();
    }
    
    /**
     * Set a columns sort type flag, for use in javascript for sorting.
     * @param colIndex The column number (zero is the left hand most column)
     * @param sortType The sort type flag.
     */
    private void addColumnSortType( int colIndex, DataColumnType sortType  ) {
    	String propertyName = COLUMN_SORT_TYPE_PROPERTY + String.valueOf( colIndex );
    	set( propertyName , sortType.toString() );
    }
    
}


-------------------------------
 DataTablePeer.java 

package echo.uicomponent.table;

import nextapp.echo.app.Component;
import nextapp.echo.app.update.ClientUpdateManager;
import nextapp.echo.app.util.Context;
import nextapp.echo.webcontainer.AbstractComponentSynchronizePeer;
import nextapp.echo.webcontainer.ServerMessage;
import nextapp.echo.webcontainer.Service;
import nextapp.echo.webcontainer.WebContainerServlet;
import nextapp.echo.webcontainer.service.JavaScriptService;

public class DataTablePeer extends AbstractComponentSynchronizePeer  {

    // Create a JavaScriptService containing the SpinButton JavaScript code.
    private static final Service JS_SERVICE = JavaScriptService.forResources(
    		"Vlst.DataTable" , new String[] {
             "/Application.DataTable.js",
             "/Sync.DataTable.js"
    		}
    );
    
    static {
        // Register JavaScriptService with the global service registry.
        WebContainerServlet.getServiceRegistry().add(JS_SERVICE);
    }

	@SuppressWarnings("unchecked")
    public DataTablePeer() {
    	super();
//    	addOutputProperty(DataTable.TABLE_DATA_SERVER_PROPERTY );.
    	
        addEvent(new EventPeer(DataTable.ROW_SELECTED_ACTION,
                DataTable.ACTION_LISTENERS_CHANGED_PROPERTY) {
            public boolean hasListeners(Context context, Component c) {
                return ((DataTable) c).hasActionListeners();
            }
        });

    	
    	
    	
    }
    
    
    @Override
    public String getClientComponentType(boolean shortType) {
        // Return client-side component type name.
        return "Vlst.DataTable";
    }
    
    @Override
	@SuppressWarnings("unchecked")
    public Class getComponentClass() {
        // Return server-side Java class.
        return DataTable.class;
    }

    
    @Override
    public void init(Context context, Component component) {
        super.init(context, component);
        // Obtain outgoing 'ServerMessage' for initial render.
        ServerMessage serverMessage = (ServerMessage) context
                .get(ServerMessage.class);
        // Add SpinButton JavaScript library to client.
        serverMessage.addLibrary(JS_SERVICE.getId());
    }
    
    
    @Override
    public Object getOutputProperty(Context context, Component component, 
            String propertyName, int propertyIndex) {

    	
//    	if ( propertyName.equals( DataTable.TABLE_DATA_SERVER_PROPERTY )) {
//            DataTable dataTable = (DataTable) component;
//            return dataTable.getEncodedTableData();
//    	} else {
            return super.getOutputProperty(context, component, propertyName, propertyIndex);
//        }
    }

    
    @Override
	@SuppressWarnings("unchecked")
    public Class getInputPropertyClass(String propertyName) {
        if( propertyName.equals( DataTable.SELECTED_ROW_CLIENT_PROPERTY  )) {
            return Integer.class;
        } else if( propertyName.equals( DataTable.TABLE_SORT_ORDER_CLIENT_PROPERTY  )) {
            return String.class;
        } else {
        	return null;
        }
    }

	public void storeInputProperty(Context context, Component component,
			String propertyName, int propertyIndex, Object newValue) {

		if (propertyName.equals(DataTable.SELECTED_ROW_CLIENT_PROPERTY)
				|| propertyName.equals(DataTable.TABLE_SORT_ORDER_CLIENT_PROPERTY) ) {

			ClientUpdateManager clientUpdateManager = (ClientUpdateManager) context
					.get(ClientUpdateManager.class);
			clientUpdateManager.setComponentProperty(component, propertyName, newValue);
		}
	}
}

-----------------------------------
 Application.DataTable.js 

if (!Core.get(window, [ "Vlst", "DataTable" ])) {
        Core.set(window, [ "Vlst", "DataTable"  ], {});
}



Vlst.DataTable = Core.extend(Echo.Component, {
	$load: function() {
    	Echo.ComponentFactory.registerType("Vlst.DataTable", this);
	},

	componentType: "Vlst.DataTable",
	
	fireRowSelectedAction: function() {
		this.fireEvent( {type: "rowSelectedAction", source:this });
	}

	
});


-----------------------------------------------
 Sync.DataTable.js 


if (!Core.get(window, [ "Vlst", "DataTableSync" ])) {
	Core.set(window, [ "Vlst", "DataTableSync" ], {});
}



Vlst.DataTableSync = Core.extend(Echo.Render.ComponentSync, {
	$load : function() {
//		this._prototypeTable = this._createPrototypeTable();
		Echo.Render.registerPeer("Vlst.DataTable", this);
	},

	$static : {
		DEFAULTS : {
			headerBgColor :"#A5AEFF",
			zebraStripeRowColor : "#EBF0F0",
			rowBgColor : "#FFFFFF",
			rowHoverColor: "#D6DDF7",
  			rowSelectedColor: "#DDDD00"
  				// tableBorder:
  				// cellBorder, (only left right?)
  				// fontColor
  				// fontType // TODO how to handle per column attribs
		}

	
	},
	
	_table :null,
	_headers :null,
	_tableData :null,
	_actualTableOrder: null,
    _sortColumn: null,
    
  
	$construct : function() {
		_headers = [];
		_tableData = [];
		_actualTableOrder = [];
        _sortColumn = 0;
	},


	
	
	renderAdd : function(update, parentElement) {
        this._table = document.createElement("table");
        Echo.Sync.Border.render(this.component.render("border"), this._table);
        //       this._table.style.outlineStyle = "none";
        this._table.tabIndex = "-1";
        this._table.style.borderCollapse = "collapse";
        this._table.cellPadding=2;
        

		this._table.id = this.component.renderId;
		
		this._table.rules="COLS";
		//////////////////////////////////////////////////////////////////////////////////////////////
		/*
		 * decode table data
		 */
		
		// decode the table data from a String to a String[][] by splitting on row and cell tokens
		var encodedData = this.component.render("tableData", "no data");
		var rowSplitToken = "<!R!>"; // keep in sync with the server side
		var cellSplitToken = "<!C!>"; // keep in sync with the server side


		var rows = encodedData.split(rowSplitToken);
		this._headers = rows[0].split(cellSplitToken);
		
		var TABLE_WIDTH = this._headers.length;

		this._tableData = [];
		for (i = 1; i < rows.length; i++) {
			// create the table body (implicit 2D array)
			// skip the header row (hence i=1 initially)
			this._tableData[i - 1] = rows[i].split(cellSplitToken); 
		}

		
		
    	// initialize the 'real table order' if not already initialized
    	// initialize to the same sequence as the original data to start
        if( this._actualTableOrder === null || this._actualTableOrder.length ===  0 ) {
        	this._actualTableOrder = [];
            for( i = 0; i < this._tableData.length; i ++ ) {
                this._actualTableOrder[i] = i;
            }
        }
        
        ////////////////////////////////////////////////////////////////////////////
        
     //   var totalWidth = 100* TABLE_WIDTH;
     //   this._table.style.width = totalWidth + "px";
    
        
        
        // Render column widths into colgroup element.
        // increases table rendering speed (client agent can avoid scanning the table twice,
        // since it will do so in it's first pass to determine the column and table width)
        var colGroup = document.createElement("colgroup");

        
        for( i = 0 ; i < TABLE_WIDTH ; i ++ ) {
        	var col = document.createElement("col");
        	col.style.width = "250px";
        	colGroup.appendChild( col );
        }
        this._table.appendChild( colGroup );

        

		var thead = document.createElement("thead");
		this._table.appendChild( thead );
		
		var headerRow = document.createElement("tr");
		thead.appendChild( headerRow );

		var headerBgColor = this.component.render("headerBgColor", Vlst.DataTableSync.DEFAULTS.headerBgColor  );
		Echo.Sync.Color.render( headerBgColor, headerRow, "backgroundColor" );
		
		
		for (i = 0; i < TABLE_WIDTH; i++) {
			
			var headerCell = document.createElement("th");
			headerRow.appendChild( headerCell );
			headerCell.style.cursor="pointer"; 
			headerCell.sortFunc = this.interpretSortType( this.component.render( "COL_SORT_TYPE_" + i ,  "TEXT" ) );
			headerCell.columnNumber = i;
//			Echo.Sync.Border.render(this.component.render("Border"), headerCell);
			var headerText = document.createTextNode(this._headers[i]);
			headerCell.appendChild(headerText);
            Core.Web.Event.add( headerCell,  "click", Core.method(this, this._processHeaderSort), false);
		}


		
		
		
		// create the table body
		var tbody = document.createElement("tbody");
		this._table.appendChild(tbody);

			
		for (i = 0; i < this._tableData.length; i++) {
			var tRow = document.createElement("tr");
			tbody.appendChild(tRow);
	
			
			tRow.originalRowNumber = this._actualTableOrder[i]; 
			tRow.actualRowNumber = i;
			
			

		   	this._setRowColor( tRow );
		   	
			Core.Web.Event.add( tRow, "click", Core.method( this, this._processRowClicked), false );
    		Core.Web.Event.add( tRow, "mouseover", Core.method( this, this._processRowMouseOver), false );
    		Core.Web.Event.add( tRow, "mouseout" , Core.method( this, this._processRowMouseOut), false );
    	
			
			// create row cell elements
			for (j = 0; j < TABLE_WIDTH; j++) {
				var td = document.createElement("td");
				tRow.appendChild(td);
				
//				Echo.Sync.Border.render(this.component.render("Border"), td);

				var cellData = this._tableData[ this._actualTableOrder[i] ][j];
		
				td.appendChild( document.createTextNode( cellData ));
				
//				var cellDataLines = cellData.split("\n");

				// replace cell content newlines characters with html line breaks
//				for (k = 0; k < cellDataLines.length; k++) {
//		//			td.appendChild(document.createTextNode(cellDataLines[k]));
//		
//					if( k+1 < cellDataLines.length ) {
//			//			td.appendChild(document.createElement("br"));
//					}
//
//				}

			}

		}
		
		
//		traverseDOMTree( document, this._table, 1 );
		parentElement.appendChild(this._table);	

},



_removeListeners: function( node, depth ) {

	if( depth > 0 ) {
		if( node.hasChildNodes() ) {
			for( i = 0; i < node.childNodes.length; i ++ ) {
				this._removeListeners( node.childNodes.item( i ), depth -1 );
			}
		}
	}
	Core.Web.Event.removeAll( node );
	
	
},


renderDispose : function(update) {
	this._removeListeners( this._table.childNodes.item( 1 ) ,2 ); // removes table header cell listeners

	
	this._removeListeners( this._table, 2 );  // removes table row listeners
	
	
	this._table = null;
	this._headers = null;
	this._tableData = null;
	this._actualTableOrder = null;
},





renderUpdate : function(update) {
/*	var element = this._table;
	var containerElement = element.parentNode;
	if( update !== null ) {
		Echo.Render.renderComponentDispose(update, update.parent);
	}
	containerElement.removeChild(element);
	this.renderAdd(update, containerElement);
	return true;
*/	
},

redrawTable: function() {
	var element = this._table;
	var containerElement = element.parentNode;
	containerElement.removeChild(element);
	this.renderAdd(null, containerElement);
	return true;
},



_processHeaderSort: function( e ) {
    if (!this.client || !this.client.verifyInput(this.component)) {
        return true;
    }
	
	
    
	 var newSortColumn = e.target.columnNumber;
     var sortFunction = e.target.sortFunc;
     
   
     // construct what the new table order should be
 	var newTableOrder = [];
 	
 	
 	// if sorting on the same column, simply reverse the column rows
 	if( this._sortColumn == newSortColumn ) {
 		for( i = 0; i < this._actualTableOrder.length; i ++ ) {
 			newTableOrder[i] = this._actualTableOrder[ this._actualTableOrder.length-i-1];
 		}
 	// otherwise actually sort the rows
 	}  else {  
 		this._sortColumn = newSortColumn;
 		var row_array = [];
 		for( i = 0; i < this._tableData.length; i ++ ) {
 			// Construct a sort array, a 2 element array, first element is the sort key,
 			// 		the second element is the original row number.
 			// After sorting this array, the contained array's second element will
 			// 	be the tables correct sort order
 			row_array[ row_array.length] = [ this._tableData[i][ this._sortColumn ], i  ];
 		}
 		
 		row_array.sort( sortFunction );
		
 		for( i = 0; i < row_array.length; i ++ ) {
 			newTableOrder[i] = row_array[i][1];
 		}
 	}
 	
 	
 	// update the table order
 	this._actualTableOrder = newTableOrder;

 	//  redraw the table
 	this.redrawTable();
     

},



_processRowMouseOver: function( e ) {
	var hoverRow = e.target.parentNode;
	var color = this.component.render("rowHoverColor", Vlst.DataTableSync.DEFAULTS.rowHoverColor );
	Echo.Sync.Color.render( color, hoverRow, "backgroundColor" );
},




_processRowMouseOut: function( e ) {
   	//alert("moust out, row num = " + rowNum + " row num %2 = " + (rowNum %2) + " row num %2 == 0 ? " + ( (rowNum %2) == 0 ));
   	this._setRowColor( e.target.parentNode );
},




_setRowColor: function( row ) {
	var rowNum = row.actualRowNumber;
   	var colorToUse;

   	if( rowNum %2 == 0 ) {
   		colorToUse = this.component.render("bgRowRowColor", Vlst.DataTableSync.DEFAULTS.rowBgColor );
   	} else {
   		colorToUse =  this.component.render("zebraStripeRowColor", Vlst.DataTableSync.DEFAULTS.zebraStripeRowColor );  
   	}
	Echo.Sync.Color.render( colorToUse , row, "backgroundColor" );
},

_processRowClicked: function( e ) {

	var selectedRow = e.target.parentNode;
	var color = this.component.render("rowSelectedColor", Vlst.DataTableSync.DEFAULTS.rowSelectedColor );
	Echo.Sync.Color.render( color , selectedRow, "backgroundColor" );
	
	var rowNumber = selectedRow.originalRowNumber;
	
//	alert("row clicked, and your actual table order is : " + this._actualTableOrder );
	this.component.set("tableSortOrder", this._encodeArrayAsString( this._actualTableOrder  ));
	this.component.set("selectedRowProperty", rowNumber ); 
	this.component.fireRowSelectedAction();

},

_encodeArrayAsString: function( array ) {
//	alert("encoding the following param: "+ array );
	var str = "";
	for( i = 0; i < array.length; i ++ ) {
		str = str + array[i] + "#";
	}	
//	alert("encoded the actual table order into: "+ str );
	//row clicked, and your actual table order is : " + this._actualTableOrder );
	
	return str;
},

/**
 * Returns a sort function based upon the 'type' of the parameter.
 */
interpretSortType: function( sortTypeKey ) {
    if( sortTypeKey === "TEXT" ) {
    	return this.sort_alpha;
    } else if( sortTypeKey === "INTEGER" ) {
    	return this.sort_integer;
    } else if( sortTypeKey === "DECIMAL" ) {
    	return this.sort_numeric;
    } else {
    	return this.sort_alpha;
    }
 },


 /**
  * Sort function for text types
  */
sort_alpha: function(a,b) {
    if (a[0]==b[0]) {
    	return 0;
    } else if (a[0]<b[0]) {
    	return -1;
    } else {
    return 1;
    }
 },
 
 
 /**
  * Sort function for integer types
  */
 sort_integer: function(a,b) {
	 var intA = parseInt( a, 10 );
	 var intB = parseInt( b, 10 );
	 
	 if( intA == intB ) {
		 return 0;
	 } else if( intA > intB ) {
		 return 1;
	 } else {
		return -1; 
	 }
 },
 
 /**
  * Sort function for floating point (decimal) types
  */
 sort_numeric: function(a,b) {
	     aa = parseFloat(a[0] );
	    if (isNaN(aa)) {
	    	aa = 0;
	    }
	    bb = parseFloat(b[0] );
	    //.replace(/[^0-9.-]/g,'')); 
	    if (isNaN(bb)) {
	    	bb = 0;
	    }
	    return aa-bb;
 },
 
 
 /**
  * Not yet used, a sort function for dates
  */
 sort_mmdd: function(a,b) {
	    mtch = a[0].match( _DATE_REGEX );
	    y = mtch[3]; d = mtch[2]; m = mtch[1];
	    if (m.length == 1) {
	    	m = '0'+m;
	    }
	    if (d.length == 1) {
	    	d = '0'+d;
 		}
	    dt1 = y+m+d;
	    mtch = b[0].match(sorttable.DATE_RE);
	    y = mtch[3]; d = mtch[2]; m = mtch[1];
	    if (m.length == 1) {
	    	m = '0'+m;
	    }
	    if (d.length == 1) {
	    	d = '0'+d;
	    }
	    dt2 = y+m+d;
	    if (dt1==dt2) {
	    	return 0;
	    }
	    if (dt1<dt2) {
	    	return -1;
	    }
	    return 1;
	  }


});



------------------------------------
 SynchronizePeerBindings.properties 

echo.uicomponent.table.DataTable       echo.uicomponent.table.DataTablePeer