Gridx in XPages – 33: Adding a Column Header Menu

Another handy feature that you can add to Gridx is a popup menu from the column headers. In this post, I’ll show how to build a menu and add it to the grid.

Gridx 33 A - Header Menu

Gridx Series

Gridx in XPages — Entire Series

Include the HeaderMenu Module

As always, you have to include the required gridx module (gridx.modules.HeaderMenu) and add it to the grid object’s modules list.

modules: [
  Resizer,
  NestedSort,
  Filter,
  FilterBar,
  QuickFilter,
  VirtualVScroller,
  Persist,
  HeaderMenu
]

Build the Menu

Create a new dijit.Menu object to build the menu. You don’t need to add any resources to the page because, even though dijit.Menu and dijit.MenuItem are not automatically made available in XPages, they are made available by Gridx.

This code creates a menu and adds 3 items to it. Each item has a simple onClick handler that displays an alert.

  var headerContextMenu = new dijit.Menu();
  headerContextMenu.addChild(new dijit.MenuItem({
    id: 'menu1',
    label: 'Option 1',
    onClick: function() {
      alert('Clicked menu item: ' + this.label);
    }	
  }));
  headerContextMenu.addChild(new dijit.MenuItem({
    id: 'menu2',
    label: 'Option 2',
    onClick: function() {
      alert('Clicked menu item: ' + this.label);
    }	
  }));
   headerContextMenu.addChild(new dijit.MenuItem({
    id: 'menu3',
    label: 'Option 3',
    onClick: function() {
      alert('Clicked menu item: ' + this.label);
    }	
  }));

Add Menu to Column Definition

Now that the menu is ready, all you have to do is add it to one or more columns via the menu attribute.

var columns = [
  {
    id: 'first',
    field: 'firstname',
    name: 'First',
    width: '70px',
    menu:headerContextMenu
  }
]

You can add the same menu to all columns or you can create different menus for different columns.

Selecting a Menu Option

When you hover over a column header of a column that has a menu, a solid downward-facing black triangle is visible on the right-hand side of the column header. (Note: The mouse cursor isn’t in the screen shot, but it was hovered over the column header.)

Gridx 33 B - Menu Icon

Here’s the strange part — the interface is not intuitive; left-clicking on the triangle does nothing but select the triangle image.

The menu will be displayed when your right-click on the triangle. (See screen shot near beginning of the post.) Then you can left-click on a menu option to select it.

Gridx 33 C - Alert

Gridx in XPages – 32: Editable Cell Widgets

In the last couple of posts in this series, I showed how to edit data inline and save the changes. But you’re not limited to plain text fields. In this post, I’ll show how to make cells editable with additional input widgets.

Gridx Series

Gridx in XPages — Entire Series

Adding Input Widgets

There are generally 3 steps to the process:

  1. Include the CellWidget module
  2. Include your chosen input widget module (if necessary)
  3. Set the column properties to include and configure the widget

To add any kind of widget for inline data input, you need to first required the CellWidget module and add it to the grid.

To do so, include "gridx/modules/CellWidget" in the require statement (or at the page level) and then also include it in the grid’s modules list.

The next two steps will be specific to the widget that you choose. Below are examples of using a combo box, date picker, and number spinner.

Combo Box

The dijit.form.ComboBox module is automatically included within XPages, so you do not need to require it.

In this example, I’ve added it to the state column. Here’s the updated column definition:

{ id: 'state', field: 'state', name: 'State', width: '80px', 
  editable: true, 
  editor: 'dijit.form.ComboBox',
  editorArgs:{
    props:'store: stateStore'
  } 
},
  • The editable property tells the grid that the column is editable
  • The editor property tells the grid what widget to use when editing
  • The editorArgs property is an object that includes properties required to configure the editor

The ComboBox requires a memory store with the list of choices, so that needed to be set up earlier in the code.

It uses the same MemoryStore module that’s already included for the grid to use. Each option must have a name and unique id.

Note: Do not declare the memory store object as a var. This causes the grid to not be able to find it.

stateStore = new MemoryStore({
  data: [
    {name:"FL", id:"FL"},
    {name:"GA", id:"GA"},
    {name:"IL", id:"IL"},
    {name:"NJ", id:"NJ"},
    {name:"PA", id:"PA"}
  ]
});

