XPages and Bluemix: Sending a targeted Websockets message to specific XPages

In this article I will demonstrate how the use of socket.io “rooms” enables us to send message to users who are only accessing specific pages within our application, rather than blanket messages to all users.

Introduction

In the previous article I demonstrated how to use a message POST to the node.js server which could then be turned into a chat message and sent out to all users. While this is a nice example it only serves as such and does not have significant business value. In this article I will begin the peel back the potential for much greater application flexibility through the controlled use of targeted Websockets messages to users of an application.

Most applications have more than one “page” within it and we may wish to send a message to users of one page rather than all pages. Conversely we may want to only send messages to users who are viewing certain types of information wherever they are within the application.

In this article we are still going to use the chat example but this will be the last time we use “chat” as the use case. In future articles I will look into more practical applications of Websockets messaging within an application.

Using rooms within socket.io

Looking at the socket.io documentation for rooms and namespaces you can see that the API exposes the ability to create individual communication channels between the server and the application users.

How this translates to our application on the node.js server looks like this:

  socket.on('joinRoom', function(room, name){
    socket.join(room);
    io.sockets.in(room).emit('notice', name+" has joined");
    console.log(name+" has joined room "+room)
  })

When the “joinRoom” event is registered on the server then the socket is joined to the room name which is passed in. A message is the broadcast specifically out to all the existing room members that the new user has joined. Note that this message is room specific because of the io.sockets.in(room) rather than a blanket message to all users.

With this code in place on any page we can register our application page (chat room in this case) with the socket server. In my case I created a generic function to take the name of the “room” from the XPage URL. The following client side JavaScript sends the “joinRoom” request to the server.

  nickname=$('.username').text()
  // Send message to server that user has joined WinX.nsf/xRoom1.xsp?open&login
  var xpageName = location.href.split('.nsf/')[1] //xRoom1.xsp?open&login
  xpageName = xpageName.split('.xsp')[0] //xRoom1
  socket.emit('joinRoom', xpageName, nickname);

On the server we can then see the console.log as someone joins the room:

s1
We can then have multiple users in individual “rooms” which are in this case 3 separate xPages xRoom1.xsp, xRoom2.xsp, xRoom3.xsp From the original chat room I am able to sen all three rooms a blanket message because they are all “listening” for “msg” as in the previous articles

 socket.on('msg', function(data) {
    console.log('msg here')
    console.log(data)
    redisClient.lpush('messages', JSON.stringify(data));
    redisClient.ltrim('messages', 0, 99);
    socket.broadcast.emit('msg', data);        //broadcast to all users indiscriminately
  });

s2

Sending a message to a specific room

Modifying the original POST code which was shown in the previous article I was then able to create an XPage which will send a targetted message to a specific room. To do this, all I had to do was pass in an additional POST parameter of the room I wanted to send a message to.

I create a new Master XPage which had 3 fields on it – one for each room. Each of the individual fields had a “room” attribute which allows me to pick up a value to specify which room to send it to.

<div id="msgRoom1" class="msgCenter">
	<input placeholder="Send a message to Room1" room="xRoom1" autocomplete="off" autofocus="autofocus" />
	<button type="button">Send</button>
</div>
<br/><br/>
<div id="msgRoom2" class="msgCenter">
	<input placeholder="Send a message to Room2"  room="xRoom2" autocomplete="off"  />
	<button type="button">Send</button>
</div>
<br/><br/>
<div id="msgRoom3" class="msgCenter">
	<input placeholder="Send a message to Room3"  room="xRoom3" autocomplete="off" />
	<button type="button">Send</button>
</div>

s3

In the following code we bind to each of the buttons so that when the are clicked they:

  • set msgField to be the jQuery object representing the pertinent field
  • create the data object to send to the socket server
  • create the newMessage passing in data

The new message function then:

  • POSTs the data object at the “/roomPost” path on the server
  var socketServerURL = (location.href.indexOf('copper.xomino.com')>0) ? "http://copper.xomino.com:3000" : 'http://xominosocket.mybluemix.net'
  var socket = io.connect(socketServerURL)

$('.msgCenter Button').on('click', function(){
  	var msgField = $(this).prev('INPUT')
	var data = { text: msgField.val(), nickname: nickname, when: new Date(), room: msgField.attr("room") };
	sendMessageToRoom(data)
	newMessage(data);
	// Clear the message field
	msgField.val('');
})

var sendMessageToRoom = function(data){
	console.log(data)
	$.ajax({ url:
	        socketServerURL+"/roomPost",
		type: "POST",
		data: data
	}).done(function( msg ) {
		console.log( msg);
	});
}

We can see the message come through on the server

s4

You can also see from the log that I was able to join 3 rooms from the Message Center page – this is as simple as creating three requests to join:

  //client side JavaScript
  socket.emit('joinRoom', 'xRoom1', nickname);
  socket.emit('joinRoom', 'xRoom2', nickname);
  socket.emit('joinRoom', 'xRoom3', nickname);

Site in action
The best way to see all this is to see it in action – as you can see from the video below.

Moving the application to Bluemix

Working locally I used to following code to determine if I was on copper.xomino.com or another server (demo.xomino.com or different again)

  var socketServerURL = (location.href.indexOf('copper.xomino.com')>0) ? "http://copper.xomino.com:3000" : 'http://xominosocket.mybluemix.net'
  var socket = io.connect(socketServerURL)

I had to make a slight change to my cors code in the local node server setup. I looked at the cors npm site to see how to dynamically add cors support and found the answer and modified my code accordingly. So you need to make sure you

