Ionic Expense Tracker Sample: Creating the App

Wijmo 5 In part 1 of this tutorial, we learned what the Ionic Framework is and how to create a basic Ionic project using the framework’s CLI. In part 2 of this tutorial, we will create some of the most important parts of the Expense Tracker sample using Ionic and Wijmo 5. Rather than recreating everything, let’s instead download an updated version of part 1’s project from here. The updated project includes all of the Expense Tracker’s front-end dependencies, templates, stylesheets, controllers, routes, and services. An updated summary of the project directory (www) can be found below:

www/                     --> directory for the project  
  index.html      --> app layout file (the main html template file of the app)  
  css/                   --> css files  
  js/                    --> javascript files  
    app.js               --> the main application module  
    app.routes.js        --> the main application module's routes  
    controllers/         --> directory containing all of the app's controllers  
    models/              --> directory containing all of the app's models  
    services/         --> directory containing all of the app's data services  
  templates/             --> angular view partials (partial html templates) used by UI-Router  
  lib/            --> 3rd party js libraries, including Ionic, Wijmo, and more  

All of the controllers and templates in the project that we’ll be working with contain boilerplate code and markup, respectively, for convenience purposes.

Data Model

Before we begin creating the Expense Tracker app, it is important that we understand the data model. Luckily, the Expense Tracker’s data model is very basic and can be seen below: Expense Tracker - ERD Diagram We also have a standalone property in our data model called Budget (not shown above) that has no relationships and is used as a benchmark for us to compare our expenses against. To work with our data model, we will use the pre-existing AngularJS services that are located in the www/js/services directory. The AngularJS services used in the Expense Tracker application are essentially wrappers around HTML5’s localStorage, which is where we will be storing our data as JSON. In the future, we could easily swap out these services to work with a RESTful API, SQLite, or some other technology for data storage.

Expense History

If we run our application at this point, the first page that we will see is the blank history page. The purpose of the history page is to list all of our expenses and provide a convenient way to delete expenses. To do this, let’s open the history.js file and add the following code to the History object:

// get data from localStorage
$scope.expenses = ExpenseSvc.getExpensesWithCategory();

This code will essentially provide our view with all the expenses currently residing in localStorage. The ExpenseSvc will not only return the expense object, but also the expense’s parent category as well. With this one line of code, we can now display the expenses in our view, history.tpl.htm. Let’s do that now by adding Ionic’s ion-list directive to the pre-existing ion-content directive.

