Metro: Declarative Data Binding

Posted by Stephen.Walther on Stephen Walter See other posts from Stephen Walter or by Stephen.Walther
Published on Mon, 27 Feb 2012 05:09:07 +0000 Indexed on 2012/03/18 18:17 UTC
Read the original article Hit count: 1460

Filed under:

The goal of this blog post is to describe how declarative data binding works in the WinJS library. In particular, you learn how to use both the data-win-bind and data-win-bindsource attributes. You also learn how to use calculated properties and converters to format the value of a property automatically when performing data binding.

By taking advantage of WinJS data binding, you can use the Model-View-ViewModel (MVVM) pattern when building Metro style applications with JavaScript. By using the MVVM pattern, you can prevent your JavaScript code from spinning into chaos. The MVVM pattern provides you with a standard pattern for organizing your JavaScript code which results in a more maintainable application.

Using Declarative Bindings

You can use the data-win-bind attribute with any HTML element in a page. The data-win-bind attribute enables you to bind (associate) an attribute of an HTML element to the value of a property.

Imagine, for example, that you want to create a product details page. You want to show a product object in a page.

clip_image002

In that case, you can create the following HTML page to display the product details:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Application1</title>

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

    <!-- Application1 references -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>
</head>
<body>    

    <h1>Product Details</h1>

    <div class="field">
        Product Name:
        <span data-win-bind="innerText:name"></span>
    </div>
    <div class="field">
        Product Price:
        <span data-win-bind="innerText:price"></span>
    </div>
    <div class="field">
        Product Picture:
        <br />
        <img data-win-bind="src:photo;alt:name" />
    </div>

</body>
</html>

The HTML page above contains three data-win-bind attributes – one attribute for each product property displayed. You use the data-win-bind attribute to set properties of the HTML element associated with the data-win-attribute. The data-win-bind attribute takes a semicolon delimited list of element property names and data source property names:

data-win-bind=”elementPropertyName:datasourcePropertyName; elementPropertyName:datasourcePropertyName;…”

In the HTML page above, the first two data-win-bind attributes are used to set the values of the innerText property of the SPAN elements. The last data-win-bind attribute is used to set the values of the IMG element’s src and alt attributes.

By the way, using data-win-bind attributes is perfectly valid HTML5. The HTML5 standard enables you to add custom attributes to an HTML document just as long as the custom attributes start with the prefix data-. So you can add custom attributes to an HTML5 document with names like data-stephen, data-funky, or data-rover-dog-is-hungry and your document will validate.

The product object displayed in the page above with the data-win-bind attributes is created in the default.js file:

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            var product = {
                name: "Tesla",
                price: 80000,
                photo: "/images/TeslaPhoto.png"
            };

            WinJS.Binding.processAll(null, product);
        }
    };

    app.start();
})();

In the code above, a product object is created with a name, price, and photo property. The WinJS.Binding.processAll() method is called to perform the actual binding (Don’t confuse WinJS.Binding.processAll() and WinJS.UI.processAll() – these are different methods).

The first parameter passed to the processAll() method represents the root element for the binding. In other words, binding happens on this element and its child elements. If you provide the value null, then binding happens on the entire body of the document (document.body).

The second parameter represents the data context. This is the object that has the properties which are displayed with the data-win-bind attributes. In the code above, the product object is passed as the data context parameter. Another word for data context is view model

Creating Complex View Models

In the previous section, we used the data-win-bind attribute to display the properties of a simple object: a single product. However, you can use binding with more complex view models including view models which represent multiple objects.

image

For example, the view model in the following default.js file represents both a customer and a product object. Furthermore, the customer object has a nested address object:

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            var viewModel = {
                customer: {
                    firstName: "Fred",
                    lastName: "Flintstone",
                    address: {
                        street: "1 Rocky Way",
                        city: "Bedrock",
                        country: "USA"
                    }
                },
                product: {
                    name: "Bowling Ball",
                    price: 34.55
                }
            };

            WinJS.Binding.processAll(null, viewModel);

        }
    };

    app.start();
})();