Now, when a cell in the State column is put into edit mode, there is a combobox with a list of choices to choose from.

Gridx 32 - ComboBox Widget

Date Picker

If you want to let the user change a date value, it is very handy to include a date picker.

Here are the properties that I added to the date column in order to enable a date picker in my example:

  editable: 'true',
  editor: 'dijit.form.DateTextBox',
  editorArgs: {
    fromEditor: storeDate
  }

I didn’t have to require the dijit.form.DateTextBox module because it’s available already within XPages.

Since it’s a date field, I needed to add a fromEditor function to parse the data before it’s stored. (See this post on formatting date columns for more information on date parsing.)

This function will return a date string that works with the formatting code already on the column:

function storeDate(storeData, gridHandle) {
  if (typeof storeData == 'object') {		
    return storeData.toLocaleDateString();
  } else {
    return '';
  }
}

Gridx 32 - DatePicker Widget

Number Spinner

For the last example, I added a NumberSpinner to a cell. This module is not automatically available in XPages, so it needs to be included.

One of the ways to do that is at the page level with a Dojo Module Resource:

<xp:this.resources>
  <xp:dojoModule name="dijit.form.NumberSpinner"></xp:dojoModule>
</xp:this.resources>

Now that the module is available to the page, it can be used for cell input.

In this example, I have a Rating column that has a number spinner that allows values from 1 to 5.

{ id: 'rating', field: 'Rating', name: 'Rating', dataType: 'number', width: '75px', 
  editable: true, 
  editor: 'dijit.form.NumberSpinner',
  editorArgs:{
    props: 'smallDelta: 1, constraints: {min:1, max:5, places:0}'
  } 
} 

Gridx 32 - NumberSpinner Widget

There are many more options; hopefully, these examples make it much easier to implement any others that you need.

XPages Tip: Component API Documentation

I’m sure you’ve seen the standard XPages Reference that’s part of the Domino Designer help, but you may not be aware that there’s also API documentation for XPages controls that’s available. It’s documentation of the API for the underlying Java classes and it can be very useful in letting you know what methods are available for each component.

Here’s an example for an Input Text control. At the top of the page, you can see the class hierarchy that leads to the component.

ControlDocs1_ClassHierarchy

This gives you the ability to also find methods that are available to higher-level classes from which this component inherits.

You can scroll down to see a summary of the methods available.

ControlDocs2_MethodSummary

If you scroll further, you can get a more detailed description of the methods.

ControlDocs3_MethodDetail

It can be helpful to peruse the list — you may come up with ideas to work with the component in ways that you didn’t realize were available because there isn’t a built-in property in the UI!

The API for standard components (9.0.1) can be found in the XPages Components documentation. Do yourself a favor and keep the link handy — it’s not exactly easy to find.

In addition to the XPages component documentation, you can also go up a level in the API and see documentation for many more classes in the runtime (9.0.1).

I had a bookmark for the same info but with a much nicer interface on the OpenNTF site, but it is no longer valid. That included the API for all xp and xe components in on place and was an excellent reference. The documentation comes with the extension library (under \Docs\ExtLib\XPages-Doc\control.html), though, so make use of it if you have that available. If you have a link to this online — please let me know and I’ll update this post.

Update: Many thanks for a quick response from Per Lausten — the link for the controls documentation on OpenNTF has been restored!

ControlDocs4_AllTogether

Localization Tip: Troubleshooting “The code for the static initializer is exceeding the 65535 bytes limit”

I came across this error message recently: “The code for the static initializer is exceeding the 65535 bytes limit” while adding supported languages to a localized XPages application. In this post, I’ll explain the cause of the error and a couple of ways to approach fixing it.

Understanding the Error

Every XPage and Custom Control is boils down to a corresponding Java class behind the scenes. You can find these .java files in Packages Explorer under Local\xsp.

Each class contains blocks of code for dealing with each component on the page as well as handling localization strings (among other things).

If this file gets too large, it throws the error message above.

The Cause

I’ve never seen this issue until recently, but when I updated a localized application to add 8 more supported languages, this error was thrown for two of the controls.

