LoginSignup
1
2

More than 5 years have passed since last update.

Các bài học lập trình ứng dụng RESTful ~ Chuyên đề phát triển Web - [Chương 6] Các thao tác quản lý data và hàm assign trong CakePHP

Last updated at Posted at 2015-10-28

:warning: Tôi đã thêm một table tên là users ở bài 5.

:large_blue_circle: Chào mừng các bạn trở lại

Chương 6 này được mình dịch từ bài viết CakePHPでデータ操作〜担当者アサイン機能の実装 - AWS上で構築するRESTfulアプリ勉強会~Web開発ワークショップ~【第6回】マニュアル của tác giả @k_shimoji , mình vừa dịch vừa làm theo nên sẽ chỉnh sửa khá nhiều sau khi mình hiểu hơn về bài học, rất mong nhận được sự góp ý của tất cả các bạn.

:warning: Kế hoạch bài học

Ở bài này chúng ta sẽ thêm khả năng assign nhiệm vụ cho cá nhân vào app TODO list.
Với việc đã thêm chức năng login ở bài trước, chúng ta sẽ cho phép ứng dụng này được sử dụng với nhiều user.

Và qua đây bạn sẽ được học về "association" trong CakePHP.

:large_blue_circle: "association" trong CakePHP

Theo bài viết trước thì chúng ta sẽ xử lý trên 2 table

  • users
  • todo_lists

Tuy nhiên không phải chỉ chọn 1 table để xử lý trong mỗi lần làm việc.
Tại ứng dụng mà chúng ta đã viết thời điểm này, bạn cần Sellect bằng cách chọn cả users và todo_lists, sẽ hay hơn nếu mình không viết SQL trực tiếp mà xử lý bằng cách của CakePHP, bạn có thể sử dụng các cơ chế "association".
Bạn có thể join các table bằng code SQL.

Tìm hiểu về association

Bây giờ mình sẽ tìm hiểu về 3 association sau

  • belongsTo
  • hasOne
  • hasMany

:warning: 「hasAndBelongsToMany」Nó đã có sẵn nên tất nhiên sẽ bị bỏ qua

Giải thích mỗi association

belongsTo

A belongsTo B, hay nói cách khác, A có một mối quan hệ với một table mà table nó thuộc về B, bạn có thể sử dụng belongsTo association.
Ví dụ, thành phần user trong table users, sẽ cần có một quốc tịch "nationality", nó sẽ có một table là "nationality master (countries)".

Mình sẽ thêm table countries ( phần mở ngoặc là các field trong table đó )
- countries(id, name)

Bởi vì mỗi user sẽ có một quốc tịch nằm trong danh sách các nationality, table users sẽ như sau

  • users(id, username, country_id)

Bạn có thể Sellect dữ liệu bằng cách join users.country_id với countries.id bằng cách viết đoạn SQL sau:

select users.id, users.username, countries.name as country_name
from users inner join countries on users.country_id = countries.id

Đây là association "belongsTo".
Code thì chúng ta sẽ xem sau.

hasOne

hasOne là cái table thứ 2 trong mối quan hệ belongsTo mà mình đã nói ở trên, Ở ví dụ của belongsTo thì phía users phải show giá trị của countries country_id, trường hợp hasOne là ngược lại của belongsTo, như kiểu thằng B chứa 1 thành phần của thẳng A thì thằng A belongTo thằng B, còn thằng B hasOne thằng A ấy.
Ví dụ bạn có 1 table mà chứa profile của user tên là profiles chẳng hạn.

  • profiles(id, user_id, description, hobbies, etc...)

Ở trường hợp này, chúng ta có user_id ở phía profiles, qua đó mình có thể Sellect bằng cách join users.id và profiles.user_id.
Đây là association "hasOne".

:warning: Phía user cũng có thể có một giá trị profile_id, nhưng sẽ có trường hợp có "non-registered profile."

  • set cho users.profile_id chấp nhận giá trị null
  • users.id = profies.user_id mình không cần quan tâm. Đây là tùy theo triết lý thiết kế database của mỗi người, tất cả đều ok.

Khi viết lệnh SQL nó sẽ như thế này.
sql
select users.id, profiles.introduce, profiles.hobbies
from users inner join profiles on users.id = profiles.user_id

Về code thì chúng ta sẽ xem sau

hasMany

