Skip to content
Martin Wendt edited this page Apr 9, 2021 · 29 revisions

About Fancytree table, grid, gridnav, and ariagrid extension.

Render tree as a table (aka tree grid) and support keyboard navigation in a grid with embedded input controls.

The navigation logic is handeled by a separate extension that comes in two flavors (ext-gridnav and ext-ariagrid). While the latter is still experimental, it may eventually become the new standard.

This document describes the following extensions:

  • ext-table
    A tree grid implementation.
  • ext-grid (experimental, new with v2.31)
    A variant of ext-table, that implements a viewport concept in order to support huge data models.
  • ext-gridnav
    This extension can be combined with ext-table or ext-grid to implement keyboard navigation for cells and embedded input controls.
  • ext-ariagrid
    Variant of ext-gridnav, that implements the W3C-ARIA Authoring Practices for Treegrid.
    This extension can be combined with ext-table or ext-grid to implement keyboard navigation for cells and embedded input controls.

Options

Options of the ext-table extension:

  • checkboxColumnIdx, type: {integer}, default: null
    Render the checkboxes into the nth column (null: prepend to node title column).

  • customStatus, type: {boolean}, default: false
    true: generate renderColumns events for status nodes Deprecated since 2.15! Use the global tree event renderStatusColumns instead.

  • indentation, type: {integer}, default: 16
    Indent every node level by 16px.

  • mergeStatusColumns, type: {boolean}, default: true
    Display tree status messages ('Loading...', 'No data', 'Error') in a single merged cell instead of the title column. @since 2.30

  • nodeColumnIdx, type: {integer}, default: 0
    Render node expander, icon, and title to the nth column.

Options of the ext-grid extension:

  • See ext--table: checkboxColumnIdx, indentation, mergeStatusColumns, and nodeColumnIdx.

Options of the ext-gridnav extension:

  • autofocusInput, type: {boolean}, default: false
    Focus first embedded input if node gets activated.

  • handleCursorKeys, type: {boolean}, default: true
    Allow UP/DOWN in inputs to move to prev/next node.

Options of the ext-ariagrid extension:

  • cellFocus, type: {string}, default: "allow"
    "allow" | "start" | "force" | "off".

Events

  • renderColumns (ext-table, ext-grid)
    Render table columns for this node's <tr>. Note that the columns defined by nodeColumnIdx and checkboxColumnIdx have already been handled by default, when this event is triggered.
    Note also that static markup (for example <input> elements) could also be created in the createNode event, thus preventing replacing the content every time a node is redrawn.
    See examples below.

  • renderStatusColumns (ext-table, ext-grid)
    Custom rendering callback for status nodes (i.e. 'errors', 'loading...', etc.).
    If this option is set to true instead of a callback, renderColumns is called, even for status nodes.
    If this option is set to false, which is the default, standard rendering is performed.
    @since v2.15

  • updateViewport (ext-grid)
    Called when a ext-grid has redrawn the viewport. @since v2.31

  • activateCell (ext-ariagrid)
    Called when a single cell is activated. @since v2.30

  • defaultGridAction (ext-ariagrid)
    Called when Enter is pressed on a row or cell. @since v2.30

Methods

  • adjustViewportSize (ext-grid)
    [ext-grid] Calculate the viewport count from current scroll wrapper height. @since v2.31

  • isViewportBottom (ext-grid)
    [ext-grid] Return true if viewport cannot be scrolled down any further. @since v2.31

  • redrawViewport (ext-grid)
    [ext-grid] Render all visible nodes into the viweport.. @since v2.31

  • setViewport(options) (ext-grid)
    [ext-grid] [ext-grid] Define a subset of rows/columns to display and redraw. @since v2.31

Example

In addition to jQuery, jQuery UI, and Fancytree, include jquery.fancytree.table.js and optionally jquery.fancytree.gridnav.js:

<script src="//code.jquery.com/jquery-3.6.0.min.js"></script>