When I looked into the class files for the controls that threw the error, it was clear what was blowing up the file size — translation strings.

The file builds an array of string arrays in a variable named s_localeStrs. Below is an example of what it looks like. The first line initializes the variable and then the rest is what is set up for each for the first few things on the page that are translated. (This is from a demo app from Connect 2014.)

private static final String[][] s_localeStrs = new String[][]{
  new String[]{ // ""
    "Hard-Coded Title, Labels, Options", // 0 formTable1/@formTitle
    "Description", // 1 formTable1/@formDescription
    "Name", // 2 formRow1/@label
    "Type", // 3 formRow2/@label
  },
  new String[]{ // de
    "[de| Hard-Coded Title, Labels, Options ]", // 0 formTable1/@formTitle
    "[de| Description ]", // 1 formTable1/@formDescription
    "[de| Name ]", // 2 formRow1/@label
    "[de| Type ]", // 3 formRow2/@label
    "[de| Type 1 ]", // 4 comboBox1/xp:selectItem[1]/@itemLabel
  },
  new String[]{ // en_GB
    "[en_GB| Hard-Coded Title, Labels, Options ]", // 0 formTable1/@formTitle
    "[en_GB| Description ]", // 1 formTable1/@formDescription
    "[en_GB| Name ]", // 2 formRow1/@label
    "[en_GB| Type ]", // 3 formRow2/@label
    "[en_GB| Type 1 ]", // 4 comboBox1/xp:selectItem[1]/@itemLabel
  },
  new String[]{ // en_IE
    "[en_IE| Hard-Coded Title, Labels, Options ]", // 0 formTable1/@formTitle
    "[en_IE| Description ]", // 1 formTable1/@formDescription
    "[en_IE| Name ]", // 2 formRow1/@label
    "[en_IE| Type ]", // 3 formRow2/@label
    "[en_IE| Type 1 ]", // 4 comboBox1/xp:selectItem[1]/@itemLabel
  },
  new String[]{ // lt
    "LT-Hard-Coded Title, Labels, Options ", // 0 formTable1/@formTitle
    "LT-Description ", // 1 formTable1/@formDescription
    "LT-Name ", // 2 formRow1/@label
    "LT-Type ", // 3 formRow2/@label
  },
};

As an application is set up to support a language, it adds the string translations for each translated property directly into the class file. So if you support 10 languages, you’ll have 10 sets of strings set up. (Note: this does not happen with translations for computed values that you manage, because the application does not translate them, so it doesn’t mix anything into the page/CC class files.)

This is something you won’t even know or care about for the most part, but when you have a lot of elements on the page to be translated and numerous translations, it can make the size of this class too large, hence the error.

In the case where I saw the error, it was on a custom control that had 864 items to translate. Once the application went from 2 to 10 languages, the size of this java class file got too large.

The Solution

The bottom line is that the class file needs to be cut down to the allowable size. You can do this by breaking the custom control into smaller controls until you get to the right size; then the problem goes away.

However, since that can become quite involved, I recommend doing some analysis on the current control’s design before taking the step to break it up.

Look for ways to modularize/reuse pieces of the page. If there are repetitive controls or combo box option lists, you can look to streamline their usage in order to cut down on the amount of things that are set up for translation.

In this case specifically, there were a bunch of combo boxes on the page with a list of every available time to choose, to the quarter hour. For example: 12:00, 12:15, 12:30, 12:45, 1:00, 1:15, 1:30, and so on.

Each combo box had 48 hard-coded options and there were numerous time fields on the page. This meant that the java class file had a set of 48 strings for each of 10 languages, multiplied by the number of time selection fields on the page. And these are things that don’t need translation! (The same thing could happen for any list of values that wouldn’t need translation, such as a list of proper names like states.)

There are several ways to go about handling this. Here are a few ideas:

  1. Create the field as a reusable custom control and handle it in one place and then use it as many times as needed
  2. If the values needed translation, you could set up one list of the values in a custom properties file and refer to it throughout the application.
  3. If the values don’t need translation, the easiest way to mitigate this issue is to compute the list of choices. Instead of dozens of select item tags, compute it to return an array
  4. Set up the list of choices once in a configuration/keyword document and look it up either directly from the control
  5. Define an array in a page-level or scope variable one time and reference it for the combo box’s list of choices as many times as needed

