Quantcast
Channel: Xcellerant » Dojo
Viewing all 54 articles
Browse latest View live

Getting the Most out of the XPages View Panel Control Part 4: Use Dojo to Fix Category Indenting

$
0
0

If you’ve ever created a view panel for a categorized view that includes a Totals column, you’ve probably noticed that the view is not indented properly. This is pretty common to find on modernization projects where views from an existing Notes client application are being surfaced in XPages. In this article, I’ll show how you can fix that with client side JavaScript and dojo.

Standard Categorized View Indentation

I created a categorized view with a long category value for my database of NFL football teams. When I create a view that just includes that category column, along with the location and team name fields, it indents the way I would expect.

CategorizedView_NoTotals_A

Categorized View with a Totals Column

However, if I add a totals column to the view and create a view panel for the view, the formatting is pretty awful — especially if I have a long category value.

CategorizedView_WithTotals_B

Since it has to display something else (the totals column) in the category row, it breaks the whole row up into cells that correspond to each column.

<tr>
<td class="xspColumnViewStart">
<div class="xspColumnViewStart">

{Source for expand/collaps button removed}

<a id="view:_id1:viewPanel1:0:viewColumn1__shrink:1link" style="cursor:pointer;" class="xspColumnViewStart">
American Football Conference - East Division</a>
</div>
</td>
<td class="xspColumnViewMiddle"></td>
<td class="xspColumnViewMiddle"></td>
<td class="xspColumnViewEnd">
	<span id="view:_id1:viewPanel1:0:viewColumn4:_internalViewText" class="xspTextViewColumn">4</span>
</td>
</tr>

Compare this with the source of the category row in the first screen shot above, where you can see that it sets the colspan attribute to cover all cells in the row, with provides proper indentation.

<tr>
<td colspan="3" class="xspColumnViewStart">
<div class="xspColumnViewStart">

{Source for expand/collapse button removed}

<a id="view:_id1:viewPanel1:0:viewColumn1__shrink:1link" style="cursor:pointer;" class="xspColumnViewStart">
American Football Conference - East Division</a>
</div>
</td>
</tr>

Dojo to the Rescue

So, what we need is a way to get the category column to span across multiple cells so that the values below it can be indented properly. Fortunately, with no extra configuration, we have dojo libraries available.

Stay Classy

The cleanest way I’ve found to help the necessary code identify the category cells easily is to apply a class to the category column in the view.

However, we need to apply it conditionally, because the class will be applied to the first table cell in every row in the view — even the ones that aren’t categories.

On the category column, I set the style class’s computed value to this:

if (rowHandle.isCategory()) {
	return 'category';
} else {
	return '';
}

This assumes that I have set my view panel’s var to rowHandle.

Code to Fix the Indentation

XPages and Custom Controls have an onClientLoad event that we can use to run code to process the view after its loaded.

This code will work on each categorized row. It will remove the blank cells between the category value and the totals column and add a colspan attribute to the category cell to use up the blank space, thereby indenting the rows below more naturally.

// Get a list of all rows in the view panel.
dojo.query('.xspDataTableViewPanel table tr').forEach(function(nodeRow, indexRow) {

  // Locate the category cells within the context of the view rows
  dojo.query('td.category', nodeRow).forEach(function(nodeCat){

    // Execute a search on the parent node (row) and remove all cells until data is found
    var emptyCells = 0;
    var keepCounting = true;
    dojo.query('td', nodeRow).forEach(function(nodeTD, indexTD){

      // Check all non-category cells until a non-empty cell is found
      if ((keepCounting) && !dojo.hasClass(nodeTD, 'category')){
        if (nodeTD.innerHTML == '') {
          emptyCells +=1;
          dojo.destroy(nodeTD);
        } else {
          keepCounting = false;
        }
      }
    });

    // Add a colspan attribute to the category cell (1 + [# of empty cells])
    dojo.attr(nodeCat, 'colspan', 1+emptyCells);
  });

});

Here is the result:

CategorizedView_WithTotals_C

Handle View Paging

The onClientLoad code works great when the view first loads, but the problem is that views are generally updated with partial refreshes as the user pages through, so we need to run the code on that event as well.

One option is to trigger a full refresh when the view is paged through, by selecting the view pager and disabling the partial refresh option, but there’s a better way.

Using the technique I demonstrated in my last post, all I need to do is move the onClientLoad event handler into the view panel and then this code will run when the page is loaded and when the user pages through the view.