The following page displays the customer (including the customer address) and the product. Notice that you can use dot notation to refer to child objects in a view model such as customer.address.street.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Application1</title>

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

    <!-- Application1 references -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>
</head>
<body>    

    <h1>Customer Details</h1>

    <div class="field">
        First Name:
        <span data-win-bind="innerText:customer.firstName"></span>
    </div>
    <div class="field">
        Last Name:
        <span data-win-bind="innerText:customer.lastName"></span>
    </div>
    <div class="field">
        Address:
        <address>
            <span data-win-bind="innerText:customer.address.street"></span>
            <br />
            <span data-win-bind="innerText:customer.address.city"></span>
            <br />
            <span data-win-bind="innerText:customer.address.country"></span>
        </address>
    </div>

    <h1>Product</h1>

    <div class="field">
        Name:
        <span data-win-bind="innerText:product.name"></span>
    </div>

    <div class="field">
        Price:
        <span data-win-bind="innerText:product.price"></span>
    </div>

</body>
</html>

A view model can be as complicated as you need and you can bind the view model to a view (an HTML document) by using declarative bindings.

Creating Calculated Properties

You might want to modify a property before displaying the property.

For example, you might want to format the product price property before displaying the property. You don’t want to display the raw product price “80000”. Instead, you want to display the formatted price “$80,000”.

You also might need to combine multiple properties. For example, you might need to display the customer full name by combining the values of the customer first and last name properties.

In these situations, it is tempting to call a function when performing binding. For example, you could create a function named fullName() which concatenates the customer first and last name. Unfortunately, the WinJS library does not support the following syntax:

<span data-win-bind=”innerText:fullName()”></span>

Instead, in these situations, you should create a new property in your view model that has a getter. For example, the customer object in the following default.js file includes a property named fullName which combines the values of the firstName and lastName properties:

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            var customer = {
                firstName: "Fred",
                lastName: "Flintstone",
                get fullName() {
                    return this.firstName + " " + this.lastName;
                }
            };

            WinJS.Binding.processAll(null, customer);

        }
    };

    app.start();
})();

The customer object has a firstName, lastName, and fullName property. Notice that the fullName property is defined with a getter function. When you read the fullName property, the values of the firstName and lastName properties are concatenated and returned.

clip_image004

The following HTML page displays the fullName property in an H1 element. You can use the fullName property in a data-win-bind attribute in exactly the same way as any other property.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Application1</title>

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

    <!-- Application1 references -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>
</head>
<body>    

    <h1 data-win-bind="innerText:fullName"></h1>

    <div class="field">
        First Name:
        <span data-win-bind="innerText:firstName"></span>
    </div>
    <div class="field">
        Last Name:
        <span data-win-bind="innerText:lastName"></span>
    </div>

</body>
</html>

Creating a Converter

In the previous section, you learned how to format the value of a property by creating a property with a getter. This approach makes sense when the formatting logic is specific to a particular view model.

If, on the other hand, you need to perform the same type of formatting for multiple view models then it makes more sense to create a converter function. A converter function is a function which you can apply whenever you are using the data-win-bind attribute.

Imagine, for example, that you want to create a general function for displaying dates. You always want to display dates using a short format such as 12/25/1988. The following JavaScript file – named converters.js – contains a shortDate() converter:

(function (WinJS) {

    var shortDate = WinJS.Binding.converter(function (date) {
        return date.getMonth() + 1 +
            "/" + date.getDate() +
            "/" + date.getFullYear();
    });

    // Export shortDate
    WinJS.Namespace.define("MyApp.Converters", {
        shortDate: shortDate
    });

})(WinJS);

The file above uses the Module Pattern, a pattern which is used through the WinJS library. To learn more about the Module Pattern, see my blog entry on namespaces and modules:

http://stephenwalther.com/blog/archive/2012/02/22/windows-web-applications-namespaces-and-modules.aspx

The file contains the definition for a converter function named shortDate(). This function converts a JavaScript date object into a short date string such as 12/1/1988.

The converter function is created with the help of the WinJS.Binding.converter() method. This method takes a normal function and converts it into a converter function.

