Archive | Dojo TreeGrid RSS for this section

Dojo Data Grids – Sample Database on OpenNTF

I’ve added a demo database with 14 dojo grid samples to OpenNTF.

http://www.openntf.org/Internal/home.nsf/project.xsp?action=openDocument&name=Dojo%20Data%20Grids%20in%20XPages

The application includes 14 demo grids that implement several features and variations of Dojo Data Grids in XPages.

  1. Basic Features
  2. Sorting – Only allow even-numbered columns to sort
  3. Opening Documents
  4. Full-Text Search
  5. Field-Specific Search
  6. Editable Columns – Including highlighting edited rows
  7. HTML and Icon Columns
  8. Enhanced Grid with Drag and Drop plugin
  9. Enhanced Grid with Filter plugin
  10. Enhanced Grid with Print plugin
  11. Enhanced Grid with Export plugin
  12. Enhanced Grid with Context Menu plugin
  13. Categorized Tree Grid
  14. Categorized Tree Grid with Totals
Advertisements

Upcoming TLCC Webinar: Dojo Grids in XPages

Join me next Thursday (6/27) at 10:30am Eastern for the next TLCC webinar.

I’ll show how you can provide a new look and feel and rich functionality with several variations of Dojo data grids. You’ll learn about the XPages Dojo Data Grid control and its key features, including infinite scrolling, sorting, and editable cells. Then you’ll see how to transform the grid into a Dojo EnhancedGrid and take advantage of enhanced plugins to provide even more functionality, such as multi-rule filtering and context menus. Finally, you’ll get a glimpse of how to bypass the grid control in order to create a categorized Dojo TreeGrid.

Click here to register.

Categorized Dojo TreeGrid in XPages – Add Totals and Counts

In a previous post, I showed how to set up a Dojo TreeGrid to create a categorized grid. In this post, I’ll show how to create a TreeGrid that can calculate entry counts and totals for the categories.

TreeGrid_a_SumOneCol

When expanded, the totals show at the bottom of the category section. When collapsed, the totals show at the category level.

Required Modules and XPage Property

See the previous post for information on including the required dojo modules and style sheets and setting the XPage property to parse dojo on load.

JSON Data Store

As in the previous post, I showed how to use an XAgent to generate my own JSON in the format required by the TreeGrid. In this post, we’ll use the same technique, but the format of the JSON must be different for the categorized view with totals.

Fortunately, this format is simpler to generate.

In thise case, we are no longer building the list of references to child items and then generating the child items separately; it now requires inclusion of the child items as an array of items under a property of the category item.

Here’s a chunk of JSON that populates the first category in the view shown above.

The first item in the list is the ‘AK’ category. It has an array of child items for all of the entries within that category. The property that contains the child items (named childItems in this code) can be whatever you want.

{
identifier: 'id',
label: 'name', 
items: [
{id:'AK', type: 'state', state:'AK', numPeople: 3, childItems:[ 
  {id:'B3093953178C98E905257838007ABC48', firstname:'Bella', lastname: 'Martin', valueToAdd: 2}, 
  {id:'7FDB9CCDE7D6923E05257838007ABC1E', firstname:'Brian', lastname: 'Leggett', valueToAdd: 2}, 
  {id:'8CD685A9D29150D005257838007ABDD7', firstname:'Jesse', lastname: 'Pack', valueToAdd: 2}, 
  {id:'B281D25B6AE91D7A05257838007ABD53', firstname:'Jose', lastname: 'Bibeau', valueToAdd: 2}, 
  {id:'A4CA4CA43B93673605257838007ABC7E', firstname:'Karen', lastname: 'Buss', valueToAdd: 2}, 
  {id:'5656D38EC6DB315E05257838007ABA5D', firstname:'Margarita', lastname: 'Levesque', valueToAdd: 2}, 
  {id:'B2CC1FA115074A9305257838007ABEEA', firstname:'Mary', lastname: 'Witzel', valueToAdd: 2}, 
  {id:'EDDFFA8D2C2169B405257838007ABB03', firstname:'Rufus', lastname: 'Davis', valueToAdd: 2}
] }, 

...

]}