Have you tackled this problem in a different way? If so, I’d love to hear how you did it.



Dojo.attr() doesn’t work as expected in IE

$
0
0

Stop me if you’ve heard this before: a cross-browser solution doesn’t work in IE. (For a recap on my feelings about the browser, feel free to review this post).

I tried to take a solution that I posted recently to fix category indenting in a view panel with a totals column and apply it to a test database and I noticed that it didn’t work in IE 9. (I didn’t test other versions.)

It found the category rows and removed the empty cells, but didn’t indent properly. It worked fine in Firefox and Chrome (where I was testing when I figured it out and wrote the original post). By displaying the outerHTML property of the category nodes, I was even able to verify that the colspan attribute was technically there, but it just didn’t take effect.

I tried using dojo.attr() to set attributes of other elements and had no success. It seems that the method just doesn’t work well in IE.

I was almost finished with a hacktastic workaround (which involved inserting my own cells by copying the existing category cell and inserting a colspan, positioning them, removing the originals) when I came across this simple JavaScript DOM method: colSpan

To make the code work cross-browser, all I had to do was change line 24 in the last code block on the last post from this:

dojo.attr(nodeCat, 'colspan', 1+emptyCells);

to this:

nodeCat.colSpan = 1+emptyCells;

Now it works in IE, Firefox, and Chrome!


Fix Dojo-Enabled Field Sizes Part 1: Date Pickers

$
0
0

Dojo-enabled fields ignore some theme, custom CSS, and inline style settings. This can cause inconsistency in your form UI. This post demonstrates how to fix the field height and width for date picker fields, so they better match the rest of your form.

I refer to anything processed by dojo after the page loads as a ‘dojo-enabled’ field. Two common types that I encounter are date pickers and type-ahead fields. Their functionality is added by dojo once the form loads, so some settings that you may expect are not preserved.

Take a look at these screen shots (the same form with and without data). I took my simple Team form in my database of NFL football teams and set the Location field to be type-ahead and the Next Game field to be a date picker. Without changing anything else or adding any CSS, you can see that the dojo-enabled fields are longer horizontally and shorter vertically. Their fonts differ as well.

DojoFieldSizes_1b DojoFieldSizes_1

Field Width via the Properties Panel

If I set the field width in the properties panel, it is not respected in the dojo-enabled fields.

DojoFieldSizes_2

Field Styles with CSS

If I use CSS to set the font color and width of all fields, I still see the same effect. The color setting is picked up, but the field widths of the dojo-enabled fields are still different. Font sizes are also not respected by the dojo-enabled fields.

Reviewing the Generated Source

This is the source of the date picker that’s passed to the browser (note that it even includes the inline ‘size’ attribute):

<input class="xspInputFieldDateTimePicker" id="view:_id1:inputText1" type="text" name="view:_id1:inputText1" size="50" />

But this is what the source ends up being after dojo processes the field to activate the date picker:

<span class="xspInputFieldDateTimePicker" style="display: inline-block;">
</span>
<div class="dijit dijitReset dijitInlineTable dijitLeft xspInputFieldDateTimePicker dijitTextBox" id="widget_view:_id1:inputText1" role="presentation">
<div style="overflow: hidden;">
<div class="dijitReset dijitValidationIcon"></div>
<div class="dijitReset dijitValidationIconText">Χ</div>
<div class="dijitReset dijitInputField"><input class="dijitReset" id="view:_id1:inputText1" tabindex="0" type="text" autocomplete="off" value="" /><input style="display: none;" type="text" name="view:_id1:inputText1" /></div>
</div>
</div>
<span class="xspInputFieldDateTimePicker" id="view:_id1:inputText1_Container" style="display: inline-block;">
<span class="dijit dijitReset dijitLeft dijitInline xspInputFieldDateTimePicker dijitButton">
<span class="dijitReset dijitRight dijitInline"><span class="dijitReset dijitInline dijitButtonNode">
<button class="dijitReset dijitStretch dijitButtonContents" id="dijit_form_Button_0" style="-moz-user-select: none; background: none repeat scroll 0% 0% transparent; margin: 0px;" tabindex="0" title="" role="button" type="button" value="">
<span class="dijitReset dijitInline xspInputFieldDatePickerIcon">
<span class="dijitReset dijitToggleButtonIconChar">✓</span>
</span>
<span class="dijitReset dijitInline dijitButtonText dijitDisplayNone" id="dijit_form_Button_0_label"></span>
</button></span></span></span></span>

Fixing the field size

