bootstapping an angular.js app on top of rails

Complete example here

– The following will skip jquery and turbolinks:

rails new [your-app-name-here] –skip-javascript

– But now you need to manually create the following file: app/assets/javascripts/application.js

– And include the following in the file contents:

//= require_tree .

– put all your angular app files inside app/assets/javascripts, and include the following in your app/views/layouts/application.html.erb template:

<%= javascript_include_tag ‘application’ %>

– Your app/views/layouts/application.html.erb will also be the home page of your single page app, so make sure you put all your initial HTML code in there, and get rid of the default  <%= yield %> in there.

– Add the following to your application controller:

def angular

render ‘layouts/application’

end

– and in your config/ routes.rb file, add a route to it:

root to: ‘application#angular’

– install bower, if you haven’t done so already:

npm install -g bower

– initialize it inside your rails project:

bower init

– create a .bowerrc file, with the following, to tell bower where you are storing your js dependencies:

{ “directory“:“vendor/assets/bower_components” }

– install the dependencies you need, and tell bower to save them in your config file:

bower install angular angular-ui-router bootstrap –save

– now that the libraries you need are installed, modify your app/assets/javascripts/application.js file to call them at page load time:

//= require angular

//= require angular-ui-router

//= require_tree .

– to add the bootstrap CSS, add the following line to the comments section on app/assets/stylesheets/application.css

*= require bootstrap/dist/css/bootstrap

*= require_tree .

Important: that goes inside the header /* comments */

At this point, you have all your js / css dependencies managed by bower, and minified and pulled in the right places by the rails app.

– place your template files inside public/templates, and modify your config file as follows:

.config([

'$stateProvider',
'$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {

$stateProvider
.state(‘home’, {
url: ‘/home’,
templateUrl: ‘templates/_home.html’,
controller: ‘MainCtrl’
}).state(‘posts’, {
url: ‘/posts/{id}’,
templateUrl: ‘templates/_posts.html’,
controller: ‘PostsCtrl’
});

$urlRouterProvider.otherwise(‘home’);
}])

– move your JS to folders inside app/assets/javascripts (create mainCtrl.js inside the home folder, and postsCtrl.js inside the posts folder), so the only thing left inside app.js is the configuration and routes.

– generate your models:

rails generate model Post title:string link:string upvotes:integer

rails generate model Comment body:string upvotes:integer post:references

rake db:migrate

– declare your associations inside your models:

class Post < ActiveRecord::Base

  has_many :comments

end

– if you have children models from one of your models, and you want the children to be returned as part of your JSON, put the following method inside your model as well:

def as_json(options = {})

     super(options.merge(include: :comments))

end

 – setup your base routes:

root to: ‘application#angular’

resources :posts, only: [:create, :index, :show] do

   resources :comments, only: [:show, :create] do

      member do put ‘/upvote’ => ‘comments#upvote’

   end

end

member do put ‘/upvote’ => ‘posts#upvote’ end end

Nesting resources will create urls like this: posts/1/comment/3

By specifying member do, you are also simplifying some of the resources out of the deep nesting

 – using “rake routes” at this point will tell you what kind of routes you have defined

– now is time to create your controllers, make sure you skip the templates and assets creation when doing so:

rails generate controller Posts –skip-assets –skip-template-engine

rails generate controller Comments –skip-assets –skip-template-engine

 – add the following line to your application controller, so it responds back in json format:

respond_to :json

– in the newer versions of rails, you will need to add this gem to your Gemfile to get that respond_to functionality:

gem ‘responders’, ‘~> 2.0’

 – tell your controllers which parameters are allowed, and also create the basic controller methods:

def index
respond_with Post.all
end

def create
respond_with Post.create(post_params)
end

def show
respond_with Post.find(params[:id])
end

def upvote
post = Post.find(params[:id])
post.increment!(:upvotes)

respond_with post
end

private
def post_params
params.require(:post).permit(:link, :title)
end
end

– back to your front end: setup your factory to be able to retrieve all of your records by means of hitting your index method (see how $http is injected, and how the getAll function is implemented as a promise)

angular.module(‘flapperNews’)

.factory(‘posts’, [‘$http’, function(){

  var o = {

    posts: []

  };

  o.getAll = function() {

    return $http.get(‘/posts.json’).success(function(data){

      angular.copy(data, o.posts);

    });

  };

  return o;

}]);

– now, if you want your view to refresh with the server data every time the UI calls “home”, you need to set your stateProvider with the “resolve” property:

$stateProvider

    .state(‘home’, {

      url: ‘/home’,

      templateUrl: ‘templates/_home.html’,

      controller: ‘MainCtrl’,

      resolve: {

            postPromise: [‘posts’, function(posts){

               return posts.getAll();

           }]

      }

    })

 – to be able to create post, add the following to your post service:

o.create = function(post) { return $http.post(‘/posts.json’, post).success(function(data){ o.posts.push(data); }); };

– and, in your main controller:

$scope.addPost = function(){ if(!$scope.title || $scope.title === ) { return; } posts.create({ title: $scope.title, link: $scope.link, }); $scope.title = ; $scope.link = ; };

– by default, rails has protection against fake posts, so in order to save the data you are posting, you will need to add the following gem (otherwise you will be getting “422 unprocessable entry” error messages

gem ‘angular_rails_csrf’

– to add user authentication via Device, you need to install the gem first:

gem ‘devise’, ‘~> 3.4.0’

– after you bundle install, initialize it, and create the user’s model:

rails generate devise:install

rails generate devise User

– if you need to, you can add more fields to the default devise model, which only contain email and password by default. We will also make the username unique:

rails generate migration AddUsernameToUser username:string:uniq

– in order to integrate devise with the front end, you can install the following js package helper via bower:

bower install angular-devise –save

– in application.js, require the newly installed package:

//= require angular-devise

– and inject the module in the main app:

angular.module(‘flapperNews’, [‘ui.router’, ‘Devise’]).

# note: when you try your registration / login forms, if there is a devise error, it may manifest in the front end as a 422 error message, you need to handle the errors as they come from the server

– to secure your posts savings (or any other controller actions for that matter) you can now use the following:

class PostsController < ApplicationController before_filter :authenticate_user!, only: [:create, :upvote]

– if you want to associate two of the models you are working with (in this case posts and users), run the following command, that will create the db migration necessary to do the work:

rails g migration AddUserRefToPosts user:references

rails g migration AddUserRefToComments user:references

rake db:migrate

– if you do so, you need to enhance the models to reflect that association:

class Comment < ActiveRecord::Base

  belongs_to :user

– and your update and create methods also need to include the relationship, so rails knows at save time what users are assigned what records:

def create

respond_with Post.create(post_params.merge(user_id: current_user.id))

end

puppet: the basics

ensure specific packages are installed in a system, creating a puppet manifest file with the following (example):

package { 'vim':
    ensure => '2:7.4.052-1ubuntu3'
}

to run it (you may have to run it as sudo):

puppet apply your_manifest_file.pp

# To ensure the order of the packages that need to run

require => Package[‘your-package-name-here’]