Archive | May 2013

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.

Get All Properties of an Object in JavaScript

I often have the need to find all of the properties of an object in order to determine what is available to work with. In this post, I’ll show you a handy function that you can use to provide this information.

As I worked through many of the posts in the Dojo Data Grid series, I was routinely slowed down by the lack of detailed documentation on many of the grid and plugin event properties. This function was very useful in inspecting the properties and giving me ideas for new approaches to try.

Fortunately, JavaScript makes it easy to inspect an object and display all of its properties. This function can be called with an object parameter and it will display a list of properties of the object.

function getAllProperties(obj) {
  var properties = '';
  for (property in obj) {
    properties += '\n' + property;
  }
  alert('Properties of object:' + properties);
}

Dojo Data Grid – Part 18: EnhancedGrid Context Menus

The Menu plugin for the Dojo EnhancedGrid gives you the ability to add context menus to grid headers, rows, cells, and selected regions. In this post, I’ll show how to add the plugin and use it.

Dojo Data Grid Series

This post assumes you already have a Dojo Data Grid control set up to use the Dojo EnhancedGrid, based on the instructions in this post.

Load the Menu Plugin (and more)

The dojox.grid.enhanced.plugins.Menu module must be included on the page, so add it to the page resources. Properties > Resources > Add > Dojo Module…

Dojo Grid 18 - 1 - Modules

You also need to include the dijit menu and dijit menu item modules, since they’re required to build the context menus.

In this grid, I’m also including the Printer plugin, so I can trigger printing functions from the context menus.

Add the Plugin to the Grid

The first step made the plugin module available, but you also need to add it to the grid.

Go to the properties of the Dojo Data Grid > Dojo and click the Add button. Add two properties as shown here:

Dojo Grid 18 - 2 - Dojo Properties

This is the first example of including more than one EnhancedGrid plugin.

contextMenus is the name of a JavaScript object I’ve created to build the context menus (shown below).

Note: The row selection property is necessary in order to select rows in the grid.

Creating the Menus

To create the context menus, you create an object that contains properties for all of the context menus to define. You then add menu items to those menus and provide onClick events to take action when a context menu option is selected. Example code is shown below.

Using the Context Menus

Right click on column header
Dojo Grid 18 - 3 - Cell Header Context Menu

Right click on row selector
Dojo Grid 18 - 4 - Row Context Menu

Right click on a cell
Dojo Grid 18 - 5 - Cell Context Menu

Right click on a selected region
Dojo Grid 18 - 6 - Selected Region Context Menu

Context Menu Code

This code builds all four types of context menus and provides several actions:

// Set up the context menu object dijit menus
var contextMenus = {
  headerMenu: new dijit.Menu(),
  rowMenu: new dijit.Menu(),
  cellMenu: new dijit.Menu(),
  selectedRegionMenu: new dijit.Menu()
};

// Header Context Menu 
contextMenus.headerMenu.addChild(new dijit.MenuItem({label: "Print All", onClick:printAll}));
contextMenus.headerMenu.addChild(new dijit.MenuItem({label: "Print Selected", onClick:printSelected}));
contextMenus.headerMenu.addChild(new dijit.MenuItem({label: "Print Custom", onClick:printCustomized}));
contextMenus.headerMenu.startup();

// Row Context Menu
contextMenus.rowMenu.addChild(new dijit.MenuItem({label: "Display Click Location", onClick: rowDisplayClickLocation}));
contextMenus.rowMenu.addChild(new dijit.MenuItem({label: "Preview All", onClick:previewAll}));
contextMenus.rowMenu.addChild(new dijit.MenuItem({label: "Preview Selected", onClick:previewSelected}));
contextMenus.rowMenu.addChild(new dijit.MenuItem({label: "Preview Custom", onClick:previewCustomized}));
contextMenus.rowMenu.startup();