Since the date picker gets a class of xspInputFieldDateTimePicker, we can use few lines dojo code to fix the problem that dojo causes. Just run code like this on the onclientload event of the XPage or custom control to fix the font size and field width as needed.

dojo.query(".xspInputFieldDateTimePicker .dijitTextBox").style({
  width:"100px",
  height:"17px"
});

Note: This logic assumes that you want all of your date pickers to be the same size. I think this is a fairly safe assumption, since your date fields should all have a consistant format. However, if you need to set some at different sizes, you can add classes to the date fields and target them separately with a few additional lines of code.

Some inline styles still take precedence

I found it noteworthy that if I set the font style inline (via the properties panel or directly in the source), the setting was preserved. This isn’t ideal design, but it is an option if you’re stuck.

Up Next

In the next post, I’ll show how to fix the field size on type-ahead fields.


Fix Dojo-Enabled Field Sizes Part 2: Type-Ahead Fields

$
0
0

Type-Ahead fields are activated by dojo after the page loads and the post-processing causes them ignore some theme, custom CSS, and inline settings. This can cause inconsistency in your form UI. This post demonstrates how to fix the field height and width for type ahead fields, so they better match the rest of your form.

My previous post showed how to fix these settings on date picker fields.

As I mentioned in the previous post, even field width settings in the properties panel are ignored.

This screen shot shows a form with a type-ahead field, then 3 regular fields, then a date picker. You can see that the field sizes are different for the type-ahead field and the date picker.

DojoFieldSizes_1

Reviewing the Generated Source

We can fix them with dojo code after the form loads (and the dojo-enabled activation happens), but we need to understand what is generated.

This is the source of the type-ahead field that’s passed to the browser (note that it even includes the inline ‘size’ attribute that I set while trying to size the field):

<span id="view:_id1:_id6"></span>
<input class="xspInputFieldEditBox" id="view:_id1:location1" type="text" name="view:_id1:location1" size="50" />

But this is what the source ends up being after dojo processes the field to activate the type-ahead field:

<span id="view:_id1:_id6" dojotype="ibm.xsp.widget.layout.data.TypeAheadReadStore" jsid="view__id1__id6" mode="partial"></span>
<div aria-labelledby="view:_id1:location1_label" widgetid="view:_id1:location1" role="combobox" class="dijit dijitReset dijitInlineTable dijitLeft xspInputFieldEditBox dijitTextBox" id="widget_view:_id1:location1" 
dojoattachevent="onmouseenter:_onMouse,onmouseleave:_onMouse,onmousedown:_onMouse" dojoattachpoint="comboNode" wairole="combobox" tabindex="-1">
<div style="overflow:hidden;">
<div role="presentation" class="dijitReset dijitRight dijitButtonNode dijitArrowButton dijitDownArrowButton" 
dojoattachpoint="downArrowNode" wairole="presentation" dojoattachevent="onmousedown:_onArrowMouseDown,onmouseup:_onMouse,onmouseenter:_onMouse,onmouseleave:_onMouse">
<div class="dijitArrowButtonInner"> </div><div class="dijitArrowButtonChar">▼</div></div>
<div class="dijitReset dijitValidationIcon"><br></div>
<div class="dijitReset dijitValidationIconText">Χ</div>
<div class="dijitReset dijitInputField"><input aria-invalid="false" value="" tabindex="0" id="view:_id1:location1" aria-autocomplete="list" aria-haspopup="true" role="textbox" name="view:_id1:location1" autocomplete="off" class="dijitReset" dojoattachevent="onkeypress:_onKeyPress,compositionend" dojoattachpoint="textbox,focusNode" wairole="textbox" waistate="haspopup-true,autocomplete-list" type="text"></div></div></div>

Fixing the field size

The logic for this one is a bit more complicated than the date picker, because there isn’t a unique class that’s applied to the field. The unique object to locate is the span tag with the dojotype attribute of ibm.xsp.widget.layout.data.TypeAheadReadStore (line 01). We then need to set the size on the subsequent div (line 02) and the field will size itself to fill the div.

We can get a handle to that div with the CSS ‘adjacent sibling’ selector: +

dojo.query('SPAN[dojotype="ibm.xsp.widget.layout.data.TypeAheadReadStore"] + DIV').style({
  height:"17px",
  width:"200px"
});

Note: This logic assumes that you want all of your type-ahead fields to be the same size. If you need to set some at different sizes, you can add unique classes to fields and target them separately with a few additional lines of code.