Here is the code that generates the data, based on a view from the FakeNames database, categorized by State. It is very similar to the code in the previous post, but it just builds one string of data per category, rather than two separate strings in the previous code.

var externalContext = facesContext.getExternalContext();
var writer = facesContext.getResponseWriter();
var response = externalContext.getResponse();

response.setContentType('application/json');
response.setHeader('Cache-Control', 'no-cache');

writer.write("{\n");
writer.write("identifier: 'id',\n");
writer.write("label: 'name', \n");
writer.write("items: [\n");

var categoryAndChildren = "";

// Walk the view and build the JSON data
var vw:NotesView = database.getView('ByState');
var nav:NotesViewNavigator = vw.createViewNav();
var ve:NotesViewEntry = nav.getFirst();

while (ve != null) {
  var cv = ve.getColumnValues();

  // When a categorized entry is reached:
  // (a) write out the previous category and children
  // (b) set up the new category element	
  if (ve.isCategory()) {
    // Write out the previous category and child entries		
    if (categoryAndChildren != "") {
      // Trim the trailing comma and space off the category item, close of the childItems array (]) and the category item (})
      categoryAndChildren = categoryAndChildren.substring(0, categoryAndChildren.length - 2);
      writer.write("\n" + categoryAndChildren + "] }, ");
    }	
	
    // Start building the new category and child entries
    categoryAndChildren = "  {id:'" + cv[0] + "', type: 'state', state:'" + cv[0] + "', numPeople: 3, childItems:[ \n";
	
  } else {
    // This isn't a category, so add another child item
    categoryAndChildren += "{id:'" + ve.getUniversalID() + "', firstname:'" + cv[1] + "', lastname: '" + cv[2] + "', valueToAdd: 2}, "
  }	
	
  // Get the next entry and recycle the current one
  var tmpentry = nav.getNext();
  ve.recycle();
  ve = tmpentry;
}

// Write out the last category and children, without the trailing commas
categoryAndChildren = categoryAndChildren.substring(0, categoryAndChildren.length - 2);
writer.write("\n" + categoryAndChildren + "] }, ");

// Close the JSON string
writer.write("]}");
writer.endDocument();

Grid Generation Code – Category Totals

The grid generation is also a bit different in this case. In the first categorized grid, all we had to do was define a few columns for the grid layout.

In this case, we need to define a more complex layout.

Lines 1-15 define the layout for the grid. Line 4 defines the State column at the category level. Line 5 defines the property of the category that will contain the array of child documents. I named the field childItems, but that is not a reserved name. However, in line 6, children is a reserved word. That must be the name of the property that contains the array of child documents. Line 11 defines that the grid will add the values in each column and display the sum total for each category.

Line 17 reads the JSON data that is generated by the XAgent page.

Lines 19-26 define the grid itself. The structure property must point to the layout that was defined above and the store property must point to the JSON data store. As mentioned in the previous post, the query property is required or else the grid will display erroneous duplicate entries. Line 26 defines the div tag where the grid will be generated, so that div tag must exist on the page.

var layout = [ 
  { cells: [
    [ 
      {field: "state", name: "State"},
      {field: "childItems",   
        children: [ 
          { field: "firstname", name: "First Name" },
          { field: "lastname", name: "Last Name" },
          { field: "valueToAdd", name: "Totals Col" }  
        ], 
      aggregate: "sum"
      }
    ]] 
  }					
]
			
var jsonStore = new dojo.data.ItemFileWriteStore({ url: "TreeGrid_DataStore_TotalsAndCounts.xsp"});

var grid = new dojox.grid.TreeGrid({
  structure: layout,
  store: jsonStore,
  query: {type: 'state'},
  queryOptions: {deep: true},
  rowSelector: true,
  openAtLevels: [10]
}, dojo.byId("treeGrid"));

grid.startup();
dojo.connect(window, "onresize", grid, "resize");

This code generates the grid shown below:

