Skip to content
jayflo edited this page Jul 2, 2014 · 43 revisions

Adaptive Web Design

The goal of adaptive web design (AWD) is create a web app that renders an interface suitable to the device accessing it, e.g. smart phones should have a clutter free, compact interface utilizing touch/gestures whereas a desktop deserves a traditional interface. Of course, we always need to aim to:

  1. maximize DRY-ness
  2. minimize the amount of code that is downloaded
  3. be able to add new features easily
  4. make it easy to add types of devices to which the app adapts
  5. maintain a file structure that scales
  6. at initialization, run tests to determine the type of device being used and have the correct (device specific) views loaded automatically througout the entire app. This has already been accomplished by ghengeveld and incorporated here.

The angular framework is perfect for the job. It is, by design, modular and dependency injection allows the programmer to insert code in modules/services/specialized object when needed. This yo generator provides the primary structure for constructing an AWD angular app.

Directory structure (development)

The angular-adaptive generator produces an app structure similar to Python's Django framework in that each feature of the app has its own folder.

app/
  common/
    directives/
    resources/
    services/
  feature1/
  feature2/
  ...
  images/
  myAppName/
  styles/

The main structural concept at work is that we differentiate between device independent and device dependent code. Each folder shown, aside from app/ and common/, contains:

  • (immediate) child files with code to be executed when any type of device uses the app

  • one folder for every type of device to which you want your app to adapt (chosen at time of creation). For example, if you want to adapt to phones, tablets and desktops, each of the above folders (except app/ and common/) will have sub-folders:

      desktop/
      phone/
      tablet/
    

Thus, app/common/directives/myDirective.js is a directive uses on phones, tables and desktops, where as app/common/services/phone/myService.js is a service only used for phones. Also note that the app/myAppName folder contains the "root" modules which are conditionally (dependent upon the type of device) bootstrapped.

Directory structure (production)

The directory structure of the code after being built for production is not too much different. One reason for keeping code broken into separate files is that we may use a script loader to retrieve them asynchronously. Secondly, we want to minimize the amount of code a user must download. If we reuse files for multiple types of devices, the user is forced to download unnecessary bytes which both slows the app's loading time and could potentially cost the user (and the people who hire you!). Here is the structure:

app/
  common/
    directives/
      devType/
      ...
    resources/
      devType/
      ...
    services/
      devType/
      ...
  feature1/
    devType/
      /partials
    ...
  ...
  myAppName/
    devType/
    ...
  styles/
    devType/
    ...

Of course, devType/ is a folder representing a type of device to which your app adapts, e.g. phone/. There will be many of these. By default (see Fine tuning), the build of the production code does the following:

The directories common/directives/, common/resources/, common/services/ and each sub-directory of app/ (other than common/) are visited and for each devType/ sub-directory, the (immediate children) files of app/sub_dir and app/sub_dir/devType are combined into a single, minified file app/sub_dir/devType/feature.min.js or, in styles/ case, a min.css file. Thus, each app/sub_dir/ contains only the sub-directories app/sub_dir/devType, each of which contains a single file (and possibly a partials/ directory).

Therefore, by default, to load the code for any specific device type, we need to load each app/sub_dir/devType/someFile.min.js and the injectables app/common/directives/devType/app.directives.min.js, etc. Deviations from this way of loading require manual editing of app/Gruntfile.js.

Adaptive web feature

Each feature has the following layout:

feature/
  devType1/
    feature.devType1.js
    ...
    partials/
      feature.html
      ...
  devType2/
    feature.devType2.js
    ...
    partials/
      feature.html
      ...
  ...
  feature.common.js
  ...

Again, each devTypeN/ refers to something like phone, tablet, desktop, etc... All (by default though subject Fine tuning) files in feature/ contain code that will execute for all device types, whereas files in feature/devType1 contain code that only executes when devType1 devices connect.

As for the structure of the code itself, here are some snippets for a fixed feature and devType:

feature/feature.common.js:

angular.module('feature.common', [
    'ngRoute',
    'app.directives.common',
    'app.resources.common',
    'app.services.common',
])
.config(['$routeProvider',
    function($routeProvider) {
        $routeProvider
            .when('/', {
                templateUrl: 'feature/{deviceType}/partials/feature.html',
                controller: 'FeatureCtrl'
        })
        .otherwise({
            redirectTo: '/'
        });
    }
])
.controller('FeatureCtrl', ['$scope',
    function($scope) {
        // code run for every type of device
    }
]);