Any of these options modularize the list of options and significantly streamline the page source. They also cut down on a lot of unnecessary elements in the properties files if you need to send them out for translation.

Gridx in XPages – 31: Saving Inline Edits

In the last post, I showed how to enable inling editing on a grid column. However, the changes made were only stored in the in-memory data store. In this post, I’ll show how to save the updated value to the back-end document.

Gridx Series

Gridx in XPages — Entire Series

General Concepts

The process will run after the change has been made. The changes will be saved asynchronously so the user isn’t blocked while waiting for the update to happen on the server.

We need a server-side method of applying the changes. In this example, I’ll be using the Domino Data Service (DDS) because it’s a little simpler. You could also create your own custom REST service to receive requests and process the changes.

Domino Data Service

DDS is a REST API that allows you to access databases on a Domino server. It can handle HTTP/S requests and JSON data.

You can use it to get database, view, folder, or document info as well as send updates. We’ll use it to save changes made in the grid.

(If your administrator won’t allow this, then you can go the route of custom REST services)

1. Enabling Domino Data Service on the Server

DDS can be enabled on the Server document (Internet Protocols tab > Domino Web Engine subtab, under the Domino Access Services section. Select Data in the Enabled Services field), but better to do on a Web Site document.

Set Enabled Services to Data under Domino Access Services and ensure that PATCH is selected under Allowed Methods (on the Configuration tab)

Gridx 31 A Enable DDS and PATCH on Web Site Doc

Note: The server document must be set to use web site documents.

2. Enabling Domino Data Service on the Database

You also need to enable it on the specific database (and views, if getting data from views, but we won’t be doing it in this example, so we only need it at the database level.)

Gridx 31 B Enable DDS on Database

HTTP PATCH

If you’re using XPages, then you’re most likely familiar with the HTTP GET and POST methods. You may assume that we’d use a POST to update a document (because that’s what happens when a form is saved), but in this case, we’ll use PATCH, because it’s more efficient.

A POST will replace the entire document with the contents that you pass whereas a PATCH will only update the specified fields in the specified document.

We need to determine the URL to which the request will be sent. In general, you’ll use a URL that’s relative to your current path. If you’re updating a document in the same database that’s currently open in the browser, then the URL to send a document update request against DDS looks like this:

/api/data/documents/unid/<documentUNID>

(Many thanks to Marky Roden for his posts with PATCH examples.)

Testing the Configuration

In order to make sure that everything is configured properly, this is a good point to test it out with a hard-coded request.

To do so, go into your database and get the UNID of a document. Also, make note of a field name to modify. Then we can use an XHR request in the browser console to test running a PATCH in order to make sure that it works (before adding the complexity of running it from the grid).

dojo.request.xhr("api/data/documents/unid/4030FB42B37B397785257D87004680BC", {
  data: '{"firstname":"Patched!"}',
  method: "PATCH",
  handleAs: "json",
  headers: {'Content-Type': 'application/json'}
}).then(function(data){
  console.log('successful');
}, function(err){
  console.log('error');
});

If it works, you’ll see the PATCH request in the browser console (or Net tab) of the browser’s dev tools:

Gridx 31 C Successful PATCH

Notes:

  • JSON requires double-quotes for properties and values
  • The data passed must be a string, so use single quotes to surround the JSON
  • handleAs must be set to “json” and the Content-Type header is also required
  • The newer Dojo xhr module is not automatically available in XPages, so you need to add it as a Dojo Module Resource
<xp:this.resources>
  <xp:dojoModule name="dojo.request.xhr"></xp:dojoModule>
</xp:this.resources>

Saving Edits in the Grid

Now that we know the server and database are configured to allow us to save changes, we can update the grid to send the proper requests.

To do so, we’ll add an event handler to the onApply event of the Edit module. This will fire whenever the user makes a change (and does not cancel it by hitting the ESC key).

The first argument that the onApply callback receives is an object that gives you access to the row, column, updated value, etc. This gives us what we need to build the proper PATCH request.

To attach a callback to the onApply event, update the grid’s modules property to include the Edit module as an object and define the onApply event callback:

{
  moduleClass: Edit,
  onApply: saveChange
}

