第5回でusersテーブルを追加しましたが、前回不参加の方に、最初伝え漏れていましたので追記いたしました(事前準備のところ)。すいませんでした!
はじめに
本投稿は、2015/6/26に行われた、CakePHPでデータ操作〜担当者アサイン機能の実装 - connpassの内容についてまとめた資料です。
今後の予定
AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - connpass
今回は、TODOに担当者を割り当てる機能を追加します。
前回ログイン機能を付けたので、複数ユーザで使用する感じにしてみます!
CakePHPの「アソシエーション」について学びます。
CakePHPのアソシエーションとは?
前回まで、
- users
- todo_lists
の2テーブルを扱っていました。
しかし、テーブル1つの単純なSelectしかしていませんでした。
今回作成するアプリケーションでは、usersとtodo_listsを結合してSelectしたりする必要がありますが、CakePHPではこういった場合にSQLを直接書くわけではなく、「アソシエーション」という仕組みを使用します。
テーブルを結合したりするのを、SQLではなくコードで表現します。
今回学ぶアソシエーション
今回は下記3つのアソシエーションを学びます。
- belongsTo
- hasOne
- hasMany
「hasAndBelongsToMany」というのもありますが、今回は割愛します。
1つずつ説明します。
belongsTo
A belongsTo B、つまり、AはBに属している、というテーブル間の関係がある場合、belongsToアソシエーションを使用します。
例えば、「ユーザ(users)」テーブルに、「国籍」というフィールドがあり、「国籍マスタ(countries)」テーブルがあるとします。
- countries(id, name)
とします。
userは一つの国籍をもちますので、
- users(id, username, country_id)
という感じです。
users.country_id と countries.id で joinしてSelectすれば取得できますね。
SQLで書くとこんな感じです。
select users.id, users.username, countries.name as country_name
from users inner join countries on users.country_id = countries.id
こういう場合に使用するのが「belongsTo」アソシエーションです。
コードは後ほど見ていきます!
hasOne
hasOneは、相手側テーブルが1件、と言う意味でbelongsToの親戚みたいなものです。
belongsToの例では、users側にcountriesのレコードを示すcountry_id
を持っていました。
例えば、ユーザのプロフィールを保持するテーブル(profiles)があるとします。
- profiles(id, user_id, 自己紹介文, 好きな食べ物, etc...)
この場合、users側ではなく、profiles側にuser_idを持っていますので、
users.id と profiles.user_id で joinしてSelectすれば取得できますね。
こういう場合に使用するのが「hasOne」アソシエーションです。
users側にprofile_idをもつ、という実装も出来ますが、「プロフィールを未登録」の場合に
- users.profile_idをnullにする実装にする
- users.id = profies.user_idとなるprofilesのレコードがないだけ
のどちらにするかと言う設計思想の違いですね。
SQLで書くとこうです。
select users.id, profiles.自己紹介文, profiles.好きな食べ物
from users inner join profiles on users.id = profiles.user_id
コードは後ほど見ていきます!
hasMany
hasManyは1:nの関係を表す場合に使用します。
例えば、ユーザの職歴を保持するテーブル(careers)があるとします。
- careers(id, user_id, 入社年度, 会社名, etc...)
この場合、usersは複数の職歴(career)レコードを持てるので、
users.id と careers.user_id で joinしてSelectすれば取得できますね。
hasOneとの違いは、この時複数件のレコードがとれてくる、と言う部分のみです。
SQLで書くと、通常はこうです。
select users.id, careers.入社年度, careers.会社名
from users inner join careers on users.id = careers.user_id
実際にはCakePHPの場合、usersのselectとcareersのselectがそれぞれ一回、計2回のselectが発行される実装になっています。
コードは後ほど見ていきます!
今回の内容
今回は、以下の機能を実現します。
機能リスト
- Todoの追加
- TODOの内容に加え「そのTODOを誰が担当するか」を全ユーザのリストから選択して指定する
- ユーザのリストは、ログインユーザがデフォルトで選択状態とする。
- TODO一覧の表示
- TODO追加を実行したユーザを「オーナ」、TODOを割り当てたユーザを「担当」として表示します。
- 下記の操作制限を実装します。
- 「削除」はオーナのみ可能。ログインユーザがオーナでないTODOの行の「削除」リンクは表示しない。
- 「詳細」の表示はオーナまたは担当のみ可能。それ以外の場合は「詳細」リンクは表示しない。
- TODO詳細
- 担当者の変更を可能とする。
画面
- TODO一覧画面
- TODO詳細画面
事前準備
事前準備は毎回同じなので、別エントリにまとめています。
全12回の勉強会でやっているGitの使い方 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~ - Qiitaを参照してください。
やることは、
- gitのブランチを整えて今回用ブランチ
vol/06
を作成する
です。まずこれをやりましょう。
それと、前回usersテーブルを追加しましたが、前回不参加の方はそっちもやっておく必要があります。こちらを参照下さい。
今回はさらに、
todo_listsテーブルに、
- オーナのユーザID(owner)
- 担当のユーザID(assignee)
を追加します。
TODO一覧テーブルへの列追加
まず、phpMyAdminにログインしましょう。
URLは、http://(PublicIP)/phpMyAdmin/
です。
変更していなければ、ユーザ名study
、パスワードstudy
でログインできます。
各列の設定
名前 | データ型 | 長さ/値 | デフォルト値 | 照合順序 | 属性 | NULL | インデックス | A_I(AutoIncrement) | コメント |
---|---|---|---|---|---|---|---|---|---|
owner | INT | 10 | なし | UNSIGNED | オーナ | ||||
assignee | INT | 10 | なし | UNSIGNED | 担当 |
準備
では、準備を始めましょう!
-
Gitのブランチを整えて、
vol/06
ブランチを作成。 - TODO一覧(todo_lists)テーブルに列を追加。
準備ができたら、Lesson1です!
Lesson1 サーバサイド
Lesson1では、サーバサイドのプログラムを実装します。
プログラムのテストには、第1回でインストールした「POSTMAN」を使用してAPIの動作確認を行います。
ポイント
今回のテーマは「アソシエーション」ですが、前述の3つのうち、実は「belongsTo」だけで今回の機能は実現できてしまいます。
それだけでは面白く無いので、「hasOne」と「hasMany」も試してみます。
まずは、belognsToを使用して、アプリケーションの実装をします。
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/Controller/TodoListsController.php | findのパラメータの設定 |
修正 | app/Model/TodoList.php | アソシエーションの設定 |
修正 | app/Controller/UsersController.php | ユーザ一覧の取得 |
TodoListsController.php
TodoListモデルのfind関数実行じのパラメータを設定します。
〜略〜
class TodoListsController extends AppController {
public function index() {
- $res = $this->TodoList->find('all');
+ $query = array (
+ 'fields' => array (
+ 'TodoList.id',
+ 'TodoList.todo',
+ 'TodoList.status',
+ 'Owner.id',
+ 'Owner.name',
+ 'Assignee.id',
+ 'Assignee.name'
+ ),
+ 'order' => "TodoList.id"
+ );
+ $res = $this->TodoList->find('all', $query);
+ // 整形
+ if (count($res) > 0) {
+ $loginUserId = $this->Auth->user()['id'];
+ foreach ( $res as $key => $row ) {
+ //「ログインユーザがオーナである」フラグ
+ $res[$key]['TodoList']['owned'] = $row['Owner']['id'] === $loginUserId;
+ //「ログインユーザが担当である」フラグ
+ $res[$key]['TodoList']['assigned'] = $row['Assignee']['id'] === $loginUserId;
+ }
+ }
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
public function view($id = null) {
$res = $this->TodoList->findById($id);
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
public function add() {
$data = $this->request->data;
+ $data['owner'] = $this->Auth->user()['id'];
$res = $this->TodoList->save($data);
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
〜以下略〜
find
関数に引数追加
- 第二引数
$query
を追加しています。$query
は配列で、下記を設定しています。-
fields
- 取得するテーブルの列。これを指定しないと、ユーザのパスワード等もとれてしまうため、必要な物のみ設定しています。
-
order
- ついでにデータの並び順も指定しました。todo_listsテーブルのidの昇順です。
-
取得したデータの整形
- 取得した1件のレコードについて、
- ログインユーザがオーナである場合、
owned = true
に設定、そうでない場合owned = false
に設定。 - ログインユーザが担当である場合、
assigned = true
に設定、そうでない場合assigned = false
に設定。
- ログインユーザがオーナである場合、
TodoList.php
アソシエーションの設定
class TodoList extends AppModel {
+ public $belongsTo = array (
+ 'Owner' => array (
+ 'className' => 'User',
+ 'foreignKey' => 'owner',
+ ),
+ 'Assignee' => array (
+ 'className' => 'User',
+ 'foreignKey' => 'assignee'
+ )
+ );
}
belongsToアソシエーション
- TODO一件に対する、
- オーナのユーザ情報
- 担当のユーザ情報
を設定しています。詳細は下記の通り。
- モデルの変数に
$belongsTo
という変数名で配列を設定します。-
Owner
(オーナ),Assignee
(担当)という名前で2つのusersテーブルを紐つけています。 - その下のパラメータ詳細(
Owner
)-
className
-> モデル名(User) - usersに対する外部キー(
todo_lists.owner
を指定)
-
- その下のパラメータ詳細(
Assignee
)-
className
-> モデル名(User) - usersに対する外部キー(
todo_lists.assignee
を指定)
-
-
addメソッド
-
owner
にログインユーザのIDを設定しています。- これで、TODOの追加操作をした際のログインユーザがオーナとして設定されます。
UsersController.php
ログイン、ログイン済みチェック、ログアウト、ユーザ登録の実装です。
class UsersController extends AppController {
+ public function index() {
+ $res = $this->User->find('all', array (
+ 'fields' => array (
+ 'User.id',
+ 'User.username',
+ 'User.name'
+ )
+ ));
+ $this->set(compact('res'));
+ $this->set('_serialize', 'res');
+ }
+
public function login() {
$user = $this->Auth->user();
$res = array();
〜以下略〜
index
関数
-
TodoListController.php
と同じ容量で、find
関数で取得するフィールドを絞っています。
### 動作確認
POSTMANで、TODO一覧取得API(http://(PublicIP)/rest-study/todo_lists.json
)をテストします。
下記のように取得できればOKです。
belongsToが効いています。
[
{
"TodoList": {
"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owned": true,
"assigned": true
},
"Owner": {
"id": "67",
"name": "山田太郎"
},
"Assignee": {
"id": "67",
"name": "山田太郎"
}
},
{
"TodoList": {
"id": "78",
"todo": "犬の散歩",
"status": "0",
"owned": true,
"assigned": false
},
"Owner": {
"id": "67",
"name": "山田太郎"
},
"Assignee": {
"id": "68",
"name": "田中花子"
}
},
{
"TodoList": {
"id": "83",
"todo": "家に電話",
"status": "1",
"owned": false,
"assigned": false
},
"Owner": {
"id": "68",
"name": "田中花子"
},
"Assignee": {
"id": "68",
"name": "田中花子"
}
}
]
実装
では、実際に修正して上記のテストをしてみましょう。
-
app/Controller/TodoListsController.php
を上記の通り修正。 -
app/Model/TodoList.php
を上記の通り修正。 -
app/Controller/UsersController.php
を上記の通り修正。 - 動作確認!
- Gitにコミット
GitHubでのdiff表示へのリンク
第6回 Lesson1 サーバサイド · suzukishouten-study/rest-study@d4fe1a4
Lesson1.5へ!
Lesson1.5 他のアソシエーションを試す。
今回実装するアプリケーションとしてはLesson1の実装でOKです。
Lesson2のクライアントの実装に入る前に、hasOne
とhasMany
のアソシエーションも試してみましょう。
ソースを修正して動作確認後、元に戻してLesson2へ行きましょう。
テストでは、下記のデータの状態で実行した結果を載せています。
ユーザは、
- 山田太郎
- 田中花子
- 佐藤一郎
が登録されていますが、佐藤一郎さんはまだTODOが1件もない(自分がオーナのものも、担当としてアサインされたものも)状態です。
hasOneをテストする
あるユーザがオーナとなっているTODOを取得するケースを考えます。
user : todo
は 1 : n
になるので本来はhasMany
を使用する場面です。
ちょっと無理矢理ですが、「田中花子さん」がオーナのTODOは1件なので、田中花子さんのTODOがちゃんと取れるか確認してみましょう。
ソース修正箇所
#####app/Controller/UsersController.php
のindex
関数を修正。
public function index() {
$res = $this->User->find('all'); //fieldsの指定を外す。下記はコメントアウト
// $res = $this->User->find('all', array (
// 'fields' => array (
// 'User.id',
// 'User.username',
// 'User.name'
// )
// ));
$this->set(compact('res'));
$this->set('_serialize', 'res');
}
app/Model/User.php
にアソシエーションを設定。
class User extends AppModel {
public $hasOne = array (
'TodoList' => array (
'className' => 'TodoList',
'foreignKey' => 'owner'
)
);
}
さて、ユーザ取得API(http://(PublicIP)/rest-study/users.json
)を叩いてみましょう!
[
{
"User": {
"id": "67",
"username": "yamada",
"password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
"name": "山田太郎"
},
"TodoList": {
"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owner": "67",
"assignee": "67"
}
},
{
"User": {
"id": "67",
"username": "yamada",
"password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
"name": "山田太郎"
},
"TodoList": {
"id": "78",
"todo": "犬の散歩",
"status": "0",
"owner": "67",
"assignee": "68"
}
},
{
"User": {
"id": "68",
"username": "tanaka",
"password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
"name": "田中花子"
},
"TodoList": {
"id": "83",
"todo": "家に電話",
"status": "1",
"owner": "68",
"assignee": "68"
}
},
{
"User": {
"id": "69",
"username": "sato",
"password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
"name": "佐藤一郎"
},
"TodoList": {
"id": null,
"todo": null,
"status": null,
"owner": null,
"assignee": null
}
}
]
ユーザに対して1件のTODOが紐ついていますね!
1件もTODOを持っていない場合は、各項目がnullの配列が一個出来るようです。
この時実行されたSQLは下記です。hasManyでは一回の実行でSQLが2回実行されます。
SELECT
`User`.`id`,
`User`.`username`,
`User`.`password`,
`User`.`name`,
`TodoList`.`id`,
`TodoList`.`todo`,
`TodoList`.`status`,
`TodoList`.`owner`,
`TodoList`.`assignee`
FROM `study`.`users` AS `User`
LEFT JOIN `study`.`todo_lists` AS `TodoList` ON (`TodoList`.`owner` = `User`.`id`)
WHERE 1 = 1
謎のWHERE 1 = 1
がついてますがこういうSQLです。普通にJOINしてるだけですね。
hasManyをテストする
user : todo
は 1 : n
になるので本来のhasMany
の使いドコロです。
ソーズ修正箇所
app/Controller/UsersController.php
はhasOneで修正した状態で置いておきましょう。
app/Model/User.php
にアソシエーションを設定。
class User extends AppModel {
public $hasMany = array (
'TodoList' => array (
'className' => 'TodoList',
'foreignKey' => 'owner'
)
);
}
hasOneをhasManyに変えただけです!
では実験。
[
{
"User": {
"id": "67",
"username": "yamada",
"password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
"name": "山田太郎"
},
"TodoList": [
{
"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owner": "67",
"assignee": "67"
},
{
"id": "78",
"todo": "犬の散歩",
"status": "0",
"owner": "67",
"assignee": "68"
}
]
},
{
"User": {
"id": "68",
"username": "tanaka",
"password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
"name": "田中花子"
},
"TodoList": [
{
"id": "83",
"todo": "家に電話",
"status": "1",
"owner": "68",
"assignee": "68"
}
]
},
{
"User": {
"id": "69",
"username": "sato",
"password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
"name": "佐藤一郎"
},
"TodoList": []
}
]
こうなりました!ちゃんとユーザに対してTODOが複数件紐ついています。
これがhasManyですね。
この時実行されたSQLは下記2つです。hasManyでは一回の実行でSQLが2回実行されます。
SELECT `User`.`id`, `User`.`username`, `User`.`password`, `User`.`name` FROM `study`.`users` AS `User` WHERE 1 = 1
SELECT `TodoList`.`id`, `TodoList`.`todo`, `TodoList`.`status`, `TodoList`.`owner`, `TodoList`.`assignee` FROM `study`.`todo_lists` AS `TodoList` WHERE `TodoList`.`owner` IN (67, 68, 69)
2回目は、1回目で取得したuserのidをownerに持つTodoListをselectしています。
こういう実装になってるんですね。
では、修正したところを元に戻して、lesson2、 画面を作りましょう。
Lesson2 クライアントサイド
Lesson2では、クライアントサイドのプログラムを実装します。
ポイント
前回までの知識でほぼ開発できますが、今回、下記の「ユーザ一覧を取得して表示する」部分だけ少し解説します。
- TODO一覧画面のユーザ一覧表示
- TODO詳細画面のユーザ一覧表示
今回の実装
Backbone、Marionetteをフル活用するなら、本来はユーザ一覧表示部分をViewにし、「Behavior」機能を使うなどして共通化するといい感じなのですが、
今回のメインは「CakePHPのアソシエーション」なので、ちょっと簡単(手抜き?)の実装になっています。
ご了承下さい。
具体的には、
- ユーザ取得API(
http://(PublicIP)/rest-study/users.json
)を叩いてcollectionにユーザ一覧を取得 - そのcollectonをループで処理し、テンプレートで用意している
<select></select>
タグの中に、<option value="ユーザのid">ユーザの氏名</option>
というタグをJQueryを使ってはめ込んでいく。
というやり方です。
これを、TODO一覧とTODO詳細の両画面で「コピペ」しています。
(DRYではないですが今回はご勘弁!)
では、ソース解説です。
編集するファイル一覧
編集 | file | 編集概要 |
---|---|---|
修正 | app/View/Layouts/default.ctp | テンプレートの修正 |
新規 | app/webroot/js/collections/user-collection.js | ユーザ一覧用collection |
修正 | app/webroot/js/models/todo-model.js | 追加項目のparse |
修正 | app/webroot/js/views/todo-layout-view.js | ユーザ一覧読み込み処理 |
修正 | app/webroot/js/views/todo-composite-view.js | ユーザ一覧表示 |
修正 | app/webroot/js/views/todo-item-view.js | 削除・詳細ボタンの表示制御 |
修正 | app/webroot/js/views/todo-detail-layout-view.js | ユーザ一覧読み込み処理 |
修正 | app/webroot/js/views/todo-detail-item-view.js | ユーザ一覧表示 |
default.ctp
テンプレートを修正。
〜 略 〜
<title>シンプルTODOアプリ</title>
</head>
<body>
<!-- ヘッダ -->
<div id="header"></div>
<!-- コンテンツ -->
<div id="main"></div>
〜 略 〜
<!-- TODO一覧表示のテンプレート -->
<script type="text/template" id="todo-composite-template">
<textarea style="width:300px;height:50px"id="new-todo" placeholder="Todo?" autofocus></textarea>
+ <select name="assignee" id="user-list">
<input type="button" id="addTodo" value="追加">
<hr>
<div>
<table border="1" width="350px">
+ <thead>
+ <tr>
+ <td>完了</td>
+ <td>ToDo</td>
+ <td>オーナ</td>
+ <td>担当</td>
+ <td colspan="2"></td>
+ </tr>
+ </thead>
<tbody></tbody>
</table>
</div>
</script>
<!-- TODO一行分のテンプレート(上のtbody部分に挿入される) -->
<script type="text/template" id="todo-item-template">
<td><input type="checkbox" class="toggle" <%- status === '1' ? 'checked' : '' %>></td>
<td style="margin:0px">
<span class="todo-edit" style="margin:0px"><%- todo %></span>
</td>
<td>
+ <span><%- Owner.name %></span>
+ </td>
+ <td>
+ <span><%- Assignee.name %></span>
+ </td>
+ <td>
<a class="remove-link" href="#">削除</a>
+ </td>
+ <td>
<a class="detail-link" href="#todo-lists/<%- id %>">詳細</a>
</td>
</script>
<!-- 詳細画面のレイアウトテンプレート -->
<script type="text/template" id="todo-detail-layout-template">
<div id="todo-item"></div>
</script>
<!-- 詳細画面の表示内容テンプレート -->
<script type="text/template" id="todo-detail-item-template">
<h2>Todo #<%- id %></h2>
<div>
<textarea style="width:300px;height:50px" id="edit-todo" autofocus placeholder="Todo?"><%- todo %></textarea>
+ <select name="assignee" id="user-list">
+ </select>
<input type="button" id="updateTodo" value="更新"></input>
<input type="button" id="updateCancel" value="キャンセル"></input>
</div>
</script>
〜 略 〜
下記の通り修正。
- TODO一覧画面
- ユーザ一覧表示用
<select></select>
を追加 - tableに
<thead></thead>
を追加(項目が増えてきたので) - [削除]と[詳細]を別の
<td></td>
に分割
- ユーザ一覧表示用
- TODO詳細画面
- ユーザ一覧表示用
<select></select>
を追加
- ユーザ一覧表示用
user-collection.js
//User一覧表示用コレクション
define(function(require) {
var UserModel = require('models/user-model');
var UserCollection = Backbone.Collection.extend({
url : '/rest-study/users.json',
model : UserModel,
parse : function(response) {
return response;
}
});
return UserCollection;
});
必要最低限のcollectionの実装です。
これはもう説明不要ですね!
todo-model.js
追加項目のOwner
とAssignee
をパースしています。
//Todoデータ1件を表すモデル
define(function() {
var TodoModel = Backbone.Model.extend({
urlRoot : '/rest-study/todo_lists',
parse : function(response) {
//モデルをパース
console.log("モデルをパース");
console.log(response);
- return response.TodoList;
+ var parsed = response.TodoList;
+ if (response.Owner) {
+ parsed.Owner = response.Owner;
+ parsed.Assignee = response.Assignee;
+ }
+ return parsed;
},
toggle : function() {
this.set('status', this.get("status") === '1' ? '0' : '1');
this.save();
}
});
return TodoModel;
});
サーバからはCakePHP任せの下記フォーマットでTODO1件のJSONが来るので、
{
"TodoList": {
"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owned": false,
"assigned": false
},
"Owner": {
"id": "67",
"name": "山田太郎"
},
"Assignee": {
"id": "67",
"name": "山田太郎"
}
}
こんなフォーマットに変更しています。
{
"id": "62",
"todo": "牛乳を買う",
"status": "0",
"owned": false,
"assigned": false,
"Owner": {
"id": "67",
"name": "山田太郎"
},
"Assignee": {
"id": "67",
"name": "山田太郎"
}
}
todo-layout-view.js
ユーザ一覧読み込み処理を追加しています。
define(function(require){
var TodoCompositeView = require('views/todo-composite-view');
var TodoCollection = require('collections/todo-collection');
+ var UserCollection = require('collections/user-collection');
var TodoLayoutView = Marionette.LayoutView.extend({
//テンプレート
template: '#todo-layout-template',
regions : {
listRegion : '#todo-lists',
},
- onRender : function(){
+ onRender : function() {
+ this.userCollection = new UserCollection();
+ this.listenTo(this.userCollection, 'reset', this.onLoadUsers, this);
+ this.userCollection.fetch({reset : true});
+ },
+
+ onLoadUsers : function(userCollection) {
var todoCollection = new TodoCollection();
this.listenTo(todoCollection , 'reset', this.showTodoList, this);
todoCollection.fetch({reset : true});
},
showTodoList : function(todoCollection){
this.listRegion.show( new TodoCompositeView({
- collection : todoCollection
+ collection : todoCollection,
+ userList : this.userCollection.models
}));
},
});
return TodoLayoutView;
});
-
collections/user-collection
をrequire
で読み込む -
onRender
関数内で、this.userCollection.fetch()
でサーバからユーザ一覧取得。 - fetchが終わると(ListenToが効いて)
onLoadUsers
関数を実行、続いてshowTodoList
が実行される -
showTodoList
関数内で、TodoCompositeView
の初期化時に、取得したユーザ一覧のcollectionを渡すようにしている- このユーザ一覧collectionを
TodoCompositeView
の処理で表示するわけです。
- このユーザ一覧collectionを
todo-composite-view.js
ユーザ一覧を表示します。
//Todo一覧表示用ビュー
define(function(require) {
var TodoItemView = require('views/todo-item-view');
var TodoCompositeView = Marionette.CompositeView.extend({
template: '#todo-composite-template',
childView : TodoItemView,
childViewContainer : 'tbody',
ui : {
addTodo : '#addTodo',
- newTodo : '#new-todo'
+ newTodo : '#new-todo',
+ userList : '#user-list'
},
events : {
'click @ui.addTodo' : 'onCreateTodo',
},
- initialize: function(){
+ initialize: function(options){
_.bindAll( this, 'onCreatedSuccess' );
+ this.userList = options.userList;
},
+ onRender : function() {
+ //ユーザ一覧を表示
+ this.showUserList(this.ui.userList, this.userList);
+ //ログインユーザをデフォルトで選択状態にする
+ this.ui.userList.val(window.application.loginUser.id);
+ },
+
+ //ユーザ一覧を表示
+ showUserList : function($list, userList){
+ $.each(userList, function(index, userModel) {
+ $list.append(
+ "<option value='"
+ + userModel.attributes.id + "'>"
+ + userModel.attributes.name + "</option>");
+ });
+ },
+
onCreateTodo : function() {
this.collection.create(this.newAttributes(), {
silent: true ,
success: this.onCreatedSuccess
});
this.ui.newTodo.val('');
},
newAttributes : function() {
return {
todo : this.ui.newTodo.val().trim(),
- status : 0
+ status : 0,
+ assignee : this.ui.userList.val()
};
},
onCreatedSuccess : function(){
this.collection.fetch({ reset : true });
},
});
return TodoCompositeView;
});
- ユーザ一覧のセレクトボックスを処理するため
ui
変数にuserList
を追加。 -
initialize
関数内で、todo-layout-viewから渡されたユーザ一覧collection(options.userList
に入ってくる)を自身の中(this.userListに保持) -
onRender
関数内で、showUserList
関数を実行してユーザ一覧の表示を行う - ユーザ一覧表示後、ログイン時に細んしている
window.application.loginUser
のユーザ情報からログインユーザのidを取得し、ユーザ一覧表示で自分(ログインユーザ)をデフォルトで選択された状態に設定。 -
showUserList
関数では、jqueryを使用し、ui
変数で定義済みの<select></select>
タグの中に<option></option>
タグを埋め込んでいます。 - todoの新規追加時(
newAttributes
関数)は、ユーザ一覧で選択したユーザを担当者(assignee
)をして設定します。
todo-item-view.js
削除・詳細ボタンの表示制御を行っています。
//Todo一覧の1件表示用ビュー
define(function(){
var TodoItemView = Marionette.ItemView.extend({
//DOMに要素追加のタグ名
tagName : 'tr',
//テンプレート
template : '#todo-item-template',
ui : {
checkBox : '.toggle',
- removeLink : '.remove-link'
+ removeLink : '.remove-link',
+ detailLink : '.detail-link'
},
//DOMイベントハンドラ設定
events : {
//チェックボックスクリック時
'click @ui.checkBox' : 'onStatusToggleClick',
//削除ボタンクリック時
'click @ui.removeLink' : 'onRemoveClick',
},
onStatusToggleClick : function() {
this.model.toggle();
},
onRemoveClick : function() {
this.model.destroy({
wait : true
});
},
+ onRender : function() {
+ if (!this.model.attributes.owned) {
+ //オーナでない場合は削除リンク非表示
+ this.ui.removeLink.css({
+ display : 'none'
+ });
+ if (!this.model.attributes.assigned) {
+ //担当者でもない場合は、
+ //詳細リンクも非表示
+ this.ui.detailLink.css({
+ display : 'none'
+ });
+ //チェックボックスも変更不可
+ this.ui.checkBox.prop('disabled', true);
+ }
+ }
+ }
});
return TodoItemView;
});
-
ui
変数に削除リンク用の変数detailLink
を追加 -
onRender
関数で、削除、詳細の各リンクの表示制御を下記の仕様で行っています。- オーナでない場合は削除リンクを非表示にする
- さらに、担当者でもない場合は、
- 詳細リンクも表示にする
- チェックボックスをdisableにして変更不可とする
- さらに、担当者でもない場合は、
- オーナでない場合は削除リンクを非表示にする
※表示制御の実装
- リンクの非表示
- jqueryのcss関数で、
display : 'none'
のスタイルを設定
- jqueryのcss関数で、
- チェックボックスの変更不可
- jqueryのprop関数で、
disabled
をtrue
に設定
- jqueryのprop関数で、
todo-detail-layout-view.js
ユーザ一覧読み込み処理を追加しています。
やっていることは、todo-layout-view.js
とほぼ同じです。
define(function(require) {
var TodoDetailItemView = require('views/todo-detail-item-view');
var TodoModel = require('models/todo-model');
+ var UserCollection = require('collections/user-collection');
var TodoDetailLayoutView = Marionette.LayoutView.extend({
//テンプレート
template : '#todo-detail-layout-template',
regions : {
itemRegion : '#todo-item',
},
onRender : function() {
- var todoModel = new TodoModel({
- id : this.options.modelId
- });
- //モデルのサーバからのデータ取得完了時、描画を行う
- this.listenTo(todoModel, 'sync', this.showItem, this);
- //サーバからデータ取得
- todoModel.fetch({
- wait : true
+ this.userCollection = new UserCollection();
+ this.listenToOnce(this.userCollection, 'reset', this.onLoadUsers, this);
+ this.userCollection.fetch({
+ reset : true
});
},
- showItem : function(todoModel) {
- this.itemRegion.show( new TodoDetailItemView({
- model : todoModel
- }));
- },
+ onLoadUsers : function(userCollection){
+ var todoModel = new TodoModel({
+ id : this.options.modelId
+ });
+ //モデルのサーバからのデータ取得完了時、描画を行う
+ this.listenToOnce(todoModel, 'sync', this.showItem, this);
+ //サーバからデータ取得
+ todoModel.fetch({
+ wait : true
+ });
+ },
+
+ showItem : function(todoModel) {
+ this.itemRegion.show(new TodoDetailItemView({
+ model : todoModel,
+ userList : this.userCollection.models
+ }));
+ },
});
return TodoDetailLayoutView;
});
-
collections/user-collection
をrequire
で読み込む -
onRender
関数内で、this.userCollection.fetch()
でサーバからユーザ一覧取得。- 元々
onRender
内で行っていた、model1件のサーバからの取得処理は、onLoadUsers
内に移動しています。
- fetchが終わると(ListenToが効いて)onLoadUsers
関数を実行、todoModel.fetch()
でTODO1件の取得を行う。
- 元々
-
showItem
関数内で、TodoDetailItemView
の初期化時に、取得したユーザ一覧のcollectionを渡すようにしている- このユーザ一覧collectionを
TodoDetailItemView
の処理で表示します。
- このユーザ一覧collectionを
todo-detail-item-view.js
ユーザ一覧を表示します。
//詳細ビュー
define(function() {
var TodoDetailItemView = Marionette.ItemView.extend({
//テンプレート
template: "#todo-detail-item-template",
ui : {
todoStatus : '#edit-todo',
updateButton : '#updateTodo',
- cancelButton : '#updateCancel'
+ cancelButton : '#updateCancel',
+ userList : '#user-list'
},
//DOMイベントハンドラ設定
events : {
//更新ボタンクリック時
'click @ui.updateButton' : 'onUpdateClick',
//キャンセルボタンクリック時
'click @ui.cancelButton' : 'onCancelClick',
},
//初期化
- initialize: function(){
+ initialize: function(options){
_.bindAll( this, 'onSaveSuccess' );
+ this.userList = options.userList;
+ },
+
+ onRender : function() {
+ //ユーザ一覧を表示
+ this.showUserList(this.ui.userList, this.userList);
+ //担当者を選択状態にする
+ this.ui.userList.val(this.model.attributes.assignee);
+ },
+
+ //ユーザ一覧を表示
+ showUserList : function($list, userList){
+ $.each(userList, function(index, userModel) {
+ $list.append(
+ "<option value='"
+ + userModel.attributes.id + "'>"
+ + userModel.attributes.name + "</option>");
+ });
},
//更新ボタンクリックのイベントハンドラ
onUpdateClick : function() {
//テキストボックスから文字を取得
- var todoString = this.ui.todoStatus.val();
+ var todoString = this.ui.todoStatus.val(); // Todo
+ var assigneeId = this.ui.userList.val(); // 担当者
this.model.save({
- todo : todoString
+ todo : todoString,
+ assignee : assigneeId
}, {
silent : true,
success : this.onSaveSuccess,
});
},
//キャンセルボタンクリックのイベントハンドラ
onCancelClick : function() {
this.backTodoLists();
},
//更新成功
onSaveSuccess : function() {
this.backTodoLists();
},
//TODOリスト画面に戻る
backTodoLists : function() {
Backbone.history.navigate('#todo-lists', true);
}
});
return TodoDetailItemView;
});
ユーザ一覧の表示に関しては、todo-composite-view.js
でやったことと同様です。
- ユーザ一覧のセレクトボックスを処理するため
ui
変数にuserList
を追加。 -
initialize
関数内で、todo-layout-viewから渡されたユーザ一覧collection(options.userList
に入ってくる)を自身の中(this.userListに保持) -
onRender
関数内で、showUserList
関数を実行してユーザ一覧の表示を行う - ユーザ一覧表示後、表示するTODOの担当者のユーザID(modelの
assignee
から取得)を選択された状態に設定。 -
showUserList
関数は、todo-composite-view.js
内の同関数のコピペです!(今回はこれでご了承を!) - 更新ボタンクリック時の処理(
onUpdateClick
関数)では、ユーザ一覧で選択したユーザを担当者(assignee
)をして設定しています。
実装
では、実際に修正して画面の動作確認をしてみましょう。
-
app/View/Layouts/default.ctp
を上記の通り修正。 -
app/webroot/js/collections/user-collection.js
を上記の通り修正。 -
app/webroot/js/models/todo-model.js
を上記の通り修正。 -
app/webroot/js/views/todo-layout-view.js
を上記の通り修正。 -
app/webroot/js/views/todo-composite-view.js
を上記の通り修正。 -
app/webroot/js/views/todo-item-view.js
を上記の通り新規作成。 -
app/webroot/js/views/todo-detail-layout-view.js
を上記の通り新規作成。 -
app/webroot/js/views/todo-detail-item-view.js
を上記の通り新規作成。 - 動作確認!
- Gitにコミット
GitHubでのdiff表示へのリンク
第6回 Lesson1 クライアントサイド · suzukishouten-study/rest-study@f1623dc
以上です!
次回予告
次回テーマは、サーバー・クライアントの両面からかけるバリデーション - connpassです。
バリデーション機能を付けて、「ちゃんとした」アプリケーションに成長させていきます。
またぜひご参加ください!
コメント/フィードバックお待ちしております。
参加者の方も、そうでない方もお気づきの点があればお願い致します。