// Cell Context Menu
contextMenus.cellMenu.addChild(new dijit.MenuItem({label: "Display Click Location", onClick:cellDisplayClickLocation}));
contextMenus.cellMenu.addChild(new dijit.MenuItem({label: "Cell Menu Item 1", iconClass:'dijitEditorIcon dijitEditorIconCopy',onClick:function(){alert('copy!')}}));
contextMenus.cellMenu.addChild(new dijit.MenuItem({label: "Cell Menu Item 2", iconClass:'dijitEditorIcon dijitEditorIconPaste', onClick:function(){alert('paste!')}}));
contextMenus.cellMenu.addChild(new dijit.MenuItem({label: "Cell Menu Item 3", iconClass:'dijitEditorIcon dijitEditorIconCut', onClick:function(){alert('cut!')}}));
contextMenus.cellMenu.startup();

// Selected Region Context Menu
contextMenus.selectedRegionMenu.addChild(new dijit.MenuItem({label: "Context Info", onClick:function(){alert('row: ' + rowIndex + '\ncell: ' + cellIndex);}}));
contextMenus.selectedRegionMenu.addChild(new dijit.MenuItem({label: "Alert", onClick:function(){alert('selected region')}}));
contextMenus.selectedRegionMenu.startup();

Note: There are references to print and print preview functions from the Printer plugin for the EnhancedGrid. The code for those functions can be found in this post.

Working with the Click Context

What we’ve seen so far is fine to trigger javascript events, but, commonly, you will want to know the context of the click so you can execute logic targeted to that context.

At least 1 of 4 events is fired on the grid when the user right clicks to bring up a context menu:

  • onRowContextMenu(e)
  • onCellContextMenu(e)
  • onHeaderCellContextMenu(e)
  • onSelectedRegionContextMenu(e)

These event handlers all automatically get a handle to an event object (e) that has context information.

To use them, you need to attach a function to the event. In that function, you can set global JavaScript variables with context information. Then, in your context menu item functions, you can access that context information.

To retrieve and use the location of a context menu click, follow these steps:

1. Define global variables and the event handler function(s) in an Output Script tag or client-side JavaScript library:

var rowIndex;
var cellIndex;
		
// onRowContextMenu Event Handler
// Retrieves the rowIndex and cellIndex and stores them in a global variable for the menu click event handlers to reference
// NOTE: This event fires when clicking on a row selector or any cell in the row
function rowContextMenuEvent (e) {
  rowIndex = e.rowIndex;
  cellIndex = e.cellIndex;
}

// onCellContextMenu Event Handler
// Retrieves the rowIndex and cellIndex and stores them in a global variable for the menu click event handlers to reference
// NOTE: This event fires when clicking on any cell in the row. If an onRowContextMenu event handler is also defined, that will fire before this event handler fires.
function cellContextMenuEvent (e) {
  rowIndex = e.rowIndex;
  cellIndex = e.cellIndex;
}

2. Attach the event handler functions to the grid events on the onClientLoad event of your page. Here are examples of attaching to two of the events:

dojo.connect(dijit.byId("#{id:djxDataGrid1}"), "onRowContextMenu", rowContextMenuEvent);
dojo.connect(dijit.byId("#{id:djxDataGrid1}"), "onCellContextMenu", cellContextMenuEvent);

3. Create one or more menu action functions that refer to the global variables and then work with the context.

// Row context menu function to display the index of the row
function rowDisplayClickLocation () {
  alert('row index: ' + rowIndex + '\ncell index: ' + cellIndex);		
}
		
// Cell context menu function to display the index of the row and cell
function cellDisplayClickLocation () {
  alert('row index: ' + rowIndex + '\ncell index: ' + cellIndex);		
}

When the user right-clicks on a row selector to bring up the row context menu, the onRowContextMenu event function runs first, then the context menu is displayed, then the user selects an option from the context menu.

It is interesting to note that if you have event handlers defined for onRowContextMenu and onCellContextMenu, the onRowContextMenu event handler will run first, then the onCellContextMenu event handler will run second. If all you’re doing is getting the row and cell index where the click happend, you don’t even need the onCellContextMenu event handler.

Properties of the Event Object

Just for kicks, I ran a little script to tell me all of the properties available to that event object that’s provided to the context menu event handlers.

Here’s the list, in case you’d like to dig into any of them further:

rowNode, rowIndex, dispatch, grid, sourceView, cellNode, cellIndex, cell, type, target, currentTarget, eventPhase, bubbles, cancelable, timeStamp, defaultPrevented, stopPropagation, preventDefault, initEvent, stopImmediatePropagation, which, rangeParent, rangeOffset, pageX, pageY, isChar, screenX, screenY, mozMovementX, mozMovementY, clientX, clientY, ctrlKey, shiftKey, altKey, metaKey, button, buttons, relatedTarget, mozPressure, mozInputSource, initMouseEvent, initNSMouseEvent, getModifierState, originalTarget, explicitOriginalTarget, preventBubble, preventCapture, getPreventDefault, isTrusted, view, detail, initUIEvent, layerX, layerY, cancelBubble, NONE, CAPTURING_PHASE, AT_TARGET, BUBBLING_PHASE, MOUSEDOWN, MOUSEUP, MOUSEOVER, MOUSEOUT, MOUSEMOVE, MOUSEDRAG, CLICK, DBLCLICK, KEYDOWN, KEYUP, KEYPRESS, DRAGDROP, FOCUS, BLUR, SELECT, CHANGE, RESET, SUBMIT, SCROLL, LOAD, UNLOAD, XFER_DONE, ABORT, ERROR, LOCATE, MOVE, RESIZE, FORWARD, HELP, BACK, TEXT, ALT_MASK, CONTROL_MASK, SHIFT_MASK, META_MASK, SCROLL_PAGE_UP, SCROLL_PAGE_DOWN, MOZ_SOURCE_UNKNOWN, MOZ_SOURCE_MOUSE, MOZ_SOURCE_PEN, MOZ_SOURCE_ERASER, MOZ_SOURCE_CURSOR, MOZ_SOURCE_TOUCH, MOZ_SOURCE_KEYBOARD

Dojo Data Grid – Part 17: EnhancedGrid Drag and Drop

The DnD (Drag and Drop) plugin for the Dojo EnhancedGrid gives you the ability to rearrange grid columns and rows. It also gives you the ability to drag cell contents to other cells. In this post, I’ll show how to add the plugin and use it.

Dojo Data Grid Series

This post assumes you already have a Dojo Data Grid control set up to use the Dojo EnhancedGrid, based on the instructions in this post.

1. Load the DnD Plugin

The dojox.grid.enhanced.plugins.DnD module must be included on the page, so add it to the page resources. Properties > Resources > Add > Dojo Module…
Data Grid 17 - 1 - Add Module

2. Add the Plugin to the Grid

The first step made the plugin module available, but you also need to add it to the grid.

Go to the properties of the Dojo Data Grid > Dojo and click the Add button. Add two properties as shown here:
Data Grid 17 - 2 - Add Plugin

Note: The row selection property is necessary in order to rearrange rows in the grid.

Moving Columns

To move a column, click on the column header to select the column, release the mouse button, then click in any column cell and drag it to the left or right. When you let it go, it will drop into the new location.

Along with rearranging the columns, it will refresh the grid and move you back to the top.

Before
Data Grid 17 - 3a - Before Column Move

After
Data Grid 17 - 3b - After Column Move

Moving Rows

To move a row, click on the row selector and then click in any cell in the row and drag it to a new location.

You can even select multiple rows and move them at the same time. If you select non-adjacent rows, they will all move to the new location together and will become adjacent.

Before
Data Grid 17 - 4a - Before Row Move

After
Data Grid 17 - 4b - After Row Move

Moving Cells

To move data from one cell to another. Click on the cell to select it, release the mouse button, then click on the selected cell and drag it to another cell. Once you drop it, the originating cell will be blank and the target cell will be overwritten with the data that you selected.

You can move data from multiple cells, but you cannot move data from multiple non-adjacent cells.

Before
Data Grid 17 - 5a - Before Cell Move

After
Data Grid 17 - 5b - After Cell Move

Note: If you are using a restJsonService, you cannot move cells unless your website document is set up to allow Post actions. It works fine with a restViewItemFileService either way.

IMPORTANT: It saves the changes automatically when you move cells! (When you move a row or column, it does not alter the original data.)

More Options

You can create a configuration object for the plugin to limit the options available (eg prevent row, column, or cell movement.)

You can even configure the plugin to allow you to drag data out of the grid and into another grid.

See the plugin documentation for more information.

Presenting at ATLUG on 5/16

