Thursday, August 1, 2013

WinJS and RequireJS

I had been looking at improving my JavaScript skills and learning WinJS at the same time.  One of the key things I wanted to do was work better with larger projects and larger numbers of JavaScript files. 

RequireJS is great for that, but I had considerable trouble getting it to work with WinJS.  When I did get it working I then had trouble getting it to work properly. 

RequireJS is expected to be initialised with an include reference in the page html with a data-main tag, but this doesn't really work with the WinJS app lifecycle, so it took a bit of work to get it behaving the way I expected.

Eventually I got it working, and working with async initialisation code which was a key part of the initialisation process in my sample app.

So the basics of getting RequireJS working in WinJS is the following - I have probably made a million big JavaScript no-no's here, but as I said, still learning.

default.html - add
    <script src="/js/require.js" ></script>
before default.js

default.js - initialise requireJS and include whatever dependencies you want to use in your startup code.

app.addEventListener("activated", 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 if (args.detail.previousExecutionState !== activation.ApplicationExecutionState.closedByUser) {
                // TODO: This application has been launched after a requested shutdown. Initialize
                // your application here.
            } else {
                // TODO: This application has been reactivated from suspension.
                // Restore application state here.
            }
 
            if (app.sessionState.history) {
                nav.history = app.sessionState.history;
            }
 
            //RequireJS Initialisation here
            require.config({
                baseUrl: '/js'
            });
 
            //this creates a promise that calls complete() when the initAsync promises finish
            //all this code happens within the require context so we are guaranteed that the dependencies are resolved before the promise is resolved
            //the initAsync methods return promises that load data from internal json data files
            //persistence.loadState is synchronous code so doesn't return a promise
            var loadPromise = new WinJS.Promise(function (complete) {
                require([
                    'persistence',
                    'game/staticdata/itemClassStore',
                    'game/staticdata/itemStore',
                    'game/gamestate/world',
                    'game/gamestate/characters',
                    'game/classes/character'
                ], function (persistence, itemClassStore, itemStore, world, characters, character) {
 
                    itemClassStore.initAsync()
                        .then(function () { return itemStore.initAsync(); })
                        .then(
                            function () {
                                var data = persistence.loadState();
                                world.fromJson(data);
                                complete();
                            }
                        );
 
 
                });
            });
 
            //args.setPromise is a function on the activate event which allows you to wait for promises to complete before continuing
            args.setPromise(
                loadPromise
                    .then(function () { return WinJS.UI.processAll(); })
                    .then(
                        function () {
                            if (nav.location) {
                                nav.history.current.initialPlaceholder = true;
                                return nav.navigate(nav.location, nav.state);
                            } else {
                                return nav.navigate(Application.navigator.home);
                            }
                        }
                    )
            );
        }
    });

xPage.js - resolve your dependencies in the page ready function with a call to require(), and perform your page initialisation as per normal. 

///  
/// 
/// 
/// 
/// 
 
(function () {
    "use strict";
 
    WinJS.Namespace.define("CharacterSelect", {
        CharacterSelectViewModel: WinJS.Class.define(
            //the viewmodel constructor takes the injected requireJS dependency from the page ready code.
            function (world) {
                this._charactersModule = world.characters;
                this._itemsDataSource = new WinJS.Binding.List(this._charactersModule.characters);
                this._inventoryDataSource = new WinJS.Binding.List(null);
            },
            {
                _charactersModule: null,
                _itemsDataSource: null,
                _inventoryDataSource: null,
                _selectedCharacterId: 0,
                selectedCharacterId: {
                    get: function () {
                        return this._selectedCharacterId;
                    },
                    set: function (value) {
                        this._selectedCharacterId = value;
                        if (value == 0) {
                            this._inventoryDataSource = new WinJS.Binding.List(null);
 
                        } else {                            
                            this._inventoryDataSource = new WinJS.Binding.List(this._charactersModule.get(value).inventory.items);
                        }
                        this._getObservable().notify("selectedCharacterId", value);
                        this._getObservable().notify("inventoryDataSource", this._inventoryDataSource);
                    }
                },
 
                listDataSource: {
                    get: function () {
                        return this._itemsDataSource;
                    }
                },
                
                inventoryDataSource: {
                    get: function () {
                        return this._inventoryDataSource;
                    }
                },
 
                addNew: function () {
                    WinJS.Navigation.navigate("/pages/newCharacter/newCharacter.html");
                },
 
                deleteCharacter: function (that) {
                    that._charactersModule.deleteCharacter(this.selectedCharacterId);
                    that._itemsDataSource = new WinJS.Binding.List(this._charactersModule.characters);
 
                    that._getObservable().notify("listDataSource", that._itemsDataSource);
                }
 
            },
            {}),
 
    });
 
    WinJS.UI.Pages.define("/pages/characterSelect/characterSelect.html", {
 
        // This function is called whenever a user navigates to this page. It
        // populates the page elements with the app's data.
        ready: function (element, options) {
            require(
                [
                    'persistence',
                    "appbar",
                    "game/gamestate/world"
                ],
                function (persistence, appbar, world) {
                    //normal page initialisation code - all dependency modules are initialised at this point
                    //create view model (passing in resolved dependencies) and bind to relevant events
                    //(WinJS only has 1-way bindings and can't declaratively bind to button events that i can see)
                    var section = element.querySelector("section");
 
                    var viewModel = new CharacterSelect.CharacterSelectViewModel(world);
                    var observableviewModel = WinJS.Binding.as(viewModel);
                    WinJS.Binding.processAll(section, observableviewModel);
 
                    document.getElementById("cmdCreate").addEventListener("click", observableviewModel.addNew, false);
                    document.getElementById("cmdDeleteCharacter").addEventListener(
                        "click",
                        function(){
                            observableviewModel.deleteCharacter(observableviewModel);
                            persistence.saveState();
                        }
                        ,
                        false
                        );
 
                    document.getElementById("characters").winControl.onselectionchanged = function (ev) {
                        var selection = document.getElementById("characters").winControl.selection;
                        if (selection.getItems()._value.length > 0) {
                            viewModel.selectedCharacterId = selection.getItems()._value[0].data.id;
                            appbar.characterSelectSelected();
                        } else {
                            viewModel.selectedCharacterId = 0;
                            appbar.characterSelectDeSelected();
                        }
 
                    };
                    appbar.characterSelectInit();
 
 
                }
            );
 
 
        },
 
        unload: function () {
            // TODO: Respond to navigations away from this page.
        },
 
        updateLayout: function (element, viewState, lastViewState) {
            /// 
 
            // TODO: Respond to changes in viewState.
        }
    });
})();