TreeGrid_b_WithTotals_SumsAllCols

Formatter Functions

By default it will attempt to sum every column in the child items. For numeric values, this is what you want, but for text values, it will just concatenate the values as shown above.

Fortunately, we can prevent that with a formatting function. Each child entry can use a formatter property that takes the name of a function to format the data.

The formatter function automatically accepts two parameters, the value in the current cell and a row index variable that denotes the level of categorization.

The child item rows have a level number that matches the nth category in the view, starting with 0. The category total rows have a negative number that corresponds to the level of categorization. Top level category rows will be -1, second-level category rows will be -2, and so on.

This formatter function will not display anything in the category totals row. To update the grid to only add up the numeric column, we will add this formatter function to the text columns.

function formatText(value, rowIndex) {
  if (rowIndex >= 0) {
    return value;
  } else {
    return '';
  }
}

Then update the text columns (First Name and Last Name, below) to use the formatter function.

var layout = [ 
  { cells: [
    [ 
      {field: "state", name: "State"},
      {field: "childItems",   
        children: [ 
          { field: "firstname", name: "First Name", formatter: formatText},
          { field: "lastname", name: "Last Name", formatter: formatText },
          { field: "valueToAdd", name: "Totals Col" }  
        ], 
      aggregate: "sum"
      }
    ]] 
  }					
]

TreeGrid_a_SumOneCol

Much better.

Row Counts

If you’d like to do a row count instead of a sum, all you need to do is change a couple of lines in the grid layout object.

Line 8 was changed to remove the formatter function so it will display the row count.

Line 10 was changed to set the aggregation type to cnt rather than sum

var layout = [ 
  { cells: [
    [ 
      {field: "state", name: "State"},
      {field: "childItems",   
        children: [ 
          { field: "firstname", name: "First Name", formatter: formatText}, 
          { field: "lastname", name: "Last Name"}
        ], 
      aggregate: "cnt"
      }
    ]] 
  }					
]

TreeGrid_c_count

openAtLevels

You may have noticed the openAtLevels attribute in the grid generation code earlier in this post.

This is a property of the tree grid that can be used to define category expansion at the time of grid generation. The attribute takes an array of values, each of which can be true, false, or a number. If you have multiple levels of categorization, then each element in the array will apply to the corresponding category level. In our case, we only have one level of categorization, so there’s only one element.

If you set it to true, it will expand all categories at that level. If you set it to false, it will collapse all categories at that level. If you give it a number, then it will automatically expand only the categories that have that many child items or less.

This property has no bearing on the grid’s ability to calculate totals or row counts.

Categorized Dojo TreeGrid in XPages – Additional Features

In the last post I showed how to create a categorized Dojo TreeGrid. In this post, we’ll take a look at a few extra features available to the grid.

The code from the last post serves as the baseline and this post will highlight any changes that are required.

defaultOpen

The defaultOpen property can be added to define whether the grid should be expanded when rendered.

var grid = new dojox.grid.TreeGrid({
  treeModel: treeModel,
  structure: layout,
  defaultOpen: true
}, 'treeGrid');

TreeGrid_2_a_DefaultOpen

expandoCell

The expandoCell property defines which cell should include the expand/collapse icon. It’s a 0-based index, so to put the icon in the second column, give it a value of 1.

var grid = new dojox.grid.TreeGrid({
  treeModel: treeModel,
  structure: layout,
  expandoCell: 1
}, 'treeGrid');

TreeGrid_2_b_ExpandoCell

Up Next

In the next post, I’ll show how to add counts and totals to the TreeGrid.

Create a Categorized Dojo TreeGrid in XPages

Neither the Dojo DataGrid nor the Dojo EnhancedGrid provide the ability to categorize data, but there is another grid module called the TreeGrid that you can use if you need a categorized grid. In this short series, we’ll take a look at how to create a TreeGrid and customize it.

Categorized Grid

Here’s a screen shot of what the data from the FakeNames database looks like when categorized by State:

TreeGrid_1_a

Programmatic Declaration