hasMany sử dụng để biểu đạt mối quan hệ mà table này chứa n thành phần của một table khác.
Ví dụ, bạn có một table chứa nghề nghiệp của user (careers).

  • careers(id, user_id, introduce, name, etc...)

Ở trường hợp này, có thể nhiều user chung nghề nghiệp (career) ,
Bạn có thể Select bằng cách join với điều kiện users.id trùng với careers.user_id.
Sự khác biệt của nó với hasOne chỉ là sẽ lấy nhiều record hơn trong một lần.

Lệnh SQL như sau

select users.id, careers.introduce, careers.name
from users inner join careers on users.id = careers.user_id

:warning: Sự thật là trong CakePHP, một khi select users và select careers, nghĩa là chúng ta đang select tổng cộng tương ứng 2 lần.

Code chúng ta sẽ xem sau

:large_blue_circle: Đi vào bài học

Sau khi hoàn thành bài học này thì chúng ta có thể hoàn tất các chức năng sau

Đặc tả chức năng

  • Thêm Todo
    • Bây giờ thì khi thêm TODO sẽ có thêm chức năng "Phân công nhiệm vụ ai sẽ phải làm TODO này" trong list tất cả các user
    • Danh sách user, logged-in user trong danh sách lựa chọn mặc định.
  • Hiển thị TODO list
    • Hiển thị "owner" tức là người đã thêm TODO này , và hiển thị người được chỉ định sẽ phải làm todo này ở cột "assignee".
      • Ngoài ra chúng ta sẽ thêm vào một vài giới hạn sau
      • Chỉ người owner mới được dùng chức năng "Delete". Nút "Delete" sẽ không hiện với những người khác. - Hiển thị "details" cho owner và người responsible. Không hiển thị link "Details" cho những người khác.
  • TODO details
    • Một vài thay đổi cá nhân

Màn hình

  • Màn hình TODO list ( Giao diện hơi khác vì mình có vọc vạch dùng thêm bootstrap và tự viết thêm vài chức năng mới, ở một bài viết tới sẽ có hướng dẫn về bootstrap, các bạn yên tâm, còn các chức năng ẩn việc đã làm, xóa tất cả... thì các bạn có thể tự làm ở nhà với những kiến thức đã học )

demo.PNG

  • Màn hình TODO detail

0.1.PNG

:large_blue_circle: Chuẩn bị

Phần chuẩn bị sẽ giống nhau ở mỗi bài, vì vậy tôi đã tổng hợp nó ở một entry riêng. Các bạn vui lòng xem ở link này All 12 times of study sessions in do I have use of Git - RESTful application workshops will be built on AWS ~ Web development workshop ~ - Qiita để chuẩn bị .

  • Bây giờ mình sẽ tạo và làm việc trên branch tên vol/06 trước đã.

Còn đây là những table và giá trị mà chúng tôi đã add ở bài trước, các bạn chưa xem bài viết trước có thể xem và tạo Tại đây.

Bây giờ mình sẽ thêm 2 trường nữa cho bảng todo_lists.

  • ownerID(owner)
  • assignerID(assignee)

Thêm cột cho bảng TODO list

Đầu tien chúng ta đăng nhập vào phpMyAdmin

URL như cũ thôi: http://(PublicIP)/phpMyAdmin/.
Nếu bạn chưa thay đổi gì thì username và password đăng nhập đều là study.

Cấu hình cho mỗi collum
Name Data type length / value default value collation attribute NULL Index A_I (AutoIncrement) Comments
owner INT 10 no   UNSIGNED       owner
assignee INT 10 no   UNSIGNED       assigner

Chuẩn bị

Bắt đầu chuẩn bị nào!

-:white_check_mark: Như đã nói ở trên mình sẽ tạo branch vol/06 cho Git.
-:white_check_mark: Thêm một column cho TODO list (todo_lists) table.

Khi đã sẵn sàng, chúng ta bắt đầu vào Bài 1.

:large_blue_circle: Bài 1: server side

Ở bài 1, chúng ta sẽ thực hiện những thao tác liên quan đến server side của ứng dụng.
Để test chương trình, sử dụng "POSTMAN" mà chúng ta đã cài đặt ở những bài đầu tiên.

Points

Mặc dù có 3 "association" mà mình đã liệt kê, nhưng thực tế mình chỉ cần sử dụng duy nhất "belongsTo", bạn sẽ nhận ra điều này.
Cơ mà như vậy thì không thú vị lắm, hãy cũng thử với cả "hasMany" và "hasOne" nữa nhé.
Đầu tiên, mình sử dụng belognsTo, và bắt đầu code.