//Marky adding CORS from copper
//app.use(cors()); //not used for whitelists

var whitelist = ['http://copper.xomino.com', 'http://demo.xomino.com', 'http://marky.psclistens.com'];
var corsOptions = {
  origin: function(origin, callback){
    var originIsWhitelisted = whitelist.indexOf(origin) !== -1;
    callback(null, originIsWhitelisted);
  }
};

marky.psclistens.com is the same local server as copper.xomino.com (I use a hosts file to make them both look at localhost). But this allows me to play with cors and in this case talk to the local or bluemix node server without needing another “Domino” server.

s6

Because the site running locally is already connected to the Jazz Hub Git repository, as with the previous examples all I have to do is commit the changes locally, push to the Jazz Hub repository, the application will then be re-built and re-deployed to Bluemix.

It’s really about as simple as that. Because the code is already primed to check to see if we are looking locally or at Bluemix, a new URL for the application now looks at Bluemix.

s5

 

So why do we need Bluemix again?

While the development was performed locally, the intention is to be able to create a capability which does not require the user to have a locally running node.js server. That is where Bluemix comes in. In essence what we are creating is a cloud hosted Websockets service which can be integrated into existing applications.

 

Conclusion

In this article we have seen that with a little creativity we are able to register different chat rooms within the same application. There are other ways to do this of course but the point of this example was to demonstrate the ability to send a specific message to specific users looking at an individual XPage.

In the next article we will take a look at the practical implications of this within an XPages application.

 

Advertisements

x$ – now a part of XPages Extension Library

I am very flattered to find out that not only is my x$ OpenNTF xSnippet being used more widely than I realized (over 600 downloads). It now being used in the latest release of the OpenNTF Extension library.

If you look here – http://bootstrap4xpages.com/xsp/.ibmxspres/.extlib/responsive/xpages/js/xsp-mixin.js and search for it you will find


//jQuery selector that works with XPages IDs
//See - http://openntf.org/XSnippets.nsf/snippet.xsp?id=x-jquery-selector-for-xpages
function x$(idTag, param){
	idTag = idTag.replace(/:/gi, "\\:") + (param ? param : "");
	return($("#" + idTag));
}

Who knew my first foray into the XPages community would have such an impact.

The lesson here boys and girls should be that you should *share* your work however small and insignificant you think it is. Like all scientific research, very little code is “a completely new way of doing things”. Generally everything grows incrementally and just because you don’t think something is significant, someone else might.

You are reading this – what have you shared recently?

🙂

 

Binding jQuery code to an XPages partialRefresh using DOM Mutation events

Introduction

In this article I will demonstrate how to bind to the event which triggers on the completion of an XPages partialRefresh. Using that binding we will then be able to action to contents of the newly added partialRefresh DOM elements.

Background

In multiple articles I have discussed the use of the onComplete event of a programmatically triggered partialRefresh to be able to re-apply a jQuery plugin’s bindings to an area of an XPage. This works very nicely and integrates with the Dojo events controlling the xhr request to the Domino server.

A problem arises when you do not have a programmatically controlled partialRefresh, say for example in a pager. XPages uses the same technology to execute a partial refresh on a viewPanel – but you and I do not have programmatic access to the onComplete event without hijacking it.

This was brought back to my attention when reading Brad Balassaitis’ excellent article on adding font awesome to his data view. In that case he does not have an event available to him through the XPages designer so he has to hijack the Dojo calls. A practical solution given the tools available.

In general though I have always found using the XPage events a non-elegant way of controlling the page and there has to be a better way – I think upon reflection this is a nice learning experience and “good to know” article but not practical in production.

DOM Mutation events

These events have been around for a while but are now “deprecated” in favor of the new MutationObserver() constructor which is unfortunately not implemented in Internet Explorer until IE11

  • DOMAttrModified
  • DOMAttributeNameChanged
  • DOMCharacterDataModified
  • DOMElementNameChanged
  • DOMNodeInserted
  • DOMNodeInsertedIntoDocument
  • DOMNodeRemoved
  • DOMNodeRemovedFromDocument
  • DOMSubtreeModified

As the mozilla article states – “The practical reasons to avoid the mutation events are performance issues…...” – watching the DOM for changes every time a change happen has very processor intensive – believe me in my experiments if you latch onto DOMSubTreeModified and you are using jQuery which is constantly changing the DOM – you can easily drag your browser to its knees.

So in this article I am going to demonstrate how to use the “old” method for IE<11 and the preferred new method. You can then decide for yourself on the right way to do things – Dojo hijacking, degrading DOM performance or if you are lucky enough to not have to support IE – the way of the future 🙂

An example of the general problem

If I have a simple view panel on a page and I use some jQuery to stripe the rows it looks pretty…..(yes there are other ways to stripe the rows this is just to demonstrate the point).

jq1

But as soon as I hit the pager – the striping is lost. The DOM elements are removed and the new elements do not have the striping applied

jq2

The partialRefresh

As I am sure most of you know the partialRefresh as genius as it is, works by POST-ing the field values on the form back to the server where the JSF licecycle processes these POST-ed values and then returns a new set of HTML to the browser. That new HTML is inserted as a direct replacement of the DOM element which was being refreshed. Looking at the response from the server you can see below that when paging through a viewPanel the viewPanel1_OUTER_TABLE is re-downloded from the server and replaces the existing Table element in the DOM.

jw3

So my striped table is deleted from the DOM and replaced – ergo no more striping.

DOM Node insertion

