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

SCALA play frameworkと angular.jsでtodoアプリ開発

プログラミング Angular.js scala Play Framework

scala play frameworkとangular.jsを連携してTODOアプリを作ったのでメモ

○概要
 タイトルとステータス(終了、未終了)のステータスの情報を持ったTodoアプリを作成する。クライアントサイドではangular.jsを使いサーバーサイドではscalaのplay frameworkを使う。サーバーサイドとクライアントサイドの通信にはajaxを使用し、ページアクセス時と更新ボタン押下時に通信を行いtodoリストの更新(登録、更新、削除)と取得を行い画面を再進化する。

○クライアントサイド
 クライアントサイドにはgithubのコード(Todo アプリ)をベースとして改修を行う。

ajax使用時の画面更新
 ajaxを使って画面とバインドしているリストを更新した場合、リストを更新しただけでは画面が更新されなかった。その場合は、$scope.$apply();を呼び出し任意のタイミングでバインド情報を更新させる。

 $scope.refresh = function(){
     var formData = todos.getList();
     var tmpList = [];
     $.ajax({
       type: "POST",
       url: "todo/insert",
       data: {"formData":formData},
     }).done(function( todoData ) {
       var todoSize = todoData.size;
       for(var i=0; i < todoSize; i++){
         tmpList.push({
           title: eval("todoData.name_" + i),
           done: eval("todoData.done_" + i),
           id:eval("todoData.id_" + i)
         });
       }
       todos.reload(tmpList);
       $scope.$apply();
     });
  };

・angular.jsでのカスタムフィルタ作成
 画面にバインドしているリストの状態属性とフィルタ情報をもとに表示を制限する場合、angular.jsのフィルタを使用する。その時はng-repeatのfilter属性にカスタムフィルタ用のメソッドを渡す。カスタムフィルタの呼び出し元は以下になる。

      <li class="todo-item"
          ng-repeat="todo in todoList | filter:todoFilter"
          ng-class="{done: todo.done == 1, editing: todo == editing}">

カスタムフィルタ本体は以下のようになる。

  $scope.todoFilter = function (todo) {
    if($scope.currentFilter == null && todo.done != -1){
      return true;
    }else if($scope.currentFilter == todo.done){
      return true;
    }
    return false;
  };


・helperフォームを使用する場合
 今回は行っていないがplay frameworkでhelperフォームを使用してangular.jsと連携したい場合、ng-controller属性指定時にplay frameworkのsymbolメソッドを使う。以下に例を示す。

	  @helper.form(action = routes.HouseholdController.insert, 'id -> "myForm",
	  	 Symbol("ng-controller") -> "RegisterController", Symbol("ng-submit") -> "addPay()" ) {
	    追加</br>
	    <input id="payDay" name="payday" type="date" placeholder="日付"/>
	    <input name="name" type="text" placeholder="名前" ng-model="loginName"/>
	    <input name="price" type="number" placeholder="値段"/>
	    <input type="submit"/>
	    <button class="btn btn-default"
                type="submit">追加</button>
	  }

○サーバーサイド scala play framework
・play-slick
 データの保存にはplay-slickを使用する。play frameworkではいろいろdbフレームワークがあるが今回はplay frameworkの時期バージョンに標準で組み込まれる予定のplay-slickを使う。
build.sbtにplay-slickの依存ライブラリ"com.typesafe.play" %% "play-slick" % "0.8.0"を追加する。javaのバージョンが6以前の場合は"9.3-1102-jdbc41"の部分を"9.3-1102-jdbc4"に変更してください。

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache,
  ws,
  "com.typesafe.play" %% "play-slick" % "0.8.0",
  "org.postgresql" % "postgresql" % "9.3-1102-jdbc41",
  "joda-time" % "joda-time" % "2.2",
  "org.joda" % "joda-convert" % "1.2" % "compile"
)

dbの接続情報はapplication.confに記述する。

テーブルの情報とdaoの作成のためにmodelを作成する。

package models

import play.api.db.slick.Config.driver.simple._


case class Todo(id: Int, name: String, status: Int)

/* Table mapping
 */
class TodoModel(tag: Tag) extends Table[Todo](tag, "TODO"){
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name", O.NotNull)
  def status = column[Int]("status", O.NotNull)
  def * = (id, name, status) <>(Todo.tupled, Todo.unapply _)
 }

object TodoModel extends DAO {
  def findById(id: Int)(implicit s: Session): Option[Todo] = {
    return TodoModel filter { _.id === id } firstOption
  }

  def findAll()(implicit s: Session): List[Todo] = {
    return TodoModel.list
  }
  def insert(todo: Todo)(implicit s: Session) {
    TodoModel += todo
  }

  def update(todo: Todo)(implicit s: Session) {
    TodoModel.filter(_.id === todo.id).update(todo)
  }

  def delete(todo: Todo)(implicit s: Session) {
    TodoModel.filter(_.id === todo.id).delete
  }
}

private[models] trait DAO {
  val TodoModel = TableQuery[TodoModel]
}

・リクエストの取得
ajaxで取得した情報を取得する。取得時に指定するパラメーターに"formData[n][title]"を直接指定するようにしている。scalaでは暗黙の型変換なる機能があるのでもっと良いリクエスト取得方法があるだろうがまだ慣れてないので一番単純そうな方法にする。データの保存ではmodelで作成したdaoをそのまま使う。

    val formBody:Option[Map[String ,Seq[String]]] = request.body.asFormUrlEncoded
    if(formBody != None) {
      val params: Map[String, Seq[String]] = formBody.get
      val dataNumber = (params.size / 4) - 1
      for (i <- 0 to dataNumber) {
        val todoId = params("formData[" + i.toString + "][id]").head
        val todoTitle = params("formData[" + i.toString + "][title]").head
        val todoState = params("formData[" + i.toString + "][done]").head

        val todo = Todo(todoId.toInt, todoTitle, todoState.toString.toInt)
        if (todoId.equals("0") && !todoState.equals("-1")) {
          //インサートを実行する
          TodoModel.insert(todo)
        } else if(todoState.equals("-1")) {
          TodoModel.delete(todo)
        }else{
          TodoModel.update(todo)
        }

      }
    }


・データ取得
 データの取得にはDaoで作成したfindAllメソッドを使用する。データを取得した後はjsに渡しやすいようにmapに格納する。

    var allTodoList:List[Todo] = TodoModel.findAll()
    val listSize = allTodoList.size

    var todoMap = Map("size" -> listSize.toString)
    for(i <- 0 to listSize -1){
      val todoData = allTodoList.head
      allTodoList = allTodoList.tail
      todoMap = todoMap + ("id_" + i.toString -> todoData.id.toString,
            "name_" + i.toString -> todoData.name.toString, "done_" + i.toString -> todoData.status.toString)

    }