Here’s the function that runs when a grid cell is changed and sends the PATCH request to save the changes:

function saveChange(gridObject) {
  var newValue = gridObject.rawData();
  var fieldName = gridObject.column.field();
  var rowUNID = gridObject.row.item()['@unid'];

  // Build set up the JSON object for the update
  var data = {};
  data[fieldName] = newValue;
  var jsonData = JSON.stringify(data);

  dojo.request.xhr("api/data/documents/unid/" + rowUNID, {
    data: jsonData,
    method: "PATCH",
    handleAs: "json",
    headers: {'Content-Type': 'application/json'}
  }).then(function(data){
    console.log('successfully updated document (' + rowUNID + ')');
  }, function(err){
    console.error('error updating document (' + rowUNID + ')');
  });
}

Line 2 gets the updated cell value from the grid (after any fromEditor processing is done)

Line 3 gets the name of the source field from the column. This will only work if your grid column is named after a single field. If it has a different name, then you’ll need to take that into account when sending the update.

Line 4 gets the UNID of the document from the data store. The item() method of the row provides the entire record for that row, so I’m pulling the @unid property that is provided by the REST service that supplies the data for the grid. (XPages REST services will automatically include it.)

Lines 7-9 set up an object with the data to send to update the back end document and then ensure that it’s formatted as JSON.

Lines 11-20 send the PATCH request to the server to update the back end document.

Only Send Changes if Data is Modified

The last thing I want to do now is prevent this from firing every time a cell goes out of edit mode. If the user didn’t change the data, then it’s a waste of bandwidth and server processing to send updates to the back-end document.

I don’t see a way to get the original value from the onApply callback, but you can use the onBegin event to store the value in a global variable, then check it before sending any changes.

Here’s a function I added to the onBegin callback:

function beforeEdit(gridObject) {
  window.OriginalValue = gridObject.rawData();
}

Here’s the module inclusion that adds the callback:

{
  moduleClass: Edit,
  onApply: saveChange,
  onBegin: beforeEdit
}

Now, the first few lines of saveChange() can be updated to check whether the value changed and cut out early if it hasn’t:

// Get the updated value (after any fromEditor callback is done)
var newValue = gridObject.rawData();

if (window.OriginalValue == newValue) {
  return;
}

Security

In case you’re wondering, security is based on your current Domino session. If you’re logged into the server, then any PATCH request will be sent with your ID. (If you’re not logged in, it’ll be sent as Anonymous.)

If the user does not have rights to edit the document in the NSF, then the PATCH request will return an error like this:

Gridx 31 D Error PATCH

Gridx in XPages – 30: Getting Started with Inline Editing

Like other modern grids, Gridx can provide the ability for users to edit data inline. This can very handy for making quick changes without having to leave the page or load another one to edit a form. In this post, I’ll show how to get started with editing data in the grid.

Gridx Series

Gridx in XPages — Entire Series

Making a Column Editable

To make a grid column editable, you need to include two modules and set a property in the column definition.

1. Add the ‘Edit’ and ‘CellWidget’ modules

Add the Edit and CellWidget modules to the require statement and to the grid’s modules property

"gridx/modules/Edit",
"gridx/modules/CellWidget",

...

modules: [
  Resizer,
  NestedSort,
  Filter,
  FilterBar,
  QuickFilter,
  VirtualVScroller,
  CellWidget,
  Edit
]

2. Add a Column Property

Now that the modules are in place, all you have to do us add the editable property to a column definition and set it to true.

{
  id: 'first',
  field: 'firstname',
  name: 'First',
  width: '70px',
  editable: true
},

Default Edit Behavior

Now you can change the firstname column to edit mode by either double-clicking on a cell or selecting the cell and hitting the [ENTER] key.

By default, it will provide a plain text field for you to modify the data.

Gridx 30A

To stop editing, either hit the [ENTER] key or click or tab out of the field. It then changes back to read mode and shows the updated value.

Note: This preserves the change in the in-memory data store, but does not save it to the back-end document. (I’ll dig into that more in the next post.)

Always in Edit Mode

If you’d rather the data in the column always be in edit mode, you can add the alwaysEditing property to the column definition and set it to true.

Gridx 30B

