Skip to content

Instantly share code, notes, and snippets.

@tommck
Last active December 17, 2015 22:19
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tommck/5681374 to your computer and use it in GitHub Desktop.
Save tommck/5681374 to your computer and use it in GitHub Desktop.
How to use Durandal 2.0 with WinJS 1.0 apps

UPDATE: I wrote a blog post (beginning of a series) on how to do this with WinJS 2.0. http://blog.appliedis.com/2014/10/15/writing-winjs-apps-with-durandaljs-on-windows-8-1/

-- Old Gist Below --

Native Apps with WinJS

This gist is to help people get started with Durandal integration with WinJS applications. The information is current for WinJS 1.0 (Windows 8.0, not 8.1). It will be updated in the future to incorporate any necessary changes for 2.0/Win8.1.

#Overview You can now develop native apps for Windows 8 tablets in JavaScript, HTML and CSS. Effectively your application is running in a chromeless version of Internet Explorer 10. Though Microsoft has created a whole new infrastructure and set of classes for developing applications in WinRT, you can get away with using very little WinJS-specific code if you desire.

The Basics

The only places you need to use WinJS-specific code are places where you need to integrate with WinRT apis (camera, Search API, etc) or where you want to take advantage of some of the built-in controls like ListView.

jQuery 2.0

First off, you should use jQuery 2.0 has added first-class support for WinJS applications, yet also works fine with Durandal. So, your application should just update jQuery to 2.0 in order to properly support WinJS

Make the HTML parsing "safe"

The Windows Runtime ("WinRT") has many security restrictions, one of which is the inability to create HTML elements dynamically by parsing text unless it is known to be "safe". So, in order to tell WinRT that the markup Durandal is parsing is safe, you have to override the "parseMarkup" method in the viewEngine like this:

    //
    // Html Parsing - override normal view engine parsing to handle "unsafe" code
    //
    var parser = viewEngine.parseMarkup;
    viewEngine.parseMarkup = function (markup) {
        // wrap the parser in an "unsafe" call to avoid exceptions with dynamic html injection
        return MSApp.execUnsafeLocalFunction(function () {
            return parser(markup);
        });
    };

Now, each time Durandal tries to render HTML markup, it will be considered "safe" by the WinJS runtime.

Default.js

