Background
In my last post, I showed how to simple it is to remove commands from the user interface and place them into the AppBar. The commands for editing records and adding new records are both hardcoded to which record gets edited and what data gets added. That is not a very user friendly way to edit/add records.
Windows 8 applications can take advantage of Flyouts for tasks such as getting data into an application, presenting a screen for editing a record, and any other interaction with the user. A Flyout is essentially a mini-form or user control (neither term is technically correct, but you get the point, I believe). Instead of swapping the currently displayed content with another screen for editing records and another screen for adding records, using Flyouts can help keep your users focused on their data and the task at hand.
Flyouts are added as markup into the page that will display them with the data-win-type=”WinJS.UI.Flyout”. The control type specification prevents them from being rendered until specifically called to be displayed. When displayed, they animate into view (flying in and out of the screen) as an overlay to the current user interface. Flyouts contain markup like any other content, and any elements in a Flyout are accessible by the JavaScript files referenced in the page.
The Sample Application
The application is very simple (as shown in Figure 1). We have an Observable List (WinJS.Binding.List) that holds multiple Observable Contact cards. There are two commands in the AppBar, one for updating a contact, and the other for adding a new contact record. In the current state of the sample application, the values added for new records are hard coded to “First”, “Last”, and “24”. The edit contact command merely adds letters to the end of the properties of the first record in the list. If you haven’t been following this series, you can download the starter project for this blog post here.
Design Goals
When adding the AppBar, I am also going to add FlyOuts to handle the editing and adding of records. Additionally, clicking on a record in the ListView will select it as the record to be edited.
The Add New Record Flyout
The first change is to change the type of the “addContact” command from “button” to “flyout”.
NOTE: There is an occasional issue with Blend where this can’t be done in the properties sidebar. Simply change the type in the markup shown below the designer. It’s one of the few issues I’ve found with Blend, which overall is an awesome tool for working with WinJS.
Once that’s changed, add a new Flyout to your form. Click on the “flyout” property in Blend and select “<Create new flyout…>” (Figure 1, left side). This will present you with the option to name the new Flyout (as shown on the right side of Figure 1), and select whether the name represents the HTML id or CSS class.
Figure 1 – Adding a new Flyout in Blend
Building The Flyout
For adding a new record, we place the standard input controls and their labels in a “–ms-grid” display layout. Of course, any layout would work, but the –ms-grid display makes it so easy. We also need to put buttons on the Flyout for executing the save or cancelling, and then wire everything together in the default.js code file.
The markup for the Flyout control (in default.html) is shown in Listing 1 as well as the new CSS (default.css) file in Listing 2.
<div id="addNewContact" data-win-control="WinJS.UI.Flyout">
<div class="win-type-large dataEntryLayout">
<div class="leftRow1">
<label for="newFirstName">First Name: </label>
</div>
<div class="leftRow2">
<label for="newLastName">Last Name: </label>
</div>
<div class="leftRow3">
<label for="newAge">Age: </label>
</div>
<div class="rightRow1">
<input id="newFirstName" type="text"/>
</div>
<div class="rightRow2">
<input id="newLastName" type="text"/>
</div>
<div class="rightRow3">
<input id="newAge" type="text"/>
</div>
<button id="saveNewContact" class="rightRow4" style="width:50px">Save</button>
<button id="cancelNewContact" class="rightRow4" style="width:50px">Cancel</button>
</div>
</div>
Listing 1 – Add New Contact Flyout
.dataEntryLayout
{
width:auto;
height:auto;
display: -ms-grid;
-ms-grid-columns: 1fr 4fr;
-ms-grid-rows: auto auto auto auto;
}
.dataEntryLayout .leftRow1
{
-ms-grid-row: 1;
-ms-grid-column: 1;
-ms-grid-column-align:end;
-ms-grid-row-align: center;
padding-right: 10px;
}
.dataEntryLayout .leftRow2
{
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-align:end;
-ms-grid-row-align: center;
padding-right: 10px;
}
.dataEntryLayout .leftRow3
{
-ms-grid-row: 3;
-ms-grid-column: 1;
-ms-grid-column-align:end;
-ms-grid-row-align: center;
padding-right: 10px;
}
.dataEntryLayout .rightRow1
{
-ms-grid-row: 1;
-ms-grid-column: 2;
}
.dataEntryLayout .rightRow2
{
-ms-grid-row: 2;
-ms-grid-column: 2;
}
.dataEntryLayout .rightRow3
{
-ms-grid-row: 3;
-ms-grid-column: 2;
}
.dataEntryLayout .rightRow4
{
-ms-grid-row: 4;
-ms-grid-column: 2;
}
Listing 2 – CSS For Add New Contact Layout
The resulting Flyout control in action looks like Figure 2.
Figure 2 – Add New Contact Flyout
The final change to be made is to wire up the commands in default.js. The existing event listener for the Add Contact button in the AppBar can be deleted, since specifying the type as flyout for a command in the AppBar will automatically show the flyout specified in the options. Therefore, the commands that need event listeners are the Save and Cancel commands in the flyout itself. The code for the event listeners is shown in Listing 3.
document.getElementById('saveNewContact') .addEventListener('click', function (e) {
ViewModel.addContact(
document.getElementById('newFirstName').value,
document.getElementById('newLastName').value,
document.getElementById('newAge').value);
hideFlyout('addNewContactFlyout');
});
document.getElementById('cancelNewContact') .addEventListener('click', function (e) {
hideFlyout('addNewContactFlyout');
});
Listing 3 – Event listener code for commands in the Add New Contact flyout
The hideFlyout function is required because even though the flyout is shown automatically, you need to specify when to hide it after you’ve processed the data in the flyout. The hideFlyout function is shown in Listing 4.
function hideFlyout(flyoutName) {
var fly = document.getElementById(flyoutName).winControl;
fly.hide();
};
Listing 4 – The hideFlyout function
With these changes in place, the user can add a new Contact by clicking on the Add Contact button, filling out the flyout form, and clicking Save.
NOTE: A production application would include data validation, but to keep this (already lengthy) post from getting even longer, I’ve omitted that from this example.
Enabling Editing
Using many of the same techniques that I used to adding contacts, the next step is to change the Edit Contact command to edit the selected item in the ListView.
Changing the AppBar Command
First, add the ‘section’ property to the command, and set it to ‘selection’ (the default is ‘global’). Commands added to the ‘global’ section appear on the right side of the AppBar, and ‘selection’ commands appear on the left. As a convention, commands that relate to a selected item should be placed on the left, and commands that apply to the application should appear on the right.
Also change the editContact command type to ‘flyout’ and create a new flyout called editContactFlyout. Since we already have a nice layout in the the Add Contact flyout with all of the controls that we need, we can clipboard inherit the markup into the edit contact flyout. The important item to remember is to change the id’s of the controls. Fortunately, if you copy and paste the markup in Visual Studio, all of the ids will be set to default values, preventing accidentally assigned two or more items the same id. The final markup for the flyout and the updated Edit Contact command are shown in Listing 5.
<div id="editContactFlyout" data-win-control="WinJS.UI.Flyout">
<div class="win-type-large dataEntryLayout">
<div class="leftRow1">
<label for="editFirstName">First Name: </label>
</div>
<div class="leftRow2">
<label for="editLastName">Last Name: </label>
</div>
<div class="leftRow3">
<label for="editAge">Age: </label>
</div>
<div class="rightRow1">
<input id="editFirstName" type="text"/>
</div>
<div class="rightRow2">
<input id="editLastName" type="text"/>
</div>
<div class="rightRow3">
<input id="editAge" type="text"/>
</div>
<div class="rightRow4">
<button id="saveEditedContact" style="width:50px">Save</button>
<button id="cancelEditContact" style="width:50px">Cancel</button>
</div>
</div>
</div>
<!-- other markup removed to brevity -->
<button id="editContact" data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{flyout:select('#editContactFlyout'), icon:'edit',
label:'Edit Contact', tooltip:'Edit Contact', type:'flyout', section:'selection'}"></button>
</div>
Listing 5 – Edit Contact Flyout and Edit Contact AppBar Command
Controlling Edit Contact Button Through Record Selection
In order to edit a record, one must be selected. If a record is selected, then the Edit Contact AppBar command should be enabled, otherwise it should be disabled. To do this, we need to make a change to the applications ‘onactivated‘ event handler to make sure that all of the controls are fully rendered before the performInitialSetup method is called, and add the ‘selectionchanged’ event handler for the ListView.
In WinJS, the WinJS.UI.processAll function sets a promise that when complete ensures that all controls are fully rendered. Change the call to the setup method to happen after the promise is completed, as shown in Listing 6.
WinJS.UI.processAll(document).then(performInitialSetup(args));
Listing 6 – Ensuring the controls are rendered prior to executing the application setup
Once we know the JavaScript controls (including the ListView) are fully rendered, we can add an event handler for the ListView (in the performInitialSetup function) to enable the Edit Contact Command if there is an item selected, and disable it if nothing is selected. The Edit Contact Command should also be disable when the application starts. This can be done in the markup or programmatically. The resulting code is shown in Listing 7.
var editContactCommand = document.getElementById('editContact');
editContactCommand.disabled = true;
var listView = document.getElementById('contactList').winControl;
listView.addEventListener('selectionchanged', function(e) {
if (this.winControl.selection.count() != 0) {
editContactCommand.disabled = false;
}
else {
editContactCommand.disabled = true;
}
});
Listing 7 – Enabling/Disabling the Edit Contact Command based on ListView Selection
To make the application more intuitive, the ListView should be changed to single selection mode as well as to toggle the selection based on users clicking, tapping, or selecting and item. In the data-win-options for the ListView, add the selectionMode property (set to single) and the tapBehavior (set to toggleSelect) properties, as shown in Listing 8. We also need to add an id attribute to the ListView so it can be referred to in code.
<div id='contactList' data-win-control="WinJS.UI.ListView"
data-win-options="{
itemDataSource:ViewModel.ContactList.dataSource,
itemTemplate:select('#listViewTemplate'),
selectionMode:'single',
tapBehavior:'toggleSelect'}"></div>
Listing 8 – Updated ListView properties
Updating the Click Event for the Edit Contact Command
When the Edit Contact command is clicked, the flyout will automatically be shown (based on the type and the flyout setting in the data-win-options). It also needs to populate the text boxes in the flyout with the current values of the selected item in the ListView to make editing the values easier. The command can only be clicked if an item is selected, so we use the getItems function of the ListView selection property to get the selected item. The getItems function returns a promise, and when that promise is fulfilled, we have access to a list of IItems. Each IItem exposes a data property that grants access to the backing item from the data source. We set the first item in the list (there will only ever be one since the selectionMode is single) to a variable defined outside the scope of the event handler. We will use that later when we update the values. The event handler is shown in Listing 9, and the user interface is shown in Figure 3.
var currentItem = null;
editContactCommand.addEventListener('click', function (e) {
listView.selection.getItems().then(function (items) {
currentItem = items[0].data;
document.getElementById('editFirstName').value = currentItem.firstName;
document.getElementById('editLastName').value = currentItem.lastName;
document.getElementById('editAge').value = currentItem.age;
});
});
Listing 9 – Adding Selected Item Values to Edit Contact flyout
Figure 3 – Editing a Contact
Finishing the Code
The last step is to program the Save and Cancel commands. The Cancel command leverages the hideFlyout function that we used in the Cancel New Contact Command. To save the updated record, assign the values in the Edit Contact flyout controls to the currentItem that we assigned in the click event of the AppBar Edit Command click event. Both of the event handlers are shown in Listing 10.
document.getElementById('saveEditedContact').addEventListener('click', function (e) {
currentItem.firstName = document.getElementById('editFirstName').value;
currentItem.lastName = document.getElementById('editLastName').value;
currentItem.age = document.getElementById('editAge').value;
hideFlyout('editContactFlyout');
});
document.getElementById('cancelEditContact') .addEventListener('click', function (e) {
hideFlyout('editContactFlyout');
});
Listing 10 – Saving/Cancelling Edit Contact Commands
Summary
In this blog post, we updated the functionality to allow the user to enter the data for a new contact as well as choose which contact to edit and what to change by using Flyouts. The Flyouts provide an unobtrusive mechanism for interacting with the user while preventing the user from having to conduct a lot of context switching.
Because the Model and the Collection are both Observable, all of the changes to the data are immediately reflected in the view without any additional effort on our part.
You can download the source code here.
About the author
Philip Japikse
|
Philip Japikse an international speaker, a Microsoft MVP, INETA Community Champion, MCSD, CSM/ CSP, and a passionate member of the developer community, Phil Japikse has been working with .Net since the first betas, developing software for over 20 years, and heavily involved in the agile community since 2005. Phil works as a Developer Evangelist for Telerik's RadControls for Windows 8 as well as the Just family of products (JustCode, JustMock, JustTrace, and JustDecompile) and hosts the Zero To Agile podcast (www.telerik.com/zerotoagile). Phil is also the Lead Director for the Cincinnati .Net User’s Group (http://www.cinnug.org). You can follow Phil on twitter via www.twitter.com/skimedic read his Telerik blog at http://blogs.telerik.com/skimedic and his personal blog at www.skimedic.com/blog. |