<link href="skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="js/jquery-ui-dependencies/jquery.fancytree.ui-deps.js"></script>
<script src="js/jquery.fancytree.js"></script>
<script src="js/jquery.fancytree.gridnav.js"></script>
<script src="js/jquery.fancytree.table.js"></script>

We also define an empty table with its headers:

<table id="tree">
	<colgroup>
	<col width="30px"></col>
	<col width="30px"></col>
	<col width="*"></col>
	<col width="50px"></col>
	<col width="30px"></col>
	</colgroup>
	<thead>
		<tr> <th> </th> <th>#</th> <th>Name</th> <th>Custom Data</th> <th>Important</th> </tr>
	</thead>
	<tbody>
	</tbody>
</table>

The tree table extension takes care of rendering the node into one of the columns. Other columns have to be rendered in the renderColumns event.

Example:

$("#tree").fancytree({
	checkbox: true,
	titlesTabbable: true,        // Add all node titles to TAB chain
	source: SOURCE,
	extensions: ["table", "gridnav"],
	table: {
		checkboxColumnIdx: 0,    // render the checkboxes into the this column index (default: nodeColumnIdx)
		indentation: 16,         // indent every node level by 16px
		nodeColumnIdx: 2         // render node expander, icon, and title to this column (default: #0)
	},
	gridnav: {
		autofocusInput:   false, // Focus first embedded input if node gets activated
		handleCursorKeys: true   // Allow UP/DOWN in inputs to move to prev/next node
	},

//	renderStatusColumns: false,	 // false: default renderer
								 // true: generate renderColumns events, even for status nodes
                                 // function: specific callback for status nodes

	renderColumns: function(event, data) {
		var node = data.node,
			$tdList = $(node.tr).find(">td");

		// Make the title cell span the remaining columns if it's a folder:
		if( node.isFolder() ) {
			$tdList.eq(2)
				.prop("colspan", 3)
				.nextAll().remove();
			return;
		}
		// (Column #0 is rendered by fancytree by adding the checkbox)

		// Column #1 should contain the index as plain text, e.g. '2.7.1'
		$tdList.eq(1)
			.text(node.getIndexHier())
			.addClass("alignRight");

		// (Column #2 is rendered by fancytree)

		// ...otherwise render remaining columns

		$tdList.eq(3).text(node.data.myCustomData);
		$tdList.eq(4).html("<input name="important" type='checkbox' value='" + node.key + "'>");
	}
});

If cell content does not depend on changing node data or status, it may be more efficient to create this markup on node creation, and let renderColumns handle the variant rendering only:

// Called when node markup is created for the first time:
createNode: function(event, data) {
	var node = data.node,
		$tdList = $(node.tr).find(">td");

	// Span the remaining columns if it's a folder.
	// We can do this in createNode instead of renderColumns, because
	// the `isFolder` status is unlikely to change later
	if( node.isFolder() ) {
		$tdList.eq(2)
			.prop("colspan", 6)
			.nextAll().remove();
	}
	$tdList.eq(4).html("<input type='text'>");
},
// Called every time node status is changed, or `node.render()` is called:
renderColumns: function(event, data) {
	var node = data.node,
		$tdList = $(node.tr).find(">td");

	// ...

	$tdList.eq(3).text(node.data.myCustomData);
	$tdList.eq(4).find("input").val(node.data.userName);
}

An even more performant approach is to define a row template, by providing a single <tr> element in the <tbody>. Here we can define invariant markup, classes, and other attributes to initialize every node:

<table id="tree">
	<colgroup>
		...
	</colgroup>
		<thead>
			<tr> <th></th> <th>#</th> <th></th> <th>Ed1</th> <th>Rb1</th> <th>Cb</th></tr>
		</thead>
		<tbody>
			<!-- Define a row template for invariant markup and attributes: -->
			<tr>
				<td></td>
				<td></td>
				<td></td>
				<td><input name="input1" type="input"></td>
				<td><input name="cb1" type="checkbox"></td>
				<td>
					<select name="sel1">
						<option value="a">A</option>
						<option value="b">B</option>
					</select>
				</td>
			</tr>
		</tbody>