{
  id: 'first',
  field: 'firstname',
  name: 'First',
  width: '70px',
  editable: true,
  alwaysEditing: true
},

Processing the Updated Value

You can also attach a function to process the user-entered value before it is updated in the data store.

In this example, I have a function that surrounds the updated value with asterisks.

function processState(storeData, gridHandle) {
  return '*' + storeData + '*';
}

The function automatically receives two parameters (it doesn’t matter what you name them): the updated value in the cell and a handle to the grid, which also gives you access to the row and column being edited, so you can get the document ID from the row (which will be necessary when saving changes to the back end document).

To call it when the column is updated, it needs to be added to the editorArgs object via the fromEditor property.

{
  id: 'state',
  field: 'state',
  name: 'State',
  width: '40px',
  editable: true,
  editorArgs:{
    fromEditor: processState
  }
},

There is also a toEditor callback that can be used to process the data before it becomes editable (but that would be more useful with other data types and edit controls).

You can also add callbacks to the onBegin, onApply, and onCancel events as needed.

Up Next

In the next post, we’ll look at saving the changes. After that, we’ll look at different types of widgets you can use to edit data in the grid.

Gridx in XPages – 29: Programmatic API for Details on Demand

In the last post, I showed how to add an expandible detail section to each row via Gridx’ Details on Demand feature. In this post, I’ll show how use the provided methods to programmatically expand and collapse the details, as well as how to attach event handlers to run after a detail section is opened or closed.

Gridx Series

Gridx in XPages — Entire Series

Methods

In the last post, I mentioned that there’s a showExpando property of the dod module that can be used to prevent the expand/collapse icon from being displayed. If you’d rather use your own button to expand/collapse the detail row, you can set up your own column with HTML content (button, icon, etc) to do the job programmatically with the built-in methods.

You can run the built-in methods via the dod object or via the grid row, which gets an extended API after loading the dod module.

For each of the methods below, there are 4 different statements that all have the same effect. Two that run the method via the dod module and two that run it via the extended row API. In each case, there’s an example of getting a handle to the row by index (0-based) or by the row ID (which, in my case, is the NoteID of a document in my sample grid). Note that the row API method names are slightly different.

show() – Display the detail section for a row

  • grid.dod.show(grid.row(0));
  • grid.dod.show(grid.row(‘FCE’));
  • grid.row(0).showDetail();
  • grid.row(‘FCE’).showDetail();

hide() – Hide the detail section for a row

  • grid.dod.hide(grid.row(0));
  • grid.dod.hide(grid.row(‘FCE’));
  • grid.row(0).hideDetail();
  • grid.row(‘FCE’).hideDetail();

toggle() – Toggle the detail section for a row

  • grid.dod.toggle(grid.row(0));
  • grid.dod.toggle(grid.row(‘FCE’));
  • grid.row(‘FCE’).toggleDetail();
  • grid.row(0).toggleDetail();

isShown() – Check whether a detail section for a row is displayed

  • grid.dod.isShown(grid.row(0));
  • grid.dod.isShown(grid.row(‘FCE’));
  • grid.row(0).isDetailShown();
  • grid.row(‘FCE’).isDetailShown();

Events

In addition to the method to trigger or check the row state, there are two events that allow you to run logic after a detail section is shown or hidden: onShow and onHide.

Here are two simple functions that display a message in the console on the onShow and onHide events, respectively:

function dodShowCallback(){
  console.log('Show Details');
}
	
function dodHideCallback() {
  console.log('Hide Details');
}

They can be added via the onShow and onHide module properties in the grid definition (lines 21 and 22 below):

grid = new Grid(
{
  id: "my_gridX",
  cacheClass: Cache,
  store: store,
  structure: columns,
  vScrollerBuffSize:10,
  modules: [
    Resizer,
    NestedSort,
    Filter,
    FilterBar,
    QuickFilter,
    VirtualVScroller,
    {
      moduleClass: Dod,
      defaultShow: false,
      useAnimation: true,
      showExpando: true,
      detailProvider: myDetailProvider,
      onShow: dodShowCallback,
      onHide: dodHideCallback
    }
  ]
});
Follow

Get every new post delivered to your Inbox.

Join 68 other followers