XPages Tip: Displaying View Panel Sort Icons on a Separate Line

If there isn’t enough room to display both the column title and sort icons in all column headers in a view panel, you can end up with an uneven look. In this post, I’ll show how to use CSS to move the sort icons down to a new line.

Column Header Layout

If you’re using a view panel with sortable columns, you will ideally not have too many columns and instead have enough screen real estate that the sort icons don’t make a difference.

View Panel Sort Icons - 1

However, a common scenario is that there are a lot more columns crammed in and some (but not all) sort icons wrap to a new line. This ends up causing an uneven look.

View Panel Sort Icons - 2

In addition, the icons take up space, so you may have columns that have a 1- or 2-letter code, but the sort icon makes the column 2-3 times wider than it needs to be, so you lose precious real estate.

If that happens and you’d like to make them consistent, you can use CSS to always move the sort icons to the next line.

Column Header Source

In order to apply CSS to adjust the layout, we need to view the page source to see what’s being generated.

This snippet contains the tags that start the view panel (line 1), start the column header row (lines 2-3), and display the first column header (lines 4-16).

<table id="view:_id1:viewPanel1" class="xspDataTable">
  <thead>
    <tr>
      <th scope="col">
        <div class="xspPanelViewColumnHeader">
          <span>
            <a id="view:_id1:viewPanel1:viewColumn1:__internal_header_title_id" href="#" class="xspPanelViewColumnHeader">
              <span class="xspPanelViewColumnHeader">Group</span>
            </a>
          </span>
          <span>
            <img id="view:_id1:viewPanel1:viewColumn1:__internal_header_sort_icon" src="/domjava/xsp/theme/common/images/sort_none.gif" 
alt="Sort Toggle" title="Sort Toggle" class="xspImageViewColumnHeaderSort">
          </span>
        </div>
      </th>

This shows that a column header div contains a span with the column title and another span with the sort icon.

Adding a New Line with CSS

This gives us enough information to know how to target the proper element with CSS. There are several ways to go about it. My example will target the table (with class name), div (with class name), and then get the first span tag. It will add the new line after the first span.

table.xspDataTable div.xspPanelViewColumnHeader span:first-of-type:after {
  content: '\A';
  white-space: pre;
}

The '\A' in the content attribute denotes a new line with CSS. It does not work to put a <br> tag there. The white-space attribute is also required or you may not see any difference.

Now, all of the sort icons appear on a separate line.

View Panel Sort Icons - 3

You can tweak it by adjusting the space between the lines or centering, etc, but this gets the ball rolling.

Also, if you want to modify the images themselves, you can target .xspImageViewColumnHeaderSort with CSS.

XPages Tip — Adding Attributes to the HTML Tag

By default, the HTML tag rendered for an XPage only includes an attribute for the page language (<html lang="en">). In this post I’ll show how to add additional attributes to the HTML tag rendered for the page.

Adding an Attribute

You can add an attribute to the HTML tag via the attrs property, which can be found in All Properties > basics > attrs. Click the plus (+) button to add an attribute and then specify the attribute name and value.

HTML Attribute

Here is what’s added to the page, below the tag:

<xp:this.attrs>
  <xp:attr name="myAttribute" value="myValue"></xp:attr>
</xp:this.attrs>

When the page is loaded in the browser, you can see the attribute in the source:

<html lang="en" myAttribute="myValue">

Usage

One handy use for this is the HTML5 manifest attribute, which specifies the location for caching the page for offline and faster access.

Here’s an example:

<xp:this.attrs>
  <xp:attr name="manifest" value="myCache.appcache"></xp:attr>
</xp:this.attrs>

Note: If you want to use an HTML5 feature, you need to change the HTML doctype to HTML5 on the Page Generation tab of the Xsp Properties design element.

Default doctype:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

HTML 5 doctype:

<!DOCTYPE html>

There are other global attributes that can also be applied to the HTML tag, such as id, style, dir (page direction – left to right or right to left), but there are other built-in properties for those on an XPage, so there isn’t a need to set them with a page-level attribute.

Note

In order to be applied to the HTML tag, the attribute must be defined at the XPage level. If added to a custom control, they’ll become attributes of a <div> tag.

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

Follow

Get every new post delivered to your Inbox.

Join 69 other followers