WinJS List view binding with observable objects and interactive elements

Hi,

In the last post we looked at how to bind and reflect changes on UI based on some user action.

In this post we are going to look at how to use and handle specific controls in list view items which can be interacted individually.

Typically we have some actions that the user can do on the list items such as select, click …. This applies to the item only. Let say we want to have a control within the list view item which we could interact separately and will not be an action on the list view item.

If you want to know how binding works please refer my earlier post.

I will proceed using my last code as a sample and try to build a scenario and sample to show how we can have individual interactive elements in the list view items.

Here is the overall scenario (Extended on top of last scenario):
1. In the items we will have a select that lists the quantity.
2. The user can click on the item. In this case the item will be selected and 1 quantity is automatically set.
3. The user can (without selecting the item) just click the quantity dropdown and select a quantity. In this case the items gets selected with the quantity being the quantity the user had selected.
4. If the user clicks a selected item, it will be unselected and revert back to default setting and UI with quantity as 0.
5. If user selects 0 quantity for a selected item, it will be unselected and revert back to default setting and UI with quantity as 0.

Simple scenario :).. Lets start …

I will only explain the changes I do in respect to the last post.

First step is to modify the template to include a select (so that user can select the quantity).
I will assign a class called “win-interactive” to this select.
This class will ensure that when the select dropdown is clicked it will be treated as an separate control and not as part of item clicked. The interaction with the select control will not be considered as a interaction with the list view item.

You can learn more at MSDN

 <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item" data-win-bind="style.background : IsSelected TemplateConverters.SelectedBackground;title: ProductName">
            <div class="productName win-type-ellipsis" data-win-bind="title: ProductName">
                <h3 class="nameLabel" data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;textContent: ProductName"></h3>
            </div>
            <select class="prodQuantity win-interactive" data-win-bind="value: QuantitySelected">
                <option value="0">0</option>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
                <option value="5">5</option>
                <option value="6">6</option>
                <option value="7">7</option>
                <option value="8">8</option>
                <option value="9">9</option>
                <option value="10">10</option>
            </select>
            <div class="details win-type-ellipsis" data-win-bind="title: LevelName">
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> <u> Details</u> </b> </span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Weight : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Weight"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Color : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Color"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Sales Price : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: SalesPrice"></span>
            </div>
        </div>
    </div>
 

Next step is to modify the CSS to accommodate this select that we have added. I just change the Item CSS to add the Select to the right top corner :

 .samplepage .mainList .item {
        -ms-grid-columns: 1fr 0.3fr; /*Two columns to accommodate the select in 2nd column */
        -ms-grid-rows: 0.5fr 1fr;
        display: -ms-grid;
        height: 120px;
        width: 350px;
        background-color:whitesmoke;
        border:1px solid black;
    }
        .samplepage .mainList .item .productName {
            -ms-grid-row:1;
            -ms-grid-column:1;
            margin-left:20px;
        }

        .samplepage .mainList .item .prodQuantity {
            -ms-grid-row:1;
            -ms-grid-column:2;
        }

        .samplepage .mainList .item .details {
            -ms-grid-row:2;
            -ms-grid-column:1;
            -ms-grid-column-span:2;
            margin-left:20px;
        }
 

Now to the Code part.

First we will add a new Property to the class object definition. I add a property called “QuantitySelected” and default it’s value to 0.

 var product = WinJS.Binding.define({
        ProductName : "",
        Weight : 0,
        Color : "",
        SalesPrice: 0.0,
        IsSelected: false,
        QuantitySelected: 0 // Added
    });

In the last post we directly assigned the template to the list view on initialization. In this we will not directly assign the template but will use a templating function to create the template for the list view.

Why am I using a templating function ? : In our requirement we need to track any changes that the user makes to the Select dropdown. This means that we need to observe the event “changed” for the select in each of the list view items. To attach event listener to the Select element in list view items we will use a templating function (this makes our work easier).

So for list view template we will have a templating function.

This templating function will be called for each of the data items for the list view.

 listview.itemTemplate = this.itemTemplate.bind(this);

Lets define this function. This function will accept a itemPromise. The function should return a template that can just be used in list view. As I mentioned this function will be called for all data items in the list view.

Here are the steps that are done in this templating function to return a template:
1. create a div element
2. get the item template defined in HTML.
3. Use the itemTemplate.winControl.render function to render the control to the div element (step 1) with the data binding (data from itemPromise).

