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
- Angular.js in XPages #5 – Routing
- Angular.js in XPages #4 – Using Domino Data
- Angular.js in XPages #3 – The first app
- Angular.js in XPages #2 – Setting up a Webstorm / Domino development environment
- Angular.js in XPages #1 – Using the right IDE for development
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.
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”
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 !! 🙂
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.
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).
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.
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 ?
Links 1-4 do not work at the top of the page. #5 works correctly.
Thank you so much – corrected
[…] website with Firefox), you can import all the files into a new database. Mark Roden has written a very good tutorial about the details for this […]
Hi! I tryed this example but each time I hit create in a new person’s form I get a 415 Unsupported Media Type in the console and network tab (Chrome -> hit F12)
What I’m doing wrong?
Is the template HTML file loading or something else? Check the network tab and see what is coming down..
Hi Mark, I can click on “Add Person” in the persons’ list page and the form is loaded without errors, then I fill the form and click on the “Create” button and then I get the error 415 for “documets?form=fUserName”.
see https://www.dropbox.com/s/tu043hxx74ew9ru/error.7z for screenshots + my nsf
(The web site document have all kind of actions allowed).
After much head scratching – the problem is your person2.html – you have not updated it with the new code (person.html above) – there is no model binding to the field – so nothing it getting posted as part of $scope.person
ng-model=”person.firstname”
Cheers,
Mark
Thank a lot Mark!
Ah, to reply to your question on the “@” fields, _I think_ these are send by Domino and are metafields like the UNID and the one with the order in the view of a document (and of course the one seen in your screnshot: @href, @unid, @noteid, etc etc).
Absolutely – but why are they in the response back to the server with the PATCH? Surely we only have the 3 fields on the form….why not just three of them?
Reason being is because of the data binding in the $scope.person.
We brought all the data (including @s) down for the document and made that set = $scope.person. The ng-model=”person.firstname” binds the field value so that as it is updated so is the $scope.person equivalent. When we PATCH we send back the $scope.person which still contains all the @s 🙂
cool!
thanks for the article & download. much appreciated!
If you do not want to enable PATCH on your server or do not have that level of access you can also make REST calls and apply a special header (X-HTTP-Method-Override) like this…
$http({method: ‘POST’,
url: ‘rest.xsp/docInfo/’ + $scope.summId,
data: $scope.summaryobj,
headers: {‘X-HTTP-Method-Override’: ‘PATCH’,
‘Content-Type’: ‘application/json’}
})
[…] My sample app will be the one I created as part of the Angular in XPages series. […]
[…] my demo application I wanted to know when the table was finished loading. The problem is in Angular that there is no […]
[…] background reading on this article please review my series on Angular.js in XPages starting here. I will be using the application described within that article as the starting point for this […]
Is it difficult to provide a pager for ‘people’s table’ ? The display of just the first 10 records does not have so much value…
We have used both an Angular pager and also infinite scrolling directives depending on needs and requirements – not hard to implement.
could you explain how you did this? I have a bit of a difficulty to have it implemented.
Patrick I do not have a simple example I can provide as it was written for a client. We have custom built REST services providing the data as well so I can’t even point you at an out of the box(ish) solution.
Sorry mate
that is sorry to hear. I thought everything was easy for you to explain. How would you approach it for the supplied NSF?
As with any pager you need to determine the number of results, divide by the number of docs per page and create you links. Each link then is calculated to the start and end point of the REST service documents you are looking for.
hi Mark
I have taken this as starter example:
http://angulartutorial.blogspot.se/2014/04/angular-js-client-side-pagination-like.html
but I fear this messes up with the PeopleListCtrl controller.
Would love to read your next post on how to include a basic pager 😉
That’s an interesting challenge, but don’t hold your breath. Angular is not the focus of my attentions right now I am afraid.