/ nodejs

Ionic Framework with PouchDB, CouchDB, and all that fun jazz

Recently I started working on an app to manage my investments.
I work for a privately traded company and have recently been invited to buy in. I've made my initial purchase (huzzah!) and decided to depress myself and run what-if scenarios based on shares offered at what price, shares that I decided to purchase, and valuations.

Since I haven't really built anything with Ionic, Couch or Pouch, I figured this would be a great opportunity to learn something new. Since I don't (currently) own a Mac PC, I will only be demonstrating the Android development of this application.

Let's get started.

Pre-Requisites

NodeJS - most of my blog uses NodeJS, Bower, Ionic Framework, Cordova, CouchDB
add-cors-to-couchdb -- to do exactly what it says.

$ npm install -g bower
$ npm install -g add-cors-to-couchdb
$ npm install -g ionic cordova
$ sudo apt-get install couchdb

Then you have to run the add-cors-to-couchdb from the command line:

$ add-cors-to-couchdb

Now you can access it from hosts outside 5984. Which is what you will need for building an app created with Ionic

If the CouchDB installation doesn't work for you, feel free to follow the tutorial on Digital Ocean

You can test out your new CouchDB installation (if you aren't familiar with it already) on my previous blog post found here. Else, give your new installation a quick curl test to make sure it's up and running

$ curl localhost:5984

Should yield something like:

{
  "couchdb": "Welcome",
  "uuid": "b959c0f17d5530befa418642ec24b128",
  "version": "1.6.1",
  "vendor": {
    "version": "1.6.1",
    "name": "The Apache Software Foundation"
  }
}

Assuming you get a similar response, we're good to go!


PRO TIP!

When testing it on mobile, you will need to configure CouchDB to listen on all interfaces instead of just 127.0.0.1. To do this, go to open up /etc/couchdb/defaults.ini and set bind_address to 0.0.0.0 then reset the service. This will save you all my headaches later on :-)


Once these finish, we're going to go ahead and create our app. You can use fancy tools like Ionic Creator to make an enthralling UI, however I'm just making this for myself and generic off-the-shelf design is good enough for me.
Using the newly installed Ionic CLI create a new app called 'invest-me' with the following command:

$ ionic start invest-me tabs

This will create a new app called invest-me with the Ionic Template called tabs.
Next we want to add the platforms that we are going to support. I'll add both for you. I will, however, only be using android for now.

$ cd invest-me
$ ionic platform add android
$ ionic platform add ios

Kick your app up in a web browser (for development purposes) using the following command:

$ ionic serve

Your browser should open up with a demo application that (imo) provides us with a bunch of gross code that we have to mess around with to get our app up and running. I prefer to structure my angular apps using the style-guide by John Papa on github but I have honestly never built any hybrid apps for mobile before so I don't know if it's feasible or not. I'll have to do a test of this in the future :)

ANYWAYS!

Next step is to setup PouchDB -- You can read all the documentation on their website but here is the rundown.

PouchDB snycs your CouchDB database to the user and keeps their content up to date. So if you make a change on one device it is synced to your other devices automatically. Now, you can imagine that if you had an app with a LOAD of user data and a LOAD of users with permissions management, access restrictions, etc. this might not be the stack for you. This app is going to keep track of one person's data and could be extensible to other one persons wanting to track their data. So we can just keep creating new databases for that user.

Let's install PouchDB then.
In your app directory and using bower:

$ bower install pouchdb --save

This is going to save to www/lib as per your .bowerrc file. Feel free to investigate those to understand how they work. Essentially it redirects your bower installed files to www/lib instead of bower_components. It's Super Effective (shameless Pokemon reference)

Inject this into your www/index.html file. I like to put it just before my application injections.

  ...
  <!-- pouchdb import-->
  <script src="lib/pouchdb/dist/pouchdb.js"></script>
    
  <!-- your app's js -->
  ...

Here is where we start picking apart, adding, removing and all around butchering this neato looking starter app provided to us by the Ionic team. I'm assuming people have a relatively okay understanding of AngularJS and will be able to follow along. If not feel free to ask in the comments below :-)

We need to initialize our datastore. This is where all the magic is going to happen and we are simply going to inject this throughout our application wherever we need to track our data.

Head over to www/js/services.js This is where we are going to set up our magic.
Create a new service called StorageService and inject $rootScope and $q then create all of our function stubs.