Danh sách những file cần chỉnh sửa

Thao tác file Mô tả
Sửa app/Controller/TodoListsController.php Setting the find parameters
Sửa app/Model/TodoList.php Association settings
Sửa app/Controller/UsersController.php lấy user-list

TodoListsController.php

Mình sẽ set các thông số của hàm find giống như TodoList model.

TodoListsController.php
〜略〜
 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 ) {
+               //truong hop 「Login user là owner」
+               $res[$key]['TodoList']['owned'] = $row['Owner']['id'] === $loginUserId;
+               //Truong hop「login user la Assignee」
+               $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');
    }

〜以下略〜

Thêm argument cho hàm find

  • Bạn cần thêm 2 argument $query. $Query là một mảng, bạn cần set như sau.
    • fields
      • Để get column của table. Bạn không cần xác định cụ thể nó, vì nó sẽ lưu trữ mật khẩu của user và vài thứ khác, chúng ta chỉ set những thứ cần thiết.
    • order
      • Hiện tại data đang được sắp xếp ngẫu nhiên theo thứ tự được xác định, tuy nhiên todo_lists sẽ là thứ tự giảm dần ID trong bảng để hiển thị những TODO mới nhất.

Xây dựng data thu được

  • Những data mình thu được trong bài 1 gồm
    • Nếu người đăng nhập là owner, set owned = true, trường hợp ngược lại set owned = false.
    • Nếu người đăng nhập là assigner, set assigned = true, nếu không phải thì assigned = false.

TodoList.php

Association setting.

TodoList.php
 class TodoList extends AppModel {
+   public $belongsTo = array (
+       'Owner' => array (
+           'className' => 'User',
+           'foreignKey' => 'owner',
+       ),
+       'Assignee' => array (
+           'className' => 'User',
+           'foreignKey' => 'assignee'
+       )
+   );
 }

belongsTo association

  • Cho mỗi lần xem TODO,
    • Thông tin của owner User
    • Thông tin của Assignee user

Chúng ta sẽ setting như sau.

  • Model của biến sẽ tạo nên một mảng biến tên là $belongsTo.
    • Tôi đã đặt tên tương ứng trong table là Owner(Owner), Assignee(người được phân công)
    • Thông số chi tiết của (Owner) như sau
      • className -> Tên model(User)
      • Thêm Foreign key cho users (Cụ thể là todo_lists.owner)
    • Thông số chi tiết của (Assignee) như sau
      • className -> Tên model(User)
      • Thêm Foreign key cho users (Cụ thể là todo_lists.assignee)

Chức năng Thêm TODO

  • Bạn phải set ID của người đó vào owner.
    • Bây giờ, user đã login nào thực hiện lệnh thêm TODO sẽ được set là owner của TODO đó

UsersController.php

Chứa các chức năng Login, kiểm tra login, log out, đăng ký.

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();

〜以下略〜

Hàm index

  • Cùng một khả năng với TodoListController.php, bạn cần cố định field để hiểu hàm find.

 Kiểm tra kết quả

