Customize the navigation control (HTML and JavaScript)

[ This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation ]

For most Windows Runtime apps, you do not need to modify the navigation control provided in the Visual Studio project templates. However, that's not true for every app. The navigation control is included as a standard project file so you can customize its behavior. This topic describes some scenarios in which you may want to customize the control to meet your own app requirements.

The navigation control is implemented in navigator.js, which is included in all Visual Studio templates for Windows Runtime apps using HTML and JavaScript, except for the Blank App template. For more info on the navigation model provided by this control, see these topics:

If you change the navigation control in a way that isn't described in this topic, make sure that you test the changes thoroughly to avoid unintended consequences. For example, if single-page navigation isn't implemented correctly, it can result in memory leaks when you navigate away from a page. (To test your app for memory leaks, use the JavaScript memory analyzer).

Note  For help choosing the best navigation pattern for your app, see Navigation patterns. Also, see the flat navigation pattern in action as part of our App features, start to finish series.

 

Customizing page animations

By default, the navigation control includes a WinJS.UI.Animation.enterPage function call. Animation Library functions like enterPage are used to provide a consistent look and feel for Windows Store apps. However, you might want to modify the implementation of animations to better fit your app requirements.

By default, the navigation control checks whether a getAnimationElements() method has been implemented on a page. If the control finds this function, it calls it and expects the page to return any DOM elements that need to be animated.

    _getAnimationElements: function () {
        if (this.pageControl && this.pageControl.getAnimationElements) {
            return this.pageControl.getAnimationElements();
        }
        return this.pageElement;
    },

If DOM elements are returned from the page, the _getAnimationElements() method returns those page elements to the calling method (_navigated). Otherwise, this method returns the current page control. Either way, the navigation control passes the returned element(s) to the enterPage function (see the following code), which initiates a standard page animation.

Let's say you want to customize the app's animation in the following ways:

  • You want to use CSS3 @keyframes animations instead of the enterPage function. (Animation Library functions like enterPage provide a wrapper around CSS3.)
  • You want to handle animations differently if an array of DOM elements, instead of the default current page control, is returned.

To customize the animation in this example, replace the following code in navigator.js:

    _navigated: function () {
        this.pageElement.style.visibility = "";
        WinJS.UI.Animation.enterPage(this._getAnimationElements()).done();
    },

with this code:

    _navigated: function () {
        this.pageElement.style.visibility = "";
        var elems = this._getAnimationElements();
        // DOM element returned.
        if (elems.style) {
            elems.style.animationDuration = "1.5s";
            elems.style.animationName = "enterElem";
        }
        // Array of DOM elements returned.
        else {
            for (var i = 0; i < elems.length; i++) {
                elems[i].style.animationDuration = "2s";
                elems[i].style.animationName = "enterElem";
            }
        }
    },

For the complete code, see Button-Tab Navigator sample.

In this code, we expect either a single element (like the current PageControl) or, if the page implements getAnimationElements, an array of DOM elements to be returned. In this example code, CSS3 animation values like animationDuration are set differently depending on what is returned.

Next, implement the getAnimationElements function in one of your pages (for example, do this in a new page that you add to the Navigation template). The following code shows an example of this. The function returns an array with a single element.

    getAnimationElements: function () {
        var animElem = document.querySelector(".elementsToAnimate");
        return new Array(animElem);
    },

Finally, to make this animation code work, add the following CSS3 @keyframes rule to your CSS file, typically in default.css:

@keyframes enterElem { 
    from { 
        transform: translateX(50px); 
        opacity: 0; 
    } 
    to { 
        transform: translateX(0px); 
        opacity: 1; 
    } 
} 

Nesting pages inside other navigation elements

