Data-Bound Application

This tutorial will help you implement the second application, to assist you in learning more about the PhoneJS framework. If you have completed the Your First Application tutorial, you have already learned how PhoneJS applications are constructed. Next, you will implement an application that includes working with data. This tutorial will help you implement this application step-by-step. You may notice that the order of application implementation is not standard - an HTML view representation is developed first, and then JavaScript code is implemented to provide data and business logic for the views. This concession is made intentionally, to allow you to better understand what you are doing and why you are doing it. This approach to developing applications is sometimes used in real-world situations as well.

In this tutorial, you will not have to implement the entire structure of a PhoneJS application from scratch. You have already done this in the Your First Application tutorial, so now you can use the application template that ships with PhoneJS. It is located in the root ApplicationTemplate folder of the PhoneJS zip archive. In addition, you can download this template from the GitHub resource. Note that this application template is not only intended for this tutorial. You will find it helpful in creating applications based on PhoneJS as well.

  • Copy the application template to the folder containing your projects.
  • Prepare the environment according to the instructions provided in the Set Up Your Application tutorial.
  • Generally, you can run the application provided by the template without writing a line of code, and see that all of its parts are interrelated and interact properly with each other in any supported device. By default, the NavBar layout is used in the application, which includes the dxNavBar widget for navigating between views. Thus, every view that you implement will be merged with this layout, and thus have the dxNavBar widget as a result.

The resulting application is available on GitHub.

Application Map

Before starting to implement an application, draw the application's map to understand which views are needed and what functionality you wish to include.

Application Map

Create List Views

Begin with the "Categories" and "Products" views, which contain lists of categories and products respectively.

Categories View

Implement an HTML template for the "Categories" view.

  • Find a sample HTML template for a view in the home.html file of the application's template.
  • Replace the default text in the view's markup with the dxList widget. Add this widget using the dxList Knockout binding.
  • To add test data to the list, use the items option of the widget's configuration object.
  • Define a template for the list items so that they have the same configuration and behavior. To do this, add a div element with the data-options attribute set to dxTemplate and set the name configuration option to "item". Use the text binding to display the name of a list item.
HTML
<div data-options="dxView : { name: 'home', title: 'Home' } " >
    <div class="home-view"  data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxList: { items: [
                { id: 1, name: 'Beverages' },
                { id: 2, name: 'Fruit' }
            ] }">
            <div data-options="dxTemplate : { name: 'item' }" data-bind="text: name" ></div>
        </div>
    </div>
</div>

As you can see, the view's markup is placed to the "content" placeholder of the "NavBar" layout, which is used by default (see the defaultLayout option specified for the application in index.js). As a result, the view's HTML template is combined with the layout's HTML markup and the style sheets are applied to the HTML elements (including the dxList widget). This forms a so-called View from the MVVM (Model-View-ViewModel) pattern. The ViewModel and Model for the "Categories" view are described below.

Products View

Implement an HTML template for the "Products" view.

  • Add an HTML file to your views folder and register it by linking in the index.html file.

    HTML
    <head>
        <link rel="dx-template" type="text/html" href="views/Products.html"/>
    </head>
  • Implement an HTML template for the "Products" view in the same manner as you did for the "Categories" (Home) view above.

HTML
<div data-options="dxView : { name: 'products', title: 'Products' } " >
    <div data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxList: { items: [
                { id: 1, name: 'Whiskey', category_id: 1 },
                { id: 2, name: 'Cognac', category_id: 1 },
                { id: 3, name: 'Banana', category_id: 2 },
                { id: 4, name: 'Pineapple', category_id: 2 }
            ] }">
            <div data-options="dxTemplate : { name: 'item' }" data-bind="text: name" ></div>
        </div>
    </div>
</div>

Create Detail Views

Implement an HTML template for the "ProductDetails" detail view.

  • Create a ProductDetails.html file within the views folder. Then, link this file in the index.html file as demonstrated above.
  • Add the "Id" and "Name" fields to the view. To help you organize several widgets into a coherent detail view, the framework supplies a set of CSS classes called fieldset. For each field within the field set, add a div element and decorate it with the dx-field class. To display a field name, add a div element and decorate it with the dx-field-label class. To display a field value, add a div element decorated with the dx-field-value class, and bind it to a value using text binding.