Sử dụng POSTMAN, Lấy danh sách TODO list bằng URL (http://(PublicIP)/rest-study/todo_lists.json).
Nếu được kết quả trả về với cấu trúc tương tự dưới đây là OK.
belongsTo đã làm việc.

[
    {
        "TodoList": {
            "id": "68",
            "todo": "1 ne hihi aaaa",
            "status": "0",
            "owned": true,
            "assigned": true
        },
        "Owner": {
            "id": "1",
            "name": "XuanTai"
        },
        "Assignee": {
            "id": "1",
            "name": "XuanTai"
        }
    },
    {
        "TodoList": {
            "id": "69",
            "todo": "1 ne hihi aaaa b",
            "status": "0",
            "owned": true,
            "assigned": true
        },
        "Owner": {
            "id": "1",
            "name": "XuanTai"
        },
        "Assignee": {
            "id": "1",
            "name": "XuanTai"
        }
    },
    {
        "TodoList": {
            "id": "70",
            "todo": "1 ne hihi aaaa1213",
            "status": "0",
            "owned": true,
            "assigned": true
        },
        "Owner": {
            "id": "1",
            "name": "XuanTai"
        },
        "Assignee": {
            "id": "1",
            "name": "XuanTai"
        }
    }
]

Như kết quả của tôi khi làm theo hướng dẫn trên:
Capture.PNG

Tổng kết

Các bước thao tác như sau

  • :white_check_mark: app/Controller/TodoListsController.php sửa như trên.
  • :white_check_mark: app/Model/TodoList.php sửa như trên.
  • :white_check_mark: app/Controller/UsersController.php sửa như trên.
  • :white_check_mark: Kiểm tra kết quả.
  • :white_check_mark: Commit lên Gỉt

Tài liệu tham khảo trên GitHub với hiển thị Diff
Chương 6 - Bài 1 · suzukishouten-study/rest-study@d4fe1a4

Sang bài 1.5 nào.

:large_blue_circle: Bài 1.5. Thử sử dụng các association khác.

Mọi thứ đã ok với cách thực hiện như bài 1.
Tuy nhiên trước khi sang bài 2, các bạn có thể thử sử dụng hasOnehasMany xem sao, để nâng cao tay nghề tí thôi mà, hehe.

Sau khi sửa code và kiểm tra xác nhận kết quả, hãy sang bài 2 với code cũ.

Ở phần test, mục tiêu của mình sẽ là chức năng như trong ảnh dưới đây.
vl.PNG

:warning: user là

  • XuanTai
  • Test5
  • Test2

Đây là những user đã được đăng ký, tuy nhiên XuanTai không thể, xem, xóa, sửa trạng thái TODO thứ 4 ( vì anh ta không phải owner cũng không phải người được phân công), anh ta có toàn qyền đối với TODO 1,2,3,7, và chỉ có quyền vào trang detail ( để sửa, xóa, assign người khác ) của TODO 5 và TODO 6 .

Kiểm tra hasOne

Chúng ta cần tự đặt điều kiện user nào thêm TODO chính là owner.
:warning: user : todo sẽ phải trở thành 1 : n nếu bạn muốn chuyển thành quan hệ hasMany.

Chi tiết các thay đổi của source

sửa hàm index của app/Controller/UsersController.php
app/Controller/UsersController.php
    public function index() {
        $res = $this->User->find('all');    //Xóa những field đã xác định, chuyển nó thành comment như dưới đây.
//      $res = $this->User->find('all', array (
//          'fields' => array (
//              'User.id',
//              'User.username',
//              'User.name'
//          )
//      ));
        $this->set(compact('res'));
        $this->set('_serialize', 'res');
    }
  • set association cho app/Model/User.php
app/Model/User.php
class User extends AppModel {
    public $hasOne = array (
        'TodoList' => array (
            'className' => 'TodoList',
            'foreignKey' => 'owner'
        )
    );
}

Ổn rồi, hãy kiểm tra kết quả ở (http://(PublicIP)/rest-study/users.json)

result
[
    {
        "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
        }
    }
]

Mỗi todo sẽ đi kèm thông tin cá nhân của một user.
Nếu ai đó không có TODO nào thì mỗi thành phần của mảng có thể là null.

Code SQL như dưới đây, chúng ta chạy 2 lần hasMany.

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

Giải thích về lý do mình sử dụng WHERE 1 = 1 thì bạn không cần quan tâm, nó là một kiểu SQL, bạn chỉ cần JOIN mà thôi.

Test hasMany

user : todo sẽ trở thành 1 : n khi sử dụng hasMany.

Các phần cần sửa trong source

app/Controller/UsersController.php mình sẽ thêm một state cố định với hasOne.

set association cho app/Model/User.php.

app/Model/User.php
class User extends AppModel {
    public $hasMany = array (
        'TodoList' => array (
            'className' => 'TodoList',
            'foreignKey' => 'owner'
        )
    );
}

hasOne tôi sẽ sửa thành hasMany
Kết quả sẽ như sau

result
[
    {
        "User": {
            "id": "67",
            "username": "yamada",
            "password": "$2a$10$KQTujh9rcjx7/7b83Hw7jOg8brjOAyT/S.H4VgY7Uknjy5jg1.iD.",
            "name": "Yamada"
        },
        "TodoList": [
            {
                "id": "62",
                "todo": "walking",
                "status": "0",
                "owner": "67",
                "assignee": "67"
            },
            {
                "id": "78",
                "todo": "studying",
                "status": "0",
                "owner": "67",
                "assignee": "68"
            }
        ]
    },
    {
        "User": {
            "id": "68",
            "username": "tanaka",
            "password": "$2a$10$ig3ul1GQlZPbQDODLt6.SuYqJVv94wNlZJwMqSPvK5H/p2qEBih6e",
            "name": "Tanaka"
        },
        "TodoList": [
            {
                "id": "83",
                "todo": "家に電話",
                "status": "1",
                "owner": "68",
                "assignee": "68"
            }
        ]
    },
    {
        "User": {
            "id": "69",
            "username": "sato",
            "password": "$2a$10$rBdmyhZHwlv4h.EvacI34OSRuE9Q3t7pYx1HysS1zxw99NOGLUVBi",
            "name": "Sato"
        },
        "TodoList": []
    }
]

Vậy là bằng cách này, TODO đã hiển thị đúng với nhiều TODO cho mỗi user.
Đây gọi là hasMany.
Thời điểm này thì SQL sẽ chạy 2 lệnh sau. Mỗi lần chạy SQL thì hasMany chạy 2 lần.

Lan1
SELECT `User`.`id`, `User`.`username`, `User`.`password`, `User`.`name` FROM `study`.`users` AS `User`   WHERE 1 = 1
Lan2
SELECT `TodoList`.`id`, `TodoList`.`todo`, `TodoList`.`status`, `TodoList`.`owner`, `TodoList`.`assignee` FROM `study`.`todo_lists` AS `TodoList`   WHERE `TodoList`.`owner` IN (67, 68, 69)

Ở lần 2 bạn phải select TodoList với owner là ID của user mà bạn nhận được ở lần 1 .
Nhưng mình đã chạy xong rồi nên hãy trở lại chỗ cũ nơi bạn đã sửa để đến với bài 2, hãy tạo một trang mới

:large_blue_circle: Bài 2: Client side

Ở bài 2 này, mình sẽ code phần client side của ứng dụng.

Chú

Bạn có thể tự làm hầu hết với những kiến thức mà bạn đã có từ bài trước, tuy nhiên ở thời điểm này, để "nhận và hiển thị danh sách user" ở phần sau sẽ cần một vài comment nhỏ.

  • Danh sách user ở trang TODO list

1.png

  • Danh sách user ở trang TODO Detail

0.1.PNG

Bắt đầu thực hiện

Ở Backbone, nếu bạn có thể tận dụng hết thế mạnh của Marionette, đầu tiên thì user list sẽ hiển thị ở View, "Behavior" khá tốt, nhưng tôi sẽ chia sẻ qua ví dụ sử dụng hàm, và đây là thời điểm dùng "CakePHP Associations" là chính, hãy nhớ rằng sẽ rất dễ dàng để implement nó.

Cụ thể

  • Nhập URL của API (http://(PublicIP)/rest-study/users.json) để lấy collection của user list.
  • Collecton này được dùng với vòng lặp, Nó sẽ nằm trong tag <select> </select> trong template, Phần thẻ <option value ="user id"> user's name </ option> sẽ được lọc bằng cách sử dụng JQuery.

Đây là cách làm

Hiện tại thì tôi đã "copy và paste" để dùng ở cả TODO lists và TODO detail.
(Và nó không DRY vì chúng ta đã vi phạm Don't Repeat Yourself, nhưng thời điểm này mình sẽ làm nhanh để ví dụ thôi !)

Và đây là chú thích về chỉnh sửa.

Các file cần thay đổi

Thao tác file Mô tả
Sửa app/View/Layouts/default.ctp Sửa template
Sửa app/webroot/js/collections/user-collection.js user collection
Sửa app/webroot/js/models/todo-model.js parse
Sửa app/webroot/js/views/todo-layout-view.js Để đọc userlist
Sửa app/webroot/js/views/todo-composite-view.js User list
Sửa app/webroot/js/views/todo-item-view.js Hiển thị các nút điều khiển như detail, delete
Sửa app/webroot/js/views/todo-detail-layout-view.js Đọc User List
Sửa app/webroot/js/views/todo-detail-item-view.js User List

default.ctp

Sửa template

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>Status</td>
+                   <td>ToDo</td>
+                   <td>Owner</td>
+                   <td>Assignee</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="#">Delete</a>
+   </td>
+   <td>
        <a class="detail-link" href="#todo-lists/<%- id %>">Detail</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="Update"></input>
    <input type="button" id="updateCancel" value="Cancel"></input>
    </div>
    </script>

〜 略 〜

Giải thích chỉnh sửa

  • TODO list screen
    • Thêm <select> </select> để hiển thị danh sách user
      • Thêm <thead> </thead> vào table (vì đã thêm các cột mới )
      • [Delete] và [Details] cần chia thêm <td> </td>
  • TODO detail screen
    • Thêm <select> </select> để hiển thị danh sách user.

user-collection.js

user-collection.js
//User list collection
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;
});

Required mình sẽ require tối thiểu một collection.
Mình không cần giải thích gì cả.

todo-model.js

Chúng ta đang phân tích OwnerAssignee khi thêm một TODO mới.

todo-model.js
 //Model representing County Todo data 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;
 });

