Tip Calculator Demo

This article is an overview of the Tip Calculator demo. The TipCalculator is a cross-platform mobile application that provides a quick and convenient way to calculate tips for a check. Inputting the sum from a check, users get the total tips due, as well as the totals and tips per person.

Technically, the TipCalculator is a simple single-screen application. It contains a single view based on an empty layout (with a single placeholder for the view). Follow this article to learn how the view is implemented using the MVVM pattern: how a ViewModel is defined, how a View is designed using DevExtreme widgets and how Knockout is used to bind the View to the ViewModel.

The TipCalculator demo is located in your Demos folder in which the product demos are installed. In addition, you can download this demo from the GitHub resource. Go through the steps below to review what is inside.

NOTE: You may notice that the TipCalculator demo is localized (you will find strings replaced by the @text keys in HTML code and globalize dictionaries linked in the application). However, localization aspects are not detailed in this article for the sake of simplicity. You can learn how to localize applications in the Localization documentation section.

If you want to learn the basics of how to create applications using the DevExtreme framework, start with the Your First Application step-by-step tutorial. It discusses the main ideas required for building applications using the DevExtreme framework.

Home ViewModel

Open the home.js file from the views folder. It contains a function that returns the 'home' view's ViewModel. This function has the same name ("home") as the view and is called when the view is displayed.

JavaScript
TipCalculator.home = function(params) {
    //...
    return {
        billTotal: billTotal,
        tipPercent: tipPercent,
        splitNum: splitNum,

        totalTip: totalTip,
        tipPerPerson: tipPerPerson,
        totalPerPerson: totalPerPerson,
        totalToPay: totalToPay,

        roundUp: roundUp,
        roundDown: roundDown,

        viewShown: viewShown
    };
};

The ViewModel represents an object whose fields bring values (data) for UI fields.

As you can see in the simulator's screen, there are three values that are specified by the end user. The ViewModel includes variables in which these input values are stored.

  • billTotal
    A variable that stores the total sum from a check.

  • tipPercent
    A variable that stores the percentage value for the tips.

  • splitNum
    A variable that stores the number of people by which to divide the payment.

JavaScript
var billTotal = ko.observable(),
    tipPercent = ko.observable(DEFAULT_TIP_PERCENT),
    splitNum = ko.observable(1);

All these variables are declared as observables - Knockout objects that can notify subscribers about changes and can automatically detect dependencies. So, the mentioned variables always store the actual values entered to the UI fields bound to them.

The input variables are used to calculate the output. The ViewModel includes the following variables to store the results.

  • totalTip
    The sum of the tips based on the arranged tip percentage.

  • totalToPay
    The sum of the total by the check and the tips.

  • totalPerPerson
    Totals for each person that takes part in the payment.

  • tipPerPerson
    Tips for each person that takes part in the payment.

All these variables are declared as computed observables - they depend on one of more observables and will automatically update whenever any of these dependencies change.

The algorithm of calculation of the outputs implies that there are three round modes.

JavaScript
var ROUND_UP = 1,
    ROUND_DOWN = -1,
    ROUND_NONE = 0,
    roundMode = ko.observable(ROUND_NONE);

The totalToPay computed observable uses the current round mode in its calculation algorithm.

JavaScript
var totalToPay = ko.computed(function() {
    var value = totalTip() + billTotalAsNumber();

    switch(roundMode()) {
        case ROUND_DOWN:
            if(Math.floor(value) >= billTotalAsNumber())
                return Math.floor(value);
            return value;

        case ROUND_UP:
            return Math.ceil(value);

        default:
            return value;
    }
});

The remaining computed observables are implemented in the following way.

JavaScript
var totalTip = ko.computed(function() {
    return 0.01 * tipPercent() * billTotalAsNumber();
});

var tipPerPerson = ko.computed(function() {
    return totalTip() / splitNum();
});

var totalPerPerson = ko.computed(function() {
    return (totalTip() + billTotalAsNumber()) / splitNum();
});

By default, the roundMode variable is set to ROUND_NONE. To set this variable to ROUND_UP a or ROUND_DOWN, the following functions are implemented.

JavaScript
function roundUp() {
    roundMode(ROUND_UP);
}

function roundDown() {
    roundMode(ROUND_DOWN);
}

The roundMode variable must be set back to the ROUND_NONE value when one of the inputs changes so that the outputs are recalculated without rounding the results. To be notified when the values of the billTotal, tipPercent and splitNum observables change, use the subscribe() function.

JavaScript
billTotal.subscribe(function() {
    roundMode(ROUND_NONE);
});

tipPercent.subscribe(function() {
    roundMode(ROUND_NONE);
});

splitNum.subscribe(function() {
    roundMode(ROUND_NONE);
});

The last ViewModel field that has not been described yet is viewShown. Read below to learn about its purpose and implementation.

