Ionic and Telerik Backend Services: A Healthy Partnership Part 2

In the first part of this article, we learned how to scaffold a basic Ionic app and connect it to the Telerik Backend services to allow users to register, login, and access content. In this part of the series, we’ll build the screens that we need for the food bank to manage their inventory. These will be:

  1. Inventory management;
  2. View low-inventory items;
  3. Request donations;
  4. View requests;

Before building these sections, however, let’s port our code into Telerik AppBuilder so that we are working in one consolidated environment.

Finish this app and win! Details at the bottom of this article.

Step 1: Port your app into AppBuilder

Navigate to your local codebase’s www folder. Select all the files and zip them up into a single file, which we will import:

archive

Log in to the Telerik Platform where you created your backend service. In the Pntry workspace, create a hybrid AppBuilder project by clicking the green button:

create a project

Create a project from the Blank template and give it a name and description:

3

Navigate to the right-hand panel where you see the Project Navigator. Right-click on the project name and select Add > From Archive:

import

Select the Archive.zip that you created on your local computer and import it to AppBuilder. Now you can run the code by choosing ‘Run’ to simulate the device of your choice and see how the app will look on various device screens.

run

The Telerik simulator works nicely to ensure that you are designing your screens correctly.

Telerik Simulator

Step 2: Build the data structure

The data for the food bank is pretty simple. All we need is a ‘Food’ content type. Let’s create it.

In the backend area of your Pntry project, navigate to Data > Types and create a content type called Food. Add three fields to this type called Name (text), RequestDate (date/time) and NumberInStock (number). This will allow the food bank to determine which foods are in stock, show items that are running low, request deliveries of new food, and show the history of requests.

Food content type

Click the ‘lock’ icon on the far right side of the top data type bar and set the permissions of the Food datatype to ‘private’.

Note, on larger screens this icon will have the label ‘permissions’.

This will let a logged-in user read and write only their own data.

permissions

Step 3: Build the screens

On the first screen that we’ll build, a member of the food bank will be able to input items to their food bank’s inventory. On the second screen, the app will display the items that are needed based on the numbers in inventory. We’ll say that if an item has fewer than two items in stock, it will appear on the ‘Current Needs’ screen. From that screen, the user can select a group of items and request a donation.

Now that we have the structure of our code well implemented, it becomes easy to create a front-end screen, hook it up to a function in the controller, which in turn calls the service. You’ll want to keep the documentation for Telerik Backend Services handy as it explains how to access your data.

The Manage Inventory Screen

First, let’s build an inventory screen to allow a user to add items. This screen will have a form at the top to add items and each item added to the list will include a button for editing the inventory quantity and a delete button to remove an item. It will look like this:

inventory screen

In templates/inventory.html, add the following markup:

<ion-view title="Inventory" ng-init="getAllFood()">
  <ion-nav-buttons side="left">
    <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
  </ion-nav-buttons>  
  <ion-content class="has-header">

    <div class="list list-inset"> 


    <div class="item item-divider item-text-wrap">
        Inventory
        <p class="small">Add food items to this list.</p>
    </div>  



        <div class="item item-input-inset">

            <label class="item-input-wrapper">          

                <input type="text" name="Name" placeholder="Add a food item" ng-model="food.Name" required>

            </label>

            <label class="item-input-wrapper">          

                <input type="text" name="NumberInStock" placeholder="Number in stock" ng-model="food.NumberInStock" required>

            </label>

            <button class="button button-small button-balanced" ng-click="addFood(food)">
              <i class="icon ion-plus"></i>
            </button>

        </div>

        <ion-list>

        <ion-item ng-repeat="food in inventory">

        <div class="item item-input-inset">

            <label class="item-wrapper">            

                <input type="text" name="Name" placeholder="food items" ng-model="food.Name" required>

            </label>

            <label class="item-input-wrapper">          

                <input type="text" name="NumberInStock" placeholder="Amount" ng-model="food.NumberInStock" required>

            </label>

            <button class="button button-small button-assertive" ng-click="removeFood($index)">
              <i class="icon ion-minus"></i>
            </button>

            <button class="button button-small button-calm" ng-click="updateFood($index)">
                <i class="icon ion-checkmark"></i>
            </button>

        </div>

       </ion-item>

      </ion-list>

     </div>

  </ion-content>