HTML
<div data-options="dxView : { name: 'product-details', title: 'Product' } " >
      <div data-options="dxContent : { targetPlaceholder: 'content' } " >
            <div class="dx-fieldset">
                  <div class="dx-field">
                        <div class="dx-field-label">Id: </div>
                        <div class="dx-field-value" data-bind="text: 1"></div>
                  </div>
                  <div class="dx-field">
                        <div class="dx-field-label">Name: </div>
                        <div class="dx-field-value" data-bind="text: 'Banana'"></div>
                  </div>
            </div>
      </div>
</div>

Add Actions

Add the actions that are planned on the application map to your views. Start with their presentation on the views' HTML markup.

Handle a List Item Click

Use the dxAction binding to navigate to a view by clicking a list item. The dxAction binding allows you to add the click handler to an HTML element.

  • Use the dxAction binding to navigate from the "Categories" view to the "Products" view when clicking a list item. Assign a string that specifies the URI to navigate to.

    HTML
    <div data-options="dxTemplate : { name: 'item' }"  data-bind="text: name, dxAction: '#products/1'"></div>

    Here, the URI navigates to the "Products" view and passes the id of the selected category as the second parameter.

  • The string that is assigned to the dxAction binding must conform the routing format declared for the application. Open your index.js file and change the default routing format to ":view/:id".

    JavaScript
    MyApp.app.router.register(":view/:id", { view: "home", id: undefined });
  • Use the dxAction binding to navigate from the "Products" view to the "ProductDetails" view when clicking a list item. Use the same technique that you used for navigating from the "Categories" view to the "Products" view.

    HTML
    <div data-options="dxTemplate : { name: 'item' } " data-bind="text: name, dxAction: '#product-details/1'"></div>

Add a Search Button

Add the Search button to the "Products" view to search a product using a combination of letters.

  • Add a command to the root of the view's markup. To do this, add a div element with the data-options attribute set to dxCommand.
  • Specify the command's options by setting fields of the configuration object.

    • Specify a location for the command within the view's layout.
    • Use the "find" icon that is built into the style sheets included into the application template (see the css folder).
    • Assign unspecified to the action field. Add the action implementation when you have data bound to the application.

    The command will be represented by the widget that is used by the view's layout to display commands from the specified location on the current device. The NavBar layout, which is used for the "Products" view by default, will display the command as a button using the appearance and location native to the device on which the application will run.

  • Add a textbox for the search input. For this purpose, use the dxTextBox widget. To add the dxTextbox widget, use the dxTextBox Knockout binding and configure this widget by specifying options within the configuration object.

HTML
<div data-options="dxView : { name: 'products', title: 'Products' } " >    
    <div data-bind="dxCommand: { title: 'Search', placeholder: 'Search...', location: 'create', icon: 'find', action: undefined }" ></div>
    <div data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxTextBox: { mode: 'search', value: ''}"></div>
        <div data-bind="dxList: { items: [
                { id: 1, name: 'Whiskey', category_id: 1 },
                { id: 2, name: 'Cognac', category_id: 1 },
                { id: 3, name: 'Banana', category_id: 2 },
                { id: 4, name: 'Pineapple', category_id: 2 }
            ] }">
            <div data-options="dxTemplate : { name: 'item' }" data-bind="text: name, dxAction: '#product-details/1'" ></div>
        </div>
    </div>
</div>

Back Button

According to the application's map, there should be a Back button that returns you to the previous view. You do not have to implement the Back button. It is added automatically (if there is no hardware Back button on the device) to the place appropriate for the current device. For details, refer to the Navigate Back topic.

Global Navigation

As you can see in the running application, the NavBar layout uses the dxNavBar widget for the application's global navigation. NavBar items (like Home and About in the template application) represent commands defined by the navigation object within the application's configuration object. See the default set of navigation commands in the index.js file.

JavaScript
MyApp.app = new DevExpress.framework.html.HtmlApplication({
    namespace: MyApp,        
    defaultLayout: "navbar",
    navigation: [
      {
        title: "Home",
        action: "#home",
        icon: "home"
      },
      {
        title: "About",
        action: "#about",
        icon: "info"
      }
    ]
});

Add ViewModels

When Views are completed, implementing ViewModels is the next step. Create JavaScript files for each view and add links to these files in the index.html file, as we did earlier. Note that there is the home.js file within the application template. So you don't have to create a JavaScript file for the "Categories" (Home) view.