Home View

Open the home.html file from the views folder. It contains the HTML markup of the "home" view. This markup, together with the CSS styles used in it, form a View - the view's visual representation according to the MVVM pattern. Below you will see how a View is defined and bound to a ViewModel.

To indicate this markup as a View, use the data-options attribute set to dxView.

HTML
<div data-options="dxView : { name: 'home' }">
    <div data-options="dxContent : { targetPlaceholder: 'content' }">
        <!-- View contents does here-->
    </div>
</div>

A name for the View is specified using the name option of the markup configuration object.

Generally, an application's screen does not only include the view's markup. There can be a common markup used on all (multiple) screens. This markup is defined in a layout. In addition, the layout has a placeholder for a view. When displaying a screen, the view that is denoted in the current URL is inserted into the layout's placeholder. As you can see in the code above, the "home" View is inserted into the "content" placeholder. To learn more about views and layouts, and how to define and specify them, refer to the Views and Layouts article.

The "home" view contains a toolbar at the top. Make a note of the "Tip Calculator" text in the center of the toolbar.

HTML
<div data-options="dxView : { name: 'home' }">
    <div data-options="dxContent : { targetPlaceholder: 'content' }">
        <div data-bind="dxToolbar: { items: [{ align: 'center', text: 'Tip Calculator' }] }"></div>
        <!-- ... -->
    </div>
</div>

The toolbar is a dxToolbar widget. This widget, along with all widgets in this view, is supplied with DevExtreme. Each widget comes with styles for different platforms and devices. To learn more about widgets, refer to the UI widgets article.

To input the total sum from the check, a number box is added to the view.

HTML
<div data-options="dxView : { name: 'home' }">
    <div data-options="dxContent : { targetPlaceholder: 'content' }">            
        <!-- ... -->
        <div class="dx-fieldset top-fieldset">            
            <div class="dx-field">
                <div class="dx-field-label">Bill Total:</div>                
                <div id="billTotalInput" class="dx-field-value" data-bind="dxNumberBox: { value: billTotal, placeholder: 'Type here...', valueChangeEvent: 'keyup', min: 0 }"></div>
            </div>
        </div>
    </div>
</div>

The number box is a dxNumberBox widget. The value that is input by an end user is assigned to the widget's value configuration option. This option is bound to the billTotal field of the view's ViewModel.

The dxNumberBox widget is added to a field set that is defined by the predefined stylesheets (dx-fieldset, dx-field, dx-field-label and dx-field-value) supplied by DevExtreme. The top-fieldset class is defined specifically for this application (see the index.css file).

To set the tip percentage and the number of persons by which to divide the payment, sliders are added.

HTML
<div data-options="dxView : { name: 'home' }">
    <div data-options="dxContent : { targetPlaceholder: 'content' }">            
        <!-- ... -->
        <div class="dx-fieldset">
            <div class="dx-field slider-container">
                <div class="slider-title">Tip, %</div>    
                <div class="slider-body">                
                    <div data-bind="dxSlider: { min: 0, max: 25, step: 1, activeStateEnabled: true, value: tipPercent }"></div>
                </div>
                <div class="slider-value" data-bind="text: Globalize.format(0.01 * tipPercent(), 'p0')"></div>
            </div>
            <div class="dx-field slider-container">
                <div class="slider-title">Split:</div>    
                <div class="slider-body">                
                    <div data-bind="dxSlider: { min: 1, step: 1, max: 10, activeStateEnabled: true, value: splitNum }"></div>
                </div>
                <div class="slider-value" data-bind="text: splitNum"></div>
            </div>
        </div>          
    </div>
</div>

The sliders are dxSlider widgets. The value that is set by end users is assigned to the widget's value configuration option. This option is bound to the ViewModel's fields (the tipPercent and splitNum fields).

To show the value that is set via a slider, a div element is associated with the ViewModel's field using Knockout text binding.

The slider-container, slider-title, slider-body and slider-value style classes are defined specifically for this application (see the index.css file).

To calculate the results using the "round up" or "round down" modes, two buttons are added.

HTML
<div data-options="dxView : { name: 'home' }">
    <div data-options="dxContent : { targetPlaceholder: 'content' }">            
        <!-- ... -->
        <div class="round-buttons">
            <div data-bind="dxButton: { text: 'Round Down', clickAction: roundDown }"></div>
            <div data-bind="dxButton: { text: 'Round Up', clickAction: roundUp }"></div>
        </div>            
    </div>
</div>

The buttons are represented by the dxButton widgets. The function that must be executed when clicking a button is assigned to the clickAction option of the widget's configuration object. To learn more about actions, refer to the Actions article.

The round-buttons style class is defined specifically for this application (see the index.css file).

To present the results of calculation, a field set is added.

