Change - Know that nothing stays the same

Rails + AngularJS - A generic approach

January 21, 2014

angularjs
opendatamdp
tutorial

One of the best things I found with the mix Rails+AngularJS is that you can have many small AngularJS applications in your project. In my case one thing is very important to me is that AngularJS doesn’t manipulates DOM, all you want to do in your view is html and some new elements, in html too, but reading a template, you can understand what’s happened all the time.

I’ll show an example to explain how AngularJS works and how you can improve some parts of your Rails application in a very easy way.

I need to say sorry if you have one of the next problems:

You see my english is not good enough. Spanish is my native language, but I’m writing in english because I couldn’t find a real example like Im posting here.

This is too much info and is very complicated how I explain all this: after one year working with AngularJS there are too much info to read and put in only one post.

My first example is about a very small web to show data from my local council. We can see it working here (http://opendatamdq.com.ar/providers/singlepage) a list of providers and how much money they received. (sorry about this web is only in spanish but you’ll see is very easy to understand what is about).

To do this in AngularJS we have a single Rails view where the application lives, in any other place outside http://opendatamdq.com.ar/providers/singlepage you access to Rails views directly.

UnderscoreJS is a great library to work in javascript and handle arrays, objects, etc. Take some time to see it and use it.

Setup Rails Side:

First of all you need to decide what part of your app will be handle by this small AngularJS application. In my case, this is a providers list with total values and detail views for each one.

In my model Provider I have some methods to get the data I need, this method’s I’ve improved to get the best performance I need for this view. A good idea is move to an AngularJS level a mature portion of your app, not something you have in dev and you don’t know how is going to be in a very short term.

models/provider.rb

We can see an example of one method with this improvement.

class Provider < ActiveRecord::Basedef self.amounts(year)
    providers = Provider.joins(:purchase_orders).where("extract(year from purchase_orders.occurred_at)=?", year).group("providers.id, providers.name, providers.cuit, providers.cached_slug").select("providers.id, providers.name, providers.cuit, providers.cached_slug, count(providers.id) as purchase_orders_count, sum(purchase_orders.amount) as total_amount")
    providers.map do |provider|
      {cached_slug: provider.cached_slug,
       id: provider.id,
       name: provider.name,
       cuit: provider.cuit,
       purchase_orders_count: provider.purchase_orders_count,
       total_amount: provider.total_amount.to_f
      }
    end
  endend

Each array’s items it looks like this:

{
  cached_slug: "abete-y-cia-sa",
  id: 226,
  name: "ABETE Y CIA S.A.",
  cuit: null,
  purchase_orders_count: 1,
  total_amount: 12219.6
  }

Our method amounts get a full list of providers and return an array with the info in the correct format. In this case I have a parameter to only show all values for a specific year.

In our controller we have an action to handle the request:

controllers/providers_controller.rb

class ProvidersController < AplicationController
  respond_to :jsondef singlepage
    providers = Provider.order(:name).amounts(Time.zone.now.year)
    respond_to do |format|
      format.html
      format.json { render :json => providers}
    end
  endend

This action has two response’s formats:

html format to render the view where the AngularJS application it should be rendered.

json format for the data.

views/providers/singlepage.html.erb

<div ng-app=“providersApp”>
  <ng-view></ng-view>
</div>

It’s important to see the use of ng-app=“providersApp” in this view, this directive tells the that inside this div, is going to reside an app called providersApp.

config/routes.rb

resources :providers do
  collection do
    get 'singlepage'
  end
end

This is all what we need in our Rails level to show it. To be clear, we handle our rails part as an API (in a very simple way), we have some data in json format and we are going to use that as a data source for our AngularJS app.

An AngularJS application like this, is going to have 5 parts:

A router, we are going to have a view for the Provider’s list, and a view for a Provider’s detail.

A place to run any kind of code of initialisation of our AngularJS app (many apps can check for authentication, or anything you need here).

A model for Provider, other model for PurchaseOrder and one last model for PurchaseOrderDetail. note: in our rails application this is Provider hasmany :purchaseorders, PurchaseOrder hasmany :purchaseorder_details

A controller to handle our Provider list and a controller for a Provider detail.

A view for render data.

Step by step:

Before all, you need to define your app and load all necessary modules it should use:

angular.module("providersApp", ["providersApp.services", "providersApp.controllers", "ngResource", "ngRoute", "domstorage"])
  • providersApp => is the name of our app
  • providersApp.services => Angular module where we define all our factories (models in Rails way)
  • providersApp.controllers => angular module where we define our controllers 
  • ngResource => angular library to handle our connection to a resource, it doesn’t matter where is the resource’s source. We can see an AngularJS Resource an object to connect with the data. It can handle CRUD actions and custom actions.
  • ngRoute => Necessary to handle only angular’s routes.
  • domstorage => This is a module I wrote to use localStorage in browsers.

To define a module we use:

angular.module(‘module_name’, [])

We define the module_name and in [] we can add any other module it may need. In this case providersApp.services is one module defined for use but is going to be used inside our providersApp and ngResource is not defined for us but is going to be used too.

  1. Routes:

    //first line duplicated on purpose, because config affects app module.
    angular.module("providersApp", ["providersApp.services", "providersApp.controllers", "ngResource", "ngRoute", "domstorage"])
    .config(["$routeProvider", "$httpProvider", function($routeProvider, $httpProvider) {
    // root route - provider’s list
    $routeProvider.when("/", {
      templateUrl: "/angular/views/providers/index.angular.html",
      controller: 'ProvidersCntl'
    });
    // detail’s route
    $routeProvider.when("/details/:slug", {
      templateUrl: "/angular/views/providers/details.angular.html",
      controller: 'ProviderDetailCntl'
    });
    // default behavior for any other route
    $routeProvider.otherwise({
      redirectTo: '/'
    });
    // interceptor
    $httpProvider.interceptors.push('DOMStorageHttpInterceptor');
    }
    ])
  2. A place to initialize:

    //first line duplicated on purpose, because run affects app module.
    angular.module("providersApp", ["providersApp.services", "providersApp.controllers", "ngResource", "ngRoute", "domstorage"])
    .run([, "$rootScope”, “$log”, function($rootScope, $log) {
    $rootScope.myFunction = function(){
    $log.log(I’m defined in rootScope”);
    }
    }]);