Khi chuyển thành JSON thì TODO1 sẽ theo format sau, trả về từ server bằng CakePHP,

ServerResponseJSON
{
    "TodoList": {
        "id": "62",
        "todo": "Watching TV",
        "status": "0",
        "owned": false,
        "assigned": false
    },
    "Owner": {
        "id": "67",
        "name": "Yamada"
    },
    "Assignee": {
        "id": "67",
        "name": "Yamada
    }
}

Nó sẽ thay đổi sang format sau

ServerResponseJSON
{
    "id": "62",
    "todo": "Watching TV",
    "status": "0",
    "owned": false,
    "assigned": false,
    "Owner": {
        "id": "67",
        "name": "Yamada"
    },
    "Assignee": {
        "id": "67",
        "name": "Yamada"
    }
}

todo-layout-view.js

Bạn cần thêm một tiến trình để đọc danh sách user.

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;
 });
  • Đọc collections/user-collection bằng require
  • Chỉ trong hàm onRender, mình sẽ lấy về danh sách user từ server bằng this.userCollection.fetch().
  • Khi đã đổ dữ liệu về xong (ListenTo đã hoạt động), Chạy hàm onLoadUsers, và kéo theo đó là showTodoList cũng được thực thi
  • Ở hàm showTodoList, trong khi khởi tạo TodoCompositeView, chúng ta cần lấy collection của user list.
  • Chúng ta không xem user list collection trong tiến trình của TodoCompositeView.