feature/devType/feature.devType.js:

angular.module('feature.devType', [
    'app.directives.devType',
    'app.resources.devType',
    'app.services.devType',
    'feature.common',
])
.controller('FeatureCtrl.DevType', ['$scope',
    function($scope) {
        // code run when 'DevType' devices use app
    }
]);

feature/devType/partials/feature.html:

<div ng-controller='FeatureCtrl.DevType'>
  <!-- feature main page -->
</div>

Things to notice:

  1. the feature.common module inherits the device independent injectables. That is, all device independent injectables are registered to the modules app.directives.common, app.resources.common,app.services.common which are defined in, e.g. app/common/directives/app.directives.common.js.

  2. the feature.devType module inherits the device dependent injectables. That is, all device dependent injectables are registered to the modules app.directives.devType, app.resources.devType,app.services.devType which are defined in, e.g. app/common/directives/devType/app.directives.devType.js.

  3. the feature.devType module has the feature.common module as a dependency and so it inherits all the device independent injectables as well.

  4. the router hands the app over to FeatureCtrl which has FeatureCtrl.DevType as a child controller. Therefore, FeatureCtrl.DevType's scope inherits from FeatureCtrl's scope so that any device independent procedures can be DRY'd up in FeatureCtrl.

  5. {say something about dynamic template fetching when completed}

The following diagram shows the 'adaptive feature' structure more clearly (squares are modules, circles are controllers). Note that there will be one feature.devType structure for every type of device to which the app will adapt, but only one (corresponding to the device the user is on) will be downloaded and executed at run time.

Adaptive Web Feature

So an adaptive web feature consists of multiple feature.devType structures which inherit from a feature.common module structure (subject to Fine tuning). Moreover, there is a feature-wide, device independent scope provided by FeatureCtrl as well as a device dependent scope provided by FeatureCtrl.DevType.

Bootstrapping

In order to load code specific to the device currently accessing the app, we must manually boostrap and use a script loader. Suppose the the name of the app is myApp. Here are snippets for discussion:

app/index.html:

<body ng-controller="MyAppCtrl">
  <div ng-controller="MyAppCtrl.{{__DEVICE__}}">
    <div class="container" ng-view=""></div>
  </div>
</body>

It may seem odd that MyAppCtrl is registered to the app.common module which is a dependency of the module MyAppCtrl.{{__DEVICE__}} is registered to, i.e. app.common.{{__DEVICE__}} (possibly with different capitalization), but it works just fine! Since app.common is a dependency, it is compiled first and defines the value of __DEVICE__. This allows angular to correctly load the MyAppCtrl.{{__DEVICE__}} controller. Note: the app.common.{{__DEVICE__}} module is (correctly) loaded and bootstrapped in the first place by the loader.js script (see below).

Also note that MyAppCtrl has an application-wide, device independent scope, while MyAppCtrl.{{__DEVICE__}}'s scope is application wide but only executed for devices of type {{__DEVICE__}}. For example, you might use MyAppCtrl for user authentication since you want to recognize users on every type of device, whereas MyAppCtrl.{{__DEVICE__}} could be used to initialize ges

app/loader.js:

Finally, there is no structural difference between app.common (defined in app/myApp/app.common.js) and any generic feature.common module (defined in app/feature/). However, the app.devType module contains every feature.devType module (defined in the directories app/myApp/devType/) as a dependency so that app.devType is the true application root for devices of type devType (even though it has a dependency, app.common, which has a controller, MyAppCtrl, that is a parent to its own controller, MyAppCtrl.DevType).

Below is a picture showing the structure of the app (see picture above for shape/color key). Note that

  1. the modules housing the device independent/dependent injectables are not shown. Simply keep in mind that every .common level module inherits the device independent injectables (e.g. app.directives.common) and every .devType level module inherits its associated device dependent injectables (e.g. app.directives.devType)

  2. to keep the picture simple, many arrows have not been drawn. For example, only MyAppCtrl is parent to all controllers in every feature, i.e. every ellipse you see. However, if MyAppCtrl.DevType1 corresponds to left most green ellipse in MyApp, it is only parent to the left most ellipse, e.g. FeatureNCtrl.DevType1, in every feature. Moreover, MyAppCtrl.DevType1 is only a controller of module app.devType1, the left most blue rectangle on the app.devType level.

awd_app

Fine tuning

Shortcomings