Runtime optimized JavaScript and CSS resources – some results

$
0
0

In Notes/Domino 8.5.3, there is a new setting on the XPages tab of the Application Properties called Use runtime optimized JavaScript and CSS resources.

Runtime Optimized JS

Methodology

I put it to the test on two applications and loaded several pages 5 times each and, using firebug (on the Net tab), noted the amount of data loaded and the load time.

Firebug_Net

Results

In the first database, I opened a page with a view panel and a page with a similar view panel, but a full-text search executed against it. I also opened a page with a document in edit mode and a page with a document in read mode.

This database uses several dojo components (in edit mode of the form), a dojo tabbed table on the form, and some dojo logic around the page with the view panel. It has a few small stylesheets, but uses the OneUI application layout, so there are plenty of stylesheets involved.

Database 1

Without Optimization With Optimization
Page Size Load Time Size Load Time
View 21k 1.78s 7k 0.96s
View-Search 7.5k 1.38s 7.3k 1.12s
Document-Edit 13.7k 2.33s 6.7k 1.47s
Document-Read 11.4k 1.18s 4.5k 0.99s

There are significant improvements in the size of the files loaded and the loading performance.

In the second database, I tested with a page loading an ExtJS grid, so it uses grid libraries and stylesheets, along with jQuery. The database also uses the OneUIv2.1 theme.

Database 2

Without Optimization With Optimization
Page Size Load Time Size Load Time
ExtJS Grid 3.2m 3.02s 3.2m 3.15s

It changed the total number of GET requests on the page from 38 to 33.

A Better Understanding

Initially, I was surprised that it didn’t make a noticeable difference in the second database, so I looked into it further and found that it is intended to improve the loading of dojo modules. It is not going to help with any external javascript libraries that you include in your application. (Quite frankly, they should already be optimized for you.)

Here’s how the What’s new in Domino Designer 8.5.3 page in the DDE help file describes it:

For performance reasons, the XPages runtime now features a new option that dynamically aggregates multiple Dojo modules, or multiple CSSs into a single file. This results in the following performance improvements:

  • a decrease in requests sent from the browser to the server
  • an increase in user performance, particularly in the context of networks with high latency
  • an increase in the speed of JS/CSS parsing from the browser
  • the freeing up of server connections to fulfill other requests

As an extreme example of the saving, let’s say the XPages Extension Library is using the page DWA_ListView.xsp. Without the aggregator enabled, 82 requests are made to the server. With the aggregator enabled, the count decreases to 6 requests, including the Ajax request that gets the data. Also, a test run on processing time might see changes from ~4 sec to ~800 ms.

Your results?

Of course, your mileage may vary. I see that Julian Buss had impressive results with a test, but I haven’t seen a lot documented.

If you’ve tested this out, what kind of results have you seen?


Dojo Data Grid – Part 7: Sorting

$
0
0

The Dojo Data Grid does its best to provide sorting options by default. Strangely enough, it takes more work to prevent sorting! This post will cover how to allow the grid to sort columns and how to prevent sorting of some columns or directions.

Dojo Data Grid Series

Default Sorting Features

When you surface a grid, it automatically attempts to provide the ability to sort any column ascending or descending. When you click on a column header, it displays a triangle pointing upward and attempts to sort that column in ascending order. When you click the column header again, it displays a triangle pointing downward and attempts to sort that column in descending order.

On every click, the data disappears and the grid is refreshed. But nothing happens on many of the column clicks!

This is because each sorting option can only work if the underlying view column already has that sort option (and, therefore, the view has an index to support that sorting option).

The sort options are provided on the Click on column header to sort properties of the underlying view columns:
Grid7_1

If your underlying view already has every column set to allow both sorting directions, then you can leave it as is and it will work like a charm.

However, if your view does not have those options set — and it is valid not to have that set because every potential sorting option requires the view to maintain another index — then you can disable sorting options with a little bit of client-side JavaScript coding.

It’s worth the effort to prevent the confusion of users seeing arrows and grid refreshes, but not seeing any difference in the data.

Preventing Specific Sorting Options

The dojo data grid has an attribue named canSort which defines a function that is called when the user clicks on a column header. It accepts a number, which is the index of the column, and returns true or false, based on whether the sorting option is allowed.

It is a one-based index (meaning, the first column is 1, the second column is 2, and so on).

The other important thing to note is that the function will be called with a positive number if an ascending sort is requested and a negative number of a descending sort is requested. So, the function will receive a 1 if it is attempting to sort the first column in ascending order, but it will receive -1 if it is attempting to sort the first column in descending order.