For this type of grid, we’ll be declaring it programmatically and not with the Dojo Data Grid control from the Extension Library / 8.5.3 UP1 / Notes9. Using a technique similar to this post it is possible to instruct a Dojo Data Grid control to render as a TreeGrid, but the data provided to the grid must be in a significantly different format than a DataGrid or EnhancedGrid, so I took a different approach to create this one.

Steps

At a high level, the steps to create the categorized grid are as follows:

  1. Include the required dojo modules and style sheets
  2. Set the XPage to parse dojo on load
  3. Define a div to render the grid
  4. Execute code onClientLoad to create the grid
  5. Provide the data for the grid

1. Include the required dojo modules and style sheets

Along with the TreeGrid module, two additional modules are required for the grid’s data store. The ItemFileWriteStore is a standard data source object, but the ForestStoreModel is also required in order to format the data properly for the TreeGrid.

In addition, you’ll need to include several dojo stylesheets. The Dojo Data Grid control loads some of these on its own, but we’ll need to include them manually because we’re not using the control this time.

The resources of the page should look like this:

<xp:this.resources>
  <xp:dojoModule name="dojox.grid.TreeGrid"></xp:dojoModule>
  <xp:dojoModule name="dijit.tree.ForestStoreModel"></xp:dojoModule>
  <xp:dojoModule name="dojo.data.ItemFileWriteStore"></xp:dojoModule>

  <xp:styleSheet
    href="/.ibmxspres/dojoroot/dojox/grid/resources/Grid.css">
  </xp:styleSheet>
  <xp:styleSheet
    href="/.ibmxspres/dojoroot/dijit/themes/tundra/tundra.css">
  </xp:styleSheet>
  <xp:styleSheet
    href="/.ibmxspres/dojoroot/dojox/grid/resources/tundraGrid.css">
  </xp:styleSheet>		
</xp:this.resources>

2. Set the XPage to parse dojo on load

The XPage’s Trigger Dojo parse on load property must be selected, or the grid will not be rendered. (This is another thing that you don’t have to set manually when using the grid control.)

TreeGrid_1_b

3. Define a div to render the grid

Since we’re not using the control to automatically define the place to render the grid, we just need to add a div tag to the page and give it an ID so we can reference it to draw the grid.

<div id="treeGrid"></div>

4. Execute code onClientLoad to create the grid

The code below actually defines and creates the grid.

Lines 1-5 define the grid layout. With the Dojo Data Grid control, the layout columns were defined by Dojo Data Grid Column controls. Since we’re generating the grid ourselves, we need to define
the layout. The category column should be listed as the first column. If it’s not included, then the expand/collapse icon will be displayed next to an ellipsis (…) without any other information that defines the category. The layout is pretty straightforward. The ‘name’ property defines the column title and the ‘field’ property defines the column name.

Line 7 sets up the data store for the grid. It retrieves the data from another XPage in the same database that provides the data in the required format. (More on that below.)

Lines 9-15 define the ForestStoreModel for the grid. This is the data model required for the categorization. It includes the data store defined above. The childrenAttrs property defines the attribute that specifies the children for each category. The query property is required to select the category items. Without this property, each set of child items is listed twice under each category, but only one set of the child items will actually collapse.

Lines 17-22 actually create the grid and define the data model and the grid layout that were set up earlier in the code. The last parameter in line 20 is the ID of the div where the grid will be rendered.

var layout = [
  { name: "State", field: "state"},
  { name: "First Name", field: "firstname"},
  { name: "Last Name", field: "lastname"}
];
			
var jsonStore = new dojo.data.ItemFileWriteStore({ url: "TreeGrid_DataStore.xsp"});

var treeModel = new dijit.tree.ForestStoreModel({
  store: jsonStore,
  query: {type: 'state'},
  rootId: 'personRoot',
  rootLabel: 'People',
  childrenAttrs: ['children']
});

var grid = new dojox.grid.TreeGrid({
  treeModel: treeModel,
  structure: layout
}, 'treeGrid');

grid.startup();

dojo.connect(window, "onresize", grid, "resize");

