Sunday, 7 September 2014

Into to Ember and Ember CLI part two

Before we get started I'd like to give a huge thank you to @geoffreyd, he pretty much fixed all my code and set me straight on some really good best practices, not to mention spent an enormous amount of time putting up with my help vampirism. Additionally all the code for this post can be found on github
So far we've displayed our transactions on the page, but the amount property in our transaction model is just using a number to represent its value and we want to format it. So we're going to utilise a helper method so it's available across our views.
Helpers in ember are not dissimilar to helpers in Rails, particularly in this use case where we're planning on formatting an existing value. The main difference is by utilising ember, we can ensure our helper method updates as the values it depends upon changes.
Today we're going to register a new helper and convert our transaction amount to something resembling currency.
Ember CLI gives us a few options to achieve our goals, but in this case what we need is the following:
import Ember from "ember";
export default Ember.Handlebars.makeBoundHelper(function(amount, options){
  var amount = (amount/100).toFixed(2);
  var currencyAmount = "$"+ amount + "";
  return currencyAmount;
}); 
Insert the above into app/helpers/to-currency.js.
We use makeBoundHelper here instead of just helper or registerBoundHelper due to ember-cli. Ember-cli utilises the makeBoundHelper and makes it accessible to the ember container, this is outside the scope of this post, but if you'd like to dig a little into our applications internals within the console, run Transactions.__container__ within your browser.
The alternative to using this is using registerBoundHelper is that we'd need to take the extra step to define the helper function, then register our helper within app.js.
If we reload our application now we should see our transaction amount looking all pretty.

Routes

This is where most guides would start discussing Ember controllers and creating new transactions from within. We're not going down that path.
A Route in Ember represents state, which is represented with a url. As we're going to be manipulating data, and thus the state of the current page, we're going to store our actions relating to creating a new transaction in a route. To start though we need to specify a new route for the user to hit.
Router.map(function() {
  this.resource('transactions', { path: '/' }, function(){
    this.route('new'); 
  });
});
Here we have made /transactions/new available. This, by convention, will ensure Ember generates a TransactionsNewRoute, but we're going to create one of our own and start handling user input. To take user input we need a form, so let's create a new template for our transactions form:
<form {{action "createTransaction" on="submit"}}>
  <label for="transaction-name">Name:</label>
  {{input type="text" value=name id="name"}}

  <label for="transaction-amount">Transaction Amount:</label>
  {{input value=dollarValue id="comp-currency"}}

  <button type="submit">Save Transaction</button>
</form>
In Ember, if we don't assign a string to a value, that then becomes available to the context object within the template, bear this in mind when we start to look at our Route object later on.
So now that we have a form, let's make our form save our transactions with a Route:
import Ember from 'ember';

export default Ember.Route.extend({
    model: function(){
        return this.store.createRecord("transaction", { amount: 0, name: 'New Transaction' });
    },

    actions: {
      createTransaction: function(){
        var self = this;
        var transaction = this.controller.get('model');
        // TODO: set date format the way you want it.
        transaction.set('occurredOn', new Date);
        transaction.save().then(function(){
          self.transitionTo('transactions.index');
        });
      }
    }

});
Ok, there's a lot going on here, so lets break it down.
First up, we instantiate a brand new instance of transaction within the route, place it inside the model hook, and set it with some defaults using the createRecord method. This then sends the model to the controller, which if you remember from our first post talks exclusively to the template.
Next is our actions, specifically our createTransaction action. We can define actions within a Route object, a Controller or a View. To make understanding when to use which earlier, if you're dealing with data changes, use the Route, if you're dealing with changes within the object or collection, use a Controller, if you're dealing with interactions between views, use the View.
Within our createTransaction action, we retrieve our controller model, which seems a bit odd as we've defined it above. However we need to explicitly retrieve the model object as it stands from the controller, which as we reminded ourselves above, talks to the template. So here, we get the object as it stands in our form. We then save our new transaction, and make use of promises to return to the transactions index once complete.
There's one piece to puzzle missing, how does our dollarValue from our template, map to the amount on our model. The answer is in fact, in our model.

Computed Properties

One of Ember's most powerful features is computed properties.
It allows you to declare functions as properties on an object. When we create a computed property we defined the properties of the object it depends on at the end of the function, we can also use this to build a computed property that depends on another computed property. Awesome!
In our case below we have a computed property of dollarValue to map dollar values to cents in our database. When we use this property in our form, it sets the amount to the unformatted amount entered in. So when it comes time to saving the form, it automatically does all the adjustments for us.
var Transaction = DS.Model.extend({
    name: DS.attr('string'),
    amount: DS.attr('number'),
    occurredOn: DS.attr('date'),
    dollarValue: function(key, value, previousValue) {
        var currency;
        if (value !== undefined) {  // set was called
            var amount = Math.round(value * 100);
            currency = accounting.unformat(amount);
            this.set("amount", currency);
        } else {
          currency = this.get('amount');
        }

        // Get the value and return for either set or get.
        amount = (currency/100).toFixed(2);
        return accounting.formatMoney(amount, "");
    }.property('amount')
});

One more thing

There's just one thing we need to do, and that's instantiate an Ember Object Controller so our form gets all the attributes proxied through. So let's create a new TransactionsNewcontroller:
import Ember from "ember";

// Make TransactionsNewController an ObjectController,·
// so that it will proxy properties. !important
export default Ember.ObjectController.extend({

});

No comments:

Post a Comment