読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでAngularJSを使ってTodoアプリを作成 - 4. ngResource + Rails API化

Rails中級 AngularJS 連載

f:id:nipe880324:20150112194806p:plain


「RailsでAngularJSを使ってTodoアプリを作成」の連載4回目です。
前回はAngularJSのコントローラーを作成することで、タスクの追加、削除、完了をしました。

今回は、RailsでAPIを作成します。(モデルやコントローラーなどの作成)
そして、AngularJSのngResourceを使い、そのAPIにアクセスし、Todoリストを作成/更新/削除を永続化できるようにします。


* 連載記事一覧

f:id:nipe880324:20150114203508j:plain:w480

動作確認

  • Rails 4.2.0
  • AngularJS 1.3.8
  • Bootstrap 3.3.1
  • UI Bootstrap 0.12.0

目次

2. AngularJSからAPIにアクセスする

2.3. AngularJSのコントローラーからServiceクラスを使う

1. RailsでTodoリストのAPIを作成する

まずTodoリストのAPIで必要となる機能を洗い出します。


概要API URLRailsのコントローラー名#アクション名対応するAngularJSのメソッド
TodoListの名前とTodoの一覧取得APIGET /api/todo_lists/:idTodoLists#showinit() Todoの追加APIPOST /api/todo_lists/:todo_list_id/todosTodos#createaddTodo(todoDescription) Todoの削除APIDELETE /api/todo_lists/:todo_list_id/todo/:idTodos#destroydeleteTodo(todo) Todoの更新API(completedなどを更新)PATCH /api/todo_lists/:todo_list_id/todo/:idTodos#updatetoggleTodo(todo)

toggleTodoメソッド以外は、第3回目で作ったメソッドと対応しています。後ほど、toggleTodoは作成します。

では、これらを実装していきます。



1.1. モデルの追加

では、TodoListモデルとTodoモデルを作成します。

bin/rails g model TodoList name
bin/rails g model Todo todo_list_id:integer description completed:boolean


NOT NULL制約とデフォルト値を設定してきます。

# db/migrate/yyyymmddhhMMss_create_todo_lists.rb
class CreateTodoLists < ActiveRecord::Migration
  def change
    create_table :todo_lists do |t|
      t.string :name, null: false   # NOT NULL制約を追加

      t.timestamps null: false
    end
  end
end


# db/migrate/yyyymmddhhMMss_create_todos.rb
class CreateTodos < ActiveRecord::Migration
  def change
    create_table :todos do |t|
      t.integer :todo_list_id, null: false # NOT NULL制約を追加
      t.string :description,   null: false # NOT NULL制約を追加
      t.boolean :completed,    null: false, default: false # NOT NULL制約とデフォルト値を追加

      t.timestamps null: false
    end
  end
end


マイグレートを実行します。

bin/rake db:migrate


では、モデルファイルに「リレーション」と「バリデーション」を追加します。

# app/models/todo_list.rb
class TodoList < ActiveRecord::Base
  # Todoの作成日の新しい順に取得する
  has_many :todos, -> { order "created_at DESC" }, dependent: :destroy

  validates :name, presence: true
end


# app/models/todo.rb
class Todo < ActiveRecord::Base
  belongs_to :todo_list

  validates :todo_list_id, presence: true
  validates :description,  presence: true, length: { maximum: 255 }
  validates :completed, inclusion: { in: [true, false] }
end


1.2. ルーティングの追加

APIのルーティングを追加します。
APIなので、ネームスペースにapiにし、デフォルトのフォーマットもjsonを指定しています。

# config/rouets.rb
Rails.application.routes.draw do
  get 'templates/index'

  namespace :api, defaults: { format: :json } do
    resources :todo_lists, only: :show do
      resources :todos, except: [:index, :new, :edit, :show]
    end
  end
end


上記の設定で作成されたルートをbin/rake routesで確認します。
/api/配下のURIパターンになっていることが確認できます。

$ bin/rake routes
             Prefix Verb   URI Pattern                                       Controller#Action
    templates_index GET    /templates/index(.:format)                        templates#index
