Tip Calculator on AngularJS Demo

The demos that come with the PhoneJS framework include two TipCalculator apps. One of these apps demonstrates how to build a simple application using the PhoneJS widgets and the PhoneJS 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 PhoneJS 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 PhoneJS widgets.
    • jquery-2.0.3.js
    • globalize.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.0.3.js"></script>
<script src="lib/globalize.min.js"></script>
<script src="lib/angular/angular.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="stylesheet" type="text/css" href="css/dx.ios.default.css" />
<link rel="stylesheet" type="text/css" href="css/dx.android.holo-dark.css" />
<link rel="stylesheet" type="text/css" href="css/dx.win8.black.small.css" />
<link rel="stylesheet" type="text/css" href="css/dx.tizen.black.css" />

To apply the styles that are appropriate for the platform on which the application is running, call the DevExpress.devices.attachCss() function on the DOM ready event.

HTML
<body>
    <script>
         DevExpress.devices.attachCss($(".dx-viewport"));
    </script>
</body>

Wherever the application runs - on iOS, Android, Tizen or Windows Phone 8 platform - the application looks native.

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"></div>
</body>

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

Application Module Declaration

Open the index.js file in the js folder. It contains a declaration of the application's module. Within the module's dependencies, the following modules are listed.

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

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

JavaScript
angular.module('tipCalculator', ['tipCalculator.controllers', 'phonejs']).
    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 includes variables to store these input values. All these variables are initially set to default values.

  • 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 the stores the number of people to divide the payment by.

The input variables are used to calculate the output. The scope 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 who takes part in the payment.

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

All these variables 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 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() {
    $scope.vm.roundMode = ROUND_UP;
}

function roundDown() {
    $scope.vm.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 variables change, 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();
});

Home View

Open the home.html file from the partials folder. It contains the HTML markup of the "home" view. In this markup, only PhoneJS widgets are used. Each widget comes with styles for different platforms and devices. To learn how to use PhoneJS widgets in AngularJS applications in detail, refer to the AngularJS Approach 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: [{ align: '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 id="billTotalInput" class="dx-field-value" dx-number-box="{ bindingOptions: { value: 'vm.billTotal' }, placeholder: 'Type here...', valueUpdateEvent: 'keyup', min: 0 }"></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 PhoneJS. 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', clickAction: vm.roundDown }"></div>
    <div dx-button="{ text: 'Round Up', clickAction: 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 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 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.