Categories ViewModel

  • Open the home.js file. It contains the home function. Named by the same name as the view's HTML template, this function will be found and called when the "Categories" (Home) view is displayed. The object that is returned by the home function is a ViewModel for the "Categories" view.
  • To provide data for the list items using a data service in the next step, replace the items option with the dataSource option. Bind the dataSource option to the dataSource field of the ViewModel object. In this step, assign the array of objects that was used for the items option earlier to the dataSource field.
  • Specify {id} as the second navigation parameter in the URI assigned to dxAction. In this instance, the id field value of the clicked category will be passed to the "Products" view.
HTML
<div data-bind="dxList: { dataSource: dataSource }">
    <div data-options="dxTemplate : { name: 'item' }" data-bind="text: name, dxAction: '#products/{id}'"></div>
</div>
JavaScript
MyApp.home = function (params) {
    var viewModel = {
        dataSource: [
            { id: 1, name: 'Beverages' },
            { id: 2, name: 'Fruit' }
        ]
    };
    return viewModel;
};

See Application Code

See Difference

Products ViewModel

  • Implement the products function in the newly created Products.js file using the home function as a template.
  • Like in the "Categories" view, use the dataSource option bound to the ViewModel's dataSource field here as well. In addition, set {id} as a parameter in the URI assigned to the dxAction option.
  • Add the categoryId field to the ViewModel and set it to the currently selected category. Get the selected category from the view navigation parameter.
  • Specify the visible option of the Search textbox by binding it to the ViewModel's showSearch field. Make the showSearch field observable and set it to false by default. This makes the Search textbox initially invisible, and provides the capability to make it visible by setting the showSearch field to true, when required.
  • Bind the Search command's action option to the ViewModel's find function. In this function, change the textbox visibility state and show an alert window.
  • Bind the Search textbox value option to the searchString ViewModel field. Make this field observable and set an empty string to it by default. This field's value will change when the browser raises the 'search', 'change' or 'keyup' events. These events are specified as the textbox valueUpdateEvent option value.
HTML
<div data-options="dxView : { name: 'products', title: 'Products' } " >
    <div data-bind="dxCommand: { title: 'Search', placeholder: 'Search...', location: 'create', icon: 'find', action: find }" ></div>
    <div data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div data-bind="dxTextBox: { mode: 'search', value: searchString, visible: showSearch, valueUpdateEvent: 'search change keyup' }"></div>
        <div data-bind="dxList: { dataSource: dataSource }">
            <div data-options="dxTemplate : { name: 'item' }" data-bind="text: name, dxAction: '#product-details/{id}'"></div>
        </div>
    </div>
</div> 
JavaScript
MyApp.products = function (params) {
    var viewModel = {
        searchString: ko.observable(''),
        find: function () {
            viewModel.showSearch(!viewModel.showSearch());
            alert('searching');
        },
        showSearch: ko.observable(false),
        categoryId: params.id,
        dataSource: [
            { id: 1, name: "Whiskey", category_id: 1 },
            { id: 2, name: "Cognac", category_id: 1 },
            { id: 3, name: "Banana", category_id: 2 },
            { id: 4, name: "Pineapple", category_id: 2 }
        ]
    };
    return viewModel;
};

ProductDetails ViewModel

  • Implement the product-details function in the newly created ProductDetails.js file using the home function as a template.
  • Bind the Id field in the detail view to the ViewModel's id field. Set the ViewModel's id field to the value passed as the id parameter value during navigation from the "Products" view.
  • Bind the Name field in the detail view to the ViewModel's name field. Make the ViewModel's name field observable so that it's possible to set it to the value taken from data (see the next step).
HTML
<div data-options="dxView : { name: 'product-details', title: 'Product' } " >
    <div data-options="dxContent : { targetPlaceholder: 'content' } " >
        <div class="dx-fieldset">
            <div class="dx-field">
                <div class="dx-field-label">Id: </div>
                <div class="dx-field-value" data-bind="text: id"></div>
            </div>
            <div class="dx-field">
                <div class="dx-field-label">Name: </div>
                <div class="dx-field-value" data-bind="text: name"></div>
            </div>
        </div>
    </div>
</div>
JavaScript
MyApp['product-details'] = function(params) {
    var viewModel = {
        id: params.id,
        name: ko.observable('')
    };
    return viewModel;
};

Bind to Real Data