Attaching the canSort function

You can use dojo to attach the function to the canSort attribute of the grid and pass it a function that will return true or false.

The function will automatically receive a single parameter with the column index. The name of the parameter does not matter.

Run the code in the onClientLoad event of the page or custom control containing the grid.

Examples

This function will prevent column 3 from being sortable. The Math.abs() function is used to return the absolute value of the index passed in, meaning it will return a positive number regardless of whether the index passed in is positive (ascending sort) or negative (descending sort):

dijit.byId('#{id:djxDataGrid1}').canSort = function(col){
  if(Math.abs(col) == 3) {
    return false;
  } else {
    return true;
  }
};

This function will prevent all column sorting:

dijit.byId('#{id:djxDataGrid1}').canSort = function(col){
  return false;
};

This function will prevent all descending sorting:

dijit.byId('#{id:djxDataGrid1}').canSort = function(col){
  if (col < 0) {
    return false;
  } else {
    return true;
  }
};

This function will only allow even numbered columns to be sorted:

dijit.byId('#{id:djxDataGrid1}').canSort = function(col){
  if (col % 2 == 0) {
    return true;
  } else {
    return false;
  }
};

Caveat with Column Reordering

This logic is all based on the column index. However, if you allow users to reorder the columns in the grid, you may have unexpected results with the canSort logic.

For example, if my canSort function prevents sorting column 3, but I move that column ahead of the second column, then I’ll be able to try to sort the column, since it’s at a different position, and I’ll lose the sorting option for column 2, because it is now column 3.

This isn’t going to cause any errors, but there is the potential for confusion, so keep that in mind.

Sorting Compared to ExtJS

It’s interesting to note the difference in how sorting works in ExtJS, which described in this post by Mark Roden.

In the Dojo Data Grid, all sorting is done remotely, unlike ExtJS, where it has the built-in feature to sort the current page of data locally (i.e. without requiring a call to the REST service). The Dojo Data Grid doesn’t do any paging, so it does a full sort on the data every time. The advantage is that the results are always what the user expects, but the disadvantage is that it requires a server round trip and a refresh of the grid.


Dojo Data Grid – Part 9: Multi-Row Entries

$
0
0

Up to this point, our grids have been standard tabular structures displaying a single row for each entry, but it’s possible to span multiple rows with each entry. In this post, I’ll show you how to implement it and work with the formatting.

Dojo Data Grid Series

Dojo Data Grid Row Control

Along with the Dojo Data Grid control and the Dojo Data Grid Column control, there is a Dojo Data Grid Row control, which is optional to use when laying out a grid.

Grid9_1

Once you add one or more row controls to the grid, you can add columns inside of them. The row control contains an editable area into which you can drag and drop column controls.

However, it is important to note that if you use row controls, all columns must be inside of a row control or they will not all display. When I have a few columns displayed directly in the grid control and a few more in a row control, the ones in the row control are not displayed in the grid.

For the sake of comparison, this screen shot shows a grid that renders exactly the same with and without a row control.

Grid9_2

Example

To have an entry span multiple rows, just add multiple row controls and add columns to them. Here’s an example of displaying a person’s name on one line and their address on a second line:

Grid9_3

The grid stacks the column headers and displays the data like this:
Grid9_4

But there’s supposed to be 4 columns in the second row!

Grid Alignment Caveat

It appears that the grid will show as many columns as are in the first row. There should have been 4 columns in the second row, but they aren’t displayed. (I tested changing the auto-width attribute and setting column widths, but that had no effect on the outcome.)

If I flip the rows so that the address row (with more columns) comes first, then I see all of the data:

Grid9_5

Example 2

If I want to take my example a step further and make it look more like a mailing label, I can add another row and move the city, state, and zip down to the third row.

However, that still leaves me with 3 columns in the last row and only two columns in the first row. The good news is that I found that I can just add an empty column to the first row in order to provide space for the third column in the last row.
Grid9_8

Grid Column Widths

If you specify column widths in the same column in multiple rows, the width set on the column in the first row will take precedence.

Spanning multiple cells

In this case, we would really want the address line to take up more space, so we could condense the column widths and make the grid entries look more natural.

When you add multiple rows to a grid, it generates a table inside the <div>for the entry to provide the multiple row layout:

Grid9_6

