Adaptive Web Design and AngularJS

Change templates based on device capability

Gert Hengeveld
May 15, 2014 · 4 min read

TL;DR: I created a provider to implement adaptive templates in AngularJS. It’s on Github: https://github.com/ghengeveld/angular-adaptive-templating

I recently attended a JavaScript meetup where I heard a talk on adaptive templates with handlebars and curl.js. The topic was very actual for me, since I had just implemented a very similar thing using RequireJS* and enquire.js. Someone in the audience asked how this could be implemented in AngularJS, which triggered me to write this article.

* Looking back, yepnope.js or curl.js might have been a better choice.

Adaptive templates are a way to implement Adaptive Web Design, which means dynamically changing the layout and function of your application based on the device’s capabilities. AWD is very similar to Responsive Web Design, in the sense that the page may render differently based on screen resolution. However, AWD takes it a bit further and also changes the function (features) of the page based technical capabilities of the device such as support for touch events, geolocation and HTML5 audio/video.

You may also know AWD as Progressive Enhancement, a slightly older term which focusses on browser capability rather than device capability. Since device capability is analogous to browser capability, AWD is just the new buzzword for an old practice.

A very useful library to detect feature support is Modernizr. Our goal is to have Angular load a different template file based on the support for a certain feature. There are three places in an Angular app where you can define a template URL:

  • Route definition
  • Directive definition
  • ngInclude ‘src’ attribute

Function

One solution is to simply define a function which dynamically generates the right template URL. For example:

When used with ngInclude, you’d have to use a $scope property:

This approach has several drawbacks. First of all it’s not very scalable. You can’t define it in one place and use it throughout the application, unless you put it on the global JavaScript scope (which you should avoid at all times). Secondly, the function will be very hard to unit test because it’s not defined as a service or filter (those aren’t available in these contexts). Finally, you lose the option to pass a string to ngInclude, which may or may not be an issue for you.

Decorator

We can avoid these limitations by using a decorator. Templates loaded by $compile and ngInclude are fetched using $http.get(), so we could decorate the get method on $http to dynamically rewrite the URL before actually fetching it. That way we solve the problem globally.

This is the basic decorator. It firsts stores the original get method in a variable, then replaces it with a new function. This function rewrites the URL (the first argument), then calls the original method with the original context and all arguments. For the sake of simplicity, I left out the actual rewriteUrl function implementation. I’ll present my version below, but it may not be the best solution in every scenario.

Feature-based template URLs

The approach I’ve chosen to implement dynamic template URLs based on feature support is to use a specific URL pattern. Take the following URL:

This should automatically be rewritten to a URL matching a combination of tests. For example, a mobile device with support for touch events should load ‘views/mobile/main.touch.html’, while a regular desktop browser should load ‘views/desktop/main.html’. The following function would work:

This function can be improved in many ways, but it displays the solution in the clearest way possible. First, it looks for words followed by a question mark, which denote a test. It then runs all the found tests using a lookup table and either removes the entire pattern from the URL if the test fails, or only removes the question mark if the test succeeds. Finally, consecutive slashes and dots are removed to form a proper URL again.

Adding flexibility

It would be nice to be able to configure the available tests in your app’s config block, or even add and remove them at runtime. For this, we’ll need to create a service provider, since providers are configurable:

Now we can configure the provider and setup the decorator as follows:

Wrapping up

The final solution presented here still leaves some things to be desired. For example, I would like to be able to configure the replacement pattern for when a test fails (e.g. it becomes ‘no-touch’ instead of an empty string). Also, it only accepts boolean tests, not functions. I’ve extended the solution with more features and flexibility, and put it on Github. It’s also available through Bower:

Feel free to fork it and send in your pull requests. ☺

Opinionated AngularJS

Practical tips and best practices for developing AngularJS…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store