AngularJS and WinJS in a Windows Phone 8.1 App

In this post I’ll run you through a sample on how to include AngularJS in a WinJS Windows Phone 8.1 app. I won’t put much emphasis on the actual app code, the integration part is more interesting. One word of caution though: The integration done here is a bit more than the usual quick and dirty sample but still not perfect. More on that later.

Getting the basic project ready

Create a new JavaScript Blank App for Windows Phone 8.1. Call it, for example, ng-Todo.

Open the NuGet Package Manager Console and install a couple of packages:

  • install-package AngularJS.Core
  • install-package AngularJS.Animate
  • install-package Angular.UI.UI-Router

AngularJS.Core gets us the AngularJS base library and AngularJS.Animate provides us animation support. The Angular.UI.UI-Router package is a view/routing module which is a bit more capable than the default Angular Route module.

To get some nice directives that ease the integration with WinJS we need to install another Angular Module called angular-winjs. Unfortunately, it is currently not published as a NuGet package. So we have to leave our comfort zone and download it manually. Head over to github and download https://raw.githubusercontent.com/codemonkeychris/angular-winjs/master/js/angular-winjs.js. Add it to the scripts folder in your project.

By now your project should look like this:

Modifying the AngularJS Source

UPDATE: PLEASE DO NOT MODIFY THE SOURCES, USE THE DYNAMIC CONTENT SHIM

Now the ugly part. We need to modify the angular sources a little bit to make them compatible with the MS app security model. (Hint: I recommend that you do a commit to your local git repo at this point – makes it easier to see the delta afterwards.).

First Change

In scripts/angular.js apply the following change around line 2685 change this:

html: function(element, value) {
    if (isUndefined(value)) {
      return element.innerHTML;
    }
    for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
      jqLiteDealoc(childNodes[i]);
    }
    element.innerHTML = value;
  },

to

  html: function(element, value) {
    if (isUndefined(value)) {
      return element.innerHTML;
    }
    for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) {
      jqLiteDealoc(childNodes[i]);
    }
    MSApp.execUnsafeLocalFunction(function () {
        element.innerHTML = value;
    });
  },

The important part is the element.innerHTML assignment. This is a security critical operation which we need to opt in with the call of MSApp.execUnsafeLocalFunction.

Second Change

At the very and of the file change:

!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-block-transitions{transition:0s all!important;-webkit-transition:0s all!important;}</style>');

to

MSApp.execUnsafeLocalFunction(function() {
    !angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}.ng-animate-block-transitions{transition:0s all!important;-webkit-transition:0s all!important;}</style>');
});

Bootstrapping AngularJS from WinJS

Create a new JavaScript file js/app.js that contains the AngularJS app (for the moment, we’ll put everything into a single file which is not best practice).

The app.js file:

/// <reference path="../scripts/angular.js" />

var app = angular.module('todoApp', []);

app.controller('todoCtrl', ['$scope', function ($scope) {
    $scope.greeting = 'Hello World';
}]);

Open ./default.html and add the AngularJS library files and the additional modules we installed via NuGet. Applying them in the header is fine for our purpose. In addition include a script tag for /js/app.js. Add an ng-controller="todoCtrl" attribute to the body tag. Finally, create a binding for the greeting we put in our $scope.

default.html should look like this now:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>ng_Todo</title>

    <!-- WinJS references -->
    <!-- At runtime, ui-themed.css resolves to ui-themed.light.css or ui-themed.dark.css 
    based on the user’s theme setting. This is part of the MRT resource loading functionality. -->
    <link href="/css/ui-themed.css" rel="stylesheet" />
    <script src="//Microsoft.Phone.WinJS.2.1/js/base.js"></script>
    <script src="//Microsoft.Phone.WinJS.2.1/js/ui.js"></script>
    
    <!-- Angular references -->
    <script src="/scripts/angular.js"></script>
    <script src="/scripts/angular-ui-router.js"></script>
    <script src="/scripts/angular-winjs.js"></script>
    <script src="/scripts/angular-animate.js"></script>

    <!-- ng_Todo references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
    <script src="/js/app.js"></script>
</head>
<body class="phone" ng-controller="todoCtrl">
    <p>Greetings: {{greeting}}</p>
</body>
</html>