api_todo_list_todos POST   /api/todo_lists/:todo_list_id/todos(.:format)     api/todos#create {:format=>:json}
 api_todo_list_todo PATCH  /api/todo_lists/:todo_list_id/todos/:id(.:format) api/todos#update {:format=>:json}
                    PUT    /api/todo_lists/:todo_list_id/todos/:id(.:format) api/todos#update {:format=>:json}
                    DELETE /api/todo_lists/:todo_list_id/todos/:id(.:format) api/todos#destroy {:format=>:json}
      api_todo_list GET    v(.:format)                     api/todo_lists#show {:format=>:json}


1.3. TodoListの名前とTodoの一覧取得API

Api::TodoListsControllershowアクションを作成します。
ファイルの配置場所はapiディレクトリ配下であること、コントローラーのソースはmodule Api内であることに注意してください。

# app/controllers/api/todo_lists_controller.rb

module Api
  # NOTE 本来はApi::BaseControllerのようなAPIの共通コントローラーを継承すべき
  class TodoListsController < ApplicationController
    def show
      @todo_list = TodoList.find(params[:id])
    end
  end
end


次にjbuilderを使ってshowアクションのJSONを実装します。
JSONの階層構造は、次のようにlistに代入する値を返す必要があります。

$scope.init = ->
  $scope.list = {
    'name'  : 'Todoリスト1',
    'todos' : [
      { 'description' : 'todo description1', 'completed' : false },
      { 'description' : 'todo description2', 'completed' : false }
    ]
  }
}


app/views/api/todo_lists/show.json.jbuilderを作成し、下記を追加してください。(idだけ新たに追加しています)

json.name  @todo_list.name
json.todos @todo_list.todos do |todo|
  json.id          todo.id
  json.description todo.description
  json.completed   todo.completed
end


では、テストのために、bin/rails cでTodoListとTodoを1つずつ作成しておきます。

$ bin/rails c
> TodoList.create name: "first todo list"  # id: 1
> TodoList.first.todos.create description: "todo desc 1", completed: false


サーバーを再起動し、localhost:3000/api/todo_lists/1にアクセスすると次のようにJSONを取得できます。(initメソッドで定義しているデータと同じ階層構造)
f:id:nipe880324:20150113213057j:plain:w480




1.4. Todoの追加/更新/削除API

TodoListsContollerと同じように、TodosControllerを作成します。
Todosコントローラーには、Todoの追加/更新/削除を行うために、create / update / destroyメソッドを追加します。

# app/controllers/api/todos_controller.rb

module Api
  class TodosController < ApplicationController

    def create
      @todo_list = TodoList.find(params[:todo_list_id])
      todo = @todo_list.todos.create!(todo_params)
      render json: todo, status: 201
    end

    def update
      @todo_list = TodoList.find(params[:todo_list_id])
      todo = @todo_list.todos.find(params[:id])
      todo.update!(todo_params)
      render nothing: true, status: 204
    end

    def destroy
      @todo_list = TodoList.find(params[:todo_list_id])
      todo = @todo_list.todos.find(params[:id])
      todo.destroy
      render nothing: true, status: 204
    end

    private

    def safe_params
      params.require(:todo).permit(:description, :completed)
    end

  end
end

重複しているコードがあるので、リファクタリングをします。

module Api
  class TodosController < ApplicationController
    before_action :set_todo_list

    def create
      todo = @todo_list.todos.create!(todo_params)
      render json: todo, status: 201
    end

    def update
      todo.update!(todo_params)
      render nothing: true, status: 204
    end

    def destroy
      todo.destroy
      render nothing: true, status: 204
    end

    private

    def set_todo_list
      @todo_list = TodoList.find(params[:todo_list_id])
    end

    def todo
      @todo = @todo_list.todos.find(params[:id])
    end

    def todo_params
      params.require(:todo).permit(:description, :completed)
    end

  end
end


これで一通りAPIが実装できました。






2. AngularJSからAPIにアクセスする

では、今作成したRailsのAPIにAngularJSからアクセスするようにします。
angular-resourceというモジュールを使うことで、AngularJSから比較的簡単にAPIにアクセスすることが可能になります。



2.1. ng-resourceのインストール

AngularJS 公式ページから以下の2つのファイルをダウンロードし、vendor/assets/javascripts/配下に配置します。

  • angular-resource.min.js
  • angular-resource.min.js.map
curl https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-resource.min.js > vendor/assets/javascripts/angular-resource.min.js
curl https://ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-resource.min.js.map > vendor/assets/javascripts/angular-resource.min.js.map


次に、application.jsangular.minの後に、下記angular-resource.minを追加します。

# app/assets/javascripts/application.js

//= require angular-resource.min


そして、sampleAppの依存関係にngResourceを追加します。

# app/assets/javascripts/app.coffee

# AngularJSの設定ファイル
# 依存ライブラリを記述する
app = angular.module('sampleApp', ['ui.bootstrap', 'ngResource'])

...


では、http://localhost:3000/templates/indexを開き、JavaScriptコンソールでエラーメッセージが発生していないか確認してください。




2.2. RailsのAPIにアクセスするサービスクラス(factory)の作成

RailsのTodoListとTodoのAPIにアクセスするサービスクラスを作成します。
サービスクラスの作り方には3種類ほどあり、今回はその中の1つのfactoryメソッドを使って作ります。

サービスクラスは、app/assets/javascripts/services/配下に作成します。

mkdir app/assets/javascripts/services
touch app/assets/javascripts/services/TodoListService.coffee
touch app/assets/javascripts/services/TodoService.coffee


まずは、TodoListクラス(TodoListのAPIにアクセスするサービスクラス)を作成します。

# app/assets/javascripts/services/TodoListService.coffee

angular.module('sampleApp').factory 'TodoList', ($resource, $http) ->
  class TodoList
    constructor: (errorHandler) ->
      @service = $resource('/api/todo_lists/:id',
        { id: '@id' },
        { update: { method: 'PUT' }})
      @errorHandler = errorHandler

    find: (id, successHandler) ->
      @service.get(id: id, ((list)->
        successHandler?(list)
        list),
        @@errorHandler)


次に、Todoクラス(TodoのAPIにアクセスするサービスクラス)を作成します。

# app/assets/javascripts/services/TodoService.coffee

angular.module('sampleApp').factory 'Todo', ($resource, $http) ->
  class Todo
    constructor: (todoListId, errorHandler) ->
      @service = $resource('/api/todo_lists/:todo_list_id/todos/:id',
        { todo_list_id: todoListId },
        { update: { method: 'PUT' }})
      @errorHandler = errorHandler

    create: (attrs) ->
      new @service(todo: attrs).$save ((todo) -> attrs.id = todo.id), @errorHandler
      attrs

    delete: (todo) ->
      new @service().$delete { id: todo.id }, (-> null), @errorHandler

    update: (todo, attrs) ->
      new @service(todo: attrs).$update {id: todo.id}, (-> null), @errorHandler


ngResouceモジュールをインクルードすると、$resouceメソッドが使えるようになります。
$resouceにはURLを指定します。すると、メソッドの返り値では、次のメソッドが使えます。各メソッドを呼ぶと、$resouceで指定したURLを元にしたRESTfulなHTTPメソッドとURLでアクセスしてくれます。

{ 'get': {method:'GET'},   // Railsのshow
 'save': {method:'POST'},  // Railsのcreate
 'query': {method:'GET', isArray:true}, // Railsのindex
 'remove': {method:'DELETE'}, // Railsのdelete
 'delete': {method:'DELETE'} }; // Railsのdelete

// Railsのupdateに対応するメソッドは定義されていないので、
// $resouceの第3引数に次のようにしていることで使えるようになる。
 { update: { method: 'PUT' }})


詳細については、ng-resource v1.2 - 公式ドキュメント(日本語)を参照ください。



2.3. AngularJSのコントローラーからServiceクラスを使う

まずは、initメソッドを修正します。

# app/assets/javascripts/controllers/TodoListCtrl.coffee

# コントローラーを定義する。今はこのように記載すると覚えておけば良い。
angular.module('sampleApp').controller "TodoListCtrl", ($scope, TodoList, Todo) ->

  # 初期データを用意するメソッド
  $scope.init = ->
    # TodoListとTodoのサービスオブジェクトを作成
    # TODO todo_listのidを動的に取得する(次の連載記事で対処)
    @todoListService = new TodoList()
    @todoService     = new Todo(1)
    # データを取得する(GET /api/todo_lists/:id => Api::TodoLists#show)
    $scope.list = @todoListService.find(1)

  ...


一番上の行のTodoListCtrlのmodule定義の、依存するモジュールを記載する箇所にTodoListTodoを追加します。
そして、initメソッド内で、サービスオブジェクトを作成し、サーバーからデータを取得しています。

では、動作を確認してみましょう。
rails cで作成したデータが表示されています。
f:id:nipe880324:20150113213217j:plain:w480



他のメソッドも修正する前に、エラーハンドラを追加しておきます。

# app/assets/javascripts/controllers/TodoListCtrl.coffee

# コントローラーを定義する。今はこのように記載すると覚えておけば良い。
angular.module('sampleApp').controller "TodoListCtrl", ($scope, TodoList, Todo) ->

  # 初期データを用意するメソッド
  $scope.init = ->
    # Todoサービスクラスを作成
    # TODO todo_listのidを動的に取得する
    @todoListService = new TodoList(serverErrorHandler)
    @todoService     = new Todo(1, serverErrorHandler)
    ...

  ...

  serverErrorHandler = ->
    alert("サーバーでエラーが発生しました。画面を更新し、もう一度試してください。")


このエラーハンドラは、サービスクラス内で呼ばれており、サーバーへのアクセスが失敗した時に、アラートを表示するようにしています。
[f:id:nipe880324:20150113213314j:plain:380]



addTododeleteTodoメソッドを修正します。
修正箇所はサービスオブジェクトを使って、サーバーにもアクセスするようにしただけです。

# app/assets/javascripts/controllers/TodoListCtrl.coffee

# コントローラーを定義する。今はこのように記載すると覚えておけば良い。
angular.module('sampleApp').controller "TodoListCtrl", ($scope, TodoList, Todo) ->

  $scope.init = ->
    ...

  $scope.addTodo = (todoDescription) ->
    # todoを追加する(POST /api/todo_lists/:todo_lsit_id/todos => Api::Todo#destroy)
    todo = @todoService.create(description: todoDescription, completed: false)
    # initメソッドで用意したtodosの一番最初にtodoを追加する
    $scope.list.todos.unshift(todo)
    # todo入力テキストフィールドを空にする
    $scope.todoDescription = ""


  $scope.deleteTodo = (todo) ->
    # todoをサーバーから削除する(DELETE /api/todo_lists/todo_list_id/todos/:id => Api::Todo#destroy)
    @todoService.delete(todo)
    # todoをangularjsのlistデータから削除する(indexOfメソッドでtodoのindexを探し、spliceメソッドで削除する)
    $scope.list.todos.splice($scope.list.todos.indexOf(todo), 1)

  ...


では、動作確認をしてみましょう。
Todoの追加やTodoの削除ができると思います。
また、画面を更新しても、表示され続けています。


最後に、Todoを完了にさせます。
チェックボタンをクリックした時に、toggleTodoメソッドを呼ぶように、ng-clickを追加します。

<!-- app/views/templates/index.html.erb -->
...
<div class="todo-completed">
  <input type="checkbox" ng-model="todo.completed" ng-click="toggleTodo(todo)">
</div>
...

コントローラー内でtoggleTodoを定義します。

# app/assets/javascripts/controllers/TodoListCtrl.coffee

angular.module('sampleApp').controller "TodoListCtrl", ($scope, TodoList, Todo) ->

  ...

  # todoの完了カラムをON/OFFするメソッド
  $scope.toggleTodo = (todo) ->
    @todoService.update(todo, completed: todo.completed)


これで、完了のON/OFFも永続化されるようになりました。
f:id:nipe880324:20150113213353j:plain:w480


今回は、RailsのAPIを作成、AngularJSでサービスクラスの作成と、コントローラーの修正をしたので長くなりました。
Rails with AngularJSのメインとなる山場なので是非ここは理解出来るようにしてください。