Since we need to track the “changed” event for Select. we will do these additional steps:
1. get the select from rendered template
2. Attach a event listener to track event “changed”.
3. In the event listener logic we will implement the logic (Check for the new value, if it is 0, means the user has unselected, so set “IsSelected” to false, else Set “IsSelected” to true).

 itemTemplate : function(itemPromise)
        {
            var that = this;
            return itemPromise.then(function (item) {

                var index = itemPromise._value.index;// Get the index of item
                var itemTemplate = document.body.querySelector(".itemtemplate"); // Get the template definition
                var container = document.createElement("div"); // Create a div element
                itemTemplate.winControl.render(item.data, container); // Render the template with data.

                // Get the Select and attach the event listener to track changes
                var selectElement = container.querySelector(".prodQuantity");
                selectElement.addEventListener("change", function (args) {
                    var e = args.srcElement;
                    var itemList = that.listData.getAt(index);
                    var quantitySelected = e.options[e.selectedIndex].value;
                    itemList.QuantitySelected = quantitySelected;
                    if(quantitySelected > 0)
                    {
                        itemList.IsSelected = true;
                    }
                    else
                    {
                        itemList.IsSelected = false;
                    }

                }, false);
                return container;
            });
        }

We are almost done. But considering the scenario (2 in the overall scenario) we have in this case we also need modify the iteminvoked method a bit. If item is selected we need to set “IsSelected” flag to true as well as “QuantitySelected” to 1 and if item is un-selected we need to set “IsSelected” flag to false as well as “QuantitySelected” to 0.

 itemInvoked: function (args) {
            var dataItem = this.listData.getAt(args.detail.itemIndex);
            if (dataItem.IsSelected) {
                dataItem.IsSelected = false;
                dataItem.QuantitySelected = 0;
            }
            else {
                dataItem.IsSelected = true;
                dataItem.QuantitySelected = 1;
            }
        }

Now you can run your code and interact with the list view item and the select separately.

Below is the full code for your reference.

listviewbinding.html

<!--THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.-->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>ListView TwoWay Binding Sample</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- ListViewTwoWayBinding references -->
    <link href="/pages/ListViewBinding/listViewBinding.css" rel="stylesheet" />
    <script src="/pages/ListViewBinding/listViewBinding.js"></script>
    <script src="/js/converters.js"></script>
</head>
<body>
    <!-- These templates are used to display each item in the ListView declared below. -->

    <div class="itemtemplate" data-win-control="WinJS.Binding.Template">
        <div class="item" data-win-bind="style.background : IsSelected TemplateConverters.SelectedBackground;title: ProductName">
            <div class="productName win-type-ellipsis" data-win-bind="title: ProductName">
                <h3 class="nameLabel" data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;textContent: ProductName"></h3>
            </div>
            <select class="prodQuantity win-interactive" data-win-bind="value: QuantitySelected">
                <option value="0">0</option>
                <option value="1">1</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
                <option value="5">5</option>
                <option value="6">6</option>
                <option value="7">7</option>
                <option value="8">8</option>
                <option value="9">9</option>
                <option value="10">10</option>
            </select>
            <div class="details win-type-ellipsis" data-win-bind="title: LevelName">
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> <u> Details</u> </b> </span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Weight : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Weight"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Color : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: Color"></span><br />
                <span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor"><b> Sales Price : </b> </span><span data-win-bind="style.color: IsSelected TemplateConverters.SelectedFontColor;innerText: SalesPrice"></span>
            </div>
        </div>
    </div>

    <!-- The content that will be loaded and displayed. -->
    <div class="samplepage fragment">
        <header aria-label="Header content" role="banner">
           <button class="win-backbutton" aria-label="Back" disabled type="button"></button>
             <h1 class="titlearea win-type-ellipsis">
                 <span class="pagetitle">List view Binding Sample</span>
             </h1>
        </header>
        <section aria-label="Main content" role="main">
            <div class="mainList win-selectionstylefilled" data-win-control="WinJS.UI.ListView" data-win-options="{ selectionMode: 'none' }"></div>
        </section>
    </div>
</body>
</html>

listviewbinding.css

/*THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.*/

.samplepage section[role=main] {
    -ms-grid-row: 1;
    -ms-grid-row-span: 2;
}