</table>

Now we only need to update non-static parts:

renderColumns: function(event, data) {
	var node = data.node,
		$tdList = $(node.tr).find(">td");

	$tdList.eq(1).text(node.getIndexHier());
	$tdList.eq(4).find("input").val(node.data.userName);
}

TIP: Specific columns can be styled using CSS rules like this:

#tree >thead th:nth-child(4),
#tree >tbody td:nth-child(4) {
	text-align: center;
}

Sometimes we need to add class names or attributes to cells of every row, for example to add responsive support with bootstrap. This may be done in the renderColumns callback:

renderColumns: function(event, data) {
	...
	$tdList.eq(2)
		.text(node.data.details)
		.addClass("hidden-xs hidden-sm");
}

However a more performant approach is to define this as part of a row template:

<table id="tree">
	<thead>
		<tr> <th>Key</th> <th>Title</th> <th class="hidden-xs hidden-sm">Details</th></tr>
	</thead>
	<tbody>
		<tr> <td /> <td /> <td class="hidden-xs hidden-sm" /> </tr>
	</tbody>
</table>

TODO: describe how to customize status nodes, e.g. use colspan to render across columns, etc.

Column Data

Sometimes we may want to maintain column-specific meta data for the tree, instead of passing this over and over again with every node.

NOTE: This pattern recommendation is not final yet.

The recommended pattern is

$("#tree").fancytree({
  // `columns` is a tree option, that can be used to define shared data per column.
  // We can pass it directly as Fancytree option like this:
  columns: [
    {tooltip: null, addClass: ""},
    {tooltip: "Detail info", addClass: "hidden-xs hidden-sm"},
    {tooltip: null, addClass: ""},
  ],
  source: { url: "/my/web/service" },
  renderColumns: function(event, data) {
    ...
    $tdList.eq(1)
      .text(node.data.details)
      .addClass(tree.columns[1].addClass)
      .attr("title", tree.columns[1].tooltip);
    $tdList.eq(2)
      .text(node.data.details)
      .addClass(tree.columns[2].addClass)
      .attr("title", tree.columns[2].tooltip);
  }

});

Sometimes it may be more convenient to get the shared columns info from the server. The source JSON data can be a list of child nodes or an object with additional meta data. Here we use the latter format to pass column information as well:

{
  "children": [
    {"title": "Books", "folder": true, "children": [
      {"title": "Little Prince", "price": 1.23},
      {"title": "The Hobbit", "price": 2.34}
    ]},
    {"title": "Computers", "folder": true, "children": [
      {"title": "PC", "price": 549.85},
      {"title": "Mac", "price": 789.00}
    ]}
  ],
  "columns": [
    {"tooltip": null, "addClass": ""},
    {"tooltip": "Detail info", "addClass": "hidden-xs hidden-sm"},
    {"tooltip": null, "addClass": ""}
  ]
}

Navigation

Keyboard navigation is implemented by additional extensions:

