Archive | Dojo EnhancedGrid RSS for this section

Dojo Data Grid – Part 28: Overriding Grid CSS

In the last post, I showed how to modify the look and feel of a Dojo Data Grid in XPages by selecting a different dojo theme. In this post, I’ll show how to further customize the UI by adding your own CSS.

Dojo Data Grid Series

Dojo Grids in XPages — All Blog Posts

Default Style

Here’s what the grid looks like by default:

Grid 28 - A

Grid Classes to Target

In order to figure out how to override the styling, we need to understand more about the structure of the grid on the page.

There are a lot of divs and tables that get generated when a grid is rendered. There’s a parent grid div, header and view divs, scrollbox and content divs, and then each grid row is a div and table.

Here’s a list of elements that it uses from the start of the grid down to the first data cell:


div.dojoxGrid > div.dojoxGridMasterView > div.dojoxGridView > div.dojoxGridScrollbox > div.dojoxGridContent > div[role="presentation"] > div.dojoxGridRow > table.dojoxGridRowTable > tbody > tr > td.dojoxGridCell

Every other div.dojoxGridRow also gets the dojoxGridRowOdd class as well.

Font and Row Color

Fortunately, we don’t need to go through all of that to find the classes that we need to customize.

You can change the row color and font by targeting .dojoxGridRow tr

/* Grid Row - Default Styles */
.dojoxGridRow tr {
  background-color: #2F4F4F;
  color: #FFFFFF;
  font-size: 14px;
}

Fixing Header/Cell Alignment with Column Widths

Once you start changing default fonts (or padding), it throws off the alignment of the column headers to the column data.

Grid 28 - B

The simplest way to fix this is to define the width of each grid column explicitly in the grid column properties. This ensures that the header and cell line up.

Grid 28 - C

Alternate Row Color

You can also change the alternate row color and font by targeting .dojoxGridRowOdd tr

/* Grid Row -- Alternate Row Background Color*/
.dojoxGridRowOdd tr {
  background-color: #778899;
}

Row Hover

It’s a little trickier, but you can also override the row hover styles. In this case, I’m changing the background color, text color, and font size.

/* Grid Row - Hover Styles */
.dojoxGridRow tr:hover td {
  background-color: white !important;
  color: #2F4F4F !important;
  font-size: 18px;
}

Grid 28 - D

Header

You can also modify the style of the column header cells. You have to target both the header cell and the div contained within the header cell in order to fully change the background color, which uses an image by default.

/* Grid Header - Styles */
.dojoxGridHeader .dojoxGridCell {
  background-image: none !important;
  background-color: #777777 !important;
}

.dojoxGridHeader .dojoxGridCell div {
  color: white;
  font-size: 12px;
}

Dojo Data Grid – Part 27: Changing the Dojo Theme

If you’d like to change the look of a dojo grid, you can do so by changing the dojo theme. As in XPages, dojo themes are a set of resources (images and style sheets) that define the UI. In this post, I’ll show the 4 built-in dojo style themes and how you can use them with your grid.

Dojo Data Grid Series

Dojo Grids in XPages — All Blog Posts

Dojo Themes

There are 4 dojo themes available:

  • tundra
  • claro
  • nihilo
  • soria

With a standard (non-XPages) web page, you would include the main style sheet for the theme and then set the class of the body tag for that theme in order to use it.

For example:

<link rel="stylesheet" href="dojo/dijit/themes/claro/claro.css" />
...
<body class="claro">

Dojo Themes in XPages

When using the dojo data grid control in XPages, it loads the tundra theme by default, as shown by these tags in the page source:

<link rel="stylesheet" type="text/css" href="/xsp/.ibmxspres/dojoroot-1.6.1/dijit/themes/tundra/tundra.css">
...
<link rel="stylesheet" type="text/css" href="/xsp/.ibmxspres/dojoroot-1.6.1/dojox/grid/resources/Grid.css">
<link rel="stylesheet" type="text/css" href="/xsp/.ibmxspres/dojoroot-1.6.1/dojox/grid/resources/tundraGrid.css">
...
<body class="xspView tundra">

Using the Claro theme

The tundra theme is used by default, but you can change the grid to use the claro theme with these two steps:

1) Add the claro grid style sheet to the page

<xp:this.resources>
  <xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/claroGrid.css"></xp:styleSheet>
</xp:this.resources>

2) Add a dojo attribute to the grid called class and its value to claro

Note:I’m not sure why, but this doesn’t work with nihilo and soria, even though similarly-named style sheets exist in the same location.

Changing the Dojo Theme

If you want to change the grid to use the soria or nihilo themes, you change the dojo theme application-wide via an XPages theme. As with a standard web page, we need to include the correct style sheet and add the class to the body tag of the page.

Credit to this solution goes to the XPages Extension Library book. I’m assuming this chapter was written by Paul Withers, since I also found it in this Lotusphere presentation.

<!— Include Dojo stylesheet —>
<resource dojoTheme="true">
  <content-type>text/css</content-type>
  <href>/.ibmxspres/dojoroot/dijit/themes/soria/soria.css</href>
</resource>

<!— Add style to body element —>
<control dojoTheme="true">
  <name>ViewRoot</name>
  <property mode="concat">
    <name>styleClass</name>
    <value>soria</value>
  </property>
</control>

The ViewRoot attribute makes this possible and the property mode ‘concat’ will append the class name to the page’s body tag.

Since we’re using a theme, this will be an application-wide setting, so it will affect the look of other dijit controls on your pages, but they will all be consistent with the same theme.

Interestingly, when I change the dojo theme for the application, then the individual class attributes for all 4 dojo themes all seem (provided you’ve included the grid-specific stylesheets on the page)!

Here’s how each style sheet is included — only include the one you’d like to use:

<xp:this.resources>
  <xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/claroGrid.css"></xp:styleSheet>
  <xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/soriaGrid.css"></xp:styleSheet>
  <xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/nihiloGrid.css"></xp:styleSheet>
</xp:this.resources>

Here’s an example of 5 copies of the same grid. The first does not have a dojo class attribute. The others have specified a class attribute that corresponds to a dojo theme. The page shown includes the 3 style sheet resources shown above and also has an XPages theme in place that changes the default dojo theme. (Note: In this case, the soria theme is loaded as the application default, so the default grid is the same as soria.) Click the image to enlarge it.

Grids - With App Theme to Change Dojo Theme

For what it’s worth, this is what the same page looks like when there is not an XPages theme in place overriding the dojo theme.

Grids - No App Theme

Dojo Themes and Enhanced Grids

It’s easier to change the dojo theme when using an EnhancedGrid, because you already have to specify the grid style sheets to load, so you can just load the ones that you’d like.

It appears that there are three options for enhanced grids:

  • default
  • claro
  • tundra
<xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/Grid.css"></xp:styleSheet>
<xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/enhanced/resources/EnhancedGrid.css"></xp:styleSheet>

Enhanced - Default

<xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/claroGrid.css"></xp:styleSheet>
<xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/enhanced/resources/claroEnhancedGrid.css"></xp:styleSheet>

Enhanced - Claro

		
<xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/resources/tundraGrid.css"></xp:styleSheet>
<xp:styleSheet href="/.ibmxspres/dojoroot/dojox/grid/enhanced/resources/tundraEnhancedGrid.css"></xp:styleSheet>

Enhanced - Tundra

More Information

Take a look at the dojo theme documentation for more information.

To see more dojo controls in different themes, check out the Dijit Theme Tester page

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

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.

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.

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>