Developers who have already some experience with AngularJS might ask themselves why there is no ng-app attribute in default.html. The reason for this is that we want to plug the AngularJS initialization into the WinJS initialization and take profit of things like the splash screen. To accomplish that we perform a manual AngularJS initialization (see AngularJS Developer Guide for details).

We perform the manual initialization in scripts/default.js. Here is the new code of the .onactivated handler function:

app.onactivated = function (args) {
    if (args.detail.kind === activation.ActivationKind.launch) {
        if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.terminated) {
            // TODO: This application has been newly launched. Initialize
            // your application here.
        } else {
            // TODO: This application has been reactivated from suspension.
            // Restore application state here.
        }

        // Bootstrap AngularJS //            
        // Use the Signal class (unfortunately currently private...) 
        // to setup a promise that we can signal as 
        // completed as soon as the AngularJS initialization 
        // is through and we've navigated to the start page.
        // This makes sure that the splash screen will 'hide'
        // the time between ready-DOM and angular bootstrap.
        var angularLoadedSignal = new WinJS._Signal();
        angular.element(document).ready(function () {
            try {
                angular.bootstrap(document, ['todoApp']);
                angularLoadedSignal.complete();
            } catch (e) {
                // Init error is caught when we've already set up 
                // the angular app (resume) - Could be done 
                // more elegant I guess... 
                // All other bootstrap errors are just passed through.
                if (!(typeof e.message == 'string' 
                    || e.message instanceof String)) {
                    throw e;
                }

                if (e.message.indexOf('[ng:btstrpd]') !== 0) {
                    throw e;
                }
            }
        });

        // The signal's promise will be completed 
        // when angular finishes its initialization.
        args.setPromise(angularLoadedSignal.promise);
    }
};

Testing the app

Press F5 to deploy and start the app. If all went well, you should see this:

Preparation Work - Adapter Service

We’ll need a little bit of infrastructure code to integrate Promises between the two worlds. Side note: JavaScript and Promises is just pure chaos. This brief article from Derick Bailey provides an introduction to the topic. I added my quick and dirty solution to js/app.js. It looks like this:

app.service('adapterSvc', ['$q', function($q) {
    return {
        toAngularPromise: function (winjsPromise) {
            var deferred = $q.defer();

            winjsPromise.then(function (value) {
                deferred.resolve(value);
            }, function(err) {
                deferred.reject(err);
            }, function(value) {
                deferred.notify(value);
            });

            return deferred.promise;
        },

        toWinJSPromise: function(angularPromise) {
            var signal = new WinJS._Signal();

            angularPromise.then(function(value) {
                signal.complete(value);
            }, function(err) {
                signal.error(err);
            }, function(value) {
                signal.progress(value);
            });

            return signal.promise;
        }
    }
}]);

On Managing Views and Navigation

This is were all existing tutorials on AngularJS and WinJS fall a bit short. It is quite easy to do a Hello World with WinJS and AngularJS, but we need to go one step further to make apps with AngularJS with WinJS really feasible.

Let’s switch gears for a second and review how WinJS handles its page navigation concept out of the box…

Eventually, you’ll find out that it does not handle page-based navigation within the library code! The page-based navigation is introduced by an extension that is part of the Visual Studio project templates. If you create a Pivot or Navigation app instead of a blank app, you’ll get a file js/navigator.js. This file contains a class called PageControlNavigator that does all the page-based navigation magic. It basically attaches itself to a couple of events in the WinJS.Navigation module. Namely: navigated and navigating. (I have uploaded the navigator.js into a Gist, if you want to have a quick look at it.). Whenever the navigating event occurs, it will manipulate the DOM to perform a page change. A page change via the PageControlNavigator is a SPA (Single Page Application) page change, that is the document is not reloaded but a specific part of the DOM is replaced. The WinJS.Navigation module can be regarded as an abstraction of the navigation concept. It knows about a navigation history and functions to go forward an backward, but does not directly perform any DOM actions. We will use this to our advantage!

AngularJS has modules that perform a SPA view navigation which is very similar to the WinJS concept and the PageControlNavigator class. The standard module is Router which is available as a separate module from the official AngularJS repo. The popular community alternative is UI Router (we installed the NuGet package already.)

The approach to bring both worlds together is to write an adapter that bridges between WinJS.Navigation and UI Router.

