In the previous post we talked about the project setup, system defined styles and a little bit about navigation. In this post we will extend that project and create a simple clock which will display current date/time. We will be using concepts like navigation, data binding, converters etc. in this post. So let’s get started!!!
Before we begin
The information I have presented below is based on my understanding of building Metro applications using HTML/JS. It is quite possible that my understanding may be incorrect. If that is so, please let me know about that and I would be glad to make the corrections.
First things first
We’ll be using some helper functions so let’s write them first. Open up helper.js and add the following code in there:
/* Gets an HTML DOM element */ function id(elementId) { return document.getElementById(elementId); } /* Gets a WinJS control */ function getWinJSControl(elementId) { var elementById = id(elementId); if (!(elementById == null || elementById == undefined)) { return WinJS.UI.getControl(id(elementId)); } return null; } /* Prepends a "0" if the value is less than 10. So "9" gets converted to "09". */ function PrependZeroIfNeeded(value) { if (parseInt(value, 10) < 10) { return "0" + value; } return value; }
Navigation
Next, let’s work on implementing the navigation. What we want is to load “savedClocks.html” page when the application starts.
1. Open up default.html and modify the <body> tag so that it looks like the following:
We’re just defining an attribute in the <body> tag which we will read in default.js file in just a moment. This attribute will hold the relative path of the savedClocks.html file.
2. Next, let’s open up default.js file and define a variable which will hold this value.
var savedClocksPage;//Holds relative path for the savedClocks.html file
3. Next we’ll read the “data-savedClocks” attribute value and assign it to the variable defined above. We’ll do that in the “onmainwindowactivated” event handler.
WinJS.Application.onmainwindowactivated = function (e) { if (e.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) { savedClocksPage = document.body.getAttribute('data-savedClocks'); } }
4. Next we’ll add an event handler for handling the “navigated” event. This event is raised when the application navigates to a particular page. Add the following line just above “WinJS.Application.start();” code in default.js.
WinJS.Navigation.addEventListener('navigated', navigated);
5. Now we’ll implement the “navigated” event handler. Add the following lines of the code just after where we defined the handler for navigated event in step 4 above.
function navigated(e) { WinJS.UI.Fragments.clone(e.detail.location, e.detail.state) .then(function (frag) { var host = document.getElementById('contentHost'); host.innerHTML = ''; host.appendChild(frag); document.body.focus(); var backButton = document.querySelector('header[role=banner] .win-backbutton'); if (backButton) { backButton.addEventListener('click', function () { WinJS.Navigation.back(); }, false); if (WinJS.Navigation.canGoBack) { backButton.removeAttribute('disabled'); } else { backButton.setAttribute('disabled', 'true'); } } } WinJS.Application.queueEvent({ type: 'fragmentappended', location: e.detail.location, fragment: host, state: e.detail.state }); }); }
The way it works is that it first copies the content of the fragment page (savedClocks.html) in our case and inserts that in a div whose id is “contentHost” in default.html. Also note that WinJS.UI.Fragments.clone() function is asynchronous function and it returns a Promise for the cloned fragment. To obtain the content of the cloned fragment, you would need to provide a function for the Promise objects’s then method to call.
6. Next we would need to write the code for actually navigate to savedClocks.html file. Because we want our users to navigate to this page directly when we start the application, best place would be to write the navigation logic in “onmainwindowactivated” event handler. However what we’ll do is define another function (let’s call it start()) and write the navigation logic in there. The reason we’re putting it in a separate function is because in subsequent steps and blogs posts, we would be doing some work where we would want to reuse this functionality. So add the following code in just after navigated() function.
function start() { WinJS.UI.processAll(); WinJS.Navigation.navigate(savedClocksPage); }
7. Next step would be to call this function from our “onmainwindowactivated” event handler. So let’s add that code.
WinJS.Application.onmainwindowactivated = function (e) { if (e.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) { savedClocksPage = document.body.getAttribute('data-savedClocks'); start(); } }
8. Let’s run the application. This is what you should see:
9. Congratulations!!! You have successfully navigated to savedClocks.html page on application startup.
Data Preparation
Now what we want to do is display the clocks in our application. In order to do that, we will need to do some data preparation. In this section we’ll do that.
1. First, let’s open clock.js and add following code:
/* An object representing custom clock. */ function ClockData(id, city, country, isCurrentCity, currentDateTime) { this.Id = id;//Unique id assigned to a clock this.Name = city; if (country != "") { this.Name += ", " + country; } this.IsCurrentCity = isCurrentCity;//Flag to indicate if it's the current date/time this.CurrentDateTime = currentDateTime;//Current date/time this.MilliSecondsSince1stJan1970 = Date.parse(currentDateTime);//Get the milliseconds since January 1, 1970 //Advances clock specified by "timerDuration" variable this.AdvanceClock = function (timerDuration) { this.MilliSecondsSince1stJan1970 += timerDuration; } } /* Gets the current time for a city based on its GMT offset */ function GetCurrentDateTimeForCity(isLocal, gmtOffsetInMinutes, observeDST) { var currentDateTime = new Date(); if (isLocal) { return currentDateTime; } var currentDateTimeInUTC = currentDateTime.valueOf() + (currentDateTime.getTimezoneOffset() * 60 * 1000); if (!observeDST) { return new Date(currentDateTimeInUTC - (gmtOffsetInMinutes * 60 * 1000)); } if (observeDST) { //To do ... logic to add/subtract hours if the current city observes DST } return new Date(currentDateTimeInUTC - (gmtOffsetInMinutes * 60 * 1000)); }
2. Next, let’s go back into default.js and define some arrays and variables. You can add the following code at the top of default.js page.
//JSON array representing the saved clocks var savedClocks = [ { "Id": "00-00000", "City": "Current", "Country": "", "RegionCode": "", "GMTOffsetInMinutes": "0", "IsCurrentCity": true, "DST": false, }, ]; var clocks = []; var selectedClocksDataSource; var clockRefreshFrequency = 1000;//Number of milliseconds after which clock will advance.
So in savedClocks variable, we’re defining a JSON array which will hold all the saved clocks. We will serialize this array to store the state of the application in later blog posts when we talk about maintaining the state of the application.
3. Next, let’s add references to various JavaScript files we will be using in our default.html file:
<script type="text/javascript" src="/js/clock.js"></script><script type="text/javascript" src="/js/helper.js"></script> <script type="text/javascript" src="/js/converters.js"></script><script type="text/javascript" src="/js/default.js"></script>
4. Now let’s write some code to read this array and from each element of the array, create a ClockData object. In default.js, add the following code.
function getClock(savedClock) { var clock = new ClockData(savedClock.Id, savedClock.City, savedClock.Country, savedClock.IsCurrentCity, GetCurrentDateTimeForCity(savedClock.IsCurrentCity, 0 - savedClock.GMTOffsetInMinutes, savedClock.DST), clockRefreshFrequency); return clock; }
5. Since we need to move the clock forward in a timely manner, we’ve used a function called “advanceClock()” and in the code we’re calling it regularly after “n” milliseconds specified by clockRefreshFrequency variable. Let’s add this function at the bottom of default.js file:
// Advances the clocks by "clockRefreshFrequency" function advanceClock() { for (var i = 0; i < clocks.length; i++) { clocks[i].AdvanceClock(clockRefreshFrequency); } }
Building the Clock
1. Now we’ll work on displaying the clocks data in our savedClocks.html fragment. First thing we’ll do is add a WinJS.UI.ListView control which is a data bound control which will be used to display all the clocks. Add the following code in savedClocks.html file between <section></section> tags:
</pre> <div id="cityTemplate" data-win-control="WinJS.Binding.Template"> <div style="width: 300px; height: auto; margin: 5px 5px 5px 5px; padding: 10px;"> <div style="margin: 5px 5px 5px 5px;"> , <div class="win-itemTextTertiary" data-win-bind="innerHTML: Name"></div> </div> </div> </div> <div id="listViewSavedClocks" class="listViewSavedClocks" data-win-control="WinJS.UI.ListView" data-win-options="{itemRenderer: cityTemplate, selectionMode: 'multi', layout: {type: WinJS.UI.GridLayout} }"></div> <pre>
What we’ve done is defined a WinJS.UI.ListView control and defined its layout as WinJS.UI.GridLayout so that clocks are displayed in a grid format consisting of rows and columns. Alternately we could have opted the layout as WinJS.UI.ListLayout where clocks are displayed as a list. We’ll cover the ListLayout in other blog post when we will talk about changing the layout of the application when the application is in snapped mode.
Another thing to mention here is the “itemRenderer” attribute for the ListView. Basically it tells how each item should be rendered. We have defined an item template above and named it as “cityTemplate”. In this template, we’re binding HTML properties of different elements to various properties of the object it is bound to. For example, the first <span> element’s innerHTML property is bound to MilliSecondsSince1stJan1970 property of our Clock object.
2. Now we would want to display time in hours, minutes and seconds format (hour:minutes:seconds) format. One way we could have achieved this is by defining properties in our Clock object. However the approach we’re taking is making use of data binding converters. Like the converters in WPF/Silverlight, converters are the function which will take one value and convert it into another format. In the first <span> element above, we’re using a timeConverter converter which will take this MilliSecondsSince1stJan1970 property and return a string which represents the time in hour:minutes:seconds format. Open up converters.js and add the following function there:
timeConverter = WinJS.Binding.converter(function (milliSeconds) { currentDateTime = new Date(milliSeconds); hour = PrependZeroIfNeeded(currentDateTime.getHours() % 12); minutes = PrependZeroIfNeeded(currentDateTime.getMinutes()); seconds = PrependZeroIfNeeded(currentDateTime.getSeconds()); return hour + ":" + minutes + ":" + seconds; });
What this function is doing is taking number of milliSeconds as input parameter and calculating hours, minutes and seconds and returning a string which represent time in hour:minutes:seconds format.
If you notice, we’ve used a few more converters in step 4 above (ampmConverter, dayOfWeekConverter, dayMonthConverter). So let’s add code for these converter functions as well in our converters.js file.
var monthAsString = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; dayMonthConverter = WinJS.Binding.converter(function (milliSeconds) { currentDateTime = new Date(milliSeconds); month = currentDateTime.getMonth(); date = PrependZeroIfNeeded(currentDateTime.getDate()); return date + " " + monthAsString[month]; }); var dayOfWeekAsString = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; dayOfWeekConverter = WinJS.Binding.converter(function (milliSeconds) { currentDateTime = new Date(milliSeconds); var dow = currentDateTime.getDay(); return dayOfWeekAsString[dow]; }); ampmConverter = WinJS.Binding.converter(function (milliSeconds) { currentDateTime = new Date(milliSeconds); hour = PrependZeroIfNeeded(currentDateTime.getHours()); if (hour >= 12) { return "PM"; } return "AM"; });
3. Now what we’ll do is initialize the clocks and bind it to our ListView control. We’ll do that in savedClocks.js after the fragment gets loaded. We’ll do so by first defining a separate function called initializeClocks() and then calling this function when the fragment is loaded. Let’s add the following code in our savedClocks.js file just after fragmentLoad() function.
// Initializes the clocks and binds it to the list view control function initializeClocks() { clocks = new Array(); for (var i = 0; i < savedClocks.length; i++) { var savedClock = savedClocks[i]; var clock = getObservableClock(savedClock); clocks.push(clock); } selectedClocksDataSource = new WinJS.UI.ArrayDataSource(clocks, { keyOf: function (item) { return item.Id; }, compareByIdentity: true, }); getWinJSControl("listViewSavedClocks").dataSource = selectedClocksDataSource; setIntervalHandler = setInterval("advanceClock()", clockRefreshFrequency); }
4. One last step would now be to call this function when the fragment gets loaded. So let’s call this initializeClocks() function when the fragment loads. We will modify out “fragmentappended” event handler and fragmentload function as per the code below:
WinJS.Application.addEventListener('fragmentappended', function handler(e) { if (e.location.indexOf('savedClocks.html') !== -1) { fragmentLoad(e.fragment, e.state); } }); function fragmentLoad(elements, options) { WinJS.UI.processAll(elements) .then(function () { initializeClocks(); }); }
5. Now let’s run the application. This is what you’ll see
Congratulations!!! You can now see the current date/time.
6. However you’ll notice that the clock does not advances. It is just stuck at the date/time when you started the application. Not Good, Huh! Well, the reason is that even though we have bound our ListView to an array but it is not an observable collection i.e. the changes in the properties of each element of the array are not communicated back to the UI even though the properties are changing. To make an item “observable” so that the changes are communicated, let’s make change to getClock() function in default.js. We’ll replace getClock() function’s code with the following:
function getClock(savedClock) { var clock = new WinJS.Binding.as(new ClockData(savedClock.Id, savedClock.City, savedClock.Country, savedClock.IsCurrentCity, GetCurrentDateTimeForCity(savedClock.IsCurrentCity, 0 - savedClock.GMTOffsetInMinutes, savedClock.DST), clockRefreshFrequency)); return clock; }
Now we’ve defined our clock object as an “observable” object.
7. Now let’s run this application and see what happens. Now when you run the application, you’ll notice that the clock is advancing every second!!! (I wish I could show the advancing clock here as an image :). I guess you would need to try it yourself).
8. For fun sake, let’s add some more cities and see how the application looks. In default.js, let’s modify the savedClocks JSON array to add a few more cities:
//JSON array representing the saved clocks var savedClocks = [ { "Id": "00-00000", "City": "Current", "Country": "", "CountryCode": "", "RegionCode": "", "GMTOffsetInMinutes": "0", "IsCurrentCity": true, "DST": false, }, { "Id": "GB-00001", "City": "London", "Country": "United Kingdom", "CountryCode": "GB", "RegionCode": "", "GMTOffsetInMinutes": "0", "DST": false, }, { "Id": "JP-00001", "City": "Tokyo", "Country": "Japan", "CountryCode": "JP", "RegionCode": "", "GMTOffsetInMinutes": "540", "DST": false, }, { "Id": "US-00001", "City": "Washington, D.C.", "Country": "United States", "CountryCode": "US", "RegionCode": "", "GMTOffsetInMinutes": "-300", "DST": false, }, ];
9. Now let’s run the application. This is what you should see.
Summary
This concludes this blog post. In this blog post we built a simple clock which displays current date/time. We extended it to display time for some other cities as well. We learnt a little bit about navigation, data binding, data converters as well. I hope you have enjoyed it. In the next post, we will extend it so that you will be able to select cities and add it to your saved clocks. We will also cover AppBar concept as well in the next post. Stay tuned!!!
Here is the source code of what we have done so far: Source Code
If you have any feedback or comments regarding this blog post, please feel free to share.