In case you don't know:

  • $rootScope - so that we can broadcast our changes to all listening portions of our application
  • $q - Angular's promise service for async functions.
angular.module('starter.services', [])
.service('StorageService', ['$rootScope', '$q', function($rootScope, $q){
  var service = {
    listen:listen,
    stopListening:stopListening,
    sync:sync,
    save:save,
    remove:remove,
    get:get,
    destroy:destroy,
    query:query
  };
  /**
  * Private Variables
  */
  var changeListener;
  
  var database = new PouchDB('invest-james');
  
  database.sync('http://localhost:5984/invest-james', {live:true, retry:true});
  
  //Return Service Reference
  return service;
  
  /**
  * Public Service Functions
  */
  function listen(){
    //TODO: Implement
  }
  function stopListening(){
    //TODO: Implement
  }
  function sync(){
    //TODO: Implement
  }
  function save(){
    //TODO: Implement
  }
  function delete(){
    //TODO: Implement
  }
  function get(){
    //TODO: Implement
  }
  function destroy(){
    //TODO: Implement
  }
  function query(){
    //TODO: Implement
  }
}])
...

PRO TIP

Localhosts only work for local development (i.e. testing in the browser) When you set up an emulator you will need a physical I.P. address. Replace localhost:5984 with xxx.xxx.xxx.xxx:5984 when emulating on IOS, Andorid, Windows (haha, windows phones). More headaches saved