Using the DOM Mutation event DomNodeInserted it is actually relatively easy to re-stripe the table.

I first surrounded the viewPanel with a div wrapper “viewPanelWrapper”. This is what I will use to listen to changes for. Because the whole outer table is replaced I cannot listen to events on it – it is removed along with my binding.

The first piece of code will demonstrate the event listener

$('.viewPanelWrapper').on("DOMNodeInserted", function(){
    console.log('a node was inserted')
})

When I run the above code snippet through firebug you will see that nothing changes (1). But when I click Next the partialRefresh is triggered and “a node was inserted”
jq5

If we then take this a step further we can add in our striping code again

$('.viewPanelWrapper').on("DOMNodeInserted", function(){
    console.log('a node was inserted')
    $('.viewPanel TR:even').css({background: '#FFCCCC'})
})

And that’s pretty much it – pretty simple really.

jq6

So then extending this simple example you can see how a jQuery plugin could be reapplied to any page after a partialRefresh has been triggered – JUST BE AWARE THAT THERE IS A PRICE TO PAY IN PERFORMANCE. If you are going to do this then make sure that you pick the smallest area to check possible and that it does not change every second – your browsers and more importantly users will not thank you. On and applying a jQuery plugin almost certainly also modifies your DOM – be careful not to create an endless loop of pluging in your plugin.

So the “better way”

This article explains the reasoning behind the new MutationObserver and more importantly why it makes more sense than what I just showed you.

DOM MutationObserver – reacting to DOM changes without killing browser performance.

Check out the “So what are these good for” section at the end – obviously they were talking about XPages 😉

Using a slightly modified version of their example we get this

var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  var list = document.querySelector('.viewPanelWrapper');

  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {

        console.log(mutation.type);
        $('.viewPanel TR:even').css({background: '#FFCCCC'})
    });
  });

  observer.observe(list, {
  	attributes: true,
  	childList: true,
  	characterData: true
   });

jq7

Which works the same but as the article explains – WAY more efficient and also gives you the control to not screw up your plugins.

Remember though the caveat is modern browsers and that it is IE11 only

Conclusion

Overall this has been a fascinating learning experience for me. I can’t recommend using the DOMNodeInserted event listener because it definitely caused me pain and anguish in browser performance. The MutationObserver is a very interesting concept but I am not convinced I would use it in an application until I better understand it.

Integrating the Bluemix Watson Translation service into an XPages application

In this article I will demonstrate how to integrate the Bluemix Watson Translation service into a functioning XPages application.

Bluemix Watson Translation Service

Following on from one from my previous posts on the subject I have been looking for a good workable example of using a Bluemix service within an XPages application. As I said before this is as much an exercise in me learning more about node.js and Bluemix as anything else – but I also love being able to share.

Based on the previous post about how to get the Watson Q and A service up and running it took me 12 minutes to get a working example of the Watson Translation service up and running on a Bluemix site. Just for disclaimers I have no idea how good the Watson service is and I am not advocating it – this is purely an exercise in being able to use a Bluemix service, more so that what it does.

So I followed the steps in my previous blog but applied them to the example post on the Watson Translation service. This was pretty simple and once again the only thing to change was the manifest.yml file to name the service and application correctly.

w2

http://xominowatsontranslate.mybluemix.net/

When you type into the TEXTAREA box (1) and then hit “Translate”(2) the result is displayed in the Output (3)

The page does a full refresh and the answer is displayed. The node.js code running behind the application is fairly self explanatory :

  • (looking at the code snippet below) we can see the values POST’d from the application are received on the node.js server in the app.POST
  • The posted field values are turned into a separate request to the Watson translation service
  • The Watson request (watson_req) is sent as a serversiude request to the Watson service
  • The results from that are sent back to the response object “watson_res”
  • The node.js response (res) back to the user is then set back to the browser using the jade template (index)
    • return res.render(‘index’,request_data);

// Handle the form POST containing the text and sid, reply with the language
app.post('/', function(req, res){

    var request_data = {
        'txt': req.body.text,
        'sid': req.body.sid,
        'rt':'text' // return type e.g. json, text or xml
    };

    var parts = url.parse(service_url);
    // create the request options to POST our question to Watson
    var options = { host: parts.hostname,
        port: parts.port,
        path: parts.pathname,
        method: 'POST',
        headers: {
            'Content-Type'  :'application/x-www-form-urlencoded', // only content type supported
            'X-synctimeout' : '30',
            'Authorization' :  auth }
    };

    // Create a request to POST to Watson
    var watson_req = https.request(options, function(result) {
        result.setEncoding('utf-8');
        var responseString = '';

        result.on("data", function(chunk) {
            responseString += chunk;
        });

        result.on('end', function() { //this is triggered when the response from Watson is completed
            // add the response to the request so we can show the text and the response in the template
            request_data.translation = responseString;
            console.log('request',request_data);
            return res.render('index',request_data); //<------response sent back to the web page
        })

    });

    watson_req.on('error', function(e) {
        return res.render('index', {'error': e.message})
    });

    // create the request to Watson
    watson_req.write(querystring.stringify(request_data));
    watson_req.end();

});

 

You can see that in action through firebug – here is the POST parameters

w3and here is the response – the new HTML containing the answer

w4

 

Modifying the service

Here is the thought process which occurred to me:

  • Well that is cool but if I want to integrate this into an XPage then I will need to return the result as JSON and not a whole HTML page
  • But I do not want to break the example so how can I do that ?
  • I can use a new route – so if I POST at a different URL, the node.js server can be made to be smart enough to do something different
  • Ah…….and then I will have CORS issues because my application will be running at xomino.com and the bluemix app is not