The last step is learning to retrieve data from a real database, for which this lesson will use a sample data service published for instructional use.

Bind Data to the Categories List

The dxList widget works with a DataSource object. This is the object that has the special methods (e.g., "load") that can be called during the widget's life cycle. The array that you assigned to the dataSource field of the "Categories" ViewModel above was actually used internally to create the DataSource object. This array was returned by the DataSource's load function. Now, when you need to get data using our sample service, create the DataSource object explicitly and assign it to the ViewModel's dataSource option.

To create a DataSource object, use the DevExpress.data.createDataSource method. Pass an object with the defined load function as a parameter. Within the load function, check that the widget's data is currently refreshed, using the refresh field of the object passed as the function's parameter. If the data is currently being refreshed, get the Category objects using the $.get('http://sampleservices.devexpress.com/api/Categories') request. Return a deferred object so that data can be loaded asynchronously.

Although the procedures defined are generally sufficient to provide data, you will need to perform mapping as well. The fields of the objects that are returned by the service do not correspond to the ViewModel fields to which the "Categories" view's elements are bound. The CategoryName field that comes from the server must be mapped to the ViewModel's name field, and the CategoryID field must be mapped to the ViewModel's id field.

JavaScript
MyApp.home = function (params) {
    var viewModel = {
        dataSource: DevExpress.data.createDataSource({
            load: function (loadOptions) {
                if (loadOptions.refresh) {
                    var deferred = new $.Deferred();
                    $.get('http://sampleservices.devexpress.com/api/Categories')
                    .done(function (result) {
                        var mapped = $.map(result, function (data) {
                            return {
                                name: data.CategoryName,
                                id: data.CategoryID
                            }
                        });
                        deferred.resolve(mapped);
                    });
                    return deferred;
                }
            }
        })
    };
    return viewModel;
};

Bind Data to the Products List

  • Create a DataSource object for the Products list in the same way you did for the Categories List. Refrain, however, from requesting all data at once, and instead request a single page each time. For this purpose, pass an object with request parameters as the second parameter of the $.get request. Set the skip and take parameters to the required values. In addition, set the categoryId parameter to the ViewModel's categoryId field value, to specify the category whose products are requested.

  • Update the ViewModel's searchString field to which the textbox value is bound with a 500 millisecond delay after an end user stops input. Then, call the reload method of the dataSource object.

  • Implement the Search action in the Products view. When an end user inputs letters for a search, the Products list reloads data. Within the load function, use the searchString field value as one of the request parameters. In addition, make the textbox empty when clicking the Search button. To do this, assign an empty string to the ViewModel's searchString field.

JavaScript
MyApp.products = function (params) {
    var skip = 0;
    var PAGE_SIZE = 10;
    var viewModel = {
        searchString: ko.observable(''),
        find: function () {
            viewModel.showSearch(!viewModel.showSearch());
            viewModel.searchString('');
        },
        showSearch: ko.observable(false),
        categoryId: params.id,
        dataSource: DevExpress.data.createDataSource({
            load: function (loadOptions) {
                if (loadOptions.refresh) {
                    skip = 0;
                }
                var deferred = new $.Deferred();
                $.get('http://sampleservices.devexpress.com/api/Products',
                    {
                        categoryId: viewModel.categoryId,
                        skip: skip,
                        take: PAGE_SIZE,
                        searchString: viewModel.searchString()
                    })
                .done(function (result) {
                    skip += PAGE_SIZE;
                    var mapped = $.map(result, function (data) {
                        return {
                            name: data.ProductName,
                            id: data.ProductID
                        };
                    });
                    deferred.resolve(mapped);
                });
                return deferred;
            }
        })
    };
    ko.computed(function () {
        return viewModel.searchString();
    }).extend({
        throttle: 500
    }).subscribe(function () {
        viewModel.dataSource.reload();
    });
    return viewModel;
};

Get Object for the ProductDetails View

To get the name of the Product object whose identifier is passed as a URL parameter, request the Product object from the sample service using the $.get() function. Assign the ProductName field value of the returned object to the ViewModel's name field.

JavaScript
MyApp['product-details'] = function (params) {
    var viewModel = {
        id: params.id,
        name: ko.observable('')
    };
    $.get('http://sampleservices.devexpress.com/api/Products/' + viewModel.id)
    .done(function (data) {
        viewModel.name(data.ProductName);
    });        
    return viewModel;
};