Even though I can see that each cell has a colspan attribute, I was unable to find an easy way to tell the column in the second row to span multiple columns. I tried adding attributes and dojo attributes in the source. I used the themeId property and tried to pass settings that work on plain tables, but they were not used by the grid. (It appears that when the dojo code runs to build the grid, it loses the theme ID, because none of the properties are picked up.)

Fortunately, there is a solution. The grid has an onStyleRow event that fires on each row (a) when the grid is created and (b) when you interact with the row (eg hover over it, etc).

Code in this method automatically receives an object that can be accessed via arguments[0] and it provides these properties: index, node, odd, selected, over, customStyles, customClasses. The index is the grid entry index. The odd property is true for every odd-numbered row. The selected and over properties track the state of the row (selected or mouse hover). The customStyles and customClasses properties allow you to dynamically change the styles and/or classes of the row.

The node property is a handle to the DOM node for the grid entry. This is great, because it gives us a starting point to look for any table cells that we need to modify.

In this case, I want the table cell (td) in the second row of the entry to have a colspan of 3. I can locate and modify the setting with this line of dojo code:

dojo.query("tr:nth-child(2) td", arguments[0].node).forEach(function(node) {node.colSpan="3";});

So, I just need to add it to the onStyleRow event of the grid and it will execute on every grid entry.

But, be careful how you add the code. If you select the grid and then go to the Events view and enter code in the onStyleRow event, it doens’t add it to the page properly. It adds this:

<xe:eventHandler event="onStyleRow" submit="false">
  <xe:this.script><![CDATA[dojo.query("tr:nth-child(2) td", arguments[0].node).forEach(function(node) {node.colSpan="3";});]]></xe:this.script>
</xe:eventHandler>

Make sure you select the grid, use the Properties view >> All Properties > events > onStyleRow and click the button in that property to enter the code and it will add it properly like this:

<xe:this.onStyleRow>
  <![CDATA[dojo.query("tr:nth-child(2) td", arguments[0].node).forEach(function(node) {node.colSpan="3";});]]>
</xe:this.onStyleRow>

You can see that the address line now spans 3 columns in the screen shot on the left (the screen shot on the right is the default alignment):
Grid9_7

Caveat to adding onStyleRow code

When you add any code to the onStyleRow even, you lose the default styling for every other row and when a row is hovered over.

You can easily replace that style logic by making use of the odd property and over event state of the object in onStyleRow and adding a class or setting the style directly.

Multiple Rows and Sorting

Breaking up grid entries across multiple rows has no bearing on sorting. You can still click column headers to sort and it works just fine.

Up Next

In the next post, we’ll look at full-text and field-specific searching to filter the grid results.


Dojo Data Grid – Part 16: Exporting Grid Data

$
0
0

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!



Dojo Data Grid – Part 17: EnhancedGrid Drag and Drop

$
0
0

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 18: EnhancedGrid Context Menus

$
0
0

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


Create a Categorized Dojo TreeGrid in XPages

$
0
0

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.


Categorized Dojo TreeGrid in XPages – Additional Features

$
0
0

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.


Categorized Dojo TreeGrid in XPages – Add Totals and Counts

$
0
0

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.


Fix Indenting on Multiple Categories in a View Panel

$
0
0

I previously demonstrated a solution for fixing category indenting in a view panel that contains a totals column. In this post, I’ll provide an updated version of the code that handles indenting multiple columns properly.

For more insight into the logic, please take a look at that previous post.

Code

Here is the code that fixes the category indentation:

function fixViewIndentation(categoryClass) {
  // Get a list of all rows in the view panel.
  dojo.query('.xspDataTableViewPanel table tr').forEach(function(nodeRow, indexRow) {
	
  // Locate the category cells within the context of the view rows
  dojo.query('td.' + categoryClass, nodeRow).forEach(function(nodeCat){
    // Execute a search on the parent node (row) and remove all cells until data is found   
    var emptyCells = 0;
    var countCells = false;
    dojo.query('td', nodeRow).forEach(function(nodeTD, indexTD){
      // Start counting when the first category cell is found (this is more for levels after the top-level categorization)
      // Check all non-category cells until a non-empty cell is found
      if (dojo.hasClass(nodeTD, categoryClass)) {
        countCells = true;
      } else if (countCells){          
        if (nodeTD.innerHTML == '') {          
          emptyCells +=1;
          dojo.destroy(nodeTD);
        } else {
          countCells = false;
        }
      }    
    });
    // Add a colspan attribute to the category cell (1 + [# of empty cells])  
    // dojo.attr(nodeCat, 'colspan', 1+emptyCells);
    nodeCat.colSpan = 1+emptyCells;
  });
  });	
}