5. Provide the data for the grid

This is much more involved than in previous grids, because the data must be in a customized format that the built-in REST services do not provide.

This sample data, taken from the dojo documentation, shows the hierarchy. This shows the code for a category that has two children displaying underneath it.

...
{ id: 'AS', name:'Asia', type:'continent',
children:[{_reference:'CN'}, {_reference:'IN'}] },
{ id: 'CN', name:'China', type:'country' },
{ id: 'IN', name:'India', type:'country' },
...

Each category item must have a children item, containing a list of references to the child elements by their ID.

In order to provide this data, I wrote code to walk through a categorized view and write out the required JSON as an XAgent. The code in the section above references the XAgent page in order to read the data.

Check out Stephan Wissel’s post if you’re unfamiliar with the concept of an XAgent

// Read view data and write out the JSON data for the categorized grid
// Each view entry is written out at the top level, but each category has a children property that is an array of IDs of child entries to indent.
// There can be any number of categorization levels -- just add 'children' properties to child entries.
// NOTE: It needs the newlines after each line between the write() statements or the browser doesn't see the output as JSON

var externalContext = facesContext.getExternalContext();
var writer = facesContext.getResponseWriter();
var response = externalContext.getResponse();

response.setContentType('application/json');
response.setHeader('Cache-Control', 'no-cache');

writer.write("{\n");
writer.write("identifier: 'id',\n");
writer.write("label: 'name', \n");
writer.write("items: [\n");

var categoryItem = "";
var childItems = "";

// Walk the view and build the JSON data
var vw:NotesView = database.getView('ByState');
var nav:NotesViewNavigator = vw.createViewNav();
var ve:NotesViewEntry = nav.getFirst();

while (ve != null) {
	var cv = ve.getColumnValues();

	// When a categorized entry is reached:
	// (a) write out the previous category and children
	// (b) set up the new category element	
	if (ve.isCategory()) {
		// Write out the previous category and child entries		
		if (categoryItem != "") {
			// Trim the trailing comma and space off the category item. 
			// (The child items still need to end with a comma before the next category item starts.)
			categoryItem = categoryItem.substring(0, categoryItem.length - 2);
			writer.write("\n" + categoryItem + "] }, \n" + childItems);
		}	
	
		// Start building the new category and child entries
		categoryItem = "  {id:'" + cv[0] + "', type: 'state', state:'" + cv[0] + "', children:[";
		childItems = "";
	
	} else {
		// This isn't a category, so simultaneously build the children property and the child entries, until the next category is reached.
		categoryItem += "{_reference:'" + ve.getUniversalID() + "'}, ";
childItems += "{id:'" + ve.getUniversalID() + "', firstname:'" + cv[1] + "', lastname: '" + cv[2] + "'}, "
	
	}	
	
	// Get the next entry and recycle the current one
	var tmpentry = nav.getNext();
	ve.recycle();
	ve = tmpentry;
}


// Write out the last category and children, without the trailing commas
categoryItem = categoryItem.substring(0, categoryItem.length - 2);
childItems = childItems.substring(0, childItems.length - 2);
writer.write("\n" + categoryItem + "] }, \n" + childItems);

// Close the JSON string
writer.write("]}");
writer.endDocument();

Strangely, the newline (\n) characters were required when writing out the data. Otherwise, the response was not interpreted as JSON — the browser would return nothing.

In line 42, you can see that I’m adding a property type: ‘state’ to each category element. I don’t need to display this value, but, as I mentioned above, it is required for the ForestStoreModel’s query parameter in order to properly categorize the documents without duplicating entries.

Important Performance Note

Because I am writing out the JSON for the entire view, this will be loading all items from the view up front, so there’s overhead in this method. Fortunately, JSON data is compressed with gzip. In this case, 1,301 records was 27k. If you need to work with larger data sets, then you may need to consider either rolling your own REST service or passing a parameter to the XAgent page to search and limit the amount of data that is generated.

Up Next

In the next post, I’ll review some of the properties available to the TreeGrid.