Angular playground: applying an infinitescroll for Domino Access Services

Intro

Inspired by a serie of blogposts from Marky Roden and popularity in the web dev community I decided to step (once again) from the XPages path (a trend?) and feed my curiosity on AngularJS. An excellent stepping stone is the example database provided by Marky and the AngularJS Fundamentals In 60-ish Minutes presentation by Dan Wahlin.

Domino Access Services

The example database contains examples for CRUD operations via Domino Access Services (way to go!) but is limited in the display of documents from a list/view. The default number of documents returned for view/folder entries is set to 10. Not much of value for a real world application. So either I had to provide some sort of pagination or look for more smartphone/tablet common feature: infinite scroll.

The pagination examples I found only handled a one time loaded data-set and a proper application can contain thousands of records (I find it a common pattern that customers underestimate the number of documents that are being created in a Notes application). Angular can be extended with custom directives and ngInfiniteScroll is such a great example.

So what did I need to do to get this directive applied to the example database?

Implementation

index.html

First I installed the ng-infinite-scroll.min script in my database and updated the header section in index.html

<head lang=”en”>
<meta charset=”UTF-8″>
<title></title>

<link href=”bootstrap/css/bootstrap.min.css” rel=”stylesheet”/>
<link href=”css/custom.css” rel=”stylesheet”/>
<script data-require=”jquery@*” data-semver=”2.0.3″ src=”http://code.jquery.com/jquery-2.0.3.min.js”></script&gt;

<script type=’text/javascript’ src=”angularjs/angular.min.js”></script>
<script type=’text/javascript’ src=”angularjs/angular-route.js”></script>

<script type=’text/javascript’ src=”js/app.js”></script>
<script type=’text/javascript’ src=”js/controller.js”></script>

<script type=’text/javascript’ src=’js/ng-infinite-scroll.min.js’></script>
</head>

I also needed to include the complete jQuery library because Angular has jQuery lite built in which doesn’t have seem to have the features for dynamic height. You should also load the jQuery script before jQlite.

app.js

Next you need to register the infinitescroll directive for my angular application:

var personApp = angular.module(‘personApp’, [
‘ngRoute’,
‘peopleControllers’,
‘infinite-scroll’
]);

factory

The examples for ngInfiniteScroll demonstrate a factory which is not used in the example database. From my limited knowledge on Angular I have understood that a factory is an injectable function.

personApp.factory(‘DAS’, function($http) {

var DAS = function() {
this.items = [];
this.busy = false;
this.after = 0;
this.count = 30;
this.order = ‘firstname';

};

DAS.prototype.nextPage = function() {
if (this.busy) return;
this.busy = true;
var url = ‘//dev1/apps/others/angular/ainx.nsf/api/data/collections/name/byFirstName5Col?open’ + ‘&count=’ + this.count + ‘&start=’ + this.after;

$http.get(url).success(function(data) {
var items = data;
for (var i = 0; i < items.length; i++) {
this.items.push(items[i]);
}
this.after = this.after + this.count;
this.busy = false;
}.bind(this));
};
return DAS;
});

My factory is called DAS (I guess I break here the naming convention). You can store your factories in a separate (new) file e.g. main.js.

Controller.js

I removed the existing PeopleListCtrl controller and replaced it with the following one:

personApp.controller(‘PeopleListCtrl’, function($scope, DAS) {
$scope.DAS = new DAS();
});

partial-list.html

With everything in place I now needed to update the display of the list, which is defined in a partial. Besides using the factory I also wanted to add some additional features such as search and sorting. This turned out to be really simple.

At the end I wanted to have something as followed:

Screenshot_3

 

Search & Sorting

For a search feature I added an input control and used the directive ngModel called query to apply a filtering via a search query

<div class=”row” >
<div class=”col-md-1″><label>Search:</label></div>
<div class=”col-md-2″>
<form class=”form-search”>
<input ng-model=”query” placeholder=”Search for person” autofocus class=”input-medium search-query”/>
</form>
</div>…

For a sorting feature I added a select control and bound that to the order property for the data via a custom directive. Further I added a radio-button group and used the directive ngModel called direction:

…<div class=”col-md-1″><label>Sorting:</label></div>
<div class=”col-md-1″>
<select ng-model=”DAS.order“>
<option value=”firstname”>First Name</option>
<option value=”lastname”>Last Name</option>
<option value=”zip”>ZIP</option>
</select>
</div>
<div class=”col-md-3″>
<label class=”formgroup”>
<input type=”radio” ng-model=”direction” name=”direction” checked> ascending
</label>
<label class=”formgroup”>
<input type=”radio” ng-model=”direction” name=”direction” value=”reverse”> descending
</label>
</div>
</div>