I will be delivering a session on XPages development at the ATLUG meeting on 5/16. The topic is: Presenting Data Effectively in XPages.

Here’s the abstract:

A critical feature of any application is the organization and display of the data. In the Lotus Notes client, data views generally all look the same, but when designing an application for the web, users expect more and XPages delivers!

In this session, we will review many options for displaying data. We will cover core controls (including the View Panel and Repeat Control), Extension Library Controls (including the Data View and Dojo Data Grid), and even a third party option. After taking a look at each control’s features and drawbacks, you’ll be armed to make the best decisions for displaying data in your applications.

Darren Duke will also be presenting on securing Traveler, so there’s a good variety of content for both admins and developers

If you’re in the Atlanta area, I hope to see you there!

Lunch will be provided by IBM. Due to IBM security you need to reply if you are planning to attend the meeting.

Please reply to randy.davison@macys.com no later than early morning, Tuesday, May 14, 2013.

Dojo Data Grid – Part 16: Exporting Grid Data

Exporter is another useful plugin available to for the Dojo EnhancedGrid. As the name indicates, it gives you the ability to export grid data. In this post, I’ll show how to implement it to export grid data in CSV format.

Dojo Data Grid Series

Exporter Plugin

The Exporter plugin is a bit tough to get a handle on — it requires more work than most plugins to get up and running.

Essentially, what it does is provide a string variable with the data to export. You have to take it from there and actually export the data.

Once you get it up and running, there’s an API full of methods that you can override in order to customize the output, but that’s beyond the scope of this post.

Using the Plugin

This post starts with the assumption that you have already have a working Dojo Data Grid control (with a REST service supplying the data) and that you have followed the instructions in this post to ensure that your Dojo Data Grid control is actually generating an EnhancedGrid, rather than a standard DataGrid.

From that point, there are two more steps to use the Exporter plugin.

1. Add the CSVWriter module

Grid_16_config1

2. Include the plugin on the grid

Grid_16_config2

Export Functions

At this point, there are two functions available to get you started with exporting data: exportGrid() and exportSelected().

The exportGrid function will export the number of records defined in the REST service’s count property. If a count is not defined,it will export 10 rows.

Fortunately, it also takes a fetchArgs property that allows you to define the starting row and number of rows to export. The example below has those values set up as parameters to the function so you can work with them dynamically.

The exportSelected function provides a handle to the selected rows in the grid and makes them available to export.

Export Process

As I mentioned earlier, the Exporter plugin functions really just give you data that’s ready to be exported; you have to take it the rest of the way.

Unfortunately, this is not as trivial a process as it might sound.

I’ll demonstrate how I made it work, but I’m sure there are ways to improve upon the process. (See the end of this post for a list of methods that I tried that were unsuccessful.)

My process includes these steps:

  1. Store the grid data in a hidden input field
  2. Use SSJS to put the data into a scope variable
  3. Launch a separate XAgent page to read the data and export it

To export, the user will click the appropriate ‘Export’ button…

Grid_16_a1

…Click OK on the prompt informing them that the export is underway (more on this below)…

Grid_16_a2

…Click ‘Open’ or ‘Save’ when the data is exported…

Grid_16_a3

…And view the data

Grid_16_a4

To export selected rows, the user will select rows and click on the Export Selected button…

Grid_16_b1

Grid_16_b2

Export Functions

The code below contains an Output Script block that defines all three export functions (export, export selected, export custom).

They all use a built-in method from the Exporter plugin to obtain the data to export and then put that data into a hidden input field on the form.

<xp:scriptBlock id="scriptBlock1">
<xp:this.value><![CDATA[
// Call the plugin's exportGrid function and store the data to export in an unbound hidden input field.
function exportGridData_All() {
  dijit.byId("#{id:djxDataGrid1}").exportGrid("csv", function (gridData) {
    dojo.byId("#{id:csvToExport}").value = gridData;
    }
  );
}

// Call the plugin's exportGrid function and store the data to export in an unbound hidden input field.
function exportGridData_Custom(intStart, intCount) {
  dijit.byId("#{id:djxDataGrid1}").exportGrid("csv", {fetchArgs: {start: intStart, count: intCount}}, function (gridData) {
    dojo.byId("#{id:csvToExport}").value = gridData;
    }
  );
}

// Call the plugin's exportSelected function and store the data to export in an unbound hidden input field.
function exportGridData_Selected() {
  var gridData = dijit.byId("#{id:djxDataGrid1}").exportSelected("csv");
  dojo.byId("#{id:csvToExport}").value = gridData;
}

]]></xp:this.value>
</xp:scriptBlock>