.samplepage .mainList {
    height: 100%;
    position: relative;
    width: 100%;
    z-index: 0;
}

    /* This selector is used to prevent ui-dark/light.css from overwriting changes
       to .win-surface. */
    .samplepage .mainList .win-horizontal.win-viewport .win-surface {
        margin-bottom: 60px;
        margin-left: 45px;
        margin-right: 115px;
        margin-top: 128px;
    }

    .samplepage .mainList .win-container {
        outline:none;
        background-color:transparent;
    }

    .samplepage .mainList .win-container:hover {
        outline:none;
        background-color:transparent;
    }

    .samplepage .mainList .item {
        -ms-grid-columns: 1fr 0.3fr;
        -ms-grid-rows: 0.5fr 1fr;
        display: -ms-grid;
        height: 120px;
        width: 350px;
        background-color:whitesmoke;
        border:1px solid black;
    }

        .samplepage .mainList .item:hover {
            border:2px solid black;
        }

        .samplepage .mainList .item .productName {
            -ms-grid-row:1;
            -ms-grid-column:1;
            margin-left:20px;
        }

        .samplepage .mainList .item .prodQuantity {
            -ms-grid-row:1;
            -ms-grid-column:2;
        }

        .samplepage .mainList .item .details {
            -ms-grid-row:2;
            -ms-grid-column:1;
            -ms-grid-column-span:2;
            margin-left:20px;
        }

@media screen and (-ms-view-state: snapped) {
    .samplepage section[role=main] {
        -ms-grid-row: 2;
        -ms-grid-row-span: 1;
    }

    .samplepage .mainList .win-vertical.win-viewport .win-surface {
        margin-bottom: 30px;
        margin-top: 0;
    }

    .samplepage .mainList .win-container {
        margin-bottom: 15px;
        margin-left: 10px;
        margin-right: 20px;
        padding: 7px;
    }

    .samplepage .mainList .item {
        height: 120px;
        width: 250px;
    }
}

listviewbinding.js

       //// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
//// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
//// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//// PARTICULAR PURPOSE.
(function () {
    "use strict";

    var appView = Windows.UI.ViewManagement.ApplicationView;

    var appViewState = Windows.UI.ViewManagement.ApplicationViewState;

    var nav = WinJS.Navigation;

    var ui = WinJS.UI;

    var product = WinJS.Binding.define({
        ProductName : "",
        Weight : 0,
        Color : "",
        SalesPrice: 0.0,
        IsSelected: false,
        QuantitySelected: 0
    });

    ui.Pages.define("/pages/ListViewBinding/listViewBinding.html", {

        listData: null,

        ready: function (element, options) {
            this.listData = new WinJS.Binding.List();
            this.createDummyData();
            var listview = element.querySelector(".mainList").winControl;

            listview.itemTemplate = this.itemTemplate.bind(this);
            listview.oniteminvoked = this.itemInvoked.bind(this);
            listview.itemDataSource = this.listData.dataSource;
            this.initializeLayout(listview, appView.value);
        },

        initializeLayout: function (listView, viewState) {
            if (viewState === appViewState.snapped) {
                listView.layout = new ui.ListLayout();
            } else {
                listView.layout = new ui.GridLayout();
            }
        },

        updateLayout: function (element, viewState, lastViewState) {
            var listview = element.querySelector(".mainList").winControl;
            if (lastViewState !== viewState) {
                if (lastViewState === appViewState.snapped || viewState === appViewState.snapped) {
                    var handler = function (e) {
                        listview.removeEventListener("contentanimating", handler, false);
                        e.preventDefault();
                    }
                    listview.removeEventListener("contentanimating", handler, false);
                    this.initializeLayout(listview, viewState);
                }
            }
        },

        itemInvoked: function (args) {
            var dataItem = this.listData.getAt(args.detail.itemIndex);
            if (dataItem.IsSelected) {
                dataItem.IsSelected = false;
                dataItem.QuantitySelected = 0;
            }
            else {
                dataItem.IsSelected = true;
                dataItem.QuantitySelected = 1;
            }
        },

        itemTemplate : function(itemPromise)
        {
            var that = this;
            return itemPromise.then(function (item) {
                var index = itemPromise._value.index;
                var itemTemplate = document.body.querySelector(".itemtemplate");
                var container = document.createElement("div");
                itemTemplate.winControl.render(item.data, container);

                var selectElement = container.querySelector(".prodQuantity");
                selectElement.addEventListener("change", function (args) {
                    var e = args.srcElement;
                    var itemList = that.listData.getAt(index);
                    var quantitySelected = e.options[e.selectedIndex].value;
                    itemList.QuantitySelected = quantitySelected;
                    if(quantitySelected > 0)
                    {
                        itemList.IsSelected = true;
                    }
                    else
                    {
                        itemList.IsSelected = false;
                    }

                }, false);
                return container;
            });
        },

        createDummyData: function () {
            // Creating dummy data .
            //Data format : Name, Quantity, SalesPrice, IsSelected 

            // TODO : Replace your data creation logic and push into the main list

            this.listData.push(WinJS.Binding.as(new product({ ProductName: "Front Brakes", Weight: 317, Color: "Silver", SalesPrice: 47.286, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "Front Derailleur", Weight: 88, Color: "Silver", SalesPrice: 40.6216, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Bottom Bracket", Weight: 170, Color: "NA", SalesPrice: 53.9416, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Crankset", Weight: 575, Color: "Black", SalesPrice: 179.8156, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Mountain Frame - Black, 38", Weight: 2.68, Color: "Black", SalesPrice: 617.0281, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Mountain Frame - Silver, 42", Weight: 2.72, Color: "Silver", SalesPrice: 623.8403, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Mountain Pedal", Weight: 185, Color: "Silver/Black", SalesPrice: 35.9596, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Frame - Black, 62", Weight: 2.3, Color: "Black", SalesPrice: 722.2568, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Frame - Red, 44", Weight: 2.12, Color: "Red", SalesPrice: 747.9682, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Front Wheel", Weight: 650, Color: "Black", SalesPrice: 146.5466, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Pedal", Weight: 149, Color: "Silver/Black", SalesPrice: 35.9596, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Road Rear Wheel", Weight: 890, Color: "Black", SalesPrice: 158.5346, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Touring Frame - Blue, 50", Weight: 3, Color: "Blue", SalesPrice: 601.7437, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "HL Touring Frame - Yellow, 50", Weight: 3, Color: "Yellow", SalesPrice: 601.7437, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Bottom Bracket", Weight: 223, Color: "NA", SalesPrice: 23.9716, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Crankset", Weight: 600, Color: "Black", SalesPrice: 77.9176, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Mountain Frame - Black, 40", Weight: 2.88, Color: "Black", SalesPrice: 136.785, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Mountain Frame - Silver, 52", Weight: 3.04, Color: "Silver", SalesPrice: 144.5938, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Mountain Pedal", Weight: 218, Color: "Silver/Black", SalesPrice: 17.9776, IsSelected: false })));
            this.listData.push(WinJS.Binding.as(new product({ ProductName: "LL Road Frame - Black, 44", Weight: 2.32, Color: "Black", SalesPrice: 176.1997, IsSelected: false })));
                   }
    });
})();

converters.js

//// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
//// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
//// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//// PARTICULAR PURPOSE.
(function () {

    WinJS.Namespace.define("TemplateConverters", {

        // Return the background color based on selection
        SelectedBackground: WinJS.Binding.converter(function (value) {
            return !value ? "WhiteSmoke" : "DarkGray";
        }),
        // Return the font color based on selection
        SelectedFontColor: WinJS.Binding.converter(function (value) {
            return !value ? "Black" : "White";
        })
    });
})();

I hope this is useful.. Let me know if you have any question(s)..

Here are some links

List view
Converters
Win JS Binding Class

Cheers …

 

Disclaimer

Advertisements

Published by

Girija Shankar Beuria

A software developer by profession with 10+ years of experience in the following technologies : Data Warehousing, Business Intelligence applications using SQL Server BI Stack, Product Frameworks and Test automation framework, MOSS , C# .Net, .NET, POWERSHELL, AMO, HTML 5, JavaScript, Reporting Service Web service, Dynamics AX, Dynamics AX 2012 BI Cubes, Dynamics AX 2012 SSRS Reports, SQL Azure, Windows Azure Web Services, ASP .NET MVC 4 Web API, WCF, Entity Framework, WPF, Excel Object Model, Windows 8 Apps, Windows Phone Apps

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s