- [ext-gridnav](https://wwWendt.de/tech/fancytree/demo/sample-ext-table.html)
- [ext-ariagrid](https://wwWendt.de/tech/fancytree/demo/sample-aria-treegrid.html)

ext-gridnav

TODO

ext-ariagrid

This is an experimental implementation of a W3C ARIA draft as dicussed here.

General behavior summary:

  • We distinguish row-mode (a whole row is focused) and cell-mode (a single table cell is focused).
    There are two different cell-modes with different behavior concerning how embedded <input> controls are handled:
    cell-nav-mode: cursor keys always move between table cells
    cell-edit-mode: cursor keys are handled by embedded <input> controls if reasonable
  • The cellFocus option controls, which mode is available and initially active.
  • When navigating in cell-mode, the focus is set to the embedded input control (if there is one inside the table cell).
  • Enter executes a default action if one is defined.

See also here and here.

Row-Mode behavior:

  • Right: If the current node is expandable expand it.
    Otherwise switch to cell-nav-mode.
  • Left: If the current node is collapsible, collapse it.
    Otherwise focus parent node (if there is one).
  • Up / Down: focus previous / next row (stop on last row).
  • Home, End: focus first / last row.
  • Enter: perform the default action for the node (invokes defaultGridAction event).

Cell-Nav-Mode behavior:

  • Right: Move focus to right neighbor cell (do nothing if the right most cell is active).
  • Left: Move focus to left neighbor cell.
    If the left most cell is active, switch to row-mode.
  • Up / Down: focus cell above / below (stop on borders).
  • Home / End: Move to first/last column.
  • Ctrl+Home / Ctrl+End: Move to first/last row (focus is kept in the same column).
  • Enter: If the cell contains an embedded <input>, focus that and switch to cell-edit-mode.
    Otherwise invoke defaultGridAction event and perform default action (unless false was returned):
    • Checkbox: toggle selection
    • Link:
  • Space: toggle embedded checkbox if any.
  • Alpha-numeric keys: Passed to embedded input if any.
  • Esc: switch to row-mode.

Cell-Edit-Mode behavior:

  • Left / Right: Let <input> control handle it, if it can (e.g. text input), otherwise same behavior as cell-nav-mode.
  • Up / Down: Let <input> control handle it, if it can (e.g. select box or numeric spinner input), otherwise same behavior as cell-nav-mode.
  • Home / End: ...
  • Enter: In row-mode: switch to cell-nav-mode. In cell-nav-mode: execute default action or switch to cell-edit-mode for embedded text input. In cell-edit-mode: switch back to cell-nav-mode.
  • Esc: In cell-nav-mode: switch to row-mode. In cell-edit-mode: switch to cell-nav-mode.

Experimental extended mode adds this behavior (not part of WAI-ARIA):

  • Embedded inputs get the focus, but we distinguish cell-nav-mode and cell-edit-mode. cell-nav-mode steals Up, Down, Left, Right, Home, End fromt input controls to always allow cell navigation.
  • Enter: In row-mode: switch to cell-nav-mode. In cell-nav-mode: execute default action or switch to cell-edit-mode for embedded text input. In cell-edit-mode: switch back to cell-nav-mode.
  • Esc: In cell-nav-mode: switch to row-mode. In cell-edit-mode: switch to cell-nav-mode.

Fixed Headers

The ext-table extension maintains a <table> element and generates <tr> rows, but does not modify the <thead> headers. So it should be possible to use existing JavaScript plugins that implement fixed table headers.

There is also an experimental extension available: ext-fixed.

Recipes

[Howto] Make a table scrollable

Wrap the table into a scrollable <div>

<div id="scrollParent" style="height: 150px; overflow: auto;">
	<table id="tree">
		...
	</table>
</div>

and tell Fancytree to use this as scrollParent:

$("#tree").fancytree({
	extensions: ["table", "gridnav"],
	source: SORUCE,
	scrollParent: $("#scrollParent"),
	table: {
		...
	},

Alternativley, we can use the browser viewport as scroll parent:

$("#tree").fancytree({
	...
	scrollParent: $(window),
	...
[Howto] Add columns dynamically

If the table layout was modified, for example by adding or removing a column, we also need to update the template that Fancytree uses to render rows, e.g.:

// For example append a header column
$("#tree thead tr").append("<th>New</th>");

// Update the row template accordingly (available as tree.rowFragment)
$(">tr", tree.rowFragment).append("<td>");

// Redraw all
tree.render(true);
[Howto] Implement paging

TODO: describe addPagingNode() etc.

[Howto] Make column and row headers fixed

TODO.