Here is the hidden input field that I’m using to store the grid data:

<xp:inputHidden id="csvToExport"></xp:inputHidden>

Export Buttons

Each export button controls all 3 steps of the export process.

  1. With client-side code, the button calls an export method in the output script block to retrieve the data and put it in a hidden input field.
  2. With server-side code, it reads the data from the hidden input field and puts it in a scope variable.
  3. With client-side code on the oncomplete event of the server-side code, it launches a new window with the XAgent page to write out the data.

The ‘Export All’ Button is shown below:

<xp:button value="Export All" id="button1">
  <xp:eventHandler event="onclick" submit="true" refreshMode="partial" refreshId="csvToExport">
    <xp:this.script><![CDATA[
      // Step 1 of 3: Execute the Exporter's export function and use client JS to put the data into an unbound hidden input text field
      exportGridData_All();

      // Pop up an alert box -- this delay provides enough of a delay to let the server-side code read the data from the hidden input field and export it
      alert('Exporting Data...');
]]></xp:this.script>

    <xp:this.action><![CDATA[#{javascript:
      // Step 2 of 3: Read the csv data from the hidden field and put it in a scope variable so the XAgent can retrieve it
      sessionScope.csvExport = getComponent('csvToExport').getValue();}]]>
    </xp:this.action>

    <xp:this.onComplete><![CDATA[
      // Step 3 of 3: oncomplete of the server code that puts the data in the scope variable, open the XAgent page to read and export
      window.open('Grid_16_ExportToCSV_XAgent.xsp');
      dojo.byId("#{id:csvToExport}").value = '';]]>
    </xp:this.onComplete>
  </xp:eventHandler>
</xp:button>

For the ‘Export Selected’ button, the only difference is that the client-side script’s first line (line 5) is this:

exportGridData_Selected();

For the ‘Export Custom’ button, the only difference is that the client-side script’s first line (line 5) is this:

exportGridData_Custom(20, 15);

XAgent Code

The XAgent’s afterRenderResponse event writes out the CSV data. In line 9, it reads the data from the scope variable (line 9) and writes it out in a way that the browser will download it as a CSV file.

// This XAgent just writes out the CSV data from the grid that is stored in a scope variable.
var exCon = facesContext.getExternalContext(); 
var writer = facesContext.getResponseWriter();
var response = exCon.getResponse();
response.setContentType("text/csv;charset=utf-8");

response.setHeader("Cache-Control", "no-cache");
response.setHeader("Content-Disposition", "attachment; filename=GridExport.csv");
writer.write(sessionScope.csvExport);
writer.endDocument();

Beware of Popup Blockers

Be aware that users must grant access to your site to display popups, or else the last step will not work. (If that’s impossible to work around in your environment, you could use location.href=XAgentPage.xsp, rather than window.open).

Further Enhancement

CSV files will generally open just fine in Excel, but, if you need to export the data as an Excel file, you can set the content type to application/vnd.ms-excel and write the data out in the format of an HTML table.

You can also customize the format of the data as it is written out by overriding the available API methods, detailed in the dojo documentation:
http://dojotoolkit.org/reference-guide/1.6/dojox/grid/EnhancedGrid/plugins/Exporter.html

Failed Attempts

This section lists some of the other methods that I attempted to make this work.

Attempt #1
My original intent was to only use client-side JavaScript to write the data out directly.

There’s an encodeURI method in JavaScript. You can actually put encoded CSV data into a URL and launch it and it will attempt to download. But it didn’t work in IE. The other problem is that there doesn’t appear to be a way to define the file name/type, so it kept downloading the data with a .part extension.

location.href='data:application/download,' + encodeURIComponent(csvData)

Attempt #2

In HTML5, there’s a new link property called download that can be used to specify a download filename.

I tried to use code like this to add a link to the page with the required properties and force the click of the link but (a) the link.click() only works in IE and (b) the download property does not work in IE.