All code inside run, is going to be executed only when the aplication starts, and only 1 time.

$rootScope is the single root scope for all the aplication, any other scope we see in our application is a descendant from rootScope. In this case I created myFunction as an example, and we’ll see how any other scope has myFunction inside because they are descendants from rootScope.

  1. Models:

    angular.module("providersApp.services", [])
    .factory("PurchaseOrder", ['$resource', function($resource) {
    var functions = {
    ocurredAt: function() {
      return new Date(this.occurred_at);
    },
    yearShow: function(){
      return this.ocurredAt().getYear()+1900;
    }
    };
    this.addFunctions = function(purchase_order) {
    return angular.extend(purchase_order, functions);
    };
    return this;
    }])
    .factory("Provider", ["$resource", "PurchaseOrder", "PurchaseOrderDetail", "$log", function($resource, PurchaseOrder, PurchaseOrderDetail, $log) {
    var ProviderResource = $resource("/providers/:id.json",
                               {id: '@id'},
                               {
                                allAmounts: { method: "get", url: "/providers/singlepage.json", isArray: true, params: {} }
                               }
                              );
    var functions = {
    purchaseOrdersByYear: function(){
      var byYear = _.groupBy(this.purchase_orders, function(purchaseOrder){
        return purchaseOrder.yearShow();
      });
      var byYearFinal = [];
      for (var key in byYear) {
        byYearFinal.push({ year: key, purchaseOrders: byYear[key], count: byYear[key].length });
      }
      return byYearFinal;
    }
    };
    this.addFunctions = function(provider) {
    angular.extend(provider, functions);
    angular.forEach(provider.purchase_orders, function(purchase_order) {
      PurchaseOrder.addFunctions(purchase_order);
      angular.forEach(purchase_order.purchase_order_details, function(purchase_order_detail) {
        PurchaseOrderDetail.addFunctions(purchase_order_detail);
      });
    });
    };
    return angular.extend(this, ProviderResource);
    }])

In providersApp.services I define all my models to handle data from external sources. Here we have two Factories (models), Provider and PurchaseOrder. Provider is the model where we connect a resource to get data, and Purchase order is some other model we need in our AngularJS app.

I like to organise my factories and extend it with custom functions they need, to do it I use var functions = {} and I define all my functions there, after all this I use the extend function from angular angular.extend(this, functions); 

  1. Controllers:

    .controller("ProvidersCntl", ["$scope", "$filter", "Provider", "$log", function($scope, $filter, Provider, $log) {
    $scope.providers = Provider.allAmounts({}, function(providers) {
    $scope.totalProvidersAmount = 0;
    $scope.totalProvidersOrders = 0;
    $scope.totalProvidersAmount = _.reduce(providers, function(memo, num) {
      return memo + num.total_amount;
    }, 0);
    $scope.totalProvidersOrders = _.reduce(providers, function(memo, num) {
      return memo + num.purchase_orders_count;
    }, 0);
    });
    $scope.$watch("search", function(query) {
    $scope.filteredProviders = $filter("filter")($scope.providers, query);
    });
    $scope.$watch("filteredProviders", function() {
    if ($scope.filteredProviders) {
      $scope.totalProvidersAmount = _.reduce($scope.filteredProviders, function(memo, num) {
        return memo + num.total_amount;
      }, 0);
      $scope.totalProvidersOrders = _.reduce($scope.filteredProviders, function(memo, num) {
        return memo + num.purchase_orders_count;
      }, 0);
    }
    });
    }])

In our controller we are going to load all necessary data to use in our view, think the controller as the glue between the data and the view.

Any property defined in $scope, or any function it should be accesible directly in our view.

  1. Views:

code: https://gist.github.com/agustinvinao/8544924

AngularJS handles views as HTML with some magic to show data, but this magic is all visible to understand it when you read it.

We have two important things to read here:

Data in an AngularJS view is handle by double brackets, anything you see here it can be a variable, a function, an expression.

AngularJS defines directives, any directive from the framework starts with ng- or ng:, you can define your own directives to extend functionality.

This is a very small explanation about how you can move something in your Rails application to an AngularJS application. Is not my intention replace anything in Rails, but after one year working with AngularJS I see many advantages for this.

Source code:

https://gist.github.com/agustinvinao/8546440

I’ll do many more of this posts to cover this topics:

  • Forms, validation with Rails rules
  • AngularJS filters
  • AngularJS Directives
  • Use of $scope.$watch to see changes in a view
  • Use of broadcast to intercept changes in other places
  • Factories, how I organise my code.
  • Javascript Promises, improve waiting time.

And many more.

If you have any questions, please email agustinvinao@gmail.com

You can see part of my work here or here.

Thanks for readying.


Agustin Vinao
Agustin Vinao.

Paradox: Life is a mystery. Don't waste time trying to figure it out.
Humor: Keep a sense of humor, especially about yourself. It is a strength beyond all measure.
Change: Know that nothing stays the same.