Note that the default sorting is set to ‘firstname’ in the factory.

Apply infinitescroll to the data-table

Finally we need to apply the nextPage function in the DAS function by wrapping the data-table with a div and add the infinite-scroll attribute to it.

<div infinite-scroll=”DAS.nextPage()” infinite-scroll-distance=”3″>
<table class=”table table-striped”>
<thead>
<tr>
<th>Position</th><th>First Name</th><th>Last Name</th><th>Zip</th><th></th><th></th>
</tr>
</thead>
<tbody>…

Then we use the ng-repeat directive and for each item or person in the data we display a new row. Here is also were we apply the filter and sorting options:

…<tr ng-repeat=”person in DAS.items | filter:query | orderBy:DAS.order:direction”>
<td>{{person[“@position”]}}</td>
<td>{{person.firstname}}</td>
<td>{{person.lastname}}</td>
<td>{{person.zip}}</td>
<td><a class=”btn btn-info” href=”#/person/{{person[‘@unid’]}}”>Edit</a></td>
<td><a class=”btn btn-warning” href=”#/person/{{person[‘@unid’]}}/delete”>Delete</a></td>
</tr>

</tbody>
</table>
<div ng-show=’DAS.busy’>Loading data…</div>
</div>

Wrap up

That’s it! Every time you hit (almost) the bottom of the list 30 new rows will be added. The search only applies to the loaded data, not all the data that is the view but not loaded/displayed yet. That requires a different set up.

Some thoughts

Angular is an exciting new world for Domino and XPages developers. It offers lot out-of-the-box with directives and so on but XPages also does that (e.g. security, data-binding). In combination with Domino Access Services you can create real world CRUD applications like Marky’s example demonstrates.

I am very curious about your findings and code examples mixing Domino and Angular!

Any recommendation for a good book about object-oriented JavaScript?

After reading Mastering Dojo plus HTML5: Up and Running and still waiting for the arrival of Mastering XPages there is place for a new book on my bedside cabinet.

Yesterday I read this post regarding object oriented JavaScript. So I did a quick search on Amazon (I used the German site since that is the nearest available from where I live (read: good shipping conditions)). The first book in the list (Object-Oriented JavaScript: Create scalable, reusable high-quality JavaScript applications and libraries) received not so good reviews, mainly because the code examples were too childish.

So that leaves me with the following question:

Do you have any recommendation for a good book about object-oriented JavaScript?

Request: Personalize landing pages using GeoTargeting

Today I received a question from a customer if PHP could run on top of a Domino server. In the discussion that followed what the customer wants to achieve it became clear that he is more looking for a script to solve a problem rather than seeking for an integrated solution.

In short,  the customer wants to check the visitor’s IP address and popup a message in case a user from country A is looking at country B’s landing page and ask the visitor if he/she wants to be redirected to country A’s landing page.

A simple case of geotargeting which I did not smash in the face of the customer.

The question remains:

Does anyone have a Domino based solution for this?

A quick search via Google did not resulted in a JS script library containing the country-codes. Thanks in advance for your guidance!

jQuery TreeView Menu from Notes View

I was looking for a way to get a web publishing toolkit in Notes away from displaying documents in a frameset.

The frameset consists of several frames:

  • a header section
  • a leftside navigation section
  • a main or content section

The reason why a frameset is used because the leftside navigation is populated from documents in a Notes View.  This navigator would be collapsed again every time it would be re-loaded again.  It is also build by an agent that generates XML which needs to be transformed in the browser.

Here is how it looks today:

nav_current

Some searching on the web brought me to these documents:

jQuery TreeView Menu and a Show ‘n Tell Thursday blogpost. Combining the 2 topics would solve my problem since the jQuery TreeView allows to re-open the treemenu by the location specified for a menu item!

( I will add the SnTT tag to this document to honour the contributors, even when it is now still Thursday)

I soon discovered that the code for the Tree Convertor from Datatribe works with documents in the same level, meaning that documents with a response hierarchy will not be supported because you do not use categorized columns in your View. Nevertheless a little trial and error and rewriting the original code resulted in the following Notes View presented as a tree:

nav_new

Note: in the example I have already applied some styling.

In this View each ‘Category’ is a document containing one or more response documents. The highlighted (red) item is the currently opened document.

So how do you get there?

  • Create a Notes View that displays your documents as you want them to be presented in the tree. In the Datatribe example this View is called (LUDocs).

nav_notesview

  • Upload all the fields you need for the jQuery TreeView (CSS, Images, JS Libraries) and reference to them in the Head section of your Document Form:

nav_header01

  • Add the fuction that will transform the HTML list into a Tree View structure.

nav_header02

Note:  -the persist: “location”- parameter ensures that the tree will be expanded by the item / document opened by default.

  • On the place where you want to display the TreeView add the following code:

nav_new_code

As you can see a script is here called which uses an Agent to collect the data from a Notes View in an HTML format ( ul and li list format). I had to make come changes to the original code, since I am using a View that support the display of response documents in a hierarchy.

Sub Initialize
‘—This agent is designed to ouput the required javascript to produce a nested HTML list
On Error Goto errorhandler

Dim s As New notessession
Dim db  As NotesDatabase
Dim doc As NotesDocument

‘–Category passed in via URL
Dim inCategory As String

‘—View name passed in via URL
Dim viewName As String

‘—Display Category ?
Dim displaycategory As String

‘—Document in the view
Dim tmpdoc As notesdocument

‘–Category Level
Dim level As Integer
‘—Current Category level
Dim currentLevel As Integer

Dim view As NotesView
Dim i As Integer
Dim JavascriptOutput As String

‘—Due text size limits we need to hold the ouput on 1 or more temporay notes items (never saved)
Dim fieldCounter As Integer
Dim holdingField As Notesitem

Set doc=s.documentcontext

‘–Set up holding field
fieldCounter=1
Set holdingField=New notesitem(doc,”HoldingField_” & fieldCounter,””)

inCategory=getqueryparam(doc.query_string(0),”category”,””)
viewName=getqueryparam(doc.query_string(0),”viewname”,””)
displaycategory=getqueryparam(doc.query_string(0),”displaycategory”,””)

Set db=s.currentdatabase

level=0
JavascriptOutput=””

If displaycategory=”false” Then

Else
If inCategory=”” Then
JavascriptOutput=JavascriptOutput & |<h3>All Documents</h3>|
Else
JavascriptOutput=JavascriptOutput & |<h3>| & inCategory & |</h3>|
End If
End If

JavascriptOutput=JavascriptOutput & |<ul id=”tree”>|

‘—-View could be passed in via QueryString to allow a more generic agent
Set view=db.GetView(viewName)

‘—If view is not found handle it – as the view name is passed in this could be missing
If view Is Nothing Then
Print |Content-Type:text/plain|
Print |document.write(‘Error – View not found -| & viewName & |’)|
Goto getout
End If

Dim entry As NotesViewEntry
Dim nav As NotesViewNavigator
If inCategory=”” Then
Set nav = view.CreateViewNav
Else
Set nav = view.CreateViewNav
End If
Set entry=nav.GetFirst

Dim edoc As NotesDocument
Dim childdoc As NotesDocument
Dim coll As notesdocumentcollection

While Not(entry Is Nothing)

Set edoc = entry.Document
Set coll = edoc.responses

If coll.Count > 0 Then
currentLevel=entry.ColumnIndentLevel+1
Select Case (entry.ColumnIndentLevel+1)
Case level
%REM
JavascriptOutput=JavascriptOutput & |</ul></li>|
%END REM
Case Is<level
For i=2 To ((level)-entry.ColumnIndentLevel)
JavascriptOutput=JavascriptOutput & |</ul></li>|
Next
End Select

‘–Update level
level=entry.ColumnIndentLevel+1

‘–Propercase it
JavascriptOutput=JavascriptOutput & |<li><strong><a href=”http://| & returnCommonServer() & |/| & swapchars(db.FilePath,”\”,”/”) & |/0/| & entry.UniversalID & |”>| & edoc.Tx_Document_Title(0) &|</a></strong><ul>|
Else
‘—At document
currentLevel=entry.ColumnIndentLevel+1
Select Case (entry.ColumnIndentLevel+1)
Case level
%REM
JavascriptOutput=JavascriptOutput & |</ul></li>|
%END REM
Case Is<level
For i=2 To ((level)-entry.ColumnIndentLevel)
JavascriptOutput=JavascriptOutput & |</ul></li>|
Next
End Select

‘–Update level
level=entry.ColumnIndentLevel+1

Set tmpdoc=db.GetDocumentByUNID(entry.UniversalID)
Set tmpdoc=entry.Document
If tmpdoc.Tx_Document_Title(0)<>”” Then
JavascriptOutput=JavascriptOutput & |<li><a href=”http://| & returnCommonServer() & |/| & swapchars(db.FilePath,”\”,”/”) & |/0/| & entry.UniversalID & |”>| & fn_escape(Cstr(tmpdoc.Tx_Document_Title(0))) & |</a></li>|
End If
End If