Custom Control for Easy Reuse

In order to reuse this feature easily, I put the above function in a client JavaScript library called viewPanel.js and I created this custom control (named ccViewIndentationFixer) that will execute the code onClientLoad.

Here’s the full source of the custom control:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
  <xp:this.resources>
    <xp:script src="/viewPanel.js" clientSide="true">
    </xp:script>
  </xp:this.resources>
  <xp:eventHandler event="onClientLoad" submit="false">
    <xp:this.script><![CDATA[fixViewIndentation("CATEGORY_CLASS_NAME");]]></xp:this.script>
  </xp:eventHandler>
</xp:view>

The call to the fixViewIndentation() function must pass the name of the class used on the category columns that need to be indented. (I showed how I set the category column class in the previous post.)

Per a great tip from Sven Hasselbach, I’ve learned that the best way to use it is to include it within a view panel facet by putting this in the page source:

<xp:viewPanel ...>
  <xp:this.facets>
    <xp:pager...></xp:pager>
    <xc:ccViewIndentationFixer xp:key="south" />
  </xp:this.facets>
</xp:viewPanel>

XPages Tip: Fixing the Display of ComboBoxes in Read Mode with Dojo

$
0
0

There’s a quirk with the display of combobox values when XPages are in read mode. Instead of just displaying the value, it generates a table with one row and one cell. This can cause problems with your form alignment if your fields are aleady in a table (especially a OneUI form layout table). This post contains a few lines of dojo code that can be used to fix the problem.

If you have a one-row two-cell table that contains a label and a combobox, it looks like this in the page source in Domino Designer:

<table>
  <tr>
    <td>
      <xp:label value="Label" id="label1" for="comboBox1"></xp:label>
    </td>
    <td>
      <xp:comboBox id="comboBox1" value="#{document1.Field1}">
        <xp:selectItem itemLabel="Value 1"></xp:selectItem>
        <xp:selectItem itemLabel="Value 2"></xp:selectItem>
        <xp:selectItem itemLabel="Value 3"></xp:selectItem>
      </xp:comboBox>
    </td>
  </tr>
</table>

But this is the output in read mode:

<table>
  <tr>
    <td>
      <label id="view:_id1:label1" for="view:_id1:comboBox1" class="xspTextLabel">Label</label>
    </td>
    <td>
      <table id="view:_id1:comboBox1">
        <tr>
          <td>Value 1</td>
        </tr>
      </table>
    </td>
  </tr>
</table>

Instead of just writing out the value of the combobox, it wraps it in a table.

(A regular field value is generally written out within a span tag that has the id of the control.)

When the combobox doesn’t have a value, then an empty table is inserted (no rows or cells):

<table id="view:_id1:comboBox1"></table>

This code, when run onClientLoad of the page, will fix this issue by replacing the table with just the value of the combobox.

if("#{javascript:document1.isEditable()}" == "false"){
  // Search for all nested tables with ids
  dojo.query('table td table[id]').forEach(function(node) {
    var value = '';

    // Locate the first table cell, which contains the value
    dojo.query('td:first-child', node).forEach(function(innerNode) {
      value = innerNode.innerHTML;
    });

    // Replace the table with only the value (or a blank)
    node.outerHTML = value;
  });
}

Line 1 tells it to only run in read mode. (Update the name of the document data source variable if you need to.)

Line 3 gets a handle on tables with ids that are nested inside of table cells.

Lines 7-9 search for the first cell within the table and retrieve it’s innerHTML, which is the value of the combobox. If there is no value, then there won’t be a table cell and it will replace the table with an empty string.

Line 12 replaces the original combobox table with just the value that needs to be displayed.

And now the output from the example shown above looks like this, as it should:

<table>
  <tr>
    <td>
      <label id="view:_id1:label1" for="view:_id1:comboBox1" class="xspTextLabel">Label</label>
    </td>
    <td>
      Value 1
    </td>
  </tr>
</table>

If you run into conflicts with this affecting other tables in your page, you may need to refine the selector in line 3.

If you need to preserve the of the element and/or wrap it in a span tag like other field values, you can tweak the code accordingly.



XPages Tip: Fixing the Display of Radio Button Groups in Read Mode with Dojo

$
0
0

In my last post, I showed a script that can be used to fix the display of combo boxes in read mode. As it turns out, the same code will fix radio button groups, because the output is the same!