Finally, the shortDate() converter is added to the MyApp.Converters namespace. You can call the shortDate() function by calling MyApp.Converters.shortDate().

The default.js file contains the customer object that we want to bind. Notice that the customer object has a firstName, lastName, and birthday property. We will use our new shortDate() converter when displaying the customer birthday property:

clip_image006

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            var customer = {
                firstName: "Fred",
                lastName: "Flintstone",
                birthday: new Date("12/1/1988")
            };

            WinJS.Binding.processAll(null, customer);

        }
    };

    app.start();
})();

We actually use our shortDate converter in the HTML document. The following HTML document displays all of the customer properties:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Application1</title>

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

    <!-- Application1 references -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>
    <script type="text/javascript" src="js/converters.js"></script>
</head>
<body>    

    <h1>Customer Details</h1>

    <div class="field">
        First Name:
        <span data-win-bind="innerText:firstName"></span>
    </div>
    <div class="field">
        Last Name:
        <span data-win-bind="innerText:lastName"></span>
    </div>
    <div class="field">
        Birthday:
        <span data-win-bind="innerText:birthday MyApp.Converters.shortDate"></span>
    </div>

</body>
</html>

Notice the data-win-bind attribute used to display the birthday property. It looks like this:

<span data-win-bind="innerText:birthday MyApp.Converters.shortDate"></span> 

The shortDate converter is applied to the birthday property when the birthday property is bound to the SPAN element’s innerText property.

Using data-win-bindsource

Normally, you pass the view model (the data context) which you want to use with the data-win-bind attributes in a page by passing the view model to the WinJS.Binding.processAll() method like this:

WinJS.Binding.processAll(null, viewModel);

As an alternative, you can specify the view model declaratively in your markup by using the data-win-datasource attribute. For example, the following default.js script exposes a view model with the fully-qualified name of MyWinWebApp.viewModel:

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {

            // Create view model
            var viewModel = {
                customer: {
                    firstName: "Fred",
                    lastName: "Flintstone"
                },
                product: {
                    name: "Bowling Ball",
                    price: 12.99
                }
            };

            // Export view model to be seen by universe
            WinJS.Namespace.define("MyWinWebApp", {
                viewModel: viewModel
            });

            // Process data-win-bind attributes
            WinJS.Binding.processAll();

        }
    };

    app.start();
})();

In the code above, a view model which represents a customer and a product is exposed as MyWinWebApp.viewModel.

The following HTML page illustrates how you can use the data-win-bindsource attribute to bind to this view model:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Application1</title>

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

    <!-- Application1 references -->
    <link href="/css/default.css" rel="stylesheet">
    <script src="/js/default.js"></script>
</head>
<body>    

    <h1>Customer Details</h1>
    <div data-win-bindsource="MyWinWebApp.viewModel.customer">
        <div class="field">
            First Name:
            <span data-win-bind="innerText:firstName"></span>
        </div>
        <div class="field">
            Last Name:
            <span data-win-bind="innerText:lastName"></span>
        </div>
    </div>

    <h1>Product</h1>
    <div data-win-bindsource="MyWinWebApp.viewModel.product">
        <div class="field">
            Name:
            <span data-win-bind="innerText:name"></span>
        </div>

        <div class="field">
            Price:
            <span data-win-bind="innerText:price"></span>
        </div>
    </div>

</body>
</html>

The data-win-bindsource attribute is used twice in the page above: it is used with the DIV element which contains the customer details and it is used with the DIV element which contains the product details.

If an element has a data-win-bindsource attribute then all of the child elements of that element are affected. The data-win-bind attributes of all of the child elements are bound to the data source represented by the data-win-bindsource attribute.

Summary

The focus of this blog entry was data binding using the WinJS library. You learned how to use the data-win-bind attribute to bind the properties of an HTML element to a view model. We also discussed several advanced features of data binding. We examined how to create calculated properties by including a property with a getter in your view model. We also discussed how you can create a converter function to format the value of a view model property when binding the property. Finally, you learned how to use the data-win-bindsource attribute to specify a view model declaratively.

© Stephen Walter or respective owner