HTML
<div data-options="dxView : { name: 'home' }">
    <div data-options="dxContent : { targetPlaceholder: 'content' }">            
        <!-- ... -->
        <div id="results" class="dx-fieldset">            
            <div class="dx-field">
                <span class="dx-field-label">Total to pay</span>
                <span class="dx-field-value" style="font-weight: bold" data-bind="text: Globalize.format(totalToPay(), 'c')"></span>
            </div>
            <div class="dx-field">
                <span class="dx-field-label">Total per person</span>
                <span class="dx-field-value" data-bind="text: Globalize.format(totalPerPerson(), 'c')"></span>
            </div>
            <div class="dx-field">
                <span class="dx-field-label">Total tip</span>
                <span class="dx-field-value" data-bind="text: Globalize.format(totalTip(), 'c')"></span>
            </div>
            <div class="dx-field">
                <span class="dx-field-label">Tip per person</span>
                <span class="dx-field-value" data-bind="text: Globalize.format(tipPerPerson(), 'c')"></span>
            </div>
        </div>
    </div>
</div>

To display the results, span elements are associated with the ViewModel's fields using Knockout text binding. To apply a currency format for the results, the format function from the globalize library is used.

To focus the Bill Total field when the view is shown, implement the viewShown function in the view's ViewModel.

JavaScript
function viewShown() {
    $("#billTotalInput").data("dxNumberBox").focus();
}

Application Page

Open the index.html file, which is the application page.

The application page contains links to the required libraries: PhoneJS, jQuery, Knockout and globalize. These libraries are contained in the js folder of the application project.

HTML
<script type="text/javascript" src="js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/knockout-3.1.0.js"></script>
<script type="text/javascript" src="js/globalize.min.js"></script>
<script type="text/javascript" src="js/dx.phonejs.js"></script>

The application page also includes links to the stylesheets required for the application. The stylesheets are contained in the css folder of the application project.

HTML
<link rel="stylesheet" type="text/css" href="css/dx.common.css" />
  <link rel="stylesheet" type="text/css" href="css/dx.spa.css" />
  <link rel="dx-theme" data-theme="android.holo-dark" href="css/dx.android.holo-dark.css" />
  <link rel="dx-theme" data-theme="ios.default" href="css/dx.ios.default.css" />
  <link rel="dx-theme" data-theme="ios7.default" href="css/dx.ios7.default.css" />
  <link rel="dx-theme" data-theme="win8.black" href="css/dx.win8.black.css" />
  <link rel="dx-theme" data-theme="tizen.black" href="css/dx.tizen.black.css" />

Wherever the application runs - on iOS, Android, Tizen or Windows Phone 8 platforms, the application looks native, because corresponding themes are applied.

DevExtreme comes with a set of predefined layout sets. Each layout set determines the layout in which a view is displayed in a particular navigation context. In the TipCalculator application, the "Empty" layout set is used. This set includes a single "Empty" layout and applies it to all views on all platforms. This layout contains a view placeholder only and is appropriate for this simple application. You can find links to the layout files in the application page as well.

Finally, the application page contains links to the view files and to the application CSS and JavaScript files.

HTML
<!-- App views -->
<script type="text/javascript" src="views/home.js"></script>
<link rel="dx-template" type="text/html" href="views/home.html"/>
<!-- App -->
<link rel="stylesheet" type="text/css" href="index.css" />
<script type="text/javascript" src="index.js"></script>

The index.css file contains the style classes that are designed for this application. They are used in the view's markup.

The index.js file is the script that is executed when the page is loaded. See its description below.

The application page only contains the view's markup and its layout. There is no additional markup in the application. See the page body.

HTML
<body>
    <div class="dx-viewport dx-ios-stripes"></div>
</body>

Application Object

Open the index.js file. It contains the script that is executed when the application page is loaded.

JavaScript
window.TipCalculator = {};
$(function() {
    document.addEventListener("deviceready", function() { navigator.splashscreen.hide(); });
    TipCalculator.app = new DevExpress.framework.html.HtmlApplication({
        namespace: TipCalculator,        
        layoutSet: DevExpress.framework.html.layoutSets['empty']
    });
    TipCalculator.app.router.register(":view", { view: "home" });
    TipCalculator.app.navigate();   
});

As you can see in the code above, the HTMLApplication object is created, a format for the application's URL is registered and the default ("home") view is navigated to when the application page is loaded.

The application object is created in the TipCalculator namespace. To specify the namespace for the application object, the namespace option of the configuration object is used. As you may have noticed, the home function, which is executed when the "home" view is loaded, is also declared in the TipCalculator namespace.

The layout used for the application's view is specified using the layoutSet option of the application's configuration object.

The navigate function, which is called after the application object is created, changes the application's URI using the registered format to the following: "#home". When the browser's URL changes, the framework displays the "home" view. To learn more about navigation, refer to the Navigation and Routing article.