By default, the navigation control and any hosted pages occupy the entire screen. You can nest the navigation control inside other page elements if you want to customize the layout. (However, a navigation control nested inside another navigation control isn't supported.)

Let's say you want to customize the layout in the following ways:

  • You want to use buttons like tabs to navigate between pages.
  • You want to host the buttons in the left column and host the pages in the right column. (In the Windows Phone UI, the buttons are on the top of the page instead of the left side.) Note  If you only need to support Windows and not Windows Phone in your app, you can use a NavBar control as one alternative navigation method.  

To do this, create an app using, in this example, the Navigation template, and replace the following code in default.html:

    <div>
        <div id="contenthost" data-win-control="Application.PageControlNavigator" 
            data-win-options="{home: '/pages/home/home.html'}"></div>
        <!-- <div id="appbar" data-win-control="WinJS.UI.AppBar">
           <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{
               id:'cmd', label:'Command', icon:'placeholder'}" type="button"></button>
        </div> -->
    </div>

with this code:

    <div id="appGrid">
        <div id="item1">
            <button class="btnPage1">1</button>
            <button class="btnPage2">2</button>
            <button class="btnPage3">3</button>
            <button class="btnPage4">4</button>
        </div>
        <div id="item2">
            <div id="contenthost" data-win-control="Application.PageControlNavigator" 
                data-win-options="{home: '/pages/home/home.html'}"></div>
            <!-- <div id="appbar" data-win-control="WinJS.UI.AppBar">
               <button data-win-control="WinJS.UI.AppBarCommand" data-win-options="{
                   id:'cmd', label:'Command', icon:'placeholder'}" type="button"></button>
            </div> -->
        </div>
    </div>

In the preceding code, an HTML Grid control provides the two-column format for the app.

Next, add three new page controls to the app, and add button click event handlers in default.js to navigate to each page. For the complete sample code, see the Button-Tab Navigator sample.

Tip  To add a new page to your app, create a folder for the page in Solution Explorer. Open the shortcut menu for the new folder, and then choose Add > New item > Page Control.

 

Add the following CSS code in default.css. The #appGrid selector specifies the HTML Grid properties, and #item1 and #item2 specify the properties for each column.

#appGrid {
    display: -ms-grid;
    border: blue;
    -ms-grid-columns: 150px 100%;
    -ms-grid-rows: 500px;
}

#item1 {
  background: transparent;
  border: orange solid 0px;
  -ms-grid-row: 1;
  -ms-grid-column: 1;
}
#item2 {
  background: transparent;
  border: green solid 0px;
  -ms-grid-row: 1;
  -ms-grid-column: 2;
}

button {
    position: relative;
    left: 50px;
    border-radius: 30px;
    padding: 1px;
    padding-top: 15px;
    padding-bottom: 15px;
    font-size: x-large;
    background-color: green;
}

.btnPage1 {
    top: 150px;
}

.btnPage2 {
    top: 175px;
}

.btnPage3 {
    top: 200px;
}

.btnPage4 {
    top: 225px;
}

Running the page unload function before loading a new page

By default, the navigation control loads a new page before calling the unload function in the page it's replacing. For some apps, this may result in unexpected behavior. If you want to call the unload function for the current page before loading the new page, you can change the _navigating function in navigator.js as follows.

Replace the following code:

    function cleanup() { 
        if (that._element.childElementCount > 1) { 
            var oldElement = that._element.firstElementChild; 
            // Cleanup and remove previous element  
            if (oldElement.winControl) { 
                if (oldElement.winControl.unload) { 
                    oldElement.winControl.unload(); 
                } 
                oldElement.winControl.dispose(); 
            } 
            oldElement.parentNode.removeChild(oldElement); 
            oldElement.innerText = ""; 
        } 
    } 

with this code:

    var oldElement;

    if (that._element.childElementCount > 1) {
        oldElement = that._element.firstElementChild;
        if (oldElement.winControl) {
            if (oldElement.winControl.unload) {
                oldElement.winControl.unload();
                }
            }
        }

    function cleanup() {
        // Cleanup and remove previous element 
        if (oldElement) {
            if (oldElement.winControl) {
                oldElement.winControl.dispose();
            }
            oldElement.parentNode.removeChild(oldElement);
            oldElement.innerText = "";
        }
    }

Now, the unload function for the current page will run before a new page gets loaded into the DOM. As before, the cleanup function that disposes the old page only runs after the new page loads; the cleanup function is called as a completion handler for the IPageControlMembers.render function.