## Select muli categories
## sử dụng rails + jquery
- Author: [hungnv950](https://github.com/hungnv950)
- Repo: [https://github.com/Hungnv950/rails-select-multi-categories](https://github.com/Hungnv950/rails-select-multi-categories)
- Representor: [https://hungnv950.github.io/2019/12/14/slect-multy-categories/#/](https://hungnv950.github.io/2019/12/14/slect-multy-categories/#/)
### Vấn đề
- Hầu hết các dự án out source và dự án rails cho thị trường Nhật nói chung việc làm việc với lựa chọn dữ liệu dùng checkbox là rất phổ biến.
- ![problem](https://fluidsurveys.com/wp-content/uploads/2009/11/56.png)
- Lựa chọn categories cho người dùng
### Cách giải quyết
![Vào code dự án cũ để xem lại.](https://as2.ftcdn.net/jpg/01/70/81/05/500_F_170810520_qTnUe8qFYZo6Rc9rcBpXuUvYDl4v1pDi.jpg "Codding")
### Cách giải quyết
Vào code dự án cũ để xem lại.
![Vào code dự án cũ để xem lại.](https://letweb.net/wp-content/uploads/2018/07/Source-Code-M%C3%A3-ngu%E1%BB%93n-l%C3%A0-g%C3%AC.png)
### Cách giải quyết
![alt text](https://res.cloudinary.com/drdoqfhly/image/upload/v1530887094/gg-1_synrgy.jpg)
### Cách giải quyết
Tạo 1 base code:
- Đơn giản
- Dễ hiểu
- Đúng vấn đề
- Có thể tái sử dụng
Các bước cần làm
- Tìm hiểu bài toán
- Cấu trúc database
- Khởi tạo model
- Xây dựng form checkbox
- Lưu trữ dữ liệu
- Lấy giá trị
### Tìm hiểu bài toán
![problem](https://fluidsurveys.com/wp-content/uploads/2009/11/56.png)
### Cấu trúc database
![Select multi categories db](/assets/images/multi-categories-db.png)
Khởi tạo model
# frozen_string_literal: true
class User < ApplicationRecord
has_many :users_categories, foreign_key: :user_id, dependent: :destroy
end
# frozen_string_literal: true
class Category < ApplicationRecord
has_many :users_categories, foreign_key: :category_id,
dependent: :destroy
end
# frozen_string_literal: true
class UsersCategory < ApplicationRecord
belongs_to :user, foreign_key: :user_id
belongs_to :category, class_name: Category.name
end
### Xây dựng form checkbox
Yêu cầu:
1. Generate form checkbox với dữ liệu category
2. Khi lựa chọn vào "other content" sẽ xuất hiện input để nhập dữ liệu
3. Trả lại đúng data khi validate sai
### Xây dựng form checkbox
#### 1. Generate form checkbox với dữ liệu category
Sử dụng nested attributes:
- accepts_nested_attributes_for
- fields_for
### Xây dựng form checkbox
#### 1. Generate form checkbox với dữ liệu category
- Thêm đoạn dưới vào model user:
```
accepts_nested_attributes_for :users_categories, allow_destroy: true
```
Xây dựng form checkbox
1. Generate form checkbox với dữ liệu category
- Trong users_controller/new tiến hành build giá trị cho users_categories:
def new
@user = User.new
@category_options =
Category.all.map do |category|
UsersCategory.new category: category, user: @user
end
end
Xây dựng form checkbox
1. Generate form checkbox với dữ liệu category
Hiển thị dữ liệu trên view với fields_for:
fields_for(record_name, record_object = nil, options = {}, &block)
check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
<%= form_for @user do |f| %>
<%= f.object.errors.full_messages %>
username: <%= f.text_field :name %>
categories:
<%= f.fields_for :users_categories, @category_options do |ff| %>
<% category_option = ff.object %>
<%= ff.check_box :_destroy, {}, checked_value: "0", unchecked_value: "1" %>
<%= ff.hidden_field :category_id %>
<%= category_option.content %>
<% end %>
<%= f.submit %>
<% end %>
### Xây dựng form checkbox
#### 1. Generate form checkbox với dữ liệu category
```
<%= ff.check_box :_destroy, {}, checked_value: "0", unchecked_value: "1" %>
```
- Mặc định `check_box` sẽ để `checked_value` = 1 và `unchecked_value` = 0. Nhưng trong trường hợp này những giá trị được checked sẽ đại diện cho `_destroy` có nghĩa là những checkbox nào được tích sẽ bị xóa.
- Vì thế nên chúng ta sẽ đổi lại giá trị mặc định `checked_value: "0"` và `unchecked_value: "1"` để giữ lại những `user_categories` được tick.
### Xây dựng form checkbox
#### 1. Generate form checkbox với dữ liệu category
- Kết quả:
![Select multi categories db](/assets/images/select-multi-categories-form-checkbox.png)
Xây dựng form checkbox
2. Khi lựa chọn vào "other content" sẽ xuất hiện input để nhập dữ liệu
- Ý tưởng: Đối với category có key_name = "other_content" sẽ generate ra input và ẩn đi, khi người dùng click vào `other_content` sẽ show và hide
<%= form_for @user do |f| %>
<%= f.object.errors.full_messages %>
username: <%= f.text_field :name %>
categories:
<%= f.fields_for :users_categories, @category_options do |ff| %>
<% users_categories = f.object.users_categories %>
<% category_option = ff.object %>
<%= ff.check_box :_destroy, {}, 0, 1 %>
<%= ff.hidden_field :category_id %>
<%= category_option.content %>
# Thêm đoạn
<% if category_option.key_name == "other_content" %>
<%= ff.text_field :other_content, value: other_content,class: "js-other_content-field hidden" %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
Xây dựng form checkbox
2. Khi lựa chọn vào "other content" sẽ xuất hiện input để nhập dữ liệu
- Xử lý một chút JS đơn giản:
$(function() {
# Xử lý với trường hợp người dùng click vào `other_category`
$('.js-select-other_content').change(function () {
var self = $(this),
otherContent = $('.js-other_content-field');
if (self.is(':checked')) {
otherContent.removeClass('hidden');
} else {
otherContent.addClass('hidden');
}
});
});
### Xây dựng form checkbox
#### 2. Khi lựa chọn vào "other content" sẽ xuất hiện input để nhập dữ liệu
- Kết quả:
![Select multi categories db](/assets/images/select-multi-categories-click-other.gif)
### Lưu trữ dữ liệu
- Permit data
- Validate
- Điền data cũ vào form nếu validate sai
Lưu trữ dữ liệu
1.Permit data
def user_params
params.require(:user).permit(
:name,
users_categories_attributes: [:id, :category_id, :other_content, :_destroy]
)
end
Lưu trữ dữ liệu
2. Validate
- số lượng category tối thiểu
- validate nội dung của "other content" khi chọn checkbox này
Lưu trữ dữ liệu
2. Validate
# user.rb
MIN_SIZE = 1
# Validate số lượng tối thiểu categories
validates :users_categories, length: {minimum: MIN_SIZE}
Hoặc sử dụng custom validate:
# user.rb
### Validate số lượng tối thiểu categories
MIN_SIZE = 1
validate :validate_users_categories
private
def validate_users_categories
errors.add(:users_categories, :minsize) if(users_categories.size < MIN_SIZE)
end
Lưu trữ dữ liệu
2. Validate
# users_category.rb
# Validate nội dung khi chọn "other_content"
validates :other_content, presence: true, if: lambda { key_name == "other_content"}
Hoặc sử dụng custom validate:
# users_category.rb
# Validate nội dung khi chọn "other_content"
validate :validate_other_content
private
def validate_other_content
errors.add(:other_content, :blank) if(key_name == OTHER_CONTENT && other_content.blank?)
end
### Lưu trữ dữ liệu
#### 3. Điền giá trị cũ vào form nếu validate sai
Có 2 trường hợp cần xử lý:
- Checkbox
- Nội dung của "other content" nếu người dùng lựa chọn ô này.
### Lưu trữ dữ liệu
#### 3. Điền giá trị cũ vào form nếu validate sai
- Checkbox
Ý tưởng: Khi submit dữ liệu lên controller, @user đã được gán giá trị khi "@user = User.new user_params". Khi này, object @user đã được khởi tạo và có association.
![Select multi categories db](/assets/images/select-ml-ct-object-user.png)
- Đây chính là những checkbox đã được checked
Lưu trữ dữ liệu
3. Điền giá trị cũ vào form nếu validate sai
# new.html.rb
<%= form_for @user do |f| %>
<%= f.object.errors.full_messages %>
username: <%= f.text_field :name %>
categories:
<%= f.fields_for :users_categories, @category_options do |ff| %>
<% users_categories = f.object.users_categories %>
<% category_option = ff.object %>
# Tìm kiếm users_categories có chứa category_option hiện tại hay không
<% is_selected = users_categories.map{|c| c.key_name}.include?(category_option.key_name) %>
# Lấy giá trị của "Other content"
<% other_content = users_categories.select{|c| c.key_name == "other_content"}.first&.other_content %>
<%= ff.check_box :_destroy, {checked: is_selected, class: "#{'js-select-other_content' if category_option.key_name == 'other_content'}"}, 0, 1 %>
<%= ff.hidden_field :category_id %>
<%= category_option.content %>
<% if category_option.key_name == "other_content" %>
<%= ff.text_field :other_content, value: other_content,class: "js-other_content-field hidden" %>
<% end %>
<% end %>
<%= f.submit %>
<% end %>
### Lấy giá trị
Có 2 trường hợp:
- Nếu user_category có key_name không phải "other content" thì sẽ lấy giá trị "content" từ bảng "category"
- Nếu users_category có key_name là "other_content" sẽ tiến hành lấy "other_content" từ trong bảng users_category
Lấy giá trị
# users_category.rb
def category_name
key_name == OTHER_CONTENT ? other_content : content
end
# user.rb
def show
@user = User.find params[:id]
@categories = @user.users_categories.map{|c| c.category_name}.join(" ,")
end
### Trường hợp edit
- Đối với trường hợp edit, khi chúng ta chạy đoạn:
```
is_selected = users_categories.map{|c| c.category_key_name}.include?(category_option.category_key_name)
```
thì sẽ phải thực hiện truy vấn thông qua database để lấy lại association. Khi đó khi chúng ta cập nhật thay đổi giá trị cho ô select, ô đó sẽ luôn là giá trị cũ
![Select multi categories db](/assets/images/1.png)
![Select multi categories db](/assets/images/2.png)
![Select multi categories db](/assets/images/3.png)
### Edit | Ý tưởng
- Khi submit params lên chúng ta sẽ lọc được những params có category đã được chọn
- Lọc và trả lại những id đó
```
@selected_ids =
users_categories_attributes.values.map {|c| c[:category_id] if c[:_destroy] == "0"}.compact
```
- Kết quả: Mảng các categories đã selected. Ví dụ: ["1", "2"]
### Edit | Ý tưởng
- Edit một chút trong view:
```
selected_ids = @selected_ids ||
users_categories.map{|c| c.category_id.to_s}
```
```
is_selected = selected_ids.include?(category_option.category_id.to_s)
```
- Kết quả: Mảng các categories đã selected. Ví dụ: ["1", "2"]
### Lưu ý
- Khi cập nhật dữ liệu cần phải permit id của nested params để tránh tình trạng tạo thêm bản ghi mới giống bản ghi cũ.
- Trong file edit.html.slim thêm hidden_field cho id của users_category.
```
<%= ff.hidden_field :id, value: users_categories.select{|c| c.key_name == category.key_name}.first&.id %>
```
- Về bản chất neseted_attributes đã hỗ trợ tự điền id vào form nested nhưng trong trường hợp này giá trị của nested form được build ra không phải là giá trị trong quan hệ mà là giá trị của object `@category_options` chúng ta truyền vào nên cần phải lọc ra như trên.
- Vị trí của categories: Thông thường vị trí của trường `other_content` sẽ là ở cuối danh sách. Nhưng không hẳn master data nào cũng chuẩn hoặc category được sinh tự động. Có một mẹo là chúng ta sẽ luôn để `other_content` ở đầu tiên của mảng (bản ghi đầu tiên) và dùng hàm `rotate(1)` để dịch bản ghi đó xuống cuối mảng
```
a = [ "a", "b", "c", "d" ]
a.rotate #=> ["b", "c", "d", "a"]
```
- Tìm hiểu kĩ design để khi import master
### End.
#### Cảm ơn mọi người đã lắng nghe!