Objective |
---|
To identify the basic compenents of a Angular application and integrate them together in a manner that creates a canonical example for one to further develop or rexamine afterward. |
========
-
At
ng-app
-
Add Library
angular 1.2.13
-
Setup an
ng-app
- Seeing
ng-model
, and two_way_data_binding
- Seeing
-
Setup an
ng-controller
- The
ng-controller
directive- A controller function
- $scope in a controller
- Using the
ng-repeat
directive- A new
$scope
- Iterating through an
Array
- Iterating through
(key, values)
- A new
- controller actions
- The
-
Setup an App
- Setup
config/routes
- generate a
BookController
- Download Angular into
vendor/assets/javascripts
- Setup
-
Providers, Services, and Factories
-
Setup a modular
BookApp
-
Setup a modular
BookAppCtrl
- Using
$http
- Using
-
Setup with
ngRoute
-
Setup with
ngResource
==========
Let's begin a project in a directory of your choosing:
rails new book_app
....
cd book_app
To get angular you can do one of the following
curl -G http://code.angularjs.org/1.2.13/angular-1.2.13.zip > vendor/assets/javascripts/angular && open vendor/assets/javascripts/angular
and just rm vendor/assets/javascripts/angular
, which might be a little over the top and UNIX-y, or you can download it from here, open it, and then
mv "~/Downloads/angular-1.2.13" vendor/assets/javascripts/
We need to load our angular.min.js
, so we add the following to the assets/javascripts/application.js
/application.js
...
//= require jquery
//= require jquery_ujs
//= require turbolinks
// ***** ADD ANGULAR HERE *****
//= require angular-1.2.13/angular.min
// *****
//= require_tree .
Let's start off with a SitesController
and a BooksController
for our BookApp
rails g controller Sites index
rails g controller Books
views/sites/index.html.erb
<div ng-app>
<input type="text" ng-model="name" placeholder="name">
<p> HELLO, {{name}}! </p>
</div>
Now we modify our routes
BookApp::Application.routes.draw do
root to: "sites#index"
resources :books
end
If angular has loaded correctly then this example should be working at localhost:3000
Change <div ng-app>
in the index to ng-app="BookApp"
/books/index.html.erb
<div ng-app="BookApp">
...
</div>
Now, make a directory in the app/assets/javascripts
for book related files, i.e.
mkdir app/assets/javascripts/books
then make or move book.js.coffee
in/into app/assets/javascripts/books
and define a BookApp
module in it.
/assets/javascripts/books/book.js.coffee
BookApp = angular.module("BookApp", [])
Create a books_controller.js
in the javascripts/books
folder created above and define a BookAppCtrls
module in it.
/assets/javascripts/books/books_controller.js
BookAppCtrls = angular.module("BookAppCtrls",[])
BookAppCtrls.controller("BooksCtrl", ["$scope", ($scope)->
$scope.message = "hello world!"
]);
Now we can add BookAppCtrls
as a module in books.js.coffee
, i.e.
/assets/javascripts/books/books.js.coffee
BookApp = angular.module("BookApp",[
"BookAppCtrls"
]);
Then we can update our /sites/index.html.erb
<div ng-controller="BooksCtrl">
{{message}}
</div>
Let's define some fakeBooks
in the BooksCtrl
/assets/javascripts/books_controller.js.coffee
...
BookAppCtrls.controller("BooksCtrl", ["$scope",
($scope)->
$scope.fakeBooks = [
id: 1
title: 'Bog Adventures'
description: 'A first rails app'
,
id: 2
title: 'Happy Tails'
description: 'Using classes in ruby'
]
])
Now we need to display these fakeBooks
in a view for our BooksCtrl
/sites/index.html.erb
<div ng-controller="BooksCtrl">
<div ng-repeat="book in fakeBooks">
<h3>{{book.name}}</h3>
</div>
</div>
We can handle routing by loading the ngRoute
module. We begin this by adding the angular-route
script into our appliction.js
/application.js
//= require jquery
//= require jquery_ujs
//= require turbolinks
// ***** ADD ANGULAR HERE *****
//= require angular-1.2.13/angular.min
// ***** ADD ANGULAR ROUTER HERE *****
//= require angular-1.2.13/angular-route.min
//*****
//= require_tree .
Now we can update our Book_app.js
to include a router module.
/assets/javascripts/books/books.js.coffee
BookApp = angular.module("BookApp",[
"BooksRouter"
]);
# Create our Book module with `ngRoute` dependency
BooksRouter = angular.module("BooksRouter", ["ngRoute"]);
Next we need to configure our route to respond to certain paths
/assets/javascripts/books/books.js.coffee
BooksRouter.config(["$routeProvider",
($routeProvider)->
$routeProvider.when("/",
templateUrl: "/books"
controller: "BooksCtrl"
)
}]);
Note we add the index
method to the BooksController
books_controller.rb
class BooksController < ApplicationController
def index
f.html { render layout: false }
end
end
Then we remove any ng-controller
tags
javascripts/books/index.html.erb
<div ng-repeat="book in fakeBooks">
<h3>{{book.name}}</h3>
</div>
Now all that is left is to render the view in the index.html, and we change it to look as follows.
/layouts/application.html.erb
<body ng-app="BookApp">
<div ng-view>
</div>
</body>
=========
- Add a route to show an individual
book
by id-
assume it also has the
BookCtrl
-
create a
views/books/show.html.erb
with only the following inside:<div> Hello world </div>
-
=========
We might want to start by adding the route to our angular BooksRouter.config
, which we do by just chaining respective .when
calls
/books/books.js.coffee
...
BooksRouter.config(["$routeProvider", ($routeProvider)->
$routeProvider.when("/", {
templateUrl: "/books/",
controller: "BooksCtrl"
}).
when("/books/:id",
templateUrl: "books/view",
controller: "BookCtrl"
)
])
Then we need to add a books#show
,
/controllers/books_controller.rb
class BooksController < ApplicationController
...
def show
render layout: false
end
end
and the respective view:
/views/books/show.html.erb
<div>
Hello, world!
</div>
Next we need a link to be able to navigate to the show.html.erb
/views/books/index.html.erb
<div ng-repeat="book in fakeBooks">
<a href="#/books/{{book.id}}">
{{book.title}}
</a>
</div>
Let's create a separate controller to handle showing the details of a particular book, which we will fetch using some routeParams
, call it BookDetailsCtrl
.
/books/books.js.coffee
...
BooksRouter.config(["$routeProvider", ($routeProvider)->
$routeProvider.when("/", {
templateUrl: "/books/",
controller: "BooksCtrl"
}).
when("/books/:id",
templateUrl: "books/view",
# A BookDetailsCtrl
controller: "BookDetailsCtrl"
)
])
Next we add this controller to our BookAppCtrls
module as BookDetailsCtrl
and pass in the $routeParams
books/books_controller.js.coffee
BookAppCtrls.controller("BookDetailsCtrl", ["$scope","$routeParams",($scope, $routeParams)->
$scope.bookId = $routeParams.id
])
Let's try making a request for more Books to our server. In order to make a request we should update our BookController#index
to respond with an array of JSON Books.
books_controller.rb
class BooksController < ApplicationController
def index
# Add some fake Books
if params[:format] == "json"
fake_books = [{title: "blah"},{title: "pretty blah"}]
end
# respond to the type of request
respond_to do |f|
f.html {render :layout => false }
f.json {render :json => fake_books}
end
end
...
end
That should be pretty straight forward. Now all we have to do is modify our BooksCtrl
to make an http resquest using Angular's $http
object.
/assets/javascripts/books/books_controller.js.coffee
...
// Add the `$http` dependency to list
BookAppCtrls.controller("BooksCtrl", ["$scope", "$http", ($scope, $http)->
...
]);
Now try writing a get request for more Books as follows.
BookAppCtrls.controller("BooksCtrl", ["$scope", "$http",
($scope)->
...
// requesting more Books
$http.get("/books.json").
success((newBooks)->
console.log(newBooks);
$scope.newBooks = $scope.fakeBooks.concat(newBooks);
);
]);
If we try to do an $http.post
request we might get a 422 an unprocessible entity respone from server. This is most likely due to not including a CSRF
token or authenticity token. Let's create a Book
model in our database, before we get any further with making post
requests.
$ rails g model Book name description
...
$ rake db:migrate
Now setup a create
method in our BooksController
books_controller.rb
class BooksController < ApplicationController
...
def create
new_book = params.require(:book).permit(:name, :description)
book = Book.create(new_book)
respond_to do |f|
f.html {redirect_to books_path}
f.json {render json: book }
end
end
end
Now we can try our first attempt at making a simple $http
post request with an included authenticity token, which we will refactor shortly.
/assets/javascripts/books/books_controller.js.coffee
...
$scope.newBook = {title: "blah"}
$scope.saveBook = ()->
$http({method: "post",
url: "/books",
data: {book:
{name: $scope.newBook},
"authenticity_token": $('meta[name=csrf-token]').attr('content')}
}).success((data)->
console.log(data)
);
As of yet our post request is sending an authenticity token in the data of the params it posts to the server. This is unsightly as truly belongs in the header, and so, we might refactor our $http
request to reflect this like the following:
/assets/javascripts/books/books_controller.js.coffee
$scope.saveBook = ()->
$http({method: "post",
url: "/Books",
data: {Book:
{name: $scope.mock_Book}},
headers: {
'X-CSRF-Token': $('meta[name=csrf-token]').attr('content')
}
}).success((data)->
console.log(data);
);
A less redundent way of adding the X-CSRF-token
to each request is just to setup the $httpProvider
to have this parameter set in the header by default.
/assets/javascripts/book.js.coffee
...
BookApp.config(["$httpProvider",($httpProvider)->
$httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');
]);
Making Book
requests using $http
should present itself as an opportunity to abstract away some of the common resource logic associated with our model, i.e. get
, post
, etc. To handle this angular has a module for creating resources, which we can then include in our application as a service.
First, we create our our BooksService
module.
/assets/javascripts/books_service.js.coffee
var BooksService = angular.module("BooksService", ["ngResource"]);
Then we setup a factory to help manage Books.
BooksService.factory("BookRes", ["$resource",
($resource)->
// DO SOMETHING HERE
]);
The $resource
automatically includes most of the requests we'll need for our application except for the UPDATE
request, so we'll need to add that to any instace of our Book
resource service. Also, we'll need to specify that we want our service to make resource requests using :id
paramater in our model, so we specify it as the second argument of our resource as @id
.
/assets/javascripts/books/books_service.js
BooksService.factory("BookRes", ["$resource",
($resource)->
return $resource("/books/:id.json"
,{id: "@id"}
,{update: {method: "PATCH"}
)
]);
Lastly we add the BookService
dependency to our list of BookApp
dependencies.
/books/books.js.coffee
var BookApp = angular.module("BookApp",[
"BooksService",
"BookAppCtrls",
"BooksRouter"
]);
Here is a link to how to use resources from angular's site.
In books controller you might instead say
/books/books_controller.js.coffee
BookRes.query((data)->
console.log(data)
)
which should log an array of books [object,object,...]