The same code will also fix the display of single-value checkbox groups and listboxes. It will not properly handle multi-valued checkbox groups or listboxes, so I’ll be working on that next.

I have updated the comments on the XSnippet accordingly.


XPages Tip: Fixing the Display of Comboboxes, Radio Button Groups, Checkbox Groups, and Listboxes in Read Mode with Dojo

$
0
0

With a couple of minor adjustments to the code shown in a previous post, I now have a script that will fix the display of comboboxes, radio button groups, checkbox groups (single or multi-valued), and listboxes (single or multi-valued) all in one fell swoop!

The output of a single-value for any of these field types looks like this:

<table id="id">
  <tr>
    <td>value </td>
  </tr>
</table>

However, if you have a field with multiple values, it separates each value out into its own row, so the previous code needed to be adjusted to read the values out of all cells in the nested table. This code will concatenate multiple values to be separated by a comma and a space, but you can easily change that by modifying the code below.

<table id="id">
  <tr>
    <td>value 1</td>
  </tr>
  <tr>
    <td>value 2</td>
  </tr>
  <tr>
    <td>value 3</td>
  </tr>
...and so on...
</table>

This code, when run onClientLoad of the page, will fix all of these fields types, by replacing the table with just the value of the field, concatenating multiple values as needed.

if("#{javascript:document1.isEditable()}" == "false"){

  dojo.query('table td table[id]').forEach(function(node) {
    var value = '';

    // Get the value out of all cells in the nested table and concatenate them
    dojo.query('td', node).forEach(function(innerNode) {
      value += innerNode.innerHTML + ', ';
    });

    // Replace the table with only the value(s) (or a blank), but cut off the trailing comma
    node.outerHTML = value.substring(0, value.length-2);
  });
}

Line 1 tells it to only run in read mode. (Update the name of the document data source variable if you need to.)

Line 3 gets a handle on tables with ids that are nested inside of table cells.

Lines 7-9 search for all table cells within the table and retrieve each cell’s innerHTML, which contains a value. If there is no value, then there won’t be a table cell and it will replace the table with an empty string.

Line 12 replaces the nested with just the value that needs to be displayed.

And now the output from the example shown above looks like this, as it should:

If you run into conflicts with this affecting other tables in your page, you may need to refine the selector in line 3.

If you need to preserve the of the element and/or wrap it in a span tag like other field values, you can tweak the code accordingly.

XSnippet

The latest version of the code is posted as an XSnippet on OpenNTF


Dojo Data Grid – Part 27: Changing the Dojo Theme

$
0
0

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 Grid – Part 28: Overriding Grid CSS

$
0
0

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 29: Displaying the Row Count

$
0
0

In this post, I’ll show how display a row count for the grid and update it after executing a search.

Dojo Data Grid Series

Dojo Grids in XPages — All Blog Posts

All About the Timing

The grid has a rowCount property which will generally return the number of rows in the grid.

It will work fine if you check it by clicking a button after the page loads. However, if you run this immediately as the page loads (even if it’s in the onClientLoad event), it will return 0, because the grid hasn’t been drawn out yet).

At this point, my solution is to introduce a delay to give it time — then it can return the correct amount.

Displaying the Count

One simple way to display the count is to put a simple span tag on the page, add a class to it so it can be programmatically updated.

I added this to my page:

Row Count: <span class="displayRowCount"></span>

Updating the Count

Another consideration is that you don’t just want to check the count when the page loads, you’ll want it to be updated any time the grid is refreshed.

In the post where I showed how to add search to a grid I mentioned that executing a search must refresh the rest service and the grid, so you should put them both in a panel and target that panel with a partial refresh.

To finish this solution, add code that will check the grid’s row count and update the display (span) tag on the onClientLoad event of the panel, so it re-runs every time the panel is refreshed.

var t=setTimeout(function(){
    var myGrid = dijit.byId('#{id:djxDataGrid1}');
    dojo.query('.displayRowCount')[0].innerHTML = myGrid.rowCount;
  }
,500);

This code waits half of a second (500 milliseconds) and then checks the grid’s row count (line 2) and updates the span tag to display it (line 3).

These screen shots show the row count below the grid after initial load and then after a search:
Grid 29 - 1

Grid 29 - 2

REST Service Note

I tested this with a JSON rest service as the source for the grid. Interestingly, the service’s count property is set to 20, so it shouldn’t be retrieving the full set of rows, but it returns the full count of over 1,300 anyway.


Viewing all 54 articles
Browse latest View live