There's been a lot of writing about Angularjs, and for good reason -- it's awesome.
I'm going to leave the heavy lifting to the excellent docs and focus on what I struggled with most: project organization.
Most of my programming time these days is spent with Ruby on Rails. The conventions around CRUD are excellent, but front end organization is lacking.
Enter Angular.
This guide assumes a good understanding of both Angular and Rails.
Dependency injection and the asset pipeline work wonderfully together. Let's get them setup together.
Make a subfolder, with the same name as the app, in apps/assets/javascript
and include the path in application.js.coffee
Within that folder create a set of folders not dissimilar from the normal Rails structure:
~/src/rails/cogs/
▾ app/
▾ assets/
▸ images/
▾ javascripts/
▾ cogs/
▸ controllers/
▸ filters/
▸ services/
app.js.coffee.erb
application.js.coffee
▸ stylesheets/
▸ templates/
▸ controllers/
▸ helpers/
▸ mailers/
▸ models/
▸ views/
▸ bin/
▸ config/
▸ db/
▸ lib/
▸ log/
▸ public/
▸ spec/
▸ tmp/
▸ vendor/
Cogs.sublime-project
config.ru
Gemfile
Gemfile.lock
Rakefile
README.md
Termfile
Initialize the app in the aptly named app.js.coffee.erb
file.
# declare modules with require param (the array) then add to them without the require param
angular.module('appServices', ['ngResource', 'ng-rails-csrf'])
angular.module('cogs', ['nt-services', 'appServices', 'momentFilters']);
# configure the app module
angular.module('cogs')
.config(['$routeProvider', '$httpProvider', (($routeProvider, $httpProvider) ->
$routeProvider.
when('/', {templateUrl: '<%%= asset_path("assets/home/_index.html") %>', controller: HomeIndexCtrl}).
otherwise({redirectTo: '/'})
$httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
)
])
Why all the extensions you ask? Yep, it's that erb tag with the asset_path call which I'll explain in a minute.
Require the tree. You'll notice I'm using Rails 4 with turbolinks.
application.js.coffee
#= require jquery
#= require jquery_ujs
#= require turbolinks
#= require underscore
#= require moment
#= require twitter/bootstrap
#= require angular
#= require angular-resource
#= require_tree ./cogs
Ok, about that erb code in the app routing config: templateUrl: '<%%= asset_path("assets/home/_index.html") %>'
, we're using front end templates for angular, which we need to configure.
config/initializers/haml_template.rb
Rails.application.assets.register_mime_type 'text/html', '.html'
Rails.application.assets.register_engine '.haml', Tilt::HamlTemplate
The asset_path call will add the appropriate digest to the asset's url in environments with digests enabled.
Finally, you'll notice I'm using angular-resource. If we look closer at the project organization, you'll see the resources have all been defined in the services folder.
~/src/rails/cogs
▾ app/
▾ assets/
▸ images/
▾ javascripts/
▾ cogs/
▾ controllers/
▾ home/
home_index_ctrl.js.coffee
▾ filters/
moment.js.coffee
▾ services/
▾ nt/
debouncer.js.coffee
▾ resource/
meeting.js.coffee
note.js.coffee
person.js.coffee
csrf.js.coffee
app.js.coffee.erb
application.js.coffee
person.js.coffee
angular.module('appServices').factory('Person', ['$resource', ($resource) ->
Person = $resource('/people/:id/:action', { format: 'json' }, {
search: { method:'GET', params:{}, isArray: true },
update: { method:'PUT' }
})
return Person
])
The extra :id
and :action
params let you do fancy things like:
# Person gets injected into our controller and somewhere along the line we call
Person.$get({ action: 'say_hi', id: 2 })
# to say hi to person 2
Ok, we've got our front end all organized, but how does that 'say_hi' get handled on the back end?
Rails 4 added jbuilder by default. I'll let you read up, but basically it allows you to define your json responses in a .jbuilder file as you would any other type of view for a given format.
I usually split it out like this:
app/views/people/index.json.jbuilder
json.array! @people do |person|
json.partial! 'full', person: person
end
app/views/people/show.json.jbuilder
json.partial! 'full', person: @person
app/views/people/_full.json.jbuilder
json.id person.id
json.name person.name
The controller is boilerplate:
app/controllers/people_controller.rb
class PeopleController < ApplicationController
before_action :set_person, only: [:show, :edit, :update, :destroy]
# GET /people
def index
@people = Person.all
end
# GET /people/1
def show
end
# GET /people/1/say_hi
def say_hi
end
# GET /people/new
def new
@person = Person.new
end
# GET /people/1/edit
def edit
end
# POST /people
def create
@person = Person.new(person_params)
respond_to do |format|
if @person.save
format.html { redirect_to @person, notice: 'Person was successfully created.' }
format.json { render :show }
else
format.html { render action: 'new' }
format.json { render json: @person.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /people/1
def update
respond_to do |format|
if @person.update(person_params)
format.html { redirect_to @person, notice: 'Person was successfully updated.' }
format.json { render :show }
else
format.html { render action: 'edit' }
format.json { render json: @person.errors, status: :unprocessable_entity }
end
end
end
# DELETE /people/1
def destroy
@person.destroy
redirect_to people_url, notice: 'Person was successfully destroyed.'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_person
@person = Person.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def person_params
params[:person].permit(:first_name, :last_name, :email)
end
end