‘–Check for field size
Call holdingfield.appendtotextlist(JavascriptOutput)
Call monitorFieldSize(doc ,fieldCounter,holdingField)
JavascriptOutput=””
Set entry=nav.GetNext(entry)
Wend

‘–Now we have finished we need to close the remaining tags
For i=1 To (currentLevel-1)
JavascriptOutput=JavascriptOutput & |</ul></li>|
Next

Call holdingfield.appendtotextlist(JavascriptOutput)

‘—-Now output Javascript

Print |Content-Type:text/plain|
Print |document.write(‘\n’+|
If fieldcounter=1 Then
Forall items In doc.GetItemValue(“HoldingField_1″)
Print|’| &  items & |\n’+|
End Forall
Else
For i=2 To fieldcounter
Forall items In doc.GetItemValue(“HoldingField_” & i)
Print|’| & items &  |\n’+|
End Forall
Next
Forall items In doc.GetItemValue(“HoldingField_1″)
Print|’| & items  |\n’+|
End Forall
End If
Print |’\n’)|

getout:
Exit Sub

errorhandler:

‘—As this is an agent that expects to return JavaScript we will return the error message in Javascript
Print |Content-Type:text/plain|
Print |document.write(‘Error – | & Error & | – | & Err & |’)|

Resume getout

End Sub

That is about it! Here is a better look of the result:

nav_result

Pagination on a View in the Notes client

A problem with Notes Views is that when they contain a lot of documents scrolling becomes inevitable. Even when you apply categorized columns it will become necessary that users click through the Views. An option would be that you just start typing and hopefully Notes will lead you to the nearest corresponding document.

When you apply categorized views the categories are displayed vertically below each other so for travelling users with smaller screens it will become tasly to open and close the categories just to see thse categories and corresponding documents displayed in a properly in the overview.

Maybe XPages in the Notes client will bring us some relief, but we are not there yet.

So how do we cope with the issue for the time being?

On the web it is common practice to have some sort of pagination on top / below lists of documents so you can navigate through the list without the need to scroll up and down or move you mouse across the screen. The pagination can be based on number of available pages or just available short cuts (example: the first letter of an available lastname). I myself have written some articles about pagination on this blog which you may check out yourself.

So this is all about the web, I adress this article to pagination in the Notes client.

The simplest way I found is to create a list of similar shortcuts, containing javascript, and render pass thru HTML in Notes. In this example I explain how I added pagination to a list (a Notes View) of persons. The thought is to show a list of persons form which their lastname start with a certain letter. What I needed to accomplish this:

  • A Form with an Embedded View. This View should be categorized by the first letter of the lastname @LowerCase(@Trim(Tx_LastName);1). SaveOptions are set to zero ofcourse.
  • The Embedded View has the ‘Show single category option’ in use.  As formula write the name of the Field that will acts as a temporary container (Tmp_LastName).
  • Add a Field named TmpLastName on top of the Embedded View. Make it editable. As default value give it the value “a” (you could check if this value is really available or not).
  • On top of the Form add the following JavaScript and CSS code:

<script language=”Javascript”>
var entryNumber = “”;
function showTab(lastName) {
entryNumber = lastName;
document.forms[0].LastName.value = entryNumber;
entryNumber = “”;
document.forms[0].set_field.click()
}
</script>

<style type=”text/css”>
font {
font-family: “Default Sans Serif”, sans-serif;
font-style: normal;
font-weight: normal;
font-size: 8pt;
color: black;
text-decoration: none;
text-align: left;
text-indent: 1ex;
}
A:link {color: blue; text-decoration: none}
A:visited {color: blue; text-decoration: none}
A:active {color: blue; text-decoration: none}
A:hover {color: blue; text-decoration: none}
</style>

  • As final step you need to add a computed text just above the Embedded View. Mark the text as passthru HTML. A Value enter:

varList:=@DbColumn( “”: “NoCache” ; “” : “” ; “PersViewCategorized” ; 1 );
tmpList:=@Implode(“<a href=\”javascript:showTab(‘” + varList + “‘)\”>” + varList+ “</a>” + ” | ” );
@LeftBack(tmpList;”|”)

Here we make a call to first column in the same Embedded View. Around each found category we wrap a JavaScript call which will put the value in the temporary field and finally we call the button to update the UI.

Here is a screendump that shows how it could look like:

paginationNotes

A problem discovered

Well everything  looked shiny untill I tried to load the Form in a Frameset. When pressing on one of the links I got the message:

paginationNotesMessage

Some work to do IBM….