Replicating the POST as code

So the first thing I did was try and replicate the code in jQuery (as I know best) so that I could mimic a POST event in ajax without using a form. In the following example you can see I added jQuery to my page (jQuerify plugin for firebug). I then simulated an AJAX POST to the translation service (emulating the form post).

w5

 

The response is the HTML of the new page, so I know this is at least the right AJAX code.

w6

 

But when I am on the copper.xomino.com site and I try and repeat the same thing, as expected CORS issues.

w7

Ryan Baxter (who frankly has been a huge support and help in learning all this) said to me

“You are in luck, CORS is dead simple with Node  – https://www.npmjs.org/package/cors” and how right he was.

Adding the CORS package to my Bluemix application

The way you add a package to your Bluemix application is by updating the package.json file. This file contains all the npm modules that will be needed and what is even cooler is that these will be npm installed within your node server automagically on deployment

w8

So then I need to add the cors enabled page to my application. But I did not want to change the core example on the site. So what I did was create a new “route” within the node.js application and told it to do something different….

Originally we had this within the example


// Handle the form POST containing the text and sid, reply with the language
//the route in this case is "/" - so basically the root of the application
app.post('/', function(req, res){
	//stuff
}

and to create a new route we just add a different app.post


// Handle the form POST containing the text and sid, reply with the language
var cors = require('cors'); //add the cors module code to the application
app.use(cors());
var corsOptions = {
    origin: 'http://copper.xomino.com' //allow copper to be a site which can work with the watson site
};

//note the new cors(corsOptions) parameter in the app.post function
app.post('/xpages', cors(corsOptions), function(req, res){ //when a user posts to  site.com/xpages do this code instead of the default '/'
	//stuff
}

What I then changed was the response code – this was the original code responding with text and rendering using the jade index.jade template

	//......
        result.on('end', function() {
            // add the response to the request so we can show the text and the response in the template
            request_data.translation = responseString;
            console.log('request',request_data);
            return res.render('index',request_data); //render the response using the index,jade template and passing the request_data
        })

Instead of res.render I used res.json to send the request_data object back to the browser directly.

	//......
        result.on('end', function() {
            // add the response to the request so we can show the text and the response in the template
            request_data.translation = responseString;
            console.log('request',request_data);
            return res.json(request_data);
        })

So in this way I left the default page alone (“/” server root) – the example still works – but I added a new route (“/xpages”) so that my XPage application could POST at it.

I committed the code and pushed back up to Jazz Hub, restarting the service as we went…….and low and behold success…..

w9

As you can see from the above image, I am at the copper.xomino.com website. Using the slightly modified code to now post at

I get a response from the Bluemix website with the JSON from the Translation service. You can see from the headers the CORS header is added for copper.xomino.com

w10

Note – if you go to http://xominowatsontranslate.mybluemix.net/xpages you will GET an error. This is because my node application does not have an app.get for the /xpages route

So then XPages….

Integrating the Watson Transation service into an XPage application

Once I have the working ajax code it is very simple to add it to an XPage and just pick up the field values on the fly….. I cheated to some extent and copied and pasted the HTML from the original example into my XPage….hey why not this is an example after all…

I added a couple of classes to the example so that I could easily pick out the field values…

a1

And then the jQuery code to add to the application. In this code I

  • Select the .translate class (the translate button)
  • In the click event I get the data from the form
    • the .sid select box for the language translation
    • The text value from the textbox (.theOriginal) field
  • I then submit to the the translation service as before
  • In the .done() of the AJAX request, the incoming msg.translation is then added to the .theTranslation field

a2


 The demo

As you can see from the video – I am able to:

  • type in any value
  • send it to the bluemix service
  • receive the answer and display it on the screen
  • save the original and translated values as notes documents within my XPages application
  • Firebug shows the fact that the POSTS are going to http://xominowatsontranslate.mybluemix.net/xpages

Conclusion

I think this is a great example of using a “service” on another website to be able to enhance the functionality in your own XPages application. If IBM are successful in their investigation to put Domino inside of Bluemix, this capability will all happen behind the scenes and could be easily written as one true application……bring it on… 🙂

 

PS

This exercise was particularly gratifying because it feels like a culmination of all the work I have done in XPages over the last 3 years.

  • Without the jQuery and Firebug work I did at the start of XPages development, prototyping this would have been much more tedious.
  • Learning XPages has forced me to have a better understanding of AJAX and jQuery
  • If it were not for Angular and the “Write once run anywhere” I did for MWLUG, I would not have learned about CORS.
  • If it was not for the Angular work I would not understand “routes” within an application
  • …..and now I am learning about node.js and Bluemix PaaS

Keep learning !

 

 

Angular in XPages #8 – Directives (did someone say plugins?)

In this article I will discuss Angular Directives and why they are near and dear to my heart

Directives

Directives in Angular are modular pieces of functionality which in some cases are very analogous to plugins in jQuery. According to the Angular documentation…

“At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS’s HTML compiler ($compile) to attach a specified behavior to that DOM element or even transform the DOM element and its children.”

For more on the directive documentation look here – https://docs.angularjs.org/guide/directive

I have already shown and demonstrated some of the core Angular directives like ngApp and ngView. Those attributes within the HTML are the instructions to “Do Something Here”

<div ng-app="personApp">
    <div ng-view></div>
</div>

The on-last-repeat directive

Within my demo application I wanted to know when the table was finished loading. The problem is in Angular that there is no “I am finished with the template load” event. The reason is due to the asynchronous nature of the Angular code.

I wanted to load the table and then take an action on it. To do this I added a directive to the “repeat control” within my template for loading people.

The following example was completely plagiarized from this article – respect !

http://www.nodewiz.biz/angular-js-final-callback-after-ng-repeat/

You can see the on-last-repeat attribute within the first TR

    <tr ng-repeat="person in people"  on-last-repeat>
        <td>{{person.firstname}}</td>
        <td>{{person.lastname}}</td>
        <td class="zipCode">{{person.zip}}</td>
        <td class="user">{{person.username}}</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>

I then created a directive within my app.js to do something with the on-last-repeat

personApp.directive('onLastRepeat', function() {
    return function(scope, element, attrs) {
        if (scope.$last) setTimeout(function(){
            scope.$emit('onRepeatLast', element, attrs);
        }, 1);
    };
})

Within the code we are creating a custom “event” which can be listened for in another part of the application

  • If the last entry is triggeredhere
    then
  • $emit the onRepeatLast event within the application

Then in the PeopleListCtrl controller we set a listener –

  • $on the onRepeatLast event
  • Do something
peopleControllers.controller('PeopleListCtrl', ['$scope', '$http', 'peopleFactory',
    function ($scope, $http, peopleFactory) {
        $scope.$on('onRepeatLast', function(scope, element, attrs){
           alert('The table has loaded')
        });

a1

 

Well that is a little simple….

Yes it is but it serves to demonstrate the concept. The best part is that there are lots of people who are writing plug and play Directives for Angular – just like jQuery plugins. If I could be bothered I might even do some blog posts on useful Directives…..But is a good resource for looking at some available directives.

http://angular-js.in/

There are multiple directives for using jQuery plugins within an angular construct – this is very helpful for data binding. There are also multiple directives which are independent of jQuery and are stand along “Angular” plugins for want of a better term.

Angular.js in XPages #7 – Writing better code using Services

In this article I will discuss a better programming practice for Angular.js than was demonstrated in the previous articles within this series. I have mentioned before, part of the purpose this blog is very much a “learning in progress” for me. Without going through the previous articles I would not have been able to get to this point and write “better code”. Hopefully with that understanding, those of you who have been along for the ride will appreciate this and grow with me 🙂

Services within Angular

Within Angular there are these programmatic entities called services – and if you read around Angular programming practices (Follow @ToddMotto – http://toddmotto.com/opinionated-angular-js-styleguide-for-teams/) you will find (as I have) that adding application logic within Controllers is generally frowned upon.

“In AngularJS world, the services are singleton objects or functions that carry out specific tasks. It holds some business logic. Separation of concern is at the heart while designing an AngularJS application. Your controller must be responsible for binding model data to views using $scope. It does not contain logic to fetch the data or manipulating it.”

http://viralpatel.net/blogs/angularjs-service-factory-tutorial/

From a maintainability perspective, as well as logical code layout perspective, having functional logic within the controller is akin to writing a 1000 line LotusScript agent  – not optimal !!

I used this article by Dan Whalin to better understand how to create a Service using a factory

So what changed?

We I changed the controller around so that code in the controller that once looked like this:

 $scope.createPerson = function(event) {
            $http({
                url: '//copper.xomino.com/xomino/ainx.nsf/api/data/documents?form=fUserName',
                data: $scope.person,
                withCredentials: true,
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                }
            })
                .success(function(data) {
                    location.href = "#/people";
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
            event.preventDefault();
            return false;
        };

now looks like this:

        $scope.createPerson = function(event) {
            dataFactory.createPerson($scope.person, event)
                .success(function(data) {
                    event.preventDefault();
                    location.href = "#/people";
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };

and the service looks like this:

    dataFactory.createPerson = function (person, event) {
        console.log(person)
        return $http({
            url: urlBase+'documents?form=fUserName',
            data: person,
            withCredentials: true,
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            }
        })
    };

 

Now I know you are looking at this and saying to yourself – hang on you just copied and pasted all the code and actually made it longer and more complicated – and that would be very perceptive of you – but (and I had to make this leap to believe it myself), it is better.

My example application only really has one purpose – manage people. In a more complex application a separation of logic into like components would make sense. By using the service I have separated all my “do stuff with people” out into a people Service. When I built the next part of the application – to do with buildings for example I would create a buildings Service and manage all my building logic there.

Let the controller do it’s job and bind the data to the View and keep your logic out of the controller and into Services.

The final Code

My service (service.js)

personApp.factory('dataFactory', ['$http', '$timeout', function($http, $timeout) {

    var urlBase = '//copper.xomino.com/xomino/ainx.nsf/api/data/';
    var dataFactory = {};

    dataFactory.getPeople = function () {
        return  $http.get(urlBase+'collections/name/byFirstName5Col?open&count=3').success(function(data) {
            $scope.people = data;
        });

    };

    dataFactory.getPerson = function (id) {
        console.log('get person')
        return  $http.get(urlBase+'documents/unid/' + id)
    };

    dataFactory.createPerson = function (person, event) {
        console.log(person)
        return $http({
            url: urlBase+'documents?form=fUserName',
            data: person,
            withCredentials: true,
            method: "POST",
            headers: {
                "Content-Type": "application/json"
            }
        })
    };

    dataFactory.savePerson = function (person, event, id) {
        console.log(id);
        return $http({
                url: urlBase+'documents/unid/' + id,
                data: person,
                withCredentials: true,
                method: "PATCH",
                headers: {
                    "Content-Type": "application/json"
                }
            })
    };

    dataFactory.deletePerson = function (id) {
            var temp = confirm('Are you sure you want to delete?')
            if (temp){
                return $http.delete(urlBase+'documents/unid/' + id)
            } else {
                location.href = "#/people"
            }
    };

    return dataFactory;
}]);

My controller

peopleControllers.controller('PersonDetailCtrl', ['$scope', '$routeParams', '$http', 'action', '$timeout', 'dataFactory',
    function($scope, $routeParams, $http, action, $timeout, dataFactory) {

        $scope.create = (action=='new' || action=="delete");

        if (action=="get") {
            dataFactory.getPerson($routeParams.docId)
                .success(function(data) {
                    $scope.person = data;
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        }

        $scope.home = function(){
            location.href='#/people'
        };
        $scope.createPerson = function(event) {
            dataFactory.createPerson($scope.person, event)
                .success(function(data) {
                    event.preventDefault();
                    location.href = "#/people";
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };

        $scope.savePerson = function(event) {
            dataFactory.savePerson($scope.person, event, $routeParams.docId)
                .success(function(data) {
                    event.preventDefault();
                    location.href = "#/people"
                })
                .error(function(data, status, headers, config) {
                        console.log('ErrorData: ' + data);
                });
        };

        if (action=="delete") {
            dataFactory.getPerson($routeParams.docId)
                .success(function(data) {
                    $scope.person = data;
                    $timeout(function(){
                        dataFactory.deletePerson($routeParams.docId)
                            .success(function(data) {
                                $scope.person = {};
                                location.href = "#/people"
                            })
                            .error(function(data, status, headers, config) {
                                console.log('ErrorData: ' + data);
                            });
                    }, 300)
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };
    }]);

 

Angular.js in XPages #6 – A People Manager CRUD application

In this article I will bring together everything discussed in the first 5 articles and demonstrate how to create you first basic CRUD application using Angular.js and XPages.

Previous articles

Introduction

In the last article we saw how routers can be used to not only create meaningful URLs but also control the application functionality. With those routers we are now able to create a functioning CRUD application which will demonstrate the core requirements for building a web based Angular.js application on Domino Data. In this article I will build on the previous article and all those in the rest of the series.

This basic Angular.js/Domino application was created with <160 lines of custom JavaScript code. It will run on Android, iPhone, IE8+, Safari, Firefox, Chrome.

As always – thanks to Dave Leedy for the fakenames.nsf database upon which this is based – and so much more !!

The routing

Inside of the app.js we create our routing table as shown below.

We have a page for the following

  • Viewing all people (/people)
  • Creating a new person (/person/new)
  • Viewing and editing a person (/person/:docId)
  • Deleting a person (/person/:docId/delete)

But as you can see form the code we have the same controller for multiple actions (PersonDetailCtrl). So to differentiate between each action within the same controller we also pass a “resolve” object containing “action: ?” which through the scope, allows us to determine what to do within the controller.

app.js

/**
 * Created by mroden on 5/4/2014.
 */

/* App Module */

var personApp = angular.module('personApp', [
    'ngRoute',
    'peopleControllers'
]);

personApp.config(['$httpProvider', function($httpProvider) {
    $httpProvider.defaults.headers.patch = {
        'Content-Type': 'application/json;charset=utf-8'
    }
}])

personApp.config(['$routeProvider',
    function($routeProvider) {
        $routeProvider.
            when('/people', {    //use the people-list.html template, 'PeopleListCtrl' controller and resolve the action: list
                templateUrl: '//copper.xomino.com/xomino/ainx.nsf/partials/people-list.html',
                controller: 'PeopleListCtrl',
                resolve: {
                    action: function(){return 'list';}
                }
            }).
            when('/person/new', {    //use the person.html template, PerconDetailCtrl controller and resolve the action: new
                templateUrl: '//copper.xomino.com/xomino/ainx.nsf/partials/person.html',
                controller: 'PersonDetailCtrl',
                resolve: {
                    action: function(){return 'new';}
                }
            }).
            when('/person/:docId/delete', {    //use the person.html template, the PersonDetailCtrl controller and resolve the action: delete
                templateUrl: '//copper.xomino.com/xomino/ainx.nsf/partials/person.html',
                controller: 'PersonDetailCtrl',
                resolve: {
                    action: function(){return 'delete';}
                }
            }).
            when('/person/:docId', {    //use the person.html template, the PersonDetailCtrl controller and resolve action: get
                templateUrl: '//copper.xomino.com/xomino/ainx.nsf/partials/person.html',
                controller: 'PersonDetailCtrl',
                resolve: {
                    action: function(){return 'get';}
                }
            }).
            otherwise({    //if all else fails then just go to people
                redirectTo: '/people'
            });
    }]);

The controller

Within controller.js we can see the two controllers peopleControllers as we had before

  peopleControllers.controller('PeopleListCtrl', ['$scope', '$http',

and

  peopleControllers.controller('PersonDetailCtrl', ['$scope', '$routeParams', '$http', 'action',
    function($scope, $routeParams, $http, action) {

Within the PersonDetailCtrl we now some IF statements – depending on what resolve variable is passed into the controller, we determine what action to take.

The resolve parameter passed in is “action:” and that is added to the controller as a parameter ‘action’. Using this ‘action’ we know the users intention.

We also create two scoped functions $scope.createPerson and $scope.savePerson. These actions will be linked into button clicks as we will see later in the article.

controller.js

/**
 * Created by mroden on 5/05/2014.
 * With help from - http://scotch.io/tutorials/javascript/creating-a-single-page-todo-app-with-node-and-angular
 */

var peopleControllers = angular.module('peopleControllers', []);

peopleControllers.controller('PeopleListCtrl', ['$scope', '$http',
    function ($scope, $http) {
        $http.get('//copper.xomino.com/xomino/ainx.nsf/api/data/collections/name/byFirstName5Col?open&').success(function(data) {
            $scope.people = data;
        });
    }]);

peopleControllers.controller('PersonDetailCtrl', ['$scope', '$routeParams', '$http', 'action', '$timeout',
    function($scope, $routeParams, $http, action, $timeout) {

        console.log("Action1: "+action);
        $scope.create = (action=='new' || action=="delete");

        if (action=="get") {    //get the user data from the DDS Document REST service
            $http.get('//copper.xomino.com/xomino/ainx.nsf/api/data/documents/unid/' + $routeParams.docId)
                .success(function(data) {
                    $scope.person = data;
                    console.log($scope);
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        }

        $scope.createPerson = function() {   //POST the $scoped person data to the DDS and create a new document
            $http.post('//copper.xomino.com/xomino/ainx.nsf/api/data/documents?form=fUserName', $scope.person)
                .success(function(data) {
                    location.href = "#/people"
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };

        $scope.savePerson = function() {    //Create a PATCH request and send the updated $scoped person data to the DDS Document
                $http({
                    url: '//copper.xomino.com/xomino/ainx.nsf/api/data/documents/unid/' + $routeParams.docId,
                    data: $scope.person,
                    method: "PATCH"
                })
                .success(function(data) {
                    location.href = "#/people"
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };

        if (action=="delete") {   //Get the person data - then delay slightly - then as the user if they want to delete
            $http.get('//copper.xomino.com/xomino/ainx.nsf/api/data/documents/unid/' + $routeParams.docId)
                .success(function(data) {
                    $scope.person = data;
                    console.log($scope);
                    $timeout(function(){  //delay 300ms and then ask the user to confirm deletion
                        var temp = confirm('Are you sure you want to delete?')
                        if (temp){    //if yes delete then send a DELETE request to the DDS Document REST service
                            $http.delete('//copper.xomino.com/xomino/ainx.nsf/api/data/documents/unid/' + $routeParams.docId)
                                .success(function(data) {
                                    $scope.person = {};
                                    location.href = "#/people"
                                })
                                .error(function(data) {
                                    console.log('Error: ' + data);
                                });
                        } else {
                            location.href = "#/people"
                        }
                    }, 300); // time here

                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });

        };

    }]);

Update people-list.html

We update the people-list with two new “buttons”. An “Add Person” button and a “Delete” button.

a1

Both buttons route to person.html, but the DELETE option loads the person’s information.

If we look in app.js we can see that #/person/new sends the action: new to the controller. But in the controller we are not actually looking for a “new” action. At initial glance though in the controller we are not looking for if (action==’new’). But we are doing is using it for something subtle but no less important.

        $scope.create = (action=='new' || action=="delete");

What we are doing is simply setting up a scope$ variable for “create”. When we come to the person.html template we will see why.

people-list.html

<pre><div class="row" style="width: 50%">
    <div class="col-sm-2">
        <a class="btn btn-default" href="#/person/new">Add Person</a>
    </div>

</div>
<table class="table table-striped">
    <thead>
    <tr>
        <th>First Name</th><th>Last Name</th><th>Zip</th><th></th><th></th>
    </tr>
    </thead>
    <tbody>

    <tr ng-repeat="person in people">
        <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>

Deleting a Person

Clicking the Delete Button performs the following:

  • Changes the URL to #/person/UNID/delete
  • Which is picked up by the router
    • Which uses the personController
    • that then executes the When(‘/person/:docId/delete’, {
    • Which uses the person.html
    • Which in turn resolves the action: delete
  • $scope.create is set to true
  • The person “form” is loaded and filled with data from Domino Data Service
  • Pause for 300 milliseconds using $timeout
  • The controller then acts on the delete action
    • if (action==”delete”) {
    • Confirms do you want to delete
    • var temp = confirm(‘Are you sure you want to delete?’)
    • IF true then loads the http DELETE at the DDS URL for the document
    • $http.delete(‘//copper.xomino.com/xomino/ainx.nsf/api/data/documents/unid/’ + $routeParams.docId)
    • and returns the user back to the original list
    • location.href = “#/people”

a2

 

Adding a new person

When we click the Add person button:

  • The URL is changed to #/person/new
  • The router picks up the change and loads person.html
  • resolves the action: new
  • $scope.create is set to true
  • The person.html template is loaded with no data – there is no action in the controller associated with new to load any

ng-show / ng-hide

We brushed over the $scope.create for delete but lets now look at it in the context of a new Person. Looking at the form for editing  person we see a “Create” button whereas for an existing person we see a “Save” button. Looking at the code for the two buttons closely we can see two new directives – ng-show and ng-hide

    <div class="col-sm-2">
        <button class="btn btn-default" ng-click="savePerson()" ng-hide="create">Save</button>
        <button class="btn btn-default" ng-click="createPerson()" ng-show="create">Create</button>
    </div>

The ng-show and ng-hide directives are used in conjunction with the $scope.create to appropriately show and hide the buttons. So with no JavaScript code written we are able to apply a programmatic hide/when to the buttons based on the $scope data. That’s cool !! 🙂

a4

Once the data is entered and we click create – the ng-click directive kicks in and calls the “createPerson()” function. Back in the controller we created the $scoped createPerson() function…

        $scope.createPerson = function() {
            $http.post('//copper.xomino.com/xomino/ainx.nsf/api/data/documents?form=fUserName', $scope.person)
                .success(function(data) {
                    location.href = "#/people"
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };

In this function we perform a “POST” at the DDS api/data/documents?form=fUserName which creates a document based on the fUserName function. The data POSTed is the $scope.person. We can see the JSON using firebug sent back to the server. We are then returned to the people-list and can see the new entry loaded.

a3

Editing an existing person

When clicking on the Edit button from the people-list.html template a person is loaded as we saw in the previous article. Looking back at the person.html “Save” button which is now visible because $scope.create is not equal to true – we can see the save button uses ng-click to call the “savePerson()” function. Once again we look to the controller.js and see where this was created.

        $scope.savePerson = function() {
                $http({
                    url: '//copper.xomino.com/xomino/ainx.nsf/api/data/documents/unid/' + $routeParams.docId,
                    data: $scope.person,
                    method: "PATCH"
                })
                .success(function(data) {
                    location.href = "#/people"
                })
                .error(function(data) {
                    console.log('Error: ' + data);
                });
        };

In this case we are using the calling $http using a slightly different pattern. I want to do a PATCH to only modify the data which has changed within the document. Out of the box Angular does not have a $http.patch method so we have to create the http call manually. We provide the url, data and method as shown above. To allow this on the server we have to enable the PATCH method within our Domino Website document (a sign that you have not got this enabled will be a 405 method not permitted error).

a5

Once we click the Save button we can see the JSON being sent to the server, using the PATCH method, and we are then routed back to the front page.

a6

Here’s a question for you guys – as you can see from the PATCH the data passed back a lot more information than just the firstname, lastname and zip  – why and where did the rest of the @fields come from??

🙂

person.html

<legend>A Person</legend>

<div class="row" style="width: 50%">
    <div class="col-sm-4">
        <label>First Name</label><br/>
        <input class="form-control" name="firstname" type="text" value="{{person.firstname}}" ng-model="person.firstname">
    </div>
    <div class="col-sm-4">
        <label>Last Name</label><br/>
        <input class="form-control" name="lastname" type="text" value="{{person.lastname}}" ng-model="person.lastname">
    </div>
    <div class="col-sm-4">
        <label>Zip</label><br/>
        <input class="form-control" name="zip" type="text" value="{{person.zip}}" ng-model="person.zip">
    </div>
</div>
        <br/>
<div class="row" style="width: 50%">
    <div class="col-sm-2">
        <a class="btn btn-default" href="#/people">Back</a>
    </div>
    <div class="col-sm-2">
        <button class="btn btn-default" ng-click="savePerson()" ng-hide="create">Save</button>
        <button class="btn btn-default" ng-click="createPerson()" ng-show="create">Create</button>
    </div>
</div>
<hr />

In this article the index.html did not change from the last article

Thoughts and Questions

We have Created, Read, Updated and Deleted data using Angular.js to control the Domino Data Services REST service. Have we created an XPages application? In the literal sense of the word(s), no I don’t think we have.

  • We have not created a single XPage
  • We have not used an XPages control
  • We have used out of the box DDS which comes with the server not with XPages.

So this is just a Domino application then?

  • We used Domino Data Services
  • Our data is coming from an IBM Domino database
  • We didn’t use any XPages or code which has to be pre-compiled on the server
  • We did however take advantage of the fact that this is a >R8.5 server/database which allows access to Package Explorer and syncing of an ODS
  • Sooooo – yes it is just a Domino application but not the one you grew up with 15 years ago

Could we have written it quicker in XPages?

  • We I certainly could have at the beginning, because I had to learn Angular as I was doing it – but that wasn’t the point of the question was it….
  • Yes we could have created it with less “XPages” code. IBM has provided some nice tools to allow us the “developer” to take action on data without having to think to hard about the inner workings of it.
  • That being said though, I believe an experienced Angular developer would be able to put this together just as quick as an experienced XPages developer.

So then what have we gained Marky?

  • Well I for one can now put “Angular.js” on my résumé as something I have at least a basic understanding of
  • Understanding how a web based application like this interacts with a NoSQL system gives me more flexibility in problem solving customer requirements
  • This application uses the best NoSQL database system in the world – that is never a bad thing
  • There are actually some other major advantages to programming an application in this manner. I am working on them for a future demonstration and presentation at MWLUG (teaser intended) – All in good time 🙂

Overall, the term “XPages development” seems to encompass more than just the out of the box “XPages” tools which IBM have provided . It has (to me anyway) come to mean the whole server package and not just XPages tooling. With that has come a broadening of the mind into truly modern web development using the modern Domino development platform – I am perfectly OK with calling this “Angular.js in XPages”.

Conclusion

Over the last few article we have opened the covers on the new web technology that is “Angular.js”. We built a CRUD application using data from arguably the world’s most secure NoSQL system. I love it 🙂

Download the example

The example can be downloaded from this link – but some work will need to be done to make it work on your server:

  • Change the URLs in app.js and controller.js to your server
  • Make sure you have PATCH enabled on your server website document (requires restart)
  • You must enable Domino Data Services on your website document
  • You must ensure DDS access to your database (DB Properties)
  • You must enable DDS access to your view
  • You will find all the files in the Package Explorer / WebContent folder for the database.

PS

For the PATCH question I asked – here’s a tip – what’s in $scope ?