Why all the trouble? Wouldn’t it be easier to use just the AngularJS navigation concept? Yes, it would be easier. But within the WinJS lib, the hardware button back presses are hard-wired with the WinJS.Navigation module. When you would decide against WinJS.Navigation, you would need to make sure that all hardware button events are correctly handled and mapped. That felt more complicated then utilizing WinJS.Navigation and writing an adapter for UI Router.

Implementing Page Navigation with UI Router

Change default.html as follows (ui-view attribute is new):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>ng_Todo</title>

    <!-- WinJS references -->
    <!-- At runtime, ui-themed.css resolves to ui-themed.light.css or ui-themed.dark.css 
    based on the user’s theme setting. This is part of the MRT resource loading functionality. -->
    <link href="/css/ui-themed.css" rel="stylesheet" />
    <script src="//Microsoft.Phone.WinJS.2.1/js/base.js"></script>
    <script src="//Microsoft.Phone.WinJS.2.1/js/ui.js"></script>
    
    <!-- Angular references -->
    <script src="/scripts/angular.js"></script>
    <script src="/scripts/angular-ui-router.js"></script>
    <script src="/scripts/angular-winjs.js"></script>

    <!-- ng_Todo references -->
    <link href="/css/default.css" rel="stylesheet" />
    <script src="/js/default.js"></script>
    <script src="/js/app.js"></script>
</head>
<body class="phone">
    <div ui-view></div>
</body>
</html>

Replace the existing controller with the following two controllers in js/app.js:

app.controller('todoListCtrl', ['$scope', 'navigationSvc', function ($scope, navigationSvc) {
    $scope.nextPage = function() {
        navigationSvc.navigateTo('todoItem', { itemId: 4711 });
    };
    $scope.greeting = 'Hello World from todoListCtrl';
}]);

app.controller('todoItemCtrl', ['$scope', '$stateParams', function ($scope, $stateParams) {
    $scope.greeting = 'Hello World from todoItemCtrl';
    $scope.itemId = $stateParams.itemId;
}]);

Besides importing some standard dependencies ($scope, $stateParams) the todoListCtrl imports a dependency called navigationSvc. This is the piece of code that brings WinJS and AngularJS together.

app.constant('homeStateName', 'todoList');

(function () {   
    var NavigationSvc = function ($q, $state, adapterSvc, homeStateName) {
        WinJS.Navigation.addEventListener('navigating', function (args) {
            var targetState = args.detail.location;
            var angularPromise = $state.go(targetState, args.detail.state);
            args.detail
                .setPromise(adapterSvc.toWinJSPromise(angularPromise));
        });

        this.goHome = function () {
            return adapterSvc
            .toAngularPromise(WinJS.Navigation.navigate(homeStateName));
        };

        this.navigateTo = function (view, initialState) {
            return adapterSvc
            .toAngularPromise(WinJS.Navigation.navigate(view, initialState));
        };

        this.goBack = function() {
            return adapterSvc
            .toAngularPromise(WinJS.Navigation.back());
        };

        this.goForward = function() {
            return adapterSvc
            .toAngularPromise(WinJS.Navigation.forward());
        }
    };

    app.service('navigationSvc', NavigationSvc);
}());

We need the configuration of the UI router:

app.config(function ($stateProvider) {
    $stateProvider
     .state('todoList', {
         url: '/todoList',
         template: '<p>{{greeting}}</p><p><button ng-click="nextPage()">Next Page</button></p>',
         controller: 'todoListCtrl',
     })
    .state('todoItem', {
        url: '/todoItem/:itemId',
        template: '<p>{{greeting}}: {{itemId}}</p>',
        controller: 'todoItemCtrl',
    });
});

And finally, tell Angular in the run() function to navigate to our home view:

app.run(function (navigationSvc) {
    navigationSvc.goHome();
});

That’s it. Press F5 and test your implementation. It should look like this:

Integrating WinJS Animations

With the current implementation the navigation between the pages feels awkward. That’s not the original Windows Phone experience users would like to see! Let’s fix that. Update you app declaration in js/app.js to add the animation module dependency:

var app = angular.module('todoApp', ['ui.router', 'ngAnimate']);

Now add an animation service:

app.animation('.turnstile-animation', function () {
    return {
        enter: function (element, done) {
            WinJS.UI.Animation.turnstileForwardIn(element[0]).then(done);
        },

        leave: function (element, done) {
            done();
        }
    };
});

