Tip Calculator on AngularJS Demo

The demos that come with the DevExtreme framework include two TipCalculator apps. One of these apps demonstrates how to build a simple application using the DevExtreme widgets and the DevExtreme SPA framework. You can read an overview on this demo in the Tip Calculator Demo article. The other TipCalculator app (download it from GitHub) is intended to illustrate how to use DevExtreme widgets in an application built using the AngularJS framework. In this article, you will learn the details of the TipCalculator application built on AngularJS.

As a base, the angular-seed application project is used. Its structure is changed in the following way.

  • lib
    Contains the necessary libraries for using DevExtreme widgets.
    • jquery-2.1.4.js
    • globalize.min.js
    • angular.min.js
    • angular-route.min.js
    • angular-sanitize.min.js
    • dx.phonejs.js
  • css
    Contains the CSS files that are required to make the application native on any platform.
  • js
    Contains the controllers.js file with a controller's declaration, and the app.js file with the application's module declaration.
  • partials
    Contains the home.html file that represents an HTML template for the single view in the application.
  • index.html
    The application's page.

Application Page

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

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

<script src="lib/jquery-2.1.4.js"></script>
<script src="lib/globalize.min.js"></script>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular.min.js"></script>
<script src="lib/angular-sanitize.min.js"></script>
<script src="lib/angular-route.min.js"></script>
<script src="lib/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="dx-theme" data-theme="ios7.default" href="css/dx.ios7.default.css" />
<link rel="dx-theme" data-theme="android5.light" href="css/dx.android5.light.css" />
<link rel="dx-theme" data-theme="win10.black" href="css/dx.win10.black.css" />
<link rel="dx-theme" data-theme="generic.light" href="css/dx.light.css" />

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

HTML
<script src="js/controllers.js"></script>

<script type="text/javascript" src="js/app.js"></script>
<link rel="stylesheet" href="css/app.css"/>

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

The app.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.

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

The dx-viewport and dx-ios-stripes classes are applied to the ng-view element.

Application Module Declaration

Open the app.js file in the js folder. It contains a declaration of the application's module. This module has the following dependencies.

  • 'ngRoute' Used for deep-linking URLs to controllers and views (HTML partials).

  • 'tipCalculator.controllers' The internal module that provides a controller for the application's view.

  • 'dx' The module that includes registered directives for all DevExtreme widgets.

JavaScript
angular.module('tipCalculator', ['ngRoute','tipCalculator.controllers', 'dx']).
  config(['$routeProvider', function ($routeProvider) {
      $routeProvider.when('/home', { templateUrl: 'partials/home.html', controller: 'HomeCtrl' });
      $routeProvider.otherwise({ redirectTo: '/home' });
  }]);

As you can see in the code above, routing is set up so that the application includes a single view called "home". A controller for this view is called "HomeCtrl".

Home Controller

Open the controllers.js file from the js folder. It contains the "HomeCtrl" controller for the application's "home" view. The controller's scope is an object whose fields bring values (data) for UI fields.

JavaScript
angular.module('tipCalculator.controllers', []).
    controller('HomeCtrl', ["$scope", function($scope) {
        //...

        $scope.vm = {
            roundMode: ROUND_NONE,
            billTotal: undefined,
            tipPercent: DEFAULT_TIP_PERCENT,
            splitNum: 1,

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

            roundUp: roundUp,
            roundDown: roundDown
        };
        //...
    }]);

As you can see on the simulator's screen, there are three values that are specified by the end user. The scope object includes fields to store these input values. All these fields are initially set to default values.

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

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

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

The input values are used to calculate the output. The scope object includes the following fields 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 who takes part in the payment.

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

All these fields are functions that define a business logic.

The output calculation algorithm implies that there are three round modes.

JavaScript
var ROUND_UP = 1,
    ROUND_DOWN = -1,
    ROUND_NONE = 0;

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

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

    switch($scope.vm.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 functions are implemented in the following way.

JavaScript
function totalTip() {
    return 0.01 * $scope.vm.tipPercent * billTotalAsNumber();
}

function tipPerPerson() {
    return totalTip() / $scope.vm.splitNum;
}

function totalPerPerson() {
    return (totalTip() + billTotalAsNumber()) / $scope.vm.splitNum;
}

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

JavaScript
function roundUp() {
    $scope.vm.roundMode = ROUND_UP;
}

function roundDown() {
    $scope.vm.roundMode = ROUND_DOWN;
}

The roundMode field 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 of changes to the values of the billTotal, tipPercent and splitNum fields, the $scope.$watch() function is used.

JavaScript
$scope.$watch('vm.billTotal', resetRoundMode);
$scope.$watch('vm.tipPercent', resetRoundMode);
$scope.$watch('vm.splitNum', resetRoundMode);

function resetRoundMode(newValue, oldValue) {
    if (newValue === oldValue)
        return;
    $scope.vm.roundMode = ROUND_NONE;
};

To focus the Bill Total field when the view is shown, handle the scope's $routeChangeSuccess event.

JavaScript
$scope.$on('$routeChangeSuccess', function() {
    $('#billTotalInput').data('dxNumberBox').focus();
});

To apply the widget styles that correspond to the platform on which the application is currently running, use the DevExpress.ui.themes.current method. When used without parameters, this method returns an object defining the current device. This information is used to assign the corresponding theme to widgets. For this purpose, the DevExpress.ui.themes.current method is called for the second time passing the required theme as a parameter.

JavaScript
$scope.$on('$viewContentLoaded', function () {
    var theme;
    switch (DevExpress.devices.current().platform) {
        case "android":
            theme = "android.holo-dark";
            break;
        case "ios":
            theme = "ios7.default";
            break;
        case "win":
            theme = "win10.black";
            $("body").css("background-color", "#000");
            break;
        default:
            theme = "generic.light";
            break;
    }

    DevExpress.ui.themes.current(theme);
    DevExpress.ui.themes.attachCssClasses("#viewport", theme);
    DevExpress.devices.attachCssClasses("#viewport");
});  

To provide css classes specific for different platforms, the attachCssClasses method is called.

Home View

Open the home.html file from the partials folder. It contains the HTML markup of the "home" view. In this markup, only DevExtreme widgets are used. Each widget comes with styles for different platforms and devices. To learn how to use DevExtreme widgets in AngularJS applications in detail, refer to the Widget Basics - AngularJS article.

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

HTML
<div dx-toolbar="{ items: [{ location: 'center', text: 'Tip Calculator' }] }"></div>

The toolbar is presented by the dxToolbar widget.

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

HTML
<div class="dx-fieldset top-fieldset">
    <div class="dx-field">
        <div class="dx-field-label">Bill Total:</div>
        <div class="dx-field-value">
            <div id="billTotalInput" dx-number-box="{ bindingOptions: { value: 'vm.billTotal' }, placeholder: 'Type here...', valueChangeEvent: 'keyup', min: 0, step: 0.01 }"></div>
        </div>
    </div>
</div>

The number box is the dxNumberBox widget. The value that is inputted by an end user is assigned to the widget's value configuration option. This option is bound to the billTotal field of the controller's scope.

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 specially for this application (see the app.css file).

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

HTML
<div class="dx-fieldset">
    <div class="dx-field slider-container">
        <div class="slider-title">Tip, %</div>
        <div class="slider-body">
            <div dx-slider="{ min: 0, max: 25, step: 1, activeStateEnabled: true, bindingOptions: { value: 'vm.tipPercent' } }"></div>
        </div>
        <div class="slider-value">{{vm.tipPercent}} %</div>
    </div>
    <div class="dx-field slider-container">
        <div class="slider-title">Split:</div>
        <div class="slider-body">
            <div dx-slider="{ min: 1, step: 1, max: 10, activeStateEnabled: true, bindingOptions: { value: 'vm.splitNum' } }"></div>
        </div>
        <div class="slider-value">{{vm.splitNum}}</div>
    </div>
</div>

The sliders are the 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 the tipPercent and splitNum fields of the controller's scope.

To show the value that is set via a slider, a div element is associated with the scope's field.

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

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

HTML
<div class="round-buttons">
    <div dx-button="{ text: 'Round Down', onClick: vm.roundDown }"></div>
    <div dx-button="{ text: 'Round Up', onClick: vm.roundUp }"></div>
</div>

The buttons are represented by dxButton widgets. The function that must be executed when clicking a button is assigned to the onClick option of the widget's configuration object.

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

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

HTML
<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">{{vm.totalToPay() | currency}}</span>
    </div>
    <div class="dx-field">
        <span class="dx-field-label">Total per person</span>
        <span class="dx-field-value">{{vm.totalPerPerson() | currency}}</span>
    </div>
    <div class="dx-field">
        <span class="dx-field-label">Total tip</span>
        <span class="dx-field-value">{{vm.totalTip() | currency}}</span>
    </div>
    <div class="dx-field">
        <span class="dx-field-label">Tip per person</span>
        <span class="dx-field-value">{{vm.tipPerPerson() | currency}}</span>
    </div>
</div>

To display the results, span elements are associated with the scope's fields and the "currency" format is applied.