todo-composite-view.js

Nó sẽ hiển thị danh sách user

todo-composite-view.js
 //View của TODO list
 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;
 });
  • Thêm userList vào biến uiđể xử lý select box của user list.
  • Ở trong hàm initialize, tại todo-layout-view từ collection danh sách user đã lấy về (đổ vào options.userList) của chính nó (và truyền vào this.userList)
  • Ở trong hàm onRender, hiển thị danh sách user bằng cách chạy hàm showUserList.
  • Sau khi có user list, để lấy ID từ thông tin user của user đã login thì mình sẽ sử dụng hàm window.application.loginUser lúc login, Và người đó (login user) sẽ được chọn mặc định ở user list.
  • Tại hàm showUserList, mình sẽ sử dụng jquery, chúng ta sẽ sử dụng tag <option> </ option> và tag <select> </ select> để chứa những thành phần được gọi trong biến ui.
  • Khi một TODO được thêm vào ( Hàm newAttributes), mình sẽ set người được chọn trong user list vào (assignee).

todo-item-view.js

Chúng ta cần thêm phần quản lý sự hiển thị các button Delete-detail.

todo-item-view.js
 //View của mỗi TODO
 define(function(){
    var TodoItemView = Marionette.ItemView.extend({
        //Tag name của thành phần trong DOM
        tagName : 'tr',

        //Tamplate
        template : '#todo-item-template',

        ui : {
            checkBox : '.toggle',
-           removeLink : '.remove-link'
+           removeLink : '.remove-link',
+           detailLink : '.detail-link'
        },

        //Cac su kien o DOM
        events : {
            //Khi click vao Checkbox
            'click @ui.checkBox' : 'onStatusToggleClick',
            //Khi click button Delete
            'click @ui.removeLink' : 'onRemoveClick',
        },

        onStatusToggleClick : function() {
            this.model.toggle();
        },

        onRemoveClick : function() {
            this.model.destroy({
                wait : true
            });
        },

+       onRender : function() {
+           if (!this.model.attributes.owned) {
+               //Neu khong phai Owner thi hide nut delete
+               this.ui.removeLink.css({
+                   display : 'none' 
+                   //hoac dung visibility : hidden cung duoc
+               });
+               if (!this.model.attributes.assigned) {
+                   //truong hop khong phải assignee
+                   //hide link vao detail
+                   this.ui.detailLink.css({
+                       display : 'none'
+                       //hoac dung visibility : hidden cung duoc
+                   });
+                   //khong cho thay doi trang thai checkbox
+                   this.ui.checkBox.prop('disabled', true);
+               }
+           }
+       }
    });
    return TodoItemView;
 }); 
  • Thêm biến detailLink và Delete link vào ui.
  • Trong hàm onRender, mình sẽ quản lý sự hiển thị hay không của các link xóa và detail theo quy tắc sau.
    • Ẩn Delete link nếu đây không phải owner
      • Trong trường hợp đây không phải là người được giao làm TODO này
        • Vẫn hiện link detail
        • Checkbox sẽ bị cố định và không thể thay đổi