This should be pretty straight forward. The only thing we're really doing that I didn't explain is initializing our datastore.
var database = new PouchDB('invest-james'); is initializing our local storage store. database.sync({'http://localhost:5984/invest-james', {live:true, retry:true}) Is syncing our local store to a remote store and is live and will retry on failure.

Let's set up the first and most important part of making this application work: the listener. We will require a listener to capture changes in the remote database so that it will be reflected in our application.

...
  function listen(){
    changeListener = database.changes({
      live:true,
      include_docs:true
    })
    .on('change', function(change){
      if(!change.deleted){
        $rootScope.$broadcast('StorageService:change', change);
      }
      else{
        $rootScope.$broadcast('StorageService:delete', change);       
      }
    });
  }
...

This is probably the second most complicated bit of code (yup it's this easy). Essentially what we do here is set up our changeListener variable and tell it to listen to database changes, sync with the remote, and to include the documents that are changed (refer to the previous CouchDB post). Then we have our change listener trigger when a 'change' event is fired. If the change does not contain a deleted object, then we broadcast that we've experienced a change in our data. Else we broadcast that we've deleted an object.

This is helpful for our app to respond appropriately. If we have a 'change' we can 'push or update' the data into our view. If we have a 'delete' we can delete the object from our view. There's not really that much else to do when dealing with CRUD operations!

Next, we'll have some super simple stuff to come down from that difficult example from earlier. Let's set up the 'That's enough, stop syncing' method.
Ready?

  ...
  function stopListening(){
    changeListener.cancel();
  }
  ...

If we're ever feeling like we need to resync our data, this is how we'd do it.

  function sync(remoteDatabase){
    database.sync(remoteDatabase, {live:true, retry:true})
  }

Where remoteDatabase is a string representation of our DB. I.E. 'http://localhost:5986/invest-james'

Our next piece will be our C&U portion of CRUD. Creating & Updating. Why not just do it all in one big function?

  function save(doc){
    var deferred = $q.defer; //the promise
    if(doc._id){ //the document has an id, so let's update it
      database.put(doc)
      .then(function(response){
        deferred.resolve(response); //resolve with response
      })
      .catch(function(err){
        deferred.reject(error); //reject with error
      });
    }
    else{ //There is no id, so this is obvs new
      database.post(doc)
      .then(function(response){
        deferred.resolve(response); //resolve with response
      })
      .catch(function(err){
        deferred.reject(err); //reject with error
      });
    }
    //Return the Promise
    return deferred.promise;
  }

That's about it for C&U. What about R&D? -> Not research and development.

  function remove(docId, rev){
    return database.remove(doc, rev);
  }
  function get(docId){
    return database.get(docId);
  }

Yup. More simplicity! I love it!
There's some scary stuff you can do. I don't do it, but you can. Which is why I'm showing you.

  function destroy(){
    return database.destroy();
  }

I think it's pretty self explanatory. Database, Destroy. This reminds me of a Dimmu Borgir tune where there's some super creepy marching then a harsh voice that comes up saying 'Gentlemen, Destroy!' then some super heavy riffs. #metalAF. I digress. Try not to destroy an important database from your front end, or let someone access the console, load your service, destroy your database and runaway laughing.

Note: This doesn't destroy remote databases. However dramatic effect is what keeps people reading

The final section to talk about is the query. This is for using those pesky design documents used for extracting data. I discussed them in a previous post but will do a quick overview of how they work here just because it's my blog and I can blog if I want to.
Design Documents are JSON docs that describe how to return data. You can create them so that they are organized for your mind and you can really do as you please with them. A bonus for design documents is that you can create then on-the-fly. Let's say your user wants to query something super random. Well let them make a map-reduce to query for it! Here is an example.

{
  _id:'_deisgn/offers',
  views:{
    list:{
      map:'function(doc){ if(doc.type === 'offer') emit(doc);}'
    }
  }
}

This is an example view that I have for this project (which you have STILL not been introduced to) It's essentially a view that rips through the documents and if they are of type 'offer' then you return it. Else you move along. Pretty simple, no?

We can make this a permanent object in our new system by using our services' 'save' function!

//Not Code to put anywhere in our app. Just an example
var design_doc = 
{
  _id:'_deisgn/offers',
  views:{
    list:{
      map:'function(doc){ if(doc.type === 'offer') emit(doc);}'
    }
  }
};

StorageService.save(design_doc);

Now we can simply call this query whenever we feel like it using the query() function!

  function query(query){
    return database.query(query);
  }
//Not code to put anywhere in our app. Just for demo
StorageService.query('offers/list').then(...).catch(...);

PouchDB does the rest for us!

Here's what our services.js looks like now. I removed the default services for the messages and stuff.

(function(){
  angular.module('starter.services', [])
  .service("StorageService", ["$rootScope", "$q", function($rootScope, $q) {
    var service ={
      listen:listen,
      stopListening:stopListening,
      sync:sync,
      save:save,
      remove:remove,
      get:get,
      destroy:destroy,
      query:query
    };
    /**
    * Private Variables
    */
    var changeListener;

    var database = new PouchDB('invest-james');

    database.sync('http://localhost:5984/invest-james', {live:true, retry:true});

    //Return Service Reference
    return service;

    /**
    * Public Service Functions
    */
    function listen(){
      changeListener = database.changes({
        live:true,
        include_docs:true
      })
      .on('change', function(change){
        if(!change.deleted){
          $rootScope.$broadcast('StorageService:change', change);
        }
        else{
          $rootScope.$broadcast('StorageService:delete', change);
        }
      });
    }
    function stopListening(){
      changeListener.cancel();
    }

    function sync(remoteDatabase){
      database.sync(remoteDatabase, {live:true, retry:true})
    }
    function save(doc){
      var deferred = $q.defer(); //the promise
      //the document has an id, so let's update it
      if(doc._id) {
        database.put(doc)
        .then(function(response){
          deferred.resolve(response); //resolve with response
        })
        .catch(function(err){
          deferred.reject(error); //reject with error
        });
      }
      //There is no id, so this is obvs new
      else{
        database.post(doc)
        .then(function(response){
          deferred.resolve(response); //resolve with response
        })
        .catch(function(err){
          deferred.reject(err); //reject with error
        });
      }
    }
    //Return the Promise
    return deferred.promise;

    function remove(docId, rev){
      return database.remove(doc, rev);
    }
    function get(docId){
      return database.get(docId);
    }
    function destroy(){
      return database.destroy();
    }
    function query(query){
      return database.query(query);
    }
  }]);
}());

Now here's where things might get a little confusing -- Editing the default generated stuff.

Here's the plan:
Delete the all the states except for .state('tab') from www/js/app.js as we're going to rebuild those.

Replace the contents of www/templates/tabs.html with the following:

<ion-tabs class="tabs-icon-top tabs-color-active-positive">

  <!-- Purchases Tab -->
  <ion-tab title="Purchases" icon-off="ion-ios-briefcase-outline" icon-on="ion-ios-briefcase" href="#/tab/purchases">
    <ion-nav-view name="tab-purchases"></ion-nav-view>
  </ion-tab>

  <!-- Offers Tab -->
  <ion-tab title="Offers" icon-off="ion-ios-list-outline" icon-on="ion-ios-list" href="#/tab/offers">
    <ion-nav-view name="tab-offers"></ion-nav-view>
  </ion-tab>


</ion-tabs>

Next we are going to gut the www/js/app.js file's states with the following:

    ...
    //omitted for berevity
    $stateProvider
    // setup an abstract state for the tabs directive
    .state('tab', {
      url: '/tab',
      abstract: true,
      templateUrl: 'templates/tabs.html'
    })
    // Each tab has its own nav history stack:
    .state('tab.offers', {
      url: '/offers',
      views: {
        'tab-offers': {
          templateUrl: 'templates/tab-offers.html',
          controller: 'OffersCtrl'
        }
      }
    })
    .state('tab.offer-detail',{
      url:'/offers/:offerId',
      views:{
        'tab-offers':{
          templateUrl:'templates/offer-detail.html',
          controller:'OfferCtrl'
        }
      },
      params:{offerId:null, offer:null}
    })
    .state('tab.purchases', {
      url: '/purchases',
      views: {
        'tab-purchases': {
          templateUrl: 'templates/tab-purchases.html',
          controller: 'PurchasesCtrl'
        }
      }
    });

    // if none of the above states are matched, use this as the fallback
    $urlRouterProvider.otherwise('/tab/purchases');
    //omitted for berevity
    ...

Next make sure there's at least an empty file in the www/templates folder with the following titles:

  • tab-offers.html: Template for listing offers
  • offer.modal.html: Template for creating a new offer (limited entry form)
  • offer-detail.html: Template detailing offer, intent and purchasing
  • offer-detail.modal.html: Template for editing offer, intent, and purchasing
  • tab-purchases.html: Template for showing our purchased stocks

Let's get started with some actual functionality now!
Tab-Purchases is our default state. What should our data look like here? You can make up your mind on that, but I'm going to use what I ultimately settled on which is a very NoSQL way of tackling this.

{
   "offerNumber": "112340",
   "type": "offer",
   "offer": {
       "date": "2016-01-30T06:00:00.000Z",
       "shares_available": 100,
       "shares_offered": 2,
       "cost_per_share": 0.5,
       "expires_on": "2016-01-30T06:00:00.000Z"
   },
   "intent": {
       "status": "submitted",
       "submitted_on": "2016-09-21T06:00:00.000Z",
       "shares_requested": 2
   },
   "purchase": {
       "status": "pending",
       "shares_purchased": 0,
       "accepted_on": ""
   }
}

This structure would work for me as this is essentially the logic of the process. I'm provided an update on the number of available shares, the maximum I'm available to purchase, and the cost per share. If i'm interested, I submit intent to purchase with the number. If there is more interest than available shares, they are drawn for randomly. If I win the draw, I get to purchase. Easy right? And it's all submitted in one document.
Now how do I get this out?

  1. Offers:
    This is easy. if the type is of 'offer' then return it, else don't.
  2. Purchases:
    This is also easy. If the type is 'offer' and the status is 'accepted' then Yay! You can decide on your logic for yourself. I'm rolling with [pending, denied, accepted] for my intent and purchase status.

Controllers

We are going to start with the OffersCtrl. This is because it is the simplest by far. We are going to set up our service, set it to listen, and then watch for broadcasts of data received. Pretty simple!

...
   angular.module('starter.controllers', ['starter.services'])
  .controller('OffersCtrl', ['$scope', '$rootScope', 'StorageService', function($scope, $rootScope, StorageService) {
    $scope.offers = {};
    StorageService.listen();
    //Listen for broadcasts
    $rootScope.$on('StorageService:change', function(ev, data){
      if(data.doc.type === 'offer'){
        $scope.offers[data.doc._id] = data.doc;
      }
      $scope.$apply();
    });
    $rootScope.$on('StorageService:delete', function(ev, data){
      if($scope.offers[data.doc._id]){
        delete $scope.offers[data.doc._id];
      }
      $scope.$apply();
    })
  }])
...

What's going on here now -- We've got a controller that picks up the StorageService and starts listening. When the service triggers a change, we check to see if it's of type 'offer'. If so, we add it to the list and expose it to the view.

Here's what the view looks like

<ion-view view-title="Offers">
  <ion-content>
    <ion-list>
      <ion-item ng-repeat="(k,v) in offers" ui-sref="tab.offer-detail({offerId:k, offer:v})">
        <h3>Offer: {{v.offerNumber}}</h3>
        <p>{{v.offer.shares_offered}} shares @ {{v.offer.cost_per_share|currency}} ea</p>
        <p>{{v.offer.date | date}}</p>
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

By the language, you can see that this is displaying a list of offers. Not bad.

Our PurchasesCtrl looks very similar:

.controller('PurchasesCtrl', ['$scope', '$rootScope', 'StorageService', function($scope, $rootScope, StorageService) {
    $scope.purchases = {};
    StorageService.listen();
    $rootScope.$on('StorageService:change', function(ev, data){
      console.log('found datas');
      if(data.doc.type === 'offer'){
        console.log(data.doc.purchase.status);
        if(data.doc.purchase.status === 'accepted'){
          console.log(data.doc);
          $scope.purchases[data.doc._id] = data.doc;
        }
      }
      $scope.$apply();
    })
  }])

The view also looks similar:

<ion-view view-title="Purchases">
  <ion-content>
    <ion-list>
      <ion-item ng-repeat="(k,v) in purchases">
        <h3>Offer: {{v.offerNumber}}</h3>
        <p>{{v.purchase.shares_purchased}} shares purchased @ {{v.offer.cost_per_share|currency}} ea</p>
        <p>{{v.purchase.date | date}}</p>
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

If you wanted to test the synchronization effect, you could simply post a new document to your CouchDB and see the pages get populated. However that's probably not going to be what you're going to do in a real app, so I guess we should make some modifications so we can create some new data.

In templates/tab-offers.html let's add an 'Add' button in the top-right corner of the headerbar and link it up to a (currently) not-real function in our OffersCtrl.

<ion-view view-title="Offers">
  <ion-nav-buttons side="secondary">
    <button class="button" ng-click="addOffer()">
      Add
    </button>
  </ion-nav-buttons>
  <ion-content>
    <ion-list>
      <ion-item ng-repeat="(k,v) in offers" ui-sref="tab.offer-detail({offerId:k, offer:v})">
        <h3>Offer: {{v.offerNumber}}</h3>
        <p>{{v.offer.shares_offered}} shares @ {{v.offer.cost_per_share|currency}} ea</p>
        <p>{{v.offer.date | date}}</p>
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

You can see that we added an action for the button click called addOffer(). Let's figure out how we want to do this.

I like to create new items using a 'popup' style view. It makes it feel like you're actually adding something to a list as opposed to just creating something in the abyss. We will use $ionicModal to do this.

In your controller, inject $ionicModal so we can access it.

  .controller('OffersCtrl', ['$scope', '$rootScope', '$ionicModal', 'StorageService', function($scope, $rootScope, $ionicModal, StorageService) {
...

Then we can set it up.

...
    $ionicModal.fromTemplateUrl('templates/offer.modal.html', {
      scope:$scope,
      animation:'slide-in-up',
      focusFirstInput:true
    })
    .then(function(modal){
      $scope.modal = modal;
    });
...

Here, we are creating a modal object that we can call upon and building it from a template view at templates/offer.modal.html which we haven't created yet. We are also exposing our controllers scope to make it feel right at home with the current state of the app.
Let's make our modal view now.

<ion-modal-view>
  <ion-header-bar>
    <h1 class="title">New Offer</h1>
    <div class="buttons">
      <button class="button" ng-click="closeModal()">Cancel</button>
    </div>
  </ion-header-bar>
  <ion-content class="padding">
    <ion-list>
      <label class="item item-input">
        <span class="input-label">Offer Number</span>
        <input type="text" ng-model="offer.offerNumber" placeholder="Offer Number">
      </label>
      <label class="item item-input">
        <span class="input-label">Received On</span>
        <input type="date" ng-model="offer.offer.date" placeholder="Received On">
      </label>
      <label class="item item-input">
        <span class="input-label">Expires On</span>
        <input type="date" ng-model="offer.offer.expires_on" placeholder="Expires On">
      </label>
      <label class="item item-input">
        <span class="input-label">Available Shares</span>
        <input type="number" ng-model="offer.offer.shares_available" placeholder="Available Shares">
      </label>
      <label class="item item-input">
        <span class="input-label">Shares Offered</span>
        <input type="number" ng-model="offer.offer.shares_offered" placeholder="Shares Offered">
      </label>
      <label class="item item-input">
        <span class="input-label">Cost Per Share (CAD)</span>
        <input type="number" step = "any" ng-model="offer.offer.cost_per_share" placeholder="Cost Per Share (CAD)">
      </label>
    </ion-list>
    <button class="button button-full button-positive" ng-click="createOffer(offer)">Save</button>
  </ion-content>
</ion-modal-view>

Here is where we wire up our offer. This view (imo) should only allow a user to enter the offer. If you're updating your offer, intent, and purchase all in one go, you're not managing your data very well :).

You may have noticed that we still haven't created our addOffer() function yet. We need this to display the modal, so we better wire that up back in our OffersCtrl. We also will need to create the function to createOffer() that is called when we save the new offer.

...
    $scope.addOffer = function(){
      $scope.modal.show();
    }
    $scope.closeModal = function(){
      $scope.modal.hide();
    }
    $scope.createOffer = function(offer){
      offer.type='offer';
      offer.intent = {status:'pending'};
      offer.purchase = {status:'pending'};
      StorageService.save(offer);
      $scope.closeModal();
    }
...

And just like that, we should be able to create new offers! Give it a shot :-D.
As you can tell though, we didn't provide any ability to add intent, or purchase functionality. For that we need to create a few more views. We'll add a 'details' view and a 'edit' modal so that we can keep track of our purchase processes.

Firstly, the details view : templates/offer-detail.html

<ion-view title = "Detail">
  <ion-nav-buttons side="secondary">
    <button class="button" ng-click="edit()">
      Edit
    </button>
  </ion-nav-buttons>
  <ion-content class="padding">
    <div class = "card">
      <div class="item item-divider">
        Offer
      </div>
      <div class="item item-text-wrap">
        <strong>Offer Number:</strong> {{offer.offerNumber}}<br/>
        <strong>Shares Available:</strong> {{offer.offer.shares_available}}<br/>
        <strong>Shares Offered:</strong> {{offer.offer.shares_offered}}<br/>
        <strong>Cost Per Share:</strong> {{offer.offer.cost_per_share | currency}}<br/>
        <strong>Expires on:</strong> {{offer.offer.expires_on | date}} {{isExpired()?'(Expired)':''}}<br/>
      </div>
    </div>
    <div class = "card">
      <div class="item item-divider">
        Intent
      </div>
      <div class="item item-text-wrap">
        <strong>Status:</strong> {{offer.intent.status}}<br/>
        <strong>Shares Requested:</strong> {{offer.intent.shares_requested}}<br/>
        <strong>Cost Committed:</strong> {{offer.offer.cost_per_share * offer.intent.shares_requested | currency}}<br/>
      </div>
    </div>
    <div class = "card">
      <div class="item item-divider">
        Purchase
      </div>
      <div class="item item-text-wrap">
        <strong>Status:</strong> {{offer.purchase.status}}<br/>
        <strong>Shares Requested:</strong> {{offer.purchase.shares_purchased}}<br/>
        <strong>Cost:</strong> {{offer.offer.cost_per_share * offer.purchase.shares_purchased | currency}}<br/>
      </div>
    </div>
  </ion-content>
</ion-view>

The modal: templates/offer-detail.modal.html

<ion-modal-view>
  <ion-header-bar>
    <h1 class="title">Edit Offer</h1>
    <div class="buttons">
      <button class="button" ng-click="closeModal()">Cancel</button>
    </div>
  </ion-header-bar>
  <ion-content class="padding">
    <ion-list>
      <div class = "item item-divider">
        Offer
      </div>
      <label class="item item-input">
        <span class = "input-label">Offer Number</span>
        <input type="text" ng-model="offer.offerNumber" placeholder="Offer Number">
      </label>
      <label class="item item-input">
        <span class = "input-label">Received On</span>
        <input type="date" ng-model="offer.offer.date" placeholder="Received On">
      </label>
      <label class="item item-input">
        <span class = "input-label">Expires On</span>
        <input type="date" ng-model="offer.offer.expires_on" placeholder="Expires On">
      </label>
      <label class="item item-input">
        <span class = "input-label">Available Shares</span>
        <input type="number" ng-model="offer.offer.shares_available" placeholder="Available Shares">
      </label>
      <label class="item item-input">
        <span class = "input-label">Shares Offered</span>
        <input type="number" ng-model="offer.offer.shares_offered" placeholder="Shared Offered">
      </label>
      <label class="item item-input">
        <span class = "input-label">Cost Per Share</span>
        <input type="number" step = "any" ng-model="offer.offer.cost_per_share" placeholder="Cost Per Share (CAD)">
      </label>

      <div class = "item item-divider">
        Intent
      </div>
      <label class="item item-input item-select">
        <div class="input-label">
          Status
        </div>
        <select ng-model='offer.intent.status'>
          <option value = "pending">Pending</option>
          <option value = "submitted">Submitted</option>
          <option value = "accepted">Accepted</option>
        </select>
      </label>
      <label class="item item-input">
        <span class="input-label">
          Submitted On
        </span>
        <input type="date" ng-model="offer.intent.submitted_on" placeholder="Submitted On">
      </label>
      <label class="item item-input">
        <span class="input-label">
          Shares Requested
        </span>
        <input type="number" step = "any" ng-model="offer.intent.shares_requested" placeholder="Shares Requested">
      </label>
      <div class = "item item-divider">
        Purchase
      </div>
      <label class="item item-input item-select">
        <span class="input-label">
          Status
        </span>
        <select ng-model='offer.purchase.status'>
          <option value = "pending">Pending</option>
          <option value = "submitted">Submitted</option>
          <option value = "accepted">Accepted</option>
        </select>
      </label>
      <label class="item item-input">
        <span class = "input-label">Shares Purchased</span>
        <input type="number" step = "any" ng-model="offer.purchase.shares_purchased" placeholder="Shares Purchased">
      </label>
      <label class="item item-input">
        <span class = "input-label">Submitted On</span>
        <input type="date" ng-model="offer.purchase.submitted_on" placeholder="Submitted On">
      </label>
      <label class="item item-input">
        <span class = "input-label">Accepted On</span>
        <input type="date" ng-model="offer.purchase.accepted_on" placeholder="Accepted On">
      </label>
      <button class="button button-full button-positive" ng-click="save()">Save</button>
    </ion-list>
  </ion-content>
</ion-modal-view>

The controller:

...
  .controller('OfferCtrl', ['$scope', '$rootScope', '$stateParams', '$ionicModal',  'StorageService',
  function($scope, $rootScope, $stateParams, $ionicModal, StorageService){
    StorageService.get($stateParams.offerId)
    .then(function(offer){
      if(offer.offer.date){
        offer.offer.date = new Date(offer.offer.date);
      }
      if(offer.offer.expires_on){
        offer.offer.expires_on = new Date(offer.offer.expires_on);
      }
      if(offer.intent.submitted_on){
        offer.intent.submitted_on = new Date(offer.intent.submitted_on);
      }
      if(offer.purchase.submitted_on){
        offer.purchase.submitted_on = new Date(offer.purchase.submitted_on);
      }
      if(offer.purchase.accepted_on){
        offer.purchase.accepted_on = new Date(offer.purchase.accepted_on);
      }
      $scope.offer = offer;
    });
    $ionicModal.fromTemplateUrl('templates/offer-detail.modal.html', {
      scope:$scope,
      animation:'slide-in-up',
      focusFirstInput:true
    })
    .then(function(modal){
      $scope.modal = modal;
    });
    $scope.edit = function(){
      $scope.modal.show();
    }
    $scope.save = function(){
      StorageService.save($scope.offer);
      $scope.modal.hide();
    }
    $scope.closeModal = function(){
      $scope.modal.hide();
    }
  }])
...

Notice that we have to cast dates to all the instances of a date in the object. This is so our HTML interacts properly. CouchDB saves dates as a string which provides us some difficulties when dealing with date objects. This remains true for most drivers when dealing with database dates so it's a good idea to get used to it. Everything else is fairly similar to the OffersCtrl.

That should be it! You can create offers, then review and edit. When they are 'purchased' they will show on your purchases tab.

There are lots of things we didn't cover in this that we definitely could have:

  • Enforcing constraints
    • You can't purchase or submit intent after the offer expires
    • You can't request more shares than were offered to you
    • You can't purchase more offers than you submitted intent to buy
    • NoSQL Solutions don't have stringent policies when it comes to constraining data types. It really relies on the programmer to make sure that they are keeping data consistent.
  • Evaluations of Stock Price:
    • It would be smart to keep track of stock valuations and trend your gain/loss as the quarters roll by.
  • Trends
    • I.E. some cool charts showing offer trends, purchase trends, valuation trends, all those savvy things to make you a slick investor.. In this one company.

I'm always looking for pointers on how to make an engaging mobile experience. If you have any pointers let me know in the comments. My typical 'back-end-logic-only' programming style keeps me naive to the requirements of user engagement.

Cheers!