var encodedUri = encodeURI(csvContent);
var link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "my_data.csv");
link.click();

Attempts 3-5
Next, I had hoped to use client JS to write the data into a field bound to a scope variable, partially refresh the field to push the data to the scope variable, then launch the XAgent page to pick up the data from the scope variable, but it didn’t work.

It was a timing problem. Both the view.postScript method and the button event handler’s onComplete events both must have started running before the partial refresh was done, because the data was not available to the XAgent. I even updated it to use XSP.partialRefreshPost to try to push the data to the scope variable and execute the XAgent page load with an oncomplete callback, but that still didn’t fix the timing problem.

If I put that code on a separate button and manually clicked it, then that provided enough time for the partial refresh to get the data into the scope variable. But that wasn’t good enough. I wanted this to be a one-click process.

Attempt 6

My next idea was to move past the dependency upon a partial refresh and just use SSJS to put the data directly into a scope variable.

Client JS to put data into a text field (NOT BOUND, BUT MUST BE AN XPAGE CONTROL)

Server JS on button to take data from component and put in scope var immediately

Client JS on complete to open XAgent page

However, the timing was still problematic. It just didn’t update fast enough for the data to be available to SSJS when the CSJS had put it in the field immediately prior. If I click the button a second time, it works fine, but it’s probably picking up the previously-stored data.

Attempt 7

I tried a few different methods (client JS and SSJS) to add delays and timers to the process (wrapped in loops to pause and then check), but none of it worked.

Attempt 8

Tweaking attempt 6 a bit, I decided to split steps 2 and 3 out to a separate hidden button and have the first button trigger the second button to execute.

The timing issue was still apparent.

The Solution

Ultimately, this is why the alert box is there in step 1. Waiting for the user to click it provides the required delay for the data to be available to SSJS.

It’s mildly annoying, but the process works.

However, if you have suggestions on how to improve this process, I’d love to hear them!

Update

The timing issue is still prevalent in some browsers. It appears to be due to the time it takes the plugin function to retrieve the data. The code doesn’t wait for that function to complete, it just moves right along, so the data isn’t available by the time the server-side code runs.

To mitigate this problem, the code can be split into two separate buttons. The first button runs the client-side function to retrieve the data to export. It then waits one second (by setting a timeout for 1,000 milliseconds) and then triggers a ‘click’ on the second button, which is hidden, but has the code to retrieve the data to a scope variable and then launch the XAgent page to write it out.

The logic is virtually the same, just split into separate buttons. The differences are that the delay is added and the user prompt is not needed. In this case, the second button could be used by all 3 of the other export buttons, because steps 2 and 3 in the process are the same.

Here is the updated code for an Export Custom button (with hard-coded parameters) and the second hidden button to finish the export. If you’re still seeing a timing issue, you can increase the time of the delay before triggering the second button.

<xp:button value="1 Export Custom" id="btnExportStep1">
  <xp:eventHandler event="onclick" submit="false">
    <xp:this.script><![CDATA[// Step 1 of 3: Execute the Exporter's export function and use client JS to put the data into an unbound hidden input text field
      exportGridData_Custom(20, 15);
      int=window.setTimeout(function(){dojo.byId('#{id:btnExportStep2}').click()},1000);]]>
    </xp:this.script>
  </xp:eventHandler>
</xp:button>

<xp:button value="2 Export Custom" id="btnExportStep2" style="display:none">
  <xp:eventHandler event="onclick" submit="true"
    refreshMode="partial" refreshId="dummyTarget">
    <xp:this.onComplete><![CDATA[// Step 3 of 3: oncomplete of the server code that puts the data in the scope variable, open the XAgent page to read and export
      window.open('Grid_16_ExportToCSV_XAgent.xsp');
      dojo.byId("#{id:csvToExport}").value = '';]]>
    </xp:this.onComplete>
    <xp:this.action><![CDATA[#{javascript:// Step 2 of 3: Read the csv data from the hidden field and put it in a scope variable so the XAgent can retrieve it
      sessionScope.csvExport = getComponent('csvToExport').getValue();}]]>
    </xp:this.action>
  </xp:eventHandler>
</xp:button>