Trong ứng dụng web hiện đại việc có những chức năng đúng mong đợi, thời gian tải nhanh, hiệu năng ổn định là không đủ. Giao diện cũng phải tinh tế, trơn chu, bao gồm tương tác phức tạp như là chuyển động các phần tử và tương tác kéo thả (drag-and-drop DnD). Kéo thả là tương tác khá phức tạp và khó không chỉ đối với người mới và lập trình viên có kinh nghiệm trong việc viết ứng dụng web, jQuery UI luôn là lựa chọn số một nhưng để áp dụng vào ứng dụng reactjs với việc tích hợp lỉnh kỉnh các thư viện cộng với việc không tương thích trong việc thao tác DOM, nên việc chọn một thư viện “thuần” reactjs sẽ được ưu tiên hơn, trong quá trình làm dự án thực tế review thư viện hiện có trên npm mình cảm thấy thư viện react-Dnd hỗ trợ khá tốt, được lập trình viên hàng đầu (như tác giả của redux Dan Abramov) cập nhật api thường xuyên, hướng dẫn cũng như tài liệu đầy đủ bên cạnh đó cộng đồng xử dụng cũng khá lớn đó là cơ sở để mình muốn giới thiệu thư viện này trong việc viết mã tương tác kéo thả trong ứng dụng reactjs. Một số rào cản lớn khi mới làm ứng dụng liên quan tới kéo thả mình nhận thấy đó là thiếu những khái niệm cần thiết, cơ chế hoạt động, tài liệu liên quan phần lớn là tiếng anh, khó hiểu, nhiều api…dẫn tới không có phương hướng để bắt đầu từ đâu, bài viết này mình sẽ đi qua một lượt những thứ đơn giản nhất tới tương tác (code thực tế), cách xử dụng api, mình không chỉ hướng tới hướng dẫn cụ thể mà chia sẻ những gì mình biết được để mọi người có thể hiểu và áp dụng vào dự án của chính mình.
Nội dung bài viết:
- Giới thiệu
- Tổng quan về DnD
- Áp dụng react-DnD qua ví dụ (cơ bản và nâng cao)
- Tài liệu tham khảo
- Giới thiệu
Kéo thả (Drag-and-drop) là tính năng rất cơ bản trong tương tác giao diện người dùng. Đó là khi bạn lấy một đối tượng và kéo nó tới một vị trí khác. Phát triển các tương tác kéo thả có thể rất rắc rối và phức tạp. Gần đây vẫn chưa có một API tiêu chuẩn trên trình duyệt cho hành vi này. Ngay cả trong trình duyệt hiện đại (ở đó tiêu chuẩn API HTML5 về kéo thả là sẵn có), API chưa ổn định đối với các trình duyệt khác nhau và không làm việc trên các thiết bị di động. Cho những lý do này, chúng ta sẽ sử dụng React DnD, một thư viện kéo thả để chúng ta làm việc theo “chuẩn React” (không thao tác trực tiếp tới DOM, luồng dữ liệu một chiều, định nghĩa logic nguồn kéo và đích thả thuần data, cùng với nhiều lợi ích khác). React DnD cho phép lập trình viên tập trung vào việc viết code liên quan tới business mà không phải quan tâm quá nhiều tới thao tác bên dưới như tương tác với Html5 api, quản lý tương thích giữa các trình duyệt, che dấu cài đặt phức tạp.
Để sử dụng react DnD cần cài đặt các gói sau từ npm:
npm install –save react-dnd
npm install –save react-dnd-html5-backend
mình sẽ giải thích các thư viện này chi tiết trong các phần tiếp theo tại sao lại dùng các thư viện này, cách sử dụng api để tương tác kéo thả trong ứng dụng. - Tổng quan về DnD
Triển khai hành vi kéo thả trong ứng dụng React thông qua thưu viện React DnD được hoàn thành qua việc sử dụng HOC (higher-order components), HOC là những hàm javascript nhận một Component như tham số và trả về một phiên bản nâng cao với việc tích hợp thêm tích năng cho component đó.
Thư viện React DnD cung cấp 3 HOC phải được sử dụng trên các components khác nhau của ứng dụng đó là: DragSource, DropTarget, and DragDropContext.
DragSource trả về một phiên bản nâng cao của component được cho thêm tính năng có thể kéo cho phần tử.
DropTarget trả về một component nâng cao với khả năng xử lý các phần tử đang được kéo vào trong nó
DragDropContext bọc component cha mà ở đó tương tác kéo thả xảy ra, cài đặt trạng thái DnD được chia sẻ.
Thư viện React DnD cũng hỗ trợ sử dụng Javascript decorators đây là tính năng của ES7 để thay thế HOC. Javascript decorators vẫn đang trong quá trình trải nghiệm chưa phải đặc tả của ES 2015, nên ví dụ trong bài viết này mình sẽ sử dụng HOC (higher-order components.)
Mình lấy ví dụ để các bạn dễ hình dung các thành phần này trong thực tế. Ví dụ mình có một Listview (dạng một danh sách item con được chồng lên nhau), người dùng muốn kéo các item trong list thả vào vị trí bất kì trong listview (lên trên hoặc xuống dưới item khác), DragDropContext là component container chứa listview, DropTarget là listview (các item chỉ được thả trong list này đây là nơi nhận và xử lý việc thả item)
DragSource là item trong listview (item được bọc bởi component này được đánh dầu là có thể kéo).
Các api này đưa ra các hàm cụ thể của riêng nó, hữu ích trong quá trình code. Mình sẽ giới thiệu các hàm và cách sử dụng những hàm thường gặp trong phần tiếp. - Áp dụng react-DnD qua ví dụ (cơ bản và nâng cao)
ví dụ cơ bản:
Tạo ứng dụng snackshop-themed app
Mô tả:
Khi mua hàng online trên gian hàng sẽ có rất nhiều item (snack) để người dùng mua, sau khi chọn được một sản phẩm ưng ý người mua kéo item này vào một vùng gọi là giỏ hàng (shopping cart area).
Ứng dụng này mình sẽ chỉ dừng lại ở mức hướng dẫn sử dụng api trong việc kéo thả bằng việc log ra các hành động khi người dùng kéo item và thả vào giỏ hàng, trong ứng dụng thực tế với tương tác phức tạp thì tùy vào bài toán của các bạn để xử lý phù hợp.Thiết kế cho ứng dụngphân tích: Ứng dụng sẽ được kết hợp 3 component (Kéo, thả, context) cùng với một App component. Snack có thể kéo (đó sẽ là component nâng cao bởi DragSource higher-order component), ShoppingCard (đó sẽ là component nâng cao bởi DropTarget higher-order component) và một Container component, mà có chứa cả ShoppingCart và các loại Snack sẽ được nâng cao bởi DragDropContext HOC để kết hợp, tổ chức việc kéo và thả làm việc giữa các Snack và ShoppingCart.
DragSource and DropTarget Higher Order Components:
DragDropContext là phần dễ dàng nhất của React DnD để cài đặt, mình sẽ triển khai từ trên xuống, có nghĩa là code từ component bọc ngoài rồi code vào component con theo cách tổ chức code của React, bắt đầu với App component, tiếp theo là Container, Snack và ShoppingCart.
Code App component (root component):Tiếp theo tạo Container component, ở đó tất cả tương tác kéo thả sẽ sảy ra. Mình sẽ tạo một vài Snack component với tên khác nhau thông qua props và một ShoppingCart component bên dưới chúng. code: chú ý rằng mình đã không export Container component mà là một HOC (higher-order component) dựa vào Container với tất cả trạng thái kéo thả và các hàm được đẩy vào trong nó. Cũng chú ý rằng mình đã import và sử dụng Html5 backend để truyền vào React DnD. Tương tác kéo thả ở đây sẽ dựa vào api mà Html5 đưa ra, xử lý tương tác với các api này sẽ được React DnD xử lý nên chúng ta không cần quan tâm, mà chỉ quan tâm vào api mà React Dnd đưa ra để thao tác theo bài toán cụ thể. Ngoài Html5 React DnD còn hỗ trợ các backend khác nhau, ví dụ cho với html5 việc thao tác chạm (touch) trên các thiết bị di động là chưa được hỗ trợ, chúng ta có thể xử dụng thư viện khác hỗ trợ cho việc này để thay để Html5 như : react-dnd-touch-backend
Tiếp theo mình sẽ tạo Snack và ShoppoingCart , đó là các component nâng cao bởi dragSource và dropTarget tương ứng. Cả dragSource và dropTarget yêu cầu một vài cài đặt sử dụng api, mình sẽ giải thích kĩ hơn. Để tạo các HOC này(higher-order component), chúng ta cần cung cấp 3 tham số là: một kiểu, một spec (đặc tả), và tập hợp các hàm để truyển vào trong component mà nó bọc để hỗ trợ cho thao tác kéo thả.
Type: đó là tên của component, trong ứng dụng UI phức tạp, có thể có nhiều kiểu nguồn kéo (drag sources) tương tác với nhiều kiểu đích thả (drop targets) , nên điểu quan trọng là phải đánh dấu chúng phân biệt được với nhau, type thường là một string.
Spec Object: mô tả làm thể nào component nâng cao “phản ứng” đối với các sự kiện kéo và thả. Một Spec là một đối tượng Javascript với những hàm mà được gọi khi tương tác kéo thả xảy ra, như là beginDrag và endDrag (trong trường hợp của một DragSource) và canDrag và onDrop (trong trường hợp của một DropTarget component).
Collecting function: tập hợp các props mà người dùng định nghĩa được lấy thông qua các api mà React DnD chìa ra để thực hiện, theo dõi, thao tác trong quá trình kéo và thả. Ví dụ mình muốn kiểm tra xem khi nào người dùng đang kéo một Snack qua ShoppingCart để mình gợi ý họ thả item vào đó (hay cụ thể là style cho cái shoppingCart này đổi màu khi có item tương thích kéo qua) mình sẽ định nghĩa một hàm isOver trong collecting function này (hàm này là kết quả mình get được từ monitor được trả ra từ React Dnd) rồi truyển cái tập hợp hàm mình định nghĩa này vào HOC, react sẽ xử lý đẩy các collect này dưới dạng props trong component mà bạn định nghĩa cho việc kéo hoặc thả, các props này được xử dụng để kiểm tra điều kiện, xử lý thay đổi trong component.
Thay vì truyển tất cả props có thể có vào trong component, reactDnD sử dụng collecting function cho chúng ta kiểm soát props sẽ được truyển như thế nào. Điều này đưa ra cho chúng ta nhiều lợi ích bao gồm khả năng xử lý props trước khi chúng ta truyền chúng, thay đổi tên của chúng,…
Khi tương tác kéo thả xảy ra, thư viện ReactDnD sẽ gọi collecting function định nghĩa trong component mà chúng ta tạo, truyền hai tham số: connector và monitor
Connector phải được map tới props mà sẽ được sử dụng trong hàm render của component để phân biệt các phần DOM của component (ví dụ đánh dấu nó có thể kéo, có thể thả…), cho dragSource components, phần này của DOM sẽ được sử dụng để đại diện component trong khi nó đang được kéo, cho dropTarget component phần này sẽ phân biệt DOM sẽ được sử dụng như vùng thả.
Monitor để cho chúng ta map props tới trạng thái của kéo thả, sử dụng monitor chúng ta có thể tạo props giống như là isDragging hoặc canDrop, ở đó nó hữu dụng cho việc render thứ khác nhau dựa vào giá trị của chúng. (như là vẽ phần tử với text hoặc thuộc tính css khác nhau khi nó đang được kéo)
ShoppingCart Component
Tiếp theo mình sẽ xây dựng ShoppingCart component đây chính là vùng thả của các item. Bắt đầu với class cơ bản mà không có dropTarget bọc ngoài: - Như bạn có thể nhìn thấy, nó là hàm render cơ bản mà trả về một thẻ div. Nó cũng chứa một inline Css style với backgroundColor set tới màu trắng.
Tiếp theo, mình sẽ implement một đối tượng spec. Nhớ rằng, đối tượng spec mô tả làm thế nào dropTarget phản ứng với sự kiện kéo thả , trong ví dụ này mình sẽ chỉ xử lý cho sự kiện thả (được gọi khi một dragSource được thả). Code được update:
trong ví dụ này mình sẽ chỉ trả về một string khi sự kiện thả xảy ra. text được trả về sẽ được sử dụng sau trong Snack component.
Kế tiếp, mình sẽ implement collect function, mà nó để cho chúng ta map React DnD connector và trạng thái tới props của component. Mình sẽ inject 3 props vào trong component: connectDropTarget (connector được yêu cầu), isOver và canDrop
Đây là collecting function:chú ý rằng tên prop mà mình đã tạo có thể có cùng tên hoặc khác tên phương thức từ connect và monitor (ví dụ draggingSomethingOverMe: monitor.isOver())
Tất cả những props này sẽ được sử dụng trong hàm render. connectDropTarget prop trả về DOM được đánh là vùng mà có thể thả đối với các object có thể kéo. isOver và canDrop props được sử dụng để hiển thị một text và một background color khác khi người dùng đang kéo một phần tử qua vùng shopping cart. Hàm render được update: - chú ý rằng tham số type cho DropTarget higher-order wrapper đang chỉ tới kiểu nguồn kéo có thể được kéo tới component này (trong trường hợp này là ‘snack’)
Snack Component:
Tiếp theo, mình sẽ implement Snack component. Việc xử lý cũng giống như đối với ShoppingCart component. Khung cơ bản:Đây là dạng cơ bản, Snack component nhận một name props và vẽ nó trong một thẻ div, nó cũng chứa một inline style mà hiện tại set opacity là 1.
Kế tiếp, implement đối tượng spec mô tả sự kiện beginDrag và endDrag, trong beginDrag mình trả về một string (cũng giống như đã làm với sự kiện thả ShoppingCart), trong endDrag sẽ làm một số thứ về hai giá trị được trả về như log thông tin tên…
Code update Snack component với spec object:Bước cuối cùng trong Snack component, chúng ta sẽ implement collecting function, ở đó mình sẽ connect DOM node được kéo và map trạng thái DnD tới props của component, khi kết nối trạng thái DnD với props trong component, chúng ta sẽ có cơ hội để làm hai thứ: khai báo thêm propTypes và sử dụng props isDragging bên trong inline style để làm phần tử mờ đi khi đang được kéo, cuối cùng mình sẽ export HOC dùng dragSource bọc bên ngoài, code hoàn thiện:vậy là code đã hoàn thành chạy npm start để theo dõi kết quả, chúng ta có thể thấy nó đã làm việc và có thể kéo snack vào shopping cart, mở console để xem text được log. Trong dự án thực thế các bạn nên export Type thành một file riêng để tái sử dụng và tránh trường hợp gõ nhầm ký tự, ví dụ:export default {
SNACK: ‘snack’
};
rồi sử dụng trong component:
export default DropTarget(constants.SNACK, ShoppingCartSpec, collect)(ShoppingCart);Mình đã hướng dẫn ví dụ cơ bản (ví dụ này được tham khảo từ quyên sách pro reactjs), ví dụ này mình có cảm giác dễ hiểu hơn rất nhiều ví dụ mà thư viện đưa ra, hi vọng mình có thể giúp các bạn phần nào hiểu và làm được một tương tác phức tạp như kéo thả trong ứng dụng react dễ dàng hơn, tiết kiệm thời gian hơn khi phải lần mò nhiều thứ và không biết bắt đầu từ đâu. Nếu có bất cứ câu hỏi nào comment bên dưới bài viết mình sẽ giải thích cụ thể. Bài tương tác phức tạp trong react mình sẽ có một vài bài guide thực tế nâng cao hơn sau bài này.
4. Tài liệu tham khảo
Tài liệu chính thức: (gốm ví dụ và các api liên quan)
http://react-dnd.github.io/react-dnd/docs-drop-target.html
Backend hỗ trợ tương tác chạm trên thiết bị di động:
https://github.com/yahoo/react-dnd-touch-backend
Mã nguồn ví dụ:
https://github.com/pro-react/sample-code/tree/master/chapter%204/dragndrop
DnD không chỉ là tương tác phức tạp đối với người mới bắt đầu với react mà còn cả lập trình viên có kinh nghiệm, hi vọng series bài biết về tương tác nâng cao trong ứng dụng react như DnD, transition….sẽ hỗ trợ và giúp ích được cho mọi người trong quá trình làm dự án thực thế.