I’ve been playing around with AngularJS since March this year but only in my spare time. So I was thrilled to be able to use AngularJS on an actual real-world project at my day job at the Academic Medical Center in Amsterdam.
So for the last three weeks I’ve been working at a small but nice web site. The web site is a training course catalogue for the nursing education program, which is ordered according to the CanMEDS Framework. Last week I released version 1.0 and next wednesday it will be formally presented to the public during an important conference.
The application is split into a backend with a REST interface and a Single Page frontend that’s basically a master-detail page with some filtering options, and added to it some necessary administration pages. I’ve built the backend of the application with Spring Data JPA, Spring REST, and Spring Security. For the frontend I’d chosen AngularJS and Boostrap 3. Below I will describe some of my experiences from the last couple of weeks.
The backend
For the ORM I chose JPA with Hibernate. And i decided to also use Spring Data JPA, which resulted in a tremendous reduction in boilerplate code, leaving me more time to work on the frontend.
Because the API would have to conform to REST, I started out using Spring Data REST. The main advantage of Spring Data REST is that it just generates a complete CRUD interface through REST, including HATEOAS. On the downside, however, you give up the fine-grained control over your API but for mere CRUD this could be all you need. After a little while, however, I decided to not use Spring Data REST for three reasons. First, I found that I did want some more control over the interface. Second, I found the performance to be kind of sluggish (mind you, this very subjective opinion), perhaps because of the overhead from HATEOAS. But the single most important reason I abandonned Spring Data REST was that AngularJS’ $resource
doesn’t support HATEOAS out-of-the-box.
As is my experience with all Spring-based projects, it was a pain to configure but once everything was properly set up, the further coding was a breeze. I must say that I made the configuration part a bit more time consuming than it could have been because I wanted to configure everything programmatically (using @Configuration
) and completely leave out any xml files. The delay was mostly due to the fact that nearly all examples of configuring Spring I found using Google were examples using XML.
Using Hibernate and a JSON REST service I quickly ran in to a problem with lazy-loading of relationships. The solution turned out to be to subclass and register a HibernateAwareObjectMapper
from the jackson-datatype-hibernate4 project.
Of course, I also had to put in some security. For authenticating users I had to connect to Active Directory. So besides core Spring Security, I did have to include the spring-security-ldap project. But I didn’t run in to any problems worth mentioning.
So the backend was quite smooth sailing. Within a week I had everything running on tomcat with different Spring Profiles for development, test, and production environments.
The frontend
Usually I will design a site completely from scratch but because I was faced with a tight deadline I decided to just use Bootstrap 3 and only added some very minor alterations to my CSS files. Bootstrap provided me with a very clean and professional look and feel and also a very nice grid system. And although nowadays there are a gazillion sites using Bootstrap, which makes the design of the web site not very original, the stakeholders really liked it in the demo I gave.
Although it was my first real project using AngularJS, I was really impressed by how productive I had become doing the frontend development within mere days of using the framework. Even though there were some concept which took some more getting used to than others, e.g. $q
and promises. I was a bit hesitant at first introducing AngularJS in the company, thinking it might be a gamble to use a new framework, and one based on JavaScript at that, but it has been one gamble that has certainly paid off.
I did the basic coding in Eclipse, which worked fine for me, and also spend a lot of time using the Chrome dev tools, which simply are amazing. The only problem with spending so much time using Chrome is that you tend to forget that there are still people using browsers like IE8. So you have to remind yourself every now and again to also check to see if everything is working in IE.
Dealing with IE
When developing sites for multiple browsers you want to stick to standards and don’t be bothered with the whims of any particular browser. But unfortunately, in the real world, you may be confronted with an organization – like the one I’m working at – that’s still stuck on Windows XP and therefore you’ll have to support IE8. Luckily there are two very nice JavaScript libraries that make IE prior to version 9 behave sort of nicely when it comes to HTLM5 and media queries. You basically include these in your page’s <head>
section, and your all good to go:
1 2 3 4 |
<!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> <![endif]--> |
To be certain, you fire up IE10, start up the dev tools (F12), set the document mode to IE8, and test to find that your page is working perfectly even on IE8. So now everything is hunky-dory, or is it?
Now at my company, the majority of workstations is still running on Windows XP with IE8 as the default browser. However, for some reason – valid or not – IE8 is set up in such a manner that it uses IE7 as its document model (the so called ‘compatibility’ mode). So basically, what I thought was IE8 turned out to be IE8 posing as IE7, rendering my web pages nearly useless. Fortunately though, instead of also having to code to IE7, you can tell IE8 not to default to the compatibility mode for your pages, but to render in ‘standards view’. You do this by adding a meta-tag to the header of every html page in your web site:
1 |
<meta http-equiv="X-UA-Compatible" content="IE=Edge"> |
"IE=Edge"
tells Internet Explorer to use the highest mode available to that version of IE. Internet Explorer 8 can support up to IE8 modes, IE9 can support IE9 modes, and so on.
And finally, the third problem I ran in to having to support IE8, was jQuery. I wanted to use the version 2 of jQuery, but they’ve dropped support for IE8 and below. Now what if, you want to use jQuery version 2 for all “modern” browsers, but have to revert to jQuery 1.x for IE8? How would you take care of that? Easy. Just use the IE conditional tags:
1 2 3 4 5 6 |
<!--[if lt IE 9]> <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> <![endif]--> <!--[if (gte IE 9) | (!IE)]><!--> <script src="http://code.jquery.com/jquery-2.0.3.min.js"></script> <!-->![endif]--> |
Now IE8 and earlier will get server jQuery version 1.10.1 whilst all the other browsers will get the latest and greatest version.
Get your resources ‘en route’
I started out configuring routes in AngularJS by defining a templateUrl and a controller for certain URLs, like so:
1 2 3 4 |
$routeProvider.when('/myUrl', { templateUrl: 'views/myDetailPage.html', controller: 'MyDetailCtrl'; }); |
Nothing wrong with that. But than I noticed that my controllers all tended to start with the retrieval of some resources, and waiting for those resources to get resolved. Something like:
1 2 3 4 5 |
.controller('MyDetailCtrl', function($scope, Detail) { $scope.myDetails = Detail.query(); // wait while myDetails are loaded // ... the rest of the controller's code... }); |
I than found out that the route
object – the second argument of the $routeProvider.when()
method, has a resolve()
function:
…an optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved or one to be rejected before the controller is instantiated. If all the promises are resolved successfully, the values of the resolved promises are injected…
So I ended up refactoring my code, moving the query()
methods to resolve()
, so that my code now resembles the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$routeProvider.when('/myUrl', { templateUrl: 'views/myDetailPage.html', controller: 'MyDetailCtrl', resolve: { details: function(Detail) { return Detail.query(); } }); .controller('MyDetailCtrl', function($scope, details) { $scope.myDetails = details; // ... the rest of the controller's code... }); |
Thanks for this very interesting post.