</ion-view>

We need to add four functions to the controller: getAllFood(), addFood(), removeFood(), and updateFood(). Note that in the addFood function we pass through the form data, whereas in the two latter functions we pass through the data associated to the index of the food item that we want to update or remove as it is set in the repeater.

In the js/controllers.js file, add a blank object at the top as a bookmark for our form data:

$scope.foodData = {
  Name: null,
  NumberInStock: 0
};

Edit the third line of the controller file so that we include a new factory in the service called Food.

.controller('AppCtrl', function($state, $scope, $ionicModal, $ionicPopup, User, Food)

Then, add the four functions to the controller.js file:

//get all the food items in Food data type
$scope.getAllFood = function() {     
Food.getAllFood().then(function(data){
      $scope.inventory = data.result;
      $scope.$apply();
    }); 
  };
//add a new food item to the Food data type. Once added, the Id is returned. Send that Id back to the service and retrieve the food item, then update scope
$scope.addFood = function(food){
    if(angular.isDefined(food) && angular.isDefined(food.Name)){
    Food.addFood(food).then(function(data){
      Food.getOneFood(data.result.Id).then(function(data){
        $scope.inventory.push(data.result);
        $scope.$apply();
      })
    })
  }
    else{
       $ionicPopup.alert({
          title: 'Food name is required',
          template: 'Please try again!'
        });
      }

  };

 //remove a food item
 $scope.removeFood = function(idx){
    var food_to_delete = $scope.inventory[idx];

      var confirmPopup = $ionicPopup.confirm({
         title: 'Delete?',
         template: 'Are you sure you want to delete this food item?'
       });
       confirmPopup.then(function(res) {
         if(res) {
            Food.removeFood(food_to_delete.Id).then(function(data){              
              $scope.inventory.splice(idx,1);
              $scope.$apply();     
            });
         } else {
           $ionicPopup.alert({
              title: 'Sorry, there was a problem removing this item',
              template: 'Please try again!'
            });
         }
       });
  };
//update the quantity
$scope.updateFood = function(idx){

    var food_to_update = $scope.inventory[idx];

      var confirmPopup = $ionicPopup.confirm({
         title: 'Update?',
         template: 'Are you sure you want to update this food item?'
       });
       confirmPopup.then(function(res) {
         if(res) {
            Food.updateFood(food_to_update).then(function(data){
              $scope.$apply();     
            });
         } else {
           $ionicPopup.alert({
              title: 'Sorry, there was a problem updating this item',
              template: 'Please try again!'
            });
         }
       });
  };

Note the $scope.$apply() function. This little gem is placed in the controller whenever a call is made to the service and data is returned in order to tell the bound data on the front-end to refresh the view. More information can be found here:

These four functions offer a little view into the way CRUD is handled by this type of Angular-based app. We can simply view data or we add an element and push data into the inventory scope. Alternately we can remove an item and splice $scope.inventory. Or we can simply update an item and get back refreshed data. Regardless, we always need to $scope.$apply() to make the front-end reflect the changes.

Create A Food Factory

A Food Factory

© http://media.treehugger.com

Let’s turn our attention to the js/services.js file. We will create a new "Food" factory that will leverage the API Key found in the API factory:

.factory('Food', function (API) {

Let’s edit the service to add the CRUD functionality we built out in the controller. When we’re done, the new factory looks like this:

.factory('Food', function (API) {
//let's call Everlive using our token
var el = new Everlive({
        apiKey: API,
        scheme: 'https',
        token: localStorage.getItem('token')
     });
//query the Food data type
var data = el.data('Food');
var query = new Everlive.Query();
  return {
  //read the Food data
    getAllFood: function(){        
        return data.get()
            .then(function (data) {
                return data;
            },
            function(error) {
                return error;
            });               
    },
    //retrieve added item
    getOneFood: function(id){
        return data.getById(id)
            .then(function (data) {
                return data;
            },
            function(error) {
                return error;
            });     
    },
    //add item, setting requested date to null 
    addFood: function(foodData){
        return data.create({Name:foodData.Name,NumberInStock:parseInt(foodData.NumberInStock),RequestDate:null})
          .then(function (data) {
                return data;
            },
            function(error) {
                return error;
            });

      },
     //remove an item
     removeFood: function(id){
         return data.destroySingle({Id:id})
          .then(function (data) {
                return data;
            },
            function(error) {
                return error;
            });

      },
      //update an item
      updateFood: function(foodData){
          return data.updateSingle({Id:foodData.Id,'NumberInStock':parseInt(foodData.NumberInStock)})
          .then(function (data) {
                return data;
            },
            function(error) {
                return error;
            });

      },

  }
});

Note, use parseInt() around numbers to make sure that the proper data type reaches Backend Services

The Current Needs Screen

The Current Needs screen is a little simpler, because all we do is list the items that have two or fewer items in stock. It will look like this:

current needs screen

Edit the templates/needs.html file to add just a few lines of markup:

<ion-view title="We Need">
  <ion-nav-buttons side="left">
    <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
  </ion-nav-buttons>  
  <ion-content class="has-header">

  <div class="list list-inset"> 

    <div class="item item-divider item-text-wrap">
        These are the foods with inventory of two or fewer items in stock
    </div>  

    <ion-list>
      <ion-item ng-repeat="food in neededfood">

             {{food.Name}}


      </ion-item>
    </ion-list>

    </div>

  </ion-content>
</ion-view>

In the controller, we need to add the getNeededFoods() function:

$scope.getNeededFoods = function() {    
  Food.getNeededFoods().then(function(data){
$scope.neededfood = data.result;
$scope.$apply();
  });
};

…and, you guessed it, add to the service:

getNeededFoods: function(){
    query.where().lte('NumberInStock', 2);
    return data.get(query)
        .then(function (data) {
            return data;
        },
        function(error) {
            return error;
        });               
},

Finally, add this function to templates/menu.html so that the data is refreshed when coming to this page via the menu:

<ion-item nav-clear menu-close ng-click="testLoginStatus();getNeededFoods()" href="#/app/needs">
    <i class="icon ion-compose"></i> Current Needs
</ion-item>

This example shows how to query your data type to get just what you need. Here, we are getting only the items where ‘NumberInStock’ is less than or equal to 2.

That’s it for two out of our four screens in our original prototype! You can test them in AppBuilder by clicking Run and then selecting the device type on which you wish to view your work.

run

The Request Donations Screen

The next screen we need to build is where a food bank will view their inventory sorted by amount in stock and request donations. The donation requests are recorded by adding the request timestamp in the Food data type column called RequestDate.

The request screen itself is pretty simple. Replace the current code in templates/requestdonations.html with this markup:

<ion-view title="Request">
  <ion-nav-buttons side="left">
    <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
  </ion-nav-buttons>  
  <ion-content class="has-header">

  <div class="list list-inset"> 

    <div class="item item-divider item-text-wrap">
        Check off foods to be requested from donors
    </div>  

    <ion-list>
      <ion-item ng-repeat="food in sortedfood"  class="item item-checkbox">

          <label class="checkbox">

        <input type="checkbox" name="selected"
          ng-click="updateSelection($event, food.Id)">

          </label>



          {{food.Name}} - {{food.NumberInStock}}


      </ion-item>
    </ion-list>

      <button class="button button-block button-balanced" ng-click="requestDonation(food)">Request Donation</button>

    </div>

  </ion-content>
</ion-view>

The end result should look like this:

donation screen

Notice the food data needs to be sorted by inventory, fewest to greatest. We are going to query the Food table again, but this time we will sort our query. In the controller, add:

$scope.getSortedFoods = function() {    
  Food.getSortedFoods().then(function(data){
    $scope.sortedfood = data.result;
    $scope.$apply();
  });

};

In the service, add:

getSortedFoods: function(){
    query.order('NumberInStock');
    return data.get(query)
        .then(function (data) {
            return data;
        },
        function(error) {
            return error;
        });               
},

…and to the menu:

<ion-item nav-clear menu-close ng-click="testLoginStatus();getSortedFoods()" href="#/app/requestdonation">
     <i class="icon ion-speakerphone"></i> Request A Donation
</ion-item>

See how easy it is to sort a dataset? Just use query.order with the field you want to sort by. Now this screen shows the entire inventory with the most-needed-items at the top of the list. When the user presses the ‘request’ button, we need to update multiple records within the dataset, setting RequestDate to the current date for those items being checked off. We need to enable the user to check and uncheck the checkboxes to update an array of items, so in the controller, add:

var selected = $scope.selected = [];
//checking or unchecking a checkbox is tracked, and $scope.selected is incremented and decremented
$scope.updateSelection = function(e,id){
   var checkbox = e.target;
   var action = (checkbox.checked ? 'add' : 'remove');
      if (action == 'add' & selected.indexOf(id) == -1) selected.push(id);
      if (action == 'remove' && selected.indexOf(id) != -1) selected.splice(selected.indexOf(id), 1);
  };

//request a donation by sending in the selected Ids and return the number of records updated
$scope.requestDonation = function() {    
  Food.requestDonation($scope.selected).then(function(data){
    $ionicPopup.alert({
       title: data.result + ' item(s) were requested',
           template: 'Track your requests on the donation requests screen.'
        });
  });

};

In the service, send an array of IDs as a filter to update only those items that are checked:

requestDonation: function(foodData){
    var now = new Date();       
    var model = {'RequestDate': now};
    query.where().isin("Id", foodData).done();
     return data.update(model, query)   
         .then(function (data) {
            return data;                      
        },
        function(error) {
           return error;
         });         
 },

Now, you are able to request a selection of items which we can then group by RequestDate in the Food data type.

It would be nice to link up this request functionality to actually tell a group of interested individuals about the needs of the bank. You could use cloud code or the Social Sharing plugin to post this request via email, Twitter, WhatsApp or on a Facebook page. More information about this plugin can be found here.

Build the Donation Request View Screen

At this point, we have almost all the screens we need for this app to be usable by a food bank. We need a way to group the requested food items into a list that is viewable and a pop-up to show what items were requested.

Due to the very simple data structure of this app, subsequent requests overwrite previous RequestDate timestamps. An alternate strategy would be to copy every request to a new record.

Currently, Telerik Backend Services does not support grouping of data via a database query as you may be used to doing in SQL. Instead, we will make use of a handy Angular grouping filter to handle this task on the front-end. In the Project Navigator, right-click on the lib folder and add a new folder called angular-filter. In this folder, create a new JavaScript file named angular-filter.min.js and copy the the code from this file..

add angular filter

Then you can add a reference to that file in index.html under the Everlive .js file:

<script src="lib/angular-filter/angular-filter.min.js"></script>

Reference it in app.js:

angular.module('pntry', ['ionic', 'pntry.controllers', 'pntry.services','angular.filter'])

Your markup for the donationrequests.html file should look like this:

<ion-view title="Requested Donations">
  <ion-nav-buttons side="left">
    <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
  </ion-nav-buttons>  
  <ion-content class="has-header">

    <div class="list list-inset"> 


    <div class="item item-divider item-text-wrap">
        Donation Requests
    </div>  

        <ion-list>

            <ion-item ng-repeat="(key, value) in requestedfood | groupBy: 'RequestDate'">
                <span class="item-icon-right" ng-click="viewRequest(key)">
                    <i class="icon ion-arrow-right-b"></i>
                        {{key}}
                </span>

       </ion-item>

      </ion-list>

     </div>

  </ion-content>
</ion-view>

Eventually this screen will show a series of dates:

donation screen

Let’s complete the controller.js function:

$scope.getAllRequests = function() {    
  Food.getAllRequests().then(function(data){
	$scope.requestedfood = (data.result);
	$scope.$apply();
  }); 
};

In the service, our last two functions show the data filter:

getAllRequests: function(){
  query.where().ne('RequestDate', null);
    return data.get(query)
        .then(function (data) {
            return data;
        },
        function(error) {
            return error;
        });               
},

Finally, add this function to templates/menu.html:

<ion-item nav-clear menu-close ng-click="testLoginStatus();getAllRequests()" href="#/app/donationrequests">
    <i class="icon ion-help-buoy"></i> Donation Requests
</ion-item>

The very last step is to display the details of the request. For now, let’s simply view it as a pop-up.

Note, because we have to filter our data against a date, and working with dates in JavaScript is tricky, I am using moment.js to format a date string against which I am testing the RequestDate. Install moment.js in the same way you installed angular-filter above; create a folder in /lib, copy the minified file into it, and reference it in index.html. You’ll be glad you did!

In the controller, add a function to get the items associated to a request when the user taps a date in the list:

$scope.viewRequest = function(key){      
    Food.getRequestByDate(key).then(function(data){
      var list = ''; 
      for (i = 0; i < data.result.length; i++) {
          list = list + ' ' + data.result[i].Name
      }
       $ionicPopup.alert({
           title: 'You requested:',
           template: list
        });
  });
};

In the service, get the requests available within a range:

getRequestByDate: function(key){
    var newkey = moment().format('MMMM Do YYYY, h:mm:ss');
    query.where().gte("RequestDate", newkey).lte("RequestDate", newkey).done();
    return data.get(query)
        .then(function (data) {
            return data;                
        },
        function(error) {
            return error;                
        });               
},

When clicking a given date, we simply show a pop-up for now with an array, formatted as a list, of the details of the request.

Final Step…Finish The App

Wow, we have done a lot of work. This app tutorial gives us a great way to see how Telerik Backend Services work with the front-end display, and how you can manipulate and filter the data your users input using the powerful methods offered by Backend Services. I encourage you to keep experimenting with all this firepower and see what more you can build!

Finish this app! If you are able to finish this app and deploy it live, please email me at jen.looper at telerik.com. The first developer of this app to release it to one or more of the app stores will receive a $50 prize which we will also match and donate to the food bank of your choice. If you need help, I’m happy to give you a hand. Documentation on releasing an app using AppBuilder can be found here. Happy Holidays!

Comments

  • Pingback: Dew Drop – December 26, 2014 (#1922) | Morning Dew()

  • Just in case you don’t see the comment on G+, formatting appears to be broken at the end of the article, around your email address.

    • Jen Looper

      Thanks Ray, I think it’s sorted now. Want to finish the app and win? 😀

      • I’m off this week so I doubt I’ll have time, but I will make time to try out the sample code anyway. I’ve been a huge fan of Parse and didn’t realize Telerik had something similar.

        • Jen Looper

          I’m coming from the same place exactly, having build several production apps using Corona SDK + Parse. But it’s really nice to have a one-stop shop with AppBuilder where you can build the FE any way you like and use the noSQL backend and firepower to your heart’s content.

  • Michael Duddles

    Hi Jen,

    Could you please clarify the following direction? What do you mean by “the top” and “bookmark” of the controller.js exactly? Maybe do you have a final version code source you can provide?

    The above direction is vague, and I’m getting white-screened after integrating further JS functions, so I want to make sure my code is right before I proceed further. Tutorial one was clear as day, but not so much on Tutorial 2. Thanks!

    You wrote:

    “In the js/controllers.js file, add a blank object at the top as a bookmark for our form data:”

    $scope.foodData = {
    Name: null,
    NumberInStock: 0
    };

  • Jen Looper

    hi, Michael, sorry you’re having issues. This is where the pedal hits the metal, so it can get complicated. You basically need to add the foodData object at the top of the file. It will be used later on. I can send you the full code via email; you can email me at jen.looper@telerik.com and I can zip it up.

    • Michael Duddles

      Perfect. Will email you then, Jen. Thanks!