Transform your site into a single-page app
How to take an accessible website and turn it into a scalable, manageable single-page app.
As the plethora of devices, platforms and browsers grows larger every day, single-page applications (SPAs) are becoming more and more prevalent. HTML5 and JavaScript are now first-class citizens in application development, as well as revolutionising how we design and create great website experiences with CSS3.
As a result, the traditional pattern for developing a comprehensive 'digital strategy' is changing fast, and becoming a lot more streamlined. Platforms like PhoneGap enable us to create apps in HTML5 and JavaScript, which can then be adapted and cross-compiled for various mobile platforms (iOS, Android, Blackberry and Windows Phone).
However, gaps still persist between languages and frameworks – especially between building 'apps' for mobile and tablet platforms, and building traditional, accessible and progressive websites.
As a freelancer, I've experienced clients asking for a website, then a mobile website, then an app (I built Kingsmill Bread's mobile and desktop/tablet websites on the MIT-licensed, .NET-based Umbraco CMS). The 'traditional' route of building this would be to do the site first, with a responsive (or specially-designed) CSS layout, then build a separate app that plugs into an underlying content store.
This model works great if you have specialists in iOS, Java and Silverlight (yes, Silverlight – the joys), but falls flat when it comes to scalability and manageability. Factor in the average cost of bespoke app development (approximately £22,000) and average return (£400 a year, if you're lucky), and suddenly you're pouring money into an awfully large technological black hole.
What about JavaScript?
"JavaScript saves the day!" I hear the frontend developers cry. Not so fast. JavaScript solves the majority of the cross-platform issues, but then you're not building a website any more. Google doesn't index content loaded with AJAX, so you've lost accessibility and progressive enhancement: the key tenets of good web architecture.
What's that I hear? "NodeJS saves the day! JavaScript everywhere!" Hold your horses there, hipster. NodeJS doesn't yet have a JavaScript-based CMS. "What about all those lovely, stable, mature content management systems that make websites awesome for their content managers?" Are you seriously suggesting direct-porting WordPress, Drupal, Umbraco et al to JavaScript just to solve this problem? (I had someone suggest this solution to me at the Scotch on the Rocks conference this year, and my reply was pretty much exactly that.)
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
So how do we design a website that's accessible, and then 'upgrade' it to look, feel and behave like an SPA? I'm glad you asked that, because that's what we're going to build in this tutorial.
Architecture is everything, I will cover a simple blog site, which you can then go on to adjust according to your project. I'm a .NET bod by trade, and it's worth noting this tutorial is partially written in .NET for the server-side code. If you don't like that, the design patterns will easily translate into an MVC framework of your choice – Zend, CodeIgniter, Ruby, Django and so on.
Bridging the gap
Most websites nowadays are built with a model-view-controller (MVC) architecture, whereas apps, which rely on data loaded on-demand, are built with a model-view-viewmodel (MVVM) architecture. The two paradigms are related, but differ slightly in event handling. Whereas a controller would have an action to handle a particular event (submission of data), a viewmodel would have a specific event defined for handling UI updates and any model binding modifications (people familiar with C# will know this as the INotifyPropertyChanged pattern).
The key tenet in this architecture is working out how to share the model and the view between the two patterns. When designing your site in this fashion, 99.9 per cent of the time your views will be identical both on the server and client sides. The only difference is when and how they are populated with model data – be it by controller on the server, or controller on the client.
We'll start off with creating a simple website (which can be downloaded at netm.ag/demo-257) using ASP.NET MVC 4. It's got a couple of controllers in it, a model created with Entity Framework to show an example of server-side model population, and some views with a responsive CSS3 template built with Bootstrap. It contains everything on the left-hand side of the diagram below – a basic model, a controller and a couple of views, just to get everything started.
It's a breeze
We're now going to install AngularJS and BreezeJS into the project. NuGet (the package manager for Visual Studio) makes this simple to add into our project. BreezeJS depends on Q, a tool for making and managing asynchronous promises in JavaScript.
To create a very simple SPA experience from our existing site foundation, we need to share the routing table in MVC on the server with AngularJS. We'll do that using a controller that serialises and spits out the routing table in a format that Angular will understand. Angular expects routing data to be configured as follows:
when('/blog', {
templateUrl: 'partials/blog.html',
controller: 'BlogListCtrl'
})
.when('/blog/post/:blogId', {
templateUrl: 'partials/blog-post.html',
controller: 'BlogPostCtrl
})
.otherwise({
redirectTo: '/index'
});
As we can see from this code, Angular's routing provider expects to know both the controller and the corresponding view. We'll be feeding AngularJS a generic controller to catch most of our requirements, and then extending it for particular views (i.e. blog list and blog post, as these contain more than just vanilla data).
To do this, we need to identify which controllers (and corresponding routes) are to be fed from the server into Angular. This is where we take advantage of .NET's reflection capabilities – code that reads code, or 'code-ception' as I like to think of it. Warning: here be dragons! We'll be marking our controllers on the server.
This is to be shared with the JS side of the site, with metadata, using a .NET feature called Reflection to gather these controllers up. On these controllers, we'll be marking out our actions with metadata to identify the corresponding JS view, model and controller, as shown in the diagram on the right.
This approach is not foolproof. If you have filters or constraints on the routes or controller actions, whatever is provided by the JS routeProvider as Angular routes may not actually end up being rendered by the server. For more information on advanced MVC routing, route constraints and filtering, I highly recommend checking out Scott Allen's courses on Pluralsight on the ASP.NET MVC stack.
Piece by Piece
Now we've provided Angular with our routing table, we need to start gluing it together. Firstly, we update our controllers to inherit from our generic RomanController. We'll be overriding some parts of this later, but for now it's a generic marker for us to identify the routes and URLs we want Angular to have access to.
Next, we need to mark the actions we want to be available in AngularJS, with a custom attribute: RomanActionAttribute.
public class RomanDemoController : RomanController {
private RomanSPA.Demo.Models.RomanSPAStarterKitEntities _context;
public RomanDemoController() : base() {
// Yes, I'm not using dependency injection for my DB context, cause this is a demo ;-)
if (_context == null) _context = new Models.RomanSPAStarterKitEntities();
}
[RomanAction]
public ActionResult Index() { return View(new IndexModel()); }
[RomanAction(Factory=typeof(BlogListFactory), ControllerName=”BlogController”, ViewPath=”/assets/blog-list.html”)]
public ActionResult Blog() { return View(_context.BlogPosts); }
[RomanAction(ControllerName=”BlogPostController”)]
public ActionResult BlogPost(string slug) {
if (_context.BlogPosts.Any(p => MakeTitleUrlFriendly(p.Title) == slug)) {
return View(_context.BlogPosts.First(p => MakeTitleUrlFriendly(p.Title) == slug));
} else {
return HttpNotFound();
}
}
[RomanAction]
public ActionResult About() { return View(); }
private string MakeTitleUrlFriendly(string title) {
return title.ToLower().Replace(“ “, “-”);
}
}
This attribute is an action filter, and has three parameters, all optional: model factory, controller name and view name. This gives our routing table the option to specify any explicit overrides we want, or to go with a generic, default behaviour (i.e. load a controller with no JSON model data, which then loads a partial view from the server). Note how [RomanActionAttribute] can be automatically shortened to [RomanAction] by the C# compiler.
The fundamental part of this exercise is that we should be able to share views and expose server data for complex transformations in our app. This means we can take advantage of HTML5 offline caching to create a great user experience on the app side.
We'll leave the Index and About views blank. For the BlogList action, we're going to specify a factory, a custom controller and a custom view location for the client-side view. For the BlogPost action, we'll specify a custom controller to load the individual post from the JSON we get from our BlogList factory.
Now, we set up a GenericController in AngularJS, in /App/ controllers, and we set up auto-routing in AppJS so that all the routes are provided from the server to the client. You can provide extra routes manually by overloading the RoutesApiController and overriding the ExtraRoutes property with your own custom list of routes to pass to AngularJS.
angular.module('RomanSPA', ['ngRoute'])
.config(['$routeProvider', function ($routeProvider, $locationProvider) {
$.ajax({
url: '/api/RouteApi/AllRoutes',
success: function (data) {
for (var i = 0; i < data.length; i++) {
$routeProvider.when(data[i].RoutePattern, {
controller: data[i].controller,
templateUrl: data[i].templateUrl
});
}
$routeProvider.otherwise({ redirectTo: '/' });
},
fail: function (data) {
}
});
}])
.value('breeze', window.breeze);
Core functions
Finally, in our GenericController, we put in two core functions – one to manually apply our specified template, and one to retrieve the model using the factory on the action attribute.
angular.module('RomanSPA')
.controller('GenericController', ['$scope', '$route', function ($scope, $route){
$scope.pageUrl = '/';
if ($route.current !== undefined) { $scope.pageUrl = $route.current.templateUrl; }
// Retrieve our view - be it from server side, or custom template location
$.get({
url: $scope.pageUrl.toString(),
beforeSend: function(xhr) { xhr.setRequestHeader('X-RomanViewRequest', 'true'); },
success: applyView
});
// Retrieve our model using the modelfactory for our current URL path
$.get({
url: $scope.pageUrl.toString(),
beforeSend: function (xhr) { xhr.setRequestHeader('X-RomanModelRequest', 'true'); },
success: applyModel
});
function applyView(data) {
$scope.$apply(function () { angular.element('body').html($compile(data)($scope)); });
}
function applyModel(data) { $scope.model = data; }
}
}]);
Finally, we're at a point where:
- We have gathered up all the routes on the server that we want AngularJS to take advantage of. These will be exported to Angular's routing library when our app boots up.
- We have specified a generic controller, from which our specific controllers can inherit. However, this means we have 'generic' SPA functionality, with pages partially loading as we navigate through the site.
- Child actions that we may want AngularJS to have access to, but shouldn't be in the routing table (such as navigation, footer and other partial views), can be marked out with [RomanPartial] .
- A request to /RomanDemo/Index will give us a full page, but when AngularJS requests it, it'll either provide a partial view or a JSON object, depending on the metadata we have supplied.
- Actions we want to specify metadata for (or export to Angular routing) – i.e. custom JSON model, custom template URL or custom AngularJS controller – are marked with [RomanAction].
We're almost set. From here, to guide you in the kind of direction you can go with your hybrid site app, we'll create a BlogController that inherits from GenericController, and use this to gather up all the blog posts we want. We'll then use this as a way to enable users to navigate a blog, whilst most of the data is held in HTML5 offline storage for us.
angular.module('RomanSPA')
.controller('BlogController', ['$scope', function ($scope) {
$controller('GenericController');
function storePostsOffline(data) {
if (!supportsLocalStorage()) { return false; }
localStorage[“RomanSPA.data.blog.posts”] = data;
return true;
}
var manager = new breeze.EntityManager('/breeze/BlogApi');
var query = new breeze.EntityQuery('BlogPost');
manager.executeQuery(query)
then(function(data) {
storePostsOffline(data);
$scope.
HTML5 offline storage is the biggest reason you'd want to do this. You can also extend the view engine to automatically store templates and views offline. By doing this, you're allowing your website visitor to treat your website more like an app – which they can use even while they're without WiFi or phone signal.
Wrapping up
We now have:
- A basic MVC site, with routes for basic pages and a blog.
- An MVC app that sits on top, powered by AngularJS, with a matching route table.
- AngularJS making AJAX requests using jQuery, which server-side MVC then interprets to return JSON model or HTML view appropriately.
The result of this mashup? The ASP.NET MVC RomanSPA framework! (vomitorium, sickle-scrapers and sulphur-tasting water all optional). You can use this foundation to build out many more complex parts to your site.
The core framework, and extensions for both AngularJS and KnockoutJS or BreezeJS, have been 'forked' into separate projects. You can either install the AngularJS or KnockoutJS editions, which will get you kick-started straight away with all the necessary prerequisite libraries installed, or you can simply install the core and write your own extensions for EmberJS or any other MVx JavaScript framework that takes your fancy.
Words: Benjamin Howarth
Benjamin Howarth is an expert in open source frameworks on the Microsoft .NET stack. This article first appeared in net magazine issue 257.
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.