By default, WinJS projects have a "default.js" file in them that sets up support for various WinJS-specific features like application life cycle events, etc. This code is required for the application to run properly. So, the simplest approach is to wire this file up as a dependency of your main module by adding a "deps" entry to your requirejs config like this:

	requirejs.config({
	    deps: ["/app/default.js"],
		...

Integrating WinJS Controls

There are times when you will want to use WinJS controls, since they take care of lots of features for you, like touch support and accelerated scrolling, amongst other things. So, when you need to use a WinJS control, you will have to explicitly call Knockout binding on the proper HTML element in your viewmodels.

Using the ListView Control

The ListView control is a powerful control that you will probably use repeatedly. It's also a good example of how to handle any of the WinJS controls. Here's the HTML for a simple ListView with a few tweaks.

<section>
    <h2 data-bind="html: displayName"></h2>
    <blockquote data-bind="html: description"></blockquote>

    <!-- Simple *WINJS* template for the ListView instantiation  -->
    <div id="smallListIconTextTemplate"
        data-win-control="WinJS.Binding.Template"
        data-bind="stopBinding: true" <!-- tell knockout not to bind this since it is a template -->
        style="display: none"> 
        <h4 style="width: 6em; text-align: center" data-bind="text: $data"></h4>
    </div>

    <!-- *WINJS* Control -->
    <div id="listView"
        class="win-selectionstylefilled"
        style="height: 300px; width: 600px;"
        data-win-control="WinJS.UI.ListView"
        data-win-options="{ 
                            itemTemplate: smallListIconTextTemplate, 
                            selectionMode: 'none', 
                            tapBehavior: 'none', 
                            swipeBehavior: 'select', 
                            layout: { type: WinJS.UI.GridLayout } 
            }">
    </div>
</section>

Note the usage of "data-win-*" for WinJS features and "data-bind" for integrating Knockout binding. This makes use of the optional side-by-side Knockout binding support. Also notice the data-bind="stopBinding: true" on the template. This is a reference to a binding from Ryan Neimeyer that causes Knockout to ignore the element and its children: http://www.knockmeout.net/2012/05/quick-tip-skip-binding.html

ViewModel With WinJS Support

So, since we're bypassing the usual way of creating "pages" in a WinJS application, the WinJS UI processing is not being called on the elements in your pages. So, when you are using WinJS controls, like the ListView, you will need to explicitly call the WinJS.UI.processAll method to handle things properly. Here's the ViewModel for the ListView HTML above:

define(function (require) {

    var test = {
        attached: function (element) {
            // wire up WinJS bindings on this view
            return WinJS.UI.processAll(element)
               .then(function () {
		            // grab the listview in order to set its datasource
	                var list = element.querySelector("#listView");
        	        var listView = list.winControl;

	                var source = new WinJS.Binding.List(test.someArray).dataSource;
	                listView.itemDataSource = source;
               });
        },
        someArray: []    
    };

    for (var i = 0; i < 100; i++) {
        test.someArray.push(i);
    }

    return test;
});

NOTE: I also have a partially completed knockout binding handler to simplify using WinJS ListViews

==================== TODO: Gist for winjsListView binding

Data Binding

WinJS has its own data binding (with "win-data-bind"), but, if you prefer, you can continue to use knockout by overriding the WinJS UI processing method and calling ko.applyBindings explicitly in there as well.

    //
    // Knockout View Binding support - applies Knockout Binding to elements that were created using WinJS binding
    //
    var oldProcessAll = WinJS.Binding.processAll;
    WinJS.Binding.processAll = function (rootElement, dataContext, skipRoot, bindingCache) {
        if (dataContext) {
            ko.applyBindings(dataContext, rootElement);
        }

        return oldProcessAll.call(this, rootElement, dataContext, skipRoot, bindingCache);
    };

Now, in our ListView example above, when the WinJS ListView code uses the template to create new blocks of HTML, it will actually call this method on each item in the ListView (passing a member of the "someArray" to each one). This method will then apply the Knockout bindings. NOTE: because it's being called individually, You will NOT have access to $parent or $root, since the objects will be individual objects with no way to get back to the parent

App Lifecylce Events

The WinRT runtime (kinda like ATM Machine) is very aggressive with memory management. It is much more like a mobile development environment than a desktop or browser environment. So, when your app is notified that it's about to be suspended, it must persist the current state quickly. Conversely, the application must be able to restore its state quickly. This state includes things like on-screen edits, current navigation history, etc.

In order to do this with Durandal, we have to shim some functionality into the framework. The approach I took for on-screen edits was to look for a method on the viewModel called "modelState" that is used as a setter/getter for any temporary state that the viewmodel needs to have persisted. Additionally, I persist the current navigation history in order to maintain that as well.

&&&&&&&&&& TODO: Add code / examples here &&&&&&&&&&

The App Bars

WinRT applications have app bars that appear when you swipe at the top or bottom of the screen (and are sometimes fixed). The app bars are added to the tag of the HTML directly. The commands that appear in the app bars are frequently added directly as HTML objects. However, I recommend using JavaScript objects instead, since it makes for an interesting way to show and hide the app bar commands.

First, I modify the default.html again to add the app bars at the top and bottom (if needed). Top app bars are typically used if you are using it as a nav bar. It depends on the style of your application whether or not you will have one. The bottom app bar is typically used for context-sensitive commands (changing based on what screen you are on, etc).

the body of the app now looks like this:

<body>
    <!-- top nav -->
    <nav>
        <div id="topnavbar" 
            data-win-control="WinJS.UI.AppBar" 
            data-win-options="{ placement: 'top', layout: 'custom' }">
        </div> 
    </nav>
    <!-- content -->
    <div>
        <div id="applicationHost">
        </div>
    </div>

    <!-- bottom app bar -->
    <div id="bottombar" 
        data-win-control="WinJS.UI.AppBar" 
        data-win-options="{ placement: 'bottom', layout: 'commands' }">
    </div> 
</body>

The app bars both start off empty, but they are populated in my "appbars" module:

&&&&&&&&&&& INSERT APP BARS MODULE CODE &&&&&&&&&&&&&&

&&&&& Show how Nav links work &&&&&&

&&&&&&&&&& Show how a particular screen will enable/disable the buttons and handle execution &&&&&&&&

Popups

        // turn app.showMessage into a WinJS MessageDialog
        app.showMessage = function (message, title, options) {

            var boxTitle = title || 'Default Title Here';
            var boxOptions = options || ['Ok'];

            var msgpopup = new Windows.UI.Popups.MessageDialog(message, boxTitle);

            // using underscore here, but you can use anything to iterate over the options
            _.each(boxOptions, function (opt) {
                // each option just returns the option text
                msgpopup.commands.append(new Windows.UI.Popups.UICommand(opt, null, opt));
            });
            
            return msgpopup.showAsync()
                .then(function (choice) {
                    // convert the return value to the option string
                    return choice.id;
                });
        };

The Back Button

In WinRT apps, since we're not in a chromed browser (no menu or standard nav bar), there's no normal back button. WinRT apps have a back button that is displayed by the HTML. This button is supposed to disappear whenever there is nowhere left to go back to. Unfortunately, the HTML 5 History API does not have any way to determine if you can go back or forward. So, Microsoft created their own Navigation methods in the WinJS.Navigation object. The default WinJS way of navigating is to use methods on this object and to bind the visibility of the Back button to "WinJS.Navigation.canGoBack". This property is dependent on an exposed array of history values in a stack on "WinJS.Navigation.history.backStack". So, I came up with this hack to allow us to just use a knockout property instead. NOTE: I declare event strings in a common "Events" module so there is no problem with typos in my code.

    function backButtonHandling() {
    	/// <summary>
        /// intercept router stuff in order to handle back button logic
        /// </summary>

        var firstNav = true;
        var backPending = false;

        var oldBack = router.navigateBack;
        router.navigateBack = function () {
           
            var stack = WinJS.Navigation.history.backStack;
            if (stack.length > 0) {
                backPending = true;
            }

            return oldBack();
        };

        router.on('router:navigation:complete', function (instance, instruction, router) {
    	    /// <summary>
	    /// Handle navigation complete so we know when the back button needs to be enabled/disabled
	    /// </summary>
	    /// <param name="instance"></param>
	    /// <param name="instruction"></param>
            /// <param name="router"></param>

            var preCan = WinJS.Navigation.canGoBack;
            var stack = WinJS.Navigation.history.backStack;

            // if it's not the first time (one nav to get to main page) and it's not a "back" navigation, 
            // push it on the stack 
            if (firstNav == false && backPending == false) {
                WinJS.Navigation.history.backStack.push(instruction.fragment);
            }

            if (backPending) {
                stack.pop();
                backPending = false;
            }

            // check to see if we need to notify of 'canGoBack' changing values
            var postCan = WinJS.Navigation.canGoBack;
            if (preCan != postCan) {
                app.trigger(Events.nav.canGoBackChanged, postCan);
            }

            firstNav = false;
        });
    }

So, somewhere in your application, you can listen for the "Events.nav.canGoBackChanged" event and set your knockout variable equal to that new value. Assuming that the variable is bound to the visibility of your back button, things will work fine.

The Page Title

Since we're not in a chromed browser, we do not have a Title bar, so the HTML <title> element is useless to us. In order to update the page title, we need to display the title elsewhere

&&&&&&&&&&&& TODO: Fill this in

@EisenbergEffect
Copy link

Have a look at Durandal's viewModelBinder. You may want to plug into the beforeBind hook to enable automated calls to WinJS.UI.processAll(element) I'm not sure, but that's how we do the KendoUI integration. See the Kendo docs for an example http://durandaljs.com/documentation/KendoUI/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment