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

Rails Webook

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

RailsでAngularJSを使ってTodoアプリを作成 - 5. ngRouteでシングルページにする

Rails中級 AngularJS 連載

f:id:nipe880324:20150112194806p:plain


「RailsでAngularJSを使ってTodoアプリを作成」の連載5回目です。
前回は、RailsでAPIを作成し、AngularJSのngResourceを使い、そのAPIにアクセスし、Todoリストを作成/更新/削除を永続化できるようにしました。

今回は、ng-routeをいれることで、複数のビューファイルをシングルページとして遷移できるようにします。

動作確認

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

目次

ダッシュボード画面を作成する
ng-routeをインストールする
ダッシュボード画面とTodoリスト画面をシングルページにする
ng-routeとRailsの処理の流れを詳しく




ダッシュボード画面を作成する

3回目の記事と4回目の記事の復習も兼ねて、全てのTodoリストを表示するダッシュボード画面を作成します。
(本来は、ユーザーモデルを作成し、ユーザーごとにダッシュボードを用意するのが普通です。)

まず、ルートを追加します。

# config/routes.rb

get '/templates/dashboard' => 'templates#dashboard'
get '/templates/todo_list' => 'templates#todo_list'
root 'templates#index'


次に、このルートから呼ばれる、viewファイルを作成します。

<!--
  app/views/templates/task_list.html.erb
  仮でhtmlを作成しておく
-->
<h1>This is index.html.erb</h1>



<!--
  app/views/templates/task_list.html.erb
  templates/index.html.erb の内容ををごそっと移動させる
-->

... templates/index.html.erb



<!--
  app/views/templates/task_list.html.erb
  ダッシュボード画面を作成する
-->
<div class="container" ng-controller="DashboardCtrl" ng-init="init()">
  <h1>ダッシュボード - Dashboard</h1>

  <div class="panel panel-success dashboard-list">
    <div class="panel-heading">
      <h4>すべての Todoリスト</h4>
    </div>

    <ul class="list-group" ng-show="lists == 0">
      <li class="list-group-item">
        リストはまだ作成されていません
      </li>
    </ul>

    <form id="new_list" ng-submit="createList(listName)">
      <div class="input-group">
        <input id="todoDescription" class="form-control input-lg" type="text" autofocus="autofocus" placeholder="リスト名" maxlength="255" ng-model="listName">
        <span class="input-group-btn">
          <button class="btn btn-success btn-lg" type="submit">作成</button>
        </span>
      </div>
    </form>

    <ul class="list-group">
      <li class="list-group-item" ng-repeat="list in lists">
        <div class="todo-list-description">
          <a href="/todo_lists/{{ list.id }}">
            {{ list.name }}
          </a>
        </div>
        <div class="todo-list-buttons pull-right">
          <button class="btn btn-danger btn-xs pull-right" type="button" ng-click="deleteList(list, $index)">
            <span class="glyphicon glyphicon-ban-circle"></span>
          </button>
        </div>
      </li>
    </ul>
  </div><!-- /.panel.panel-success.dashboard-list -->
</div>


次にDashboardCtrlを作成します。

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

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

  $scope.init = ->
    @listsService = new TodoList(serverErrorHandler)
    $scope.lists = @listsService.all()

  $scope.createList = (listName) ->
    list = @listsService.create(name: listName)
    $scope.lists.push(list)
    $scope.listName = ""

  $scope.deleteList = (list, index) ->
    if confirm "リストを削除しますか?"
      @listsService.delete(list)
      $scope.lists.splice(index, 1)

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

AngularJSのTodoListサービスクラスにall, create, deleteメソッドを追加します。

# 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

    all: ->
      @service.query((-> null), @errorHandler)

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

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

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


次は、TodoListサービスからアクセスするAPIをRails側で作成します。
routes.rbindex, create, destroyを追加します。

# config/routes.rb

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


そして、TodoListsControllerにメソッドを追加します。

# app/controllers/todo_lists_controller.rb

module Api
  class TodoListsController < ApplicationController
    before_action :set_todo_list, only: [:show, :destroy]

    def index
      render json: TodoList.all
    end

    def show
    end

    def create
      list = TodoList.create!(todo_list_params)
      render json: list, status: 201
    end

    def destroy
      @todo_list.destroy
      render nothing: true
    end

    private

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

      def todo_list_params
        params.require(:todo_list).permit(:name)
      end
  end
end

では、動作確認をします。ダッシュボードが表示され、TodoListの追加と削除ができることを確認してください。
img 2




ng-routeをインストールする

AngularJSでシングルページのルーティングを行うには、ng-routeモジュールが必要です。まずは、プロジェクトにインストールしていきます。

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

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


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

# app/assets/javascripts/application.js

//= require angular-route.min


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

# app/assets/javascripts/app.coffee

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

...


モジュールが正しくインクルードされていることを確認するために、画面を開き、JavaScriptコンソールにエラーが表示されていないことを確認してください。
f:id:nipe880324:20150114195415j:plain:w480




ダッシュボード画面とTodoリスト画面をシングルページにする

では、ngRouteを使い、「ダッシュボード画面」と「Todoリスト画面」をシングルページにしていきます。

まず、$routeProvidorでルートの設定を記載します

# app/assets/javascripts/app.coffee
...

# ルートの設定
app.config ($routeProvider, $locationProvider) ->
  # html5モードを有効にする
  # / にアクセスすると、 /dashboard にリダイレクトする
  # /dashboard にアクセスすると、 /templates/dashboard.html を表示する(合わせてDashboardCtrlを読み込む)
  # /todo_lists/:list_id にアクセスすると、 /templates/task_list.html を表示する(合わせてTodoListCtrlを読み込む)
  $locationProvider.html5Mode true
  $routeProvider.when '/',                    redirectTo:  '/dashboard'
  $routeProvider.when '/dashboard',           templateUrl: '/templates/dashboard.html', controller: 'DashboardCtrl'
  $routeProvider.when '/todo_lists/:list_id', templateUrl: '/templates/todo_list.html', controller: 'TodoListCtrl'


html5Modeをtrueにした場合、baseタグが必要になるので、bash href="/"を追加します。

# app/views/layouts/application.html.erb

<!DOCTYPE html>
<html ng-app="sampleApp">
<head>
  <base href="/">
  <title>AngularjsTest</title>
  ...


次にindex.html.erbng-viewディレクティブを追加します。

<!-- app/views/templates/index.html.erb -->
<div ng-view>
</div>

ng-viewをメインの(index.html)ファイルに記載することで、上記の$routeProviderで設定したルート通りのテンプレートが表示されたり、リダイレクトが行われたりするようになります
ng-view$routeProviderはAngularJSでシングルページアプリとしてルーティングするには必須の機能ですので忘れないようにしといてください。


では、今度はAngularJSが$routeProviderでの設定に則って、templateUrlのHTMLにアクセスできるようにRailsのルーティングを修正します。

# config/routes.rb

Rails.application.routes.draw do
  namespace :api, defaults: { format: :json } do
    resources :todo_lists, only: [:index, :show, :create, :destroy] do
      resources :todos, except: [:index, :new, :edit, :show]
    end
  end

  # 修正箇所 開始
  get '/dashboard'      => 'templates#index'
  get '/todo_lists/:id' => 'templates#index'
  get '/templates/:path.html' => 'templates#template', constraints: { path: /.+/ }
  # 修正箇所 終了

  root 'templates#index'
end


そして、Templatesコントローラーにtemplateメソッドを追加します。

# app/controllers/templates_controller.rb

class TemplatesController < ApplicationController
  ...

  def template
    render template: "templates/#{params[:path]}", layout: nil
  end
end


ダッシュボート画面とTodoリスト画面からng-contollerディレクティブを削除します。
($routeProviderによりコントローラーを既に指定しているので画面で指定しなくてもよいため。)

<!-- app/views/templates/dashboard.html.erb -->
<div class="container" ng-init="init()">
  <h1>ダッシュボード - Dashboard</h1>
  ...


<!-- app/views/templates/todo_list.html.erb -->
<div class="container" ng-init="init()">
  <h1>Todo リスト</h1>
  ...


最後に、Todoリスト画面の一番下に「戻る」ボタンを追加します。

<!-- app/views/templates/todo_list.html.erb -->
  ...
  <a href="dashboard" class="btn btn-default">戻る</a>
</div><!-- 一番下のdiv要素 -->


では画面を確認してみましょう。
http://localhost:3000/にアクセスすると、自動的にhttp://localhost:30000/dashboardにリダイレクトされ、ダッシュボード画面が表示されます。
f:id:nipe880324:20150114195434j:plain:w480


そして、todoリストのリンクをクリックすると、http://localhost:30000/todo_lists/:idに遷移し、Todoリスト画面が表示されます。
f:id:nipe880324:20150114195446j:plain:w480


下にある「戻る」ボタンを押せば、ダッシュボード画面に戻れます。
この間、画面のリロードは行われていないので、シングルページで実装できました。





ng-routeとRailsの処理の流れを詳しく

上記でダッシュボード画面、Todoリスト画面と画面遷移を確認できましたので、これが裏側でどうなっているかを詳しく流れを追ってみます。
まず、config/routes.rb/, /dashboard, /todo_list/:idといったこのアプリで使うURLほとんどのURLをtempaltes/index.html.erbに遷移するようにしています。

# config/routes.rb

root 'templates#index'
get '/dashboard'      => 'templates#index'
get '/todo_lists/:id' => 'templates#index'
get '/templates/:path.html' => 'templates#template', constraints: { path: /.+/ }


/にアクセスすると、templates#index.html.erbが表示されます。
ここには、ng-viewディレクティブが記載されていて、これは、$routeProviderで設定したルートに合わせた画面を表示します(templateToで指定したhtmlファイルを表示したり、redirectToで指定したURLにリダイレクトする)

<!-- app/views/templates/index.html.erb -->
<div ng-view>
</div>


/の場合は、/dashboardにリダイレクトされると記載されているので、自動的に/dashboardにリダイレクトされます。

# app/assets/javascripts/app.coffee

app.config ($routeProvider, $locationProvider) ->
  $locationProvider.html5Mode true
  $routeProvider.when '/',                    redirectTo:  '/dashboard'
  $routeProvider.when '/dashboard',           templateUrl: '/templates/dashboard.html', controller: 'DashboardCtrl'
  $routeProvider.when '/todo_lists/:list_id', templateUrl: '/templates/todo_list.html', controller: 'TodoListCtrl'


すると、config/routes.rbにより、また、templates#index.html.erbが表示されます。
その中のng-view$routeProviderの設定により、AngularJSは/templates/dashboard.htmlを表示しようとします。

ここで、config/routes.rbに記載して、次のルートとコントローラーによりより、dashboard.html.erbが画面に表示されます。

# config/routes.rb
get '/templates/:path.html' => 'templates#template', constraints: { path: /.+/ }

# app/controllers/templates_controller.rb
def template
  render template: "templates/#{params[:path]}", layout: nil
end


というように、ng-view$routeProviderの設定により基本的なシングルページのアクセスを行い、そこからのアクセスのために、Railsのconfig/routes.rbでルートを通しておくという流れになると思います。


以上です。

本連載で、RailsにAngularJSをインストールする箇所から、Controller、Service、ルーティングとAngularJSの機能を一通り使ってきました。
また、filter, directiveの作成というところはやっていませんが、RailsとAngularJSをどう使ったら良いかというのが少しでもわかっていただけたら幸いです。

他にも良く使うディレクティブがありますので、リファレンスなどを眺めて便利な機能を確認すると良いと思います。