The animation service basically calls into the WinJS animation library and applies the animation to the view element which is replaced by Angular’s UI router module. Great to have such degree of modularity and extensibility in the framework!

Now just add a CSS class called turnstile-animation to the div decorated with ui-view in default.html:

<body class="phone">
    <div class="turnstile-animation" ui-view></div>
</body>

Again, hit F5 and see some animation in action. AngularJS’ animation hooks are very flexible and you should be able get the right combinations in place to provide a solid Windows Phone experience.

Adding Basic Styling

Before we are going introduce a couple of WinJS controls, we need to tweak and prepare the CSS a little bit. To make things easier, let us hard-code the use of the light theme (default.html) and put a contenthost id on our main div:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>ng_Todo</title>
    
    <!-- NEW: Hard coded light theme, we don't want runtime changes. -->
    <link href="//Microsoft.Phone.WinJS.2.1/css/ui-light.css" 
          rel="stylesheet" />
     <!-- REMOVED: "/css/ui-themed.css" /> -->

     <!-- ... all other script/link tags unchanged... -->

</head>
<body class="phone">
    <div id="contenthost" class="turnstile-animation" ui-view></div>
</body>
</html>

Next, get the default.css prepared to define the basic view/page layout and the look and feel of our input forms:

#contenthost {
    height: 100%;
    width: 100%;
}

/* Default View Layout */
.fragment {
    /* Define a grid with rows for a banner and a body */
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 60px 1fr;
    display: -ms-grid;
    height: 100%;
    width: 100%;
}

    .fragment header[role=banner] {        
        -ms-grid-columns: 1fr;
        -ms-grid-rows: 1fr;
        display: -ms-grid;
        background: #67BCDB;
        color: #fff;
    }
       
        .fragment header[role=banner] .titlearea {
            -ms-grid-column: 1;
            -ms-grid-row-align: center;
            margin-left: 10px;            
        }

            .fragment header[role=banner] .titlearea .pagetitle {
                width: calc(100% - 20px);
            }

    .fragment section[role=main] {
        -ms-grid-row: 2;
        height: 100%;
    }

/* Default Forms */
label {
  display: inline-block;  
  margin-bottom: 0;
  font-weight: bold;
}

.viewForm {
    margin: 1em;
}

.form-group {
    margin-bottom: 15px;
}

.form-group label {
    font-size: larger;
}
    
.form-group input[type=text] {
    margin: 0px;
    margin-top: 4px;
}

input[readonly]  {  
    background: #eee;
    color: #999;
}

Now we create two additional CSS files (/css/todoListView.css and /css/todoItemView.css) that will contain just the view/page specific layout:

/* todoListView.css */
.todoListView.fragment .win-listview {
    height: auto;
}

.todoListView.fragment .win-listview .win-container {
    height: 60px;    
}

.todoListView.fragment .win-listview .win-item {
    background-color: #fff;
    height: calc(100% - 20px); 
    margin: 0px;   
    padding: 10px;
    color: #333;
    font-size: large;
}

.todoListView.fragment .win-listview .win-item hr {
    display: block;
    height: 1px;
    border: 0;
    border-top: 1px solid #ccc;
    padding: 0;
}
/* todoItemView.css */

.todoItemView.fragment header[role=banner]{
    background: #A2AB58;
}

Put the two link tags for the style sheets in default.html:

<!DOCTYPE html>
<html>
<head>   
    <!-- ... -->    
    <link href="/css/todoListView.css" rel="stylesheet" />
    <link href="/css/todoItemView.css" rel="stylesheet" />
    <!-- ... -->
</head>
    <!-- ... -->
</html>

Introducing WinJS Controls and HTML Template Files for the Views

With the CSS prepared it is time to refactor our HTML a bit. Before we just hard-coded the templates for the views directly within app.js and didn’t make use of any WinJS control. Time to change that. Create a top-level folder /views. Within the folder views create two HTML files:

todoListView.html

<div class="todoListView fragment">
    <header aria-label="Header content" role="banner">
        <h1 class="titlearea win-type-ellipsis">
            <span class="pagetitle">Todos</span>
        </h1>
    </header>
    <section aria-label="Main content" role="main">

        <win-list-view item-data-source="todoItems" 
                       oniteminvoked="itemClicked($event)">
            <win-item-template>
                <span>{{item.data.title}}</span>
                <hr />
            </win-item-template>
            <win-list-layout></win-list-layout>
        </win-list-view>

    </section>