※Cách thực hiện việc quản lý ẩn hiện.

  • Ản link
    • Thêm css bằng jquery, thêm vào style display : 'none'
  • Check box cố định
    • Thêm hàm prop bằng jquery, disabled chuyển thành true.

todo-detail-layout-view.js

Bạn phải thêm một tiến trình đọc user list.
Những thứ bạn đang làm gần như giống với todo-layout-view.js.

todo-detail-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;
 });
  • Đọc collections/user-collection thông qua require
  • Trong hàm onRender, Chạy hàm this.userCollection.fetch() để đổ dữ liệu danh sách user về từ server.
  • Được thực hiện từ onRender, tiến trình nhận dữ liệu ở model thứ 1 từ server, bạn đã được chuyển đến onLoadUsers.
  • Khi quá trình đổ dữ liệu hoàn tất (ListenTo đã chạy), Chạy hàm onLoadUsers,todoModel.fetch() để lấy dữ liệu của TODO 1.
  • Trong hàm showItem, trong phần initialization của TodoDetailItemView, mình cần truyền collection của user list
  • Hiển thị user list collection trong các tiến trình của TodoDetailItemView.

todo-detail-item-view.js

Hiển thị todo list

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;
 });

Để hiển thị user list, tương tự như đã làm với todo-composite-view.js.

  • Thêm userList vào biến ui để xử lý select box của user list.
  • Trong hàm initialize, tại todo-layout-view được truyền từ user list collection ( vào options.userList) của chính nó (nên được lưu ở this.userList)
  • Trong hàm onRender, hiển thị userlist bằng cách chạy hàm showUserList.
  • Sau user list mình set vào state được chọn ( lấy từ assignee của model) user ID của TODO của mỗi cá nhân sẽ được hiển thị.
  • Hàm ShowUserList là việc copy và paste của cùng 1 hàm từ todo-composite-view.js! ( Các bạn có thể xem giải thích ở trên kia!)
  • Trong tiến trình của việc click button Update (onUpdateClick function), mình sẽ sẽ cho phép người dùng được chọn ở danh sách user được phân công (assignee).

Tổng kết bài học

Các bước thực hiện của ta như sau.

  • :white_check_mark: app/View/Layouts/default.ctp sửa như trên.
  • :white_check_mark: app/webroot/js/collections/user-collection.js sửa như trên.
  • :white_check_mark: app/webroot/js/models/todo-model.js sửa như trên.
  • :white_check_mark: app/webroot/js/views/todo-layout-view.js sửa như trên.
  • :white_check_mark: app/webroot/js/views/todo-composite-view.js sửa như trên.
  • :white_check_mark: app/webroot/js/views/todo-item-view.js truyền thêm những giá trị mới như trên.
  • :white_check_mark: app/webroot/js/views/todo-detail-layout-view.js truyền thêm những giá trị mới như trên.
  • :white_check_mark: app/webroot/js/views/todo-detail-item-view.js truyền thêm những giá trị mới như trên.
  • :white_check_mark: Kiểm tra kết quả.
  • :white_check_mark: Commit lên Git

Tham khảo Diff hoàn chỉnh trên Github

第6回 Lesson1 クライアントサイド · suzukishouten-study/rest-study@f1623dc

Đó là tất cả bài học này.

Tôi rất mong nhận được sự phản hồi / góp ý của các bạn

Cảm ơn các bạn đã quan tâm theo dõi series, rất cả các bài học tôi sẽ đều chỉnh sửa sau khi hiểu thêm kỹ hơn vấn đề gì, hoặc nhận ra sai sót, rất mong luôn nhận được sự góp ý từ các bạn.

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2