I recently completed a 5 month project for a small realestate company called BresicWhitney in which we built their customer facing website. This project was a milestone for me for two reasons, the first being that I was returning to coding after doing other stuff for two years, the second being that it was the first time I put on the frontend developer hat. The last time I had coded, I was on a project where we wrote a single page web app for an insurance company using sammy.js. A bit has happened in that time, noteably:
- single page apps are more popular now, as evidenced by the variety of frameworks
- web page designs and CSS has come a long way. The difference to what the designer does in photoshop and what actually gets built has got much less.
Anyway, I was determined to provide the best user experience I could so I decided to go the single page app route again (to avoid page refreshes and still have the URL to match the application’s state etc). I chose Angular.js after hearing good things about it from colleagues. Rails was the team’s choice for the server-side due to our collective skill sets.
For this post, I’ve cherry picked some the challenges I had, particularly integrating Angular.js with Rails. It is in no way a holistic guide.
The main components of the customer facing website are shown below. I’ve left out the back-office application (kinda like the CMS) because it did not use Angular.
- page templates (displayed in the ng-view element)
- directive templates
Directives are an awesome feature which I made use of extensively.
first attempt: put everything in the header
To get myself up and running, I put the
= yield statement in the
<head> section of the frontend Rails layout and then a loop in the included index.haml to bring in the templates. A snippet is shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
The template files themselves were put in the same folder as index.haml. Thus, whenever I needed a new template, I’d create it and then add it to the array above. I could then refer to the templates via
This worked okay, except for the fact that there was a lot of “stuff” in my
<head> section. Also, the above technique requires that all templates be loaded on the initial page load.
second attempt: loading via ajax and bringing in the asset pipeline
One of Rails great fetures IMHO is the asset pipeline (provided by the sprockets library). It basically allows for indefinite browser caching for static assets – a feature I wanted to take advantage of for the templates. Unfortunately, sprockets does not support Haml (the authors don’t seem to like HTML DSLs) and the “haml” gem only supports putting haml templates in the “views” folder. After reading through the sprockets and sass gem source code, I cobbled together the following code which allows Haml files in the asset pipeline for development mode as well as pre-compilation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
This allowed me to put the templates in
app/assets/templates (of course this path is easy to change)
An then to load the templates from the browser side, I created a file called
1 2 3 4 5 6 7 8 9 10 11 12 13 14
And, from my routes and directive files, I could link to the template via “templateUrl”, e.g.
templateUrl: Frontend.Template '_tenancy_application_template.html.haml'. Keeping the template ID the same as the actual file name made the template files easy to find.
One “gotcha” with this method is that if you create a new template, you need to increment
template_version_number so that the templates file is recompiled with the new template. This is not required for modifications to existing templates though.
This method has it drawbacks as even the homepage requires many templates to be loaded – and hence many HTTP requests. If I had more time, I would have pre-loaded the templates required for the homepage, potentially using the technique from my first attempt above.
adding efficiency, lazy-loading templates
To make subsequent page transitions faster, I added some code to templates.coffee.erb to pre-load any remaining templates after 15 seconds – there by allowing the homepage to complete its loading first:
1 2 3 4 5 6 7 8
I then called the
LazyLoadTemplates function from the
run method so it could be injected with what it needs.
adding a CDN, dealing with CORS
The next step was to load the templates (as well as the other static assets) from a CDN such as Cloudfront. Unfortunately for this, I had to deal with CORS as I was loading the assets via ajax. I say unfortunately as Angular makes this pretty hard. The problem is that its template loading code is hardwired to use the
$http component which is not setup to handle CORS. The fact that
$http does not handle CORS is understandable as CORS imposes too many restrictions for a general HTTP component. It would be nice if the template loading process was more configurable/pluggable though.
Complicating matters further was having to support IE 8/9 which rely on the
In the end, I ran out of time to deal with this issue properly, so I worked around it by only loading the lazily-loaded templates from the CDN for “modern” browsers.
templates.coffee.erb the became:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
Thick model, thin controllers
In most of the “hello world” tutorials, you’ll see all the logic shoved into the controllers. If you do this on larger applications, you’ll start to get controllers which resemble a big ball of mud. Borrowing from the Rails world, I wanted to structure my code in a similar way as the Rails does with its controllers and ActiveRecord models. The “rent listing” model object is shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
The corresponding controller is:
1 2 3 4 5
In general, my controllers only did the folling types of things:
- attached data to the scope, making it available to the view
- handled routing logic such as redirects or back-button functionality
- a little view-only logic such as hover states and the like
Model objects followed the pattern shown for the “rent listing” example above, ie they had:
- “static” functions (e.g.
RentListing.find) to retrieve data from the server using the $http service and which called the constuctor function
The model contructor functions could then be tested in isolation to the ajax interactions and view-controller.
In order to resuse code between model objects, I injected the “common” constructor fuction and used it as a template with
IE 8 gotchas
In addition to following the advice on the Angular.js website, I found I need to do the following extra things in order to use “transclude” in my directives:
- I needed to set
replace = true
- I needed to use the attribute form rather than the element form of the directive.
Luckilly, other than that things worked okay on IE 8.
I would be happy to use Angular.js again on future projects. It provides many things which aid the structure of a thick browser app:
- dependency injection
- ansychronous IO with promises
My only complaint is the size of Angular.js – both in terms of bytes and the number of features. As an example, Angular.js comes with jqLite (low footprint version of jQuery) but I needed more than it provided, so I ended up including jQuery anyway. I nearly ran into the same issue with it’s lightweight $q promises service as it lacked a few functions such as map/reduce.
Apparently, Angular.js has had a great influence on the feature pipelines for the major browsers. As more features are implemented natively, the library itself should get a lot smaller.
Can Angular.js become the “Rails” of the browser world?