</div>

todoItemView.html

<div class="todoItemView fragment">
    <header aria-label="Header content" role="banner">
        <h1 class="titlearea win-type-ellipsis">
            <span class="pagetitle">Edit Todo Item</span>
        </h1>
    </header>
    <section aria-label="Main content" role="main">
        <div>
            <form class="viewForm">
                <div class="form-group">
                    <label for="title">Title</label>
                    <input id="title" 
                           placeholder="enter title"
                           ng-model="itemTitle" 
                           type="text" />
                </div>
                <div class="form-group">
                    <label for="id">Item Id</label>
                    <input readonly id="id" ng-model="itemId" type="text" />
                </div>
            </form>            
        </div>
    </section>
    
    <win-app-bar>
        <win-app-bar-command 
            icon="'accept'" label="'ok'" ng-click="ok()">
        </win-app-bar-command>
        <win-app-bar-command 
            icon="'cancel'" label="'cancel'" ng-click="cancel()">
        </win-app-bar-command>
    </win-app-bar>
</div>

If you are not that familiar with AngularJS you might be irritated by the strange looking tags in the view templates, e.g. win-app-bar. These are custom directives that allow AngularJS to interface with WinJS controls in a declarative manner. These directives are defined in scripts/angular-winjs.js (the file which we manually imported at the very beginning).

Now that the templates are defined, we need to update the UI Router configuration in js/app.js. Replace the old config section with:

app.config(function ($stateProvider) {
    $stateProvider
     .state('todoList', {
         url: '/todoList',
         templateUrl: '/views/todoListView.html',
         controller: 'todoListCtrl',
     })
    .state('todoItem', {
        url: '/todoItem/:itemId',
        templateUrl: '/views/todoItemView.html',
        controller: 'todoItemCtrl',
    });
});

In order to get our WinJS directives supported, we need to load the module. Replace the existing app definition with:

var app = angular.module('todoApp', ['ui.router', 'ngAnimate', 'winjs']);

To get some actual behavior, we need to tweak the controllers, too. Replace the existing controllers with:

app.controller('todoListCtrl', 
    ['$scope', 'navigationSvc', 
     'todoSvc', function ($scope, navigationSvc, todoSvc) {    
    $scope.todoItems = todoSvc.getTodoItems();

    $scope.itemClicked = function(e) {
        e.detail.itemPromise.then(function (item) {
            navigationSvc.navigateTo('todoItem', { itemId: item.data.itemId });
        });
    };
}]);

app.controller('todoItemCtrl', 
    ['$scope', '$stateParams', 
     'navigationSvc', 'todoSvc', 
    function ($scope, $stateParams, navigationSvc, todoSvc) {
    var itemId = parseInt($stateParams.itemId, 10);
    var todoItem = todoSvc.getById(itemId);
    
    $scope.itemId = itemId;
    $scope.itemTitle = todoItem.title;

    $scope.ok = function () {
        todoItem.title = $scope.itemTitle;
        navigationSvc.goBack();
    };

    $scope.cancel = function() {
        navigationSvc.goBack();
    };
}]);

And define an AngularJS service that holds our Todo Items in memory (just append to js/app.js):

app.service('todoSvc', function () {
    var todoItems = [
                { itemId: 1, title: 'clean kitchen' },
                { itemId: 2, title: 'call mum' },
                { itemId: 3, title: 'relax' }
    ];

    return {
        getTodoItems: function() {
            return todoItems;
        },

        getById: function (id) {           
            for (var i = 0; i < todoItems.length; i++) {
                if (todoItems[i].itemId === id) {
                    return todoItems[i];
                }
            }

            return null;
        },        
    }
});

If everything is set up correctly, you should see an app in action like this:

Summary

Yes, it is possible to integrate WinJS and AngularJS with each other. The process is currently not ideal, because WinJS wasn’t build with modularization in mind. At the moment is more a take all or nothing approach. At //build, however, the WinJS team announced to change this in future (see roadmap at end of the talk). That would be very welcome!

Source Code

 
comments powered by Disqus