<ion-view title="History">
  <ion-nav-buttons side="right">
    <a class="button button-icon icon ion-plus" href="#/app/create"></a>
  </ion-nav-buttons>
  <ion-content class="has-header">
    <ion-list>
      <ion-item ng-repeat="expense in expenses | orderBy:&#39;date&#39;:reverse track by expense.id" class="item item-icon-left">
        <i class="icon ion-record {{ expense.category.cssClass }}"></i>
        <div class="row">
          <div class="col-50">
            <h2>{{ expense.title }}</h2>
          </div>
          <div class="col-25">
            <small class="light-grey">{{ expense.date | date: &#39;shortDate&#39; }}</small>
          </div>
          <div class="col-25">
            {{ expense.amount | currency }}
          </div>
        </div>        
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

The ion-list directive, if you haven’t guessed already, is an Angular directive that creates a styled order list, and its children, ion-item, is a directive for an HTML list item. Of course these directives add some additional functionality, but that is the gist of it. You may also notice the “track by” clause in the ngRepeat directive, which was added to help boost performance when modifying the collection (Ben Nadel has a great blog post on ngRepeat’s “track by” at http://bit.ly/1oJTNRG). Let’s run the application using the ionic serve command to see the expenses that are preloaded by the app’s services. Let’s now take this one step further and add the ability to delete an expense by swiping left on an item and clicking a delete button. Since we already have the template open, let’s start there by adding an ion-option-button before the closing ion-item tag.

<ion-option-button class="button button-assertive" on-tap="confirmDelete(expense.id)">Delete</ion-option-button>

The ion-option-button is another directive provided by Ionic that is specifically for use within the ion-item directive. By default, ion-option-buttons are hidden, but if you were to swipe an ion-item, the button would be revealed. This is very handy for maximizing screen real estate on smaller devices. If we wanted to toggle this functionality on/off based on some condition like a user’s permissions, we can do so simply by setting can-swipe to true/false on the ion-list directive. Now let’s switch back to our history controller and enable the deletion of expenses:

function confirmDelete(expenseId) {
   // delete expense by its id property
   $scope.expenses = ExpenseSvc.deleteExpense(expenseId);
}

All we’re doing here is adding a method to our scope that accepts an expense’s id property that in turn calls the ExpenseSvc’s deleteExpense method to delete the expense item. The deleteExpense method also returns the expenses so that we can update our expense collection on the client. In a real world scenario, returning the whole expense collection again is probably not ideal, but it’s okay for our purposes. Now, let’s run the sample and delete a few expenses. Delete Expense Before we move on, let’s add a confirmation dialog so that our end user’s don’t accidently delete their expenses. While we could use a simple confirm to handle this, let’s use Ionic’s $ionicActionSheet service instead for a better user experience. In the history controller, let’s update the confrimDelete method to use the action sheet:

// method to confirm and likely delete expense
$scope.confirmDelete = function (expenseId) {
  // show ionic actionSheet to confirm delete operation
  // show() returns a function to hide the actionSheet
  var hideSheet = $ionicActionSheet.show({
    titleText: 'Are you sure that you'd like to delete this expense?',
    cancelText: 'Cancel',
    destructiveText: 'Delete',
    cancel: function () {
      // if the user cancel's deletion, hide the list item's delete button
      $ionicListDelegate.closeOptionButtons();
    },
    destructiveButtonClicked: function () {
      // delete expense by its id property            
      $scope.expenses = ExpenseSvc.deleteExpense(expenseId);

      // hide the confirmation dialog
      hideSheet();
    }
  });
};

The ionicActionSheet service offers several options to customize the popup, and we’re only using a subset. Notice that the action sheet service’s show method returns a function to hide the sheet, and we’re using that function to hide the action sheet within the destructive button’s click handler. Also in the cancel function, we’re using Ionic’s $ionicListDelegate service to hide the ion-list’s delete button if the user decides that he or she doesn’t actually want to delete the expense. If we run the sample now and attempt to delete an expense, we should now have to confirm that we want to delete the expense before the expense is actually deleted. Confirm Delete

Create Expense

Now that we’re able to see all of our expenses, we’ll naturally want to create new expenses as well. Let’s build an input form in the createExpense template so that our end users are able to create expenses.

<ion-view title="Create">
  <ion-content class="has-header padding">
    <form name="frmCreate">
      <div class="custom-form-list list">
        <label class="item item-input">
          <i class="icon ion-alert-circled placeholder-icon assertive" ng-show="!frmCreate.title.$pristine &amp;&amp; frmCreate.title.$invalid"></i>
          <input name="title" type="text" placeholder="Title" ng-model="expense.title" ng-maxlength="55" required>
        </label>
        <wj-input-number value="expense.amount" min="0" step="5" format="c2"></wj-input-number>
        <wj-calendar value="expense.date"></wj-calendar>                
        <wj-combo-box items-source="categories" 
                      display-member-path="htmlContent"
                      selected-value-path="id"
                      selected-value="expense.categoryId" 
                      is-editable="false" 
                      is-content-html="true"></wj-combo-box>
        <label class="item item-input">
          <textarea placeholder="Description" ng-model="expense.description"></textarea>
        </label>
      </div>
      <div class="button-bar">
        <button type="button" class="button button-dark icon-left ion-close" on-tap="cancel()">Cancel</button>
        <button type="button" class="button button-balanced icon-left ion-checkmark" on-tap="addExpense()" ng-disabled="frmCreate.title.$invalid">Save</button>
      </div>
    </form>
  </ion-content>
</ion-view>

The markup we’ve just added is fairly straight forward. Again, we’re using the ion-view and ion-content directive’s to host our content. Inside of these directives, we’ve added a form specifically to enable validation on the expense’s title field via ngShow – the Wijmo directives don’t need to be validated in this case because we’ve specified properties to actively enforce the values that can be set, such as the min value on the expense’s amount. While the Wijmo Calendar and InputNumber should be self-explanatory, the ComboBox may not be. The purpose of the ComboBox is to associate an expense with a category, which is a must for our data model. In this case, we’ve bound the ComboBox to our categories collection via the itemsSource property. The ComboBox’s displayMemberPath property tells the control what to display in its dropdown, the category’s htmlContent computed property, and the selectedValuePath property notifies the control which field should be used for the selectedItem’s selectedValue, in this case the category’s id property. Lastly, we’ve set the selectedValue to be associated with the expense’s categoryId property – this is the expense property that will get set whenever an end user actually selects an item from the dropdown. The last thing that we need to do before we can create an expense is update our createExpense controller.

// init Expense object that the user will create
$scope.expense = new Expense('', 0, new Date(), '', null);

// get categories with HTML content property
$scope.categories = CategorySvc.getCategoriesWithHtmlContent();

// method to save expense in localStorage
$scope.addExpense = function () {
  // insert expense
  ExpenseSvc.insertExpense($scope.expense);
  $scope.cancel();
};

// method for cancelling (i.e. navigate to the dashboard)
$scope.cancel = function () {
  $state.go('app.overview');
};

The first two lines will create data for the template to bind to. In the case of $scope.expense, we’re simply creating a new expense object using our Expense constructor function. In the case of $scope.categories, we’re using the CategorySvc to populate the categories array from localStorage. The addExpense method will be used to commit the new expense to localStorage when the view’s save button is clicked. Rather than directly inserting the new expense into localStorage, we’ll use the ExpenseSvc to handle that for us, keeping our controller as lean as possible. The last step in the controller’s addExpense method will redirect us back to the overview screen using the UI Router’s $state service. Let’s run the app now, if it isn’t already, and add a few expenses. New Expense

Details Grid

In the previous steps, we’ve learned how to view, create, and delete expenses. In this step, we’ll learn how to handle batch updating of expenses using the Wijmo 5’s FlexGrid and CollectionView. Let’s open the detailsGrid template and add the following markup:

<ion-view title="Details Grid">
  <!-- set overflow-scroll="true" and hand scrolling to native -->
  <ion-content class="has-header" overflow-scroll="true">
    <wj-flex-grid auto-generate-columns="false" items-source="data" selection-mode="Row" row-edit-ending="rowEditEnding(s,e)" style="position:relative">
      <wj-flex-grid-column width="2*" min-width="250" header="Title" binding="title"></wj-flex-grid-column>
      <wj-flex-grid-column width="*" min-width="100" header="Amount" binding="amount" format="c2"></wj-flex-grid-column>
      <wj-flex-grid-column width="*" min-width="100" header="Date" binding="date"></wj-flex-grid-column>
      <wj-flex-grid-column width="2*" min-width="250" header="Description" binding="description"></wj-flex-grid-column>
    </wj-flex-grid>
  </ion-content>
  <ion-footer-bar class="bar button-bar-footer">
    <div class="button-bar">
      <button type="button" class="button button-dark icon-left ion-close" on-tap="cancel()">Cancel</button>
      <button type="button" class="button button-balanced icon-left ion-checkmark" ng-disabled="!data.itemsEdited.length" on-tap="update()">Save</button>
    </div>
  </ion-footer-bar>
</ion-view>

Aside from the FlexGrid directive, the template’s markup is probably already familiar to you: we have two buttons, Cancel and Save, that when clicked will either cancel the operation or save our changes to localStorage. The save button’s ngDisabled expression may not make sense at the moment, but it will once we get to the controller implementation. The FlexGrid directive will, of course, generate a Wijmo 5 FlexGrid control within the template. At the top most level, we’re using the itemsSource property to define the collection of items that we’ll be binding, our expenses in this case. We’re also informing the FlexGrid that we want to generate the columns by hand (autoGenerateColumns=”false”) and that we want to enable row selection. We’re also specifying the FlexGrid’s rowEditEnding event handler, which will be used to cancel edits if the changes are invalid. Within the FlexGrid directive, we’re defining our columns. For each column that we’ve defined, we specified its header, binding, and width as well. With the view template out of the way, let’s wire everything up in the detailsGrid controller:

// get expenses from localStorage and initialize CollectionView
$scope.data = new wijmo.collections.CollectionView(ExpenseSvc.getExpenses());

// enable CollectionView's change tracking
$scope.data.trackChanges = true;

// method for batch update of expenses
$scope.update = function () {
  // make sure items have been edited
  if ($scope.data.itemsEdited.length) {
    // bulk update expenses
    ExpenseSvc.updateExpenses($scope.data.itemsEdited);

    // return to overview page
    $scope.cancel();
  }
};

// method to cancel (i.e. navigate to dashboard)
$scope.cancel = function () {
  $state.go('app.overview');
};

// FlexGrid.rowEditEnding event handler
$scope.rowEditEnding = function (sender, args) {
  var expense = $scope.data.currentEditItem,      // get expense being edited
      isValid = isExpenseValid(expense);          // validate expense

  // if the expense isn't valid, cancel the edit operation
  if (!isValid) {
    $scope.data.cancelEdit();
    return;
  }
};

// local validation function to make sure the expense is valid before committing the row edit
function isExpenseValid(expense) {
  return expense &amp;&amp;
    t expense.title !== '' &amp;&amp;
    t expense.title.length < 55 &amp;&amp;     t 
         wijmo.isNumber(expense.amount) &amp;&amp;     t 
         wijmo.isDate(expense.date) &amp;&amp;     t 
         expense.amount >= 0;
}

The first line is grabbing our expenses from localStorage via the ExpenseSvc and initializing the CollectionView using said expenses. The third line is enabling the CollectionView’s change tracking mechanism, which is the backbone for everything else to come. The CollectionView’s change tracking feature allows us to know which, if any, items have been added, edited, and deleted via its itemsAdded, itemsEdited, and itemsDeleted collections. If you look back at the view template, we use the CollectionView’s itemsEdited collection to determine if the save button should be enabled or disabled based on its length. Next up are the update and cancel methods. We’ve seen the cancel method already and this one is no different: it uses UI Router’s $state service to take us back to the overview screen. The update method is similar as well, and it checks the CollectionView.itemsEdited collection’s length to determine if any of the expenses have been modified. If one or more expenses have been modified, we pass the modified expenses to the ExpenseSvc in order to update the data in localStorage. Lastly, we have the rowEditEnding function that is used as an event handler for the FlexGrid. The FlexGrid’s rowEditEnding event occurs when a row edit is ending, but before the changes have been committed or cancelled. It is in this event that we will validate if the modifications to an expense are valid or not by using the isExpenseValid utility function. If the changes to an expense are invalid, we prevent the changes from being committed to the FlexGrid’s underlying data source (a CollectionView in our case). If we examine the isExpenseValid function further, we can also see that it makes use of two of Wijmo’s assorted utility functions, isNumber and isDate. Next up, we will finish this tutorial series with my favorite section of the application, the overview page.

Overview

The overview page, for all intents and purposes, serves as a dashboard of sorts for the Expense Tracker app. Before we create the view and controller for the overview page, let’s make this the home screen for the app as well. To do this, open app.routes.js and change the following line:

$urlRouterProvider.otherwise('/app/history');

to:

$urlRouterProvider.otherwise('/app/overview');

This small change will tell the UI Router that if no route can be resolved, it should show the end user the dashboard page. With that out of the way, let’s add the overview page’s markup as seen below:

<ion-view title="Overview">
  <ion-nav-buttons side="right">
    <a class="button button-icon icon ion-plus" href="#/app/create"></a>
  </ion-nav-buttons>
  <ion-content class="has-header padding">
    <div ng-show="hasExpenses">
      <hgroup class="text-center padding-vertical">
        <h2 class="title">
          <span ng-class="expensesCssClass">{{ totalExpenses | currency }}</span> of {{ budget | currency }}
        </h2>
        <h4>{{ budgetMsg }}</h4>
      </hgroup>
      <wj-flex-chart items-source="categories"
                     chart-type="Bar" binding-x="name"
                     tooltip-content=""
                     selection-mode="Point"
                     footer="Tap the chart&#39;s bars to see history by category"
                     selection-changed="selectionChanged(s)"
                     item-formatter="itemFormatter"
                     style="height: 400px;">
        <wj-flex-chart-series binding="total"></wj-flex-chart-series>
        <wj-flex-chart-axis wj-property="axisX" format="c0"></wj-flex-chart-axis>
        <wj-flex-chart-axis wj-property="axisY" reversed="true" major-grid="false" axis-line="true"></wj-flex-chart-axis>
      </wj-flex-chart>
    </div>
    <div ng-hide="hasExpenses">
      <h4 class="padding text-center">You haven&#39;t added any expenses yet!  Click the <i class="icon ion-plus"></i> button to get started!</h4>
    </div>
  </ion-content>
</ion-view>

The hgroup element will be used to display the sum of our expenses against our budget and will be color-coded to better visualize this benchmark. Below the hgroup, we will use Wijmo 5's FlexChart to display an expense total for each category in the application so that we can see where our hard earned money is being spent. In the FlexChart directive, we’ve specified its attributes, data series, and both, X and Y, axes. Eventually, clicking (or tapping) one of the FlexChart’s plot elements will drill down on that category and list each expense used in calculating the aggregate value. If no expenses have been added to the application, we also have a “getting started” message explaining to end users that this page does nothing until expenses have been entered into the system. For all of this to work, we need to add the following code to the Overview function in overview.js:

// get budget from localStorage
$scope.budget = BudgetSvc.getBudget();

// Boolean flag to determine if any expenses have been added
$scope.hasExpenses = ExpenseSvc.hasExpenses();

// get sum of all expenses
$scope.totalExpenses = ExpenseSvc.getExpenseTotal();

// get expense summary for all categories
$scope.categories = ExpenseSvc.getCategoriesExpenseSummary();

// init CSS class to style expense sum when compared against budget
$scope.expensesCssClass = 'energized';


//*** set the budget message
// NOTE: use $filter service to format the total prior to concatenating the string
$scope.budgetMsg = $scope.totalExpenses <= $scope.budget
tttttt? $filter('currency')($scope.budget - $scope.totalExpenses).concat(' until you reach your monthly limit')
tttttt: $filter('currency')($scope.totalExpenses - $scope.budget).concat(' over your monthly limit');


//*** set the class for budget message
$scope.expensesCssClass = 0 === $scope.totalExpenses
tttt? 'dark'
tttt: $scope.totalExpenses === $scope.budget
tttt     ? 'energized'
tttt     : $scope.totalExpenses > $scope.budget
tttt          ? 'assertive'
ttttt  : 'balanced';


//*** FlexChart's selectionChanged event handler
$scope.selectionChanged = function (sender) {
  var category = null;
  if (sender.selection && sender.selection.collectionView.currentItem) {
  t// get the currently selected category
  tcategory = sender.selection.collectionView.currentItem;

  t// navigate to the category history page to display expenses for selected category
  t$state.go('app.category-history', { category: category.slug });
  }
};


//*** set color of FlexChart's plot elements
$scope.itemFormatter = function (engine, hitTestInfo, defaultFormat) {
  if (hitTestInfo.chartElement === wijmo.chart.ChartElement.SeriesSymbol) {
  t// set the SVG fill and stroke based on the category's bgColor property
  tengine.fill = hitTestInfo.item.bgColor;
  tengine.stroke = hitTestInfo.item.bgColor;
  tdefaultFormat();
  }
};

The first few lines are pretty self-explanatory, and are either collecting data from local storage via our services or simply initializing variables used by the view. Next up, we determine the budget message and the CSS class that will be used to style the budget message. To determine the value of these variables, we compare the sum of all expenses against the app’s allotted budget. We also use Angular’s $filter provider to format the difference between the expense total and allotted budget. Lastly, we have two supporting functions for the FlexChart: selectionChanged and itemFormatter. The selectionChanged function acts as an event handler for the FlexChart’s event of the same name. It is in this event handler that we obtain the current category through the selection’s underlying CollectionView and use the category’s slug to navigate to the drilldown page (i.e. Category History). The itemFormatter is used to customize the FlexChart’s elements as they are being rendered, and we will use it to set the fill and stroke of the plot element based on the category it’s bound to. If we run the sample at this point, we should see the Overview screen as follows:

Overview

Closing Thoughts

And that’s it; we’ve created most of the Expense Tracker! Now while the Expense Tracker probably isn’t ready for Google or Apple’s marketplace, it serves as a nice introduction to Ionic and some of the many ways Wijmo can be used conjunction with the Ionic Framework. The full source code for the Expense Tracker app can be found in the Wijmo download. Once you have accessed the zip folder, click "Samples" then "JS" and then "Ionic."

GrapeCity

GrapeCity Developer Tools
comments powered by Disqus