Trong phần này, chúng ta sẽ tìm hiểu thêm về JavaScript và cách tạo ra một khung popup bằng JavaScript mỗi khi di chuyển chuột qua tên của một user trong ứng dụng.
Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:
- Phần 1: Hello, World
- Phần 2: Tìm hiểu về template
- Phần 3: Tìm hiểu về Web Forms
- Phần 4: Sử dụng cơ sở dữ liệu
- Phần 5: Xử lý đăng nhập
- Phần 6: Hồ sơ cá nhân và ảnh đại diện
- Phần 7: Xử lý lỗi
- Phần 8: Tạo chức năng follower
- Phần 9: Phân trang
- Phần 10: Hỗ trợ email
- Phần 11: Nâng cấp giao diện
- Phần 12: Xử lý thời gian
- Phần 13: Hỗ trợ đa ngôn ngữ
- Phần 14: Sử dụng Ajax
- Phần 15: Tinh chỉnh cấu trúc ứng dụng
- Phần 16: Hỗ trợ tìm kiếm
- Phần 17: Triển khai ứng dụng trên Linux
- Phần 18: Triển khai ứng dụng với Heroku
- Phần 19: Triển khai ứng dụng với Docker
- Phần 20: JavaScript nâng cao (Bài viết này)
- Phần 21: Thông báo cho người sử dụng
- Phần 22: Tìm hiểu về tác vụ nền
- Phần 23: Xây dựng API
Bạn có thể truy cập mã nguồn cho phần này tại GitHub.
Ngày nay, việc xây dựng một ứng dụng Web mà không sử dụng JavaScript hầu như là “nhiệm vụ bất khả thi”. Và có lẽ bạn cũng biết, lý do chính là vì JavaScript là ngôn ngữ lập trình duy nhất có thể viết các chương trình thi hành được trực tiếp trên các trình duyệt mà không cần hỗ trợ (native). Trong Phần 14, chúng ta đã dùng JavaScript để xây dựng ra các liên kết cho tính năng phiên dịch theo thời gian thực. Trong phần này, chúng ta sẽ đi sâu hơn vào chủ đề về JavaScript và tìm hiểu cách sử dụng một số mẹo hữu ích với JavaScript để làm cho ứng dụng hấp dẫn hơn với người sử dụng.
Hiện nay, các mạng xã hội thường sử dụng một thiết kế giao diện rất phổ biến để cho phép người sử dụng có thể tương tác với nhau là một khung popup có hiển thị tóm tắt thông tin về một user mỗi khi bạn di chuyển chuột qua tên của user đó ở bất cứ nơi nào trong trang Web. Nếu bạn không để ý đến chức năng này, bạn có thể thử tại Twitter, Facebook, LinkedIn hoặc các mạng xã hội lớn khác. Mỗi khi bạn thấy một tên user trong các ứng dụng này, bạn có thể để con trỏ chuột (mouse pointer) trên các tên đó một vài giây và bạn sẽ thấy popup xuất hiện. Chúng ta sẽ cùng tìm hiểu cách xây dựng chức năng này trong Myblog như ví dụ dưới đây:
Hỗ trợ từ Server
Trước khi chúng ta bắt đầu làm việc với mã nguồn từ phía client (trình duyệt), chúng ta cần cập nhật mã nguồn của ứng dụng ở phía server để hỗ trợ cho các popup này. Nội dung được hiển thị trong popup sẽ là một phiên bản rút gọn của phần hồ sơ cá nhân có sẵn và sẽ được trả về bởi một địa chỉ mới. Sau đây là hàm hiển thị:
app/main/routes.py: Hàm hiển thị popup
1 2 3 4 5 |
@bp.route('/user/<username>/popup') @login_required def user_popup(username): user = User.query.filter_by(username=username).first_or_404() return render_template('user_popup.html', user=user) |
Địa chỉ mới này sẽ có dạng thức /user/<username>/popup và chịu trách nhiệm đọc thông tin về user từ cơ sở dữ liệu và hiển thị chúng bằng một template. Template này một bản rút gọn của template đã được dùng cho trang hồ sơ cá nhân:
app/templates/user_popup.html: Template hiển thị thông tin về user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<table class="table"> <tr> <td width="64" style="border: 0px;"><img src="{{ user.avatar(64) }}"></td> <td style="border: 0px;"> <p> <a href="{{ url_for('main.user', username=user.username) }}"> {{ user.username }} </a> </p> <small> {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} {% if user.last_seen %} <p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('lll') }}</p> {% endif %} <p>{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}</p> {% if user != current_user %} {% if not current_user.is_following(user) %} <a href="{{ url_for('main.follow', username=user.username) }}"> {{ _('Follow') }} </a> {% else %} <a href="{{ url_for('main.unfollow', username=user.username) }}"> {{ _('Unfollow') }} </a> {% endif %} {% endif %} </small> </td> </tr> </table> |
Đoạn mã JavaScript mà chúng ta sẽ viết trong phần sau sẽ gởi yêu cầu về địa chỉ này khi user di chuyển con trỏ chuột qua tên của một user bất kỳ trong ứng dụng. Server sẽ trả lời bằng cách trả về nội dung HTML của popup, và trình duyệt sẽ hiển thị popup này. Khi user di chuyển con trỏ chuột ra khỏi tên user, popup sẽ được xóa đi. Rất đơn giản phải không?
Bây giờ, nếu bạn muốn biết popup trông như thế nào, bạn có thể thực thi ứng dụng, truy cập bất kỳ trang hồ sơ cá nhân nào và nhập thêm chuỗi /popup vào URL của trong trong thanh địa chỉ của trình duyệt để thấy popup trong chế độ toàn màn hình.
Vài khái niệm cơ bản về cách hoạt động của JavaScript và HTML.
Trước khi bắt tay viết mã nguồn cho phía client, chúng ta cần ôn lại một số khái niệm cơ bản về cách hoạt động của HTML, JavaScript cũng như cách tương tác giữa JavaScript và các phần tử HTML.
Khi người sử dụng nhập địa chỉ (URL) của một trang Web vào trình duyệt, trình duyệt sẽ gởi một yêu cầu (request) dưới dạng văn bản theo giao thức HTTP đến server. Server sẽ xử lý yêu cầu này và nếu đây là một địa chỉ hợp lệ và được chấp nhận bởi một hoặc nhiều ứng dụng trên server, nó sẽ gởi về một hồi đáp (response) cũng dưới dạng văn bản và theo giao thức HTTP về trình duyệt. Hồi đáp này bao gồm nội dung trang Web được yêu cầu và một số các thông tin hỗ trợ khác. Trình duyệt sẽ phân tích nội dung của hồi đáp này để lấy được nội dung của trang Web và hiển thị nó. Quá trình này được gọi là nạp trang (Page load). Kết thúc quá trình này, nội dung của trang Web sẽ được hiển thị đầy đủ trên trình duyệt, đồng thời, trình duyệt cũng sẽ tạo ra một cấu trúc cây (hierarchy) đại diện cho từng phần tử (element) trong trang Web trong bộ nhớ tương ứng gọi là DOM (Document Object Model). Mỗi khi người sử dụng tương tác với một phần tử trên trang Web (ví dụ như bấm – click – vào các nút bấm hoặc liên kết, di chuyển con trỏ chuột – hover, …) thì trình duyệt sẽ tạo ra các sự kiện (event) tương ứng. Và đặc biệt, nếu cấu trúc của DOM có thay đổi (ví dụ như có phần tử mới được chèn vào hoặc một phần tử được xóa bỏ khỏi DOM hoặc các thuộc tính của các phần tử có thay đổi) thì trang Web cũng sẽ được cập nhật theo để phản ánh các thay đổi này.
Nhờ JavaScript, chúng ta có thể can thiệp vào cấu trúc của DOM cũng như xử lý các sự kiện được tạo ra khi người sử dụng tương tác với các phần tử trong trang Web. Kết quả là nội dung trang Web có thể được cập nhật mà không cần đến sự can thiệp của server. Và tố hơn nữa, chúng ta có thể dùng JavaScrip để lấy dữ liệu từ server và cập nhật một phần hoặc toàn bộ một trang Web nào đó. Đây cũng là bí quyết của kỹ thuật AJAX mà chúng ta sẽ sử dụng trong phần này.
Giới thiệu thành phần (component) Popover của Bootstrap
Trong Phần 11, chúng ta đã sử dụng thư viện CSS Bootstrap để hỗ trợ trong việc tạo ra các trang Web đẹp và thân thiện với người dùng. Tuy nhiên, cho đến giờ, chúng ta mới chỉ sử dụng một phần nhỏ của thư viện này. Bootstrap có rất nhiều thành phần giao diện người dùng (User Interface) để chúng ta có thể sử dụng trong các trường hợp khác nhau có kèm theo các hướng dẫn và ví dụ chi tiết trong trang tài liệu trực tuyến của nó. Một trong các thành phần này là Popover – được định nghĩa là một khung nhỏ dạng popupđể hiển thị các thông tin phụ – và đây chính xác là cái mà chúng ta cần.
Hầu hết các thành phần của Bootstrap được khai báo bằng các thẻ HTML với tham chiếu đến các thuộc tính được định nghĩa bởi Bootstrap và được thiết kế đẹp. Một số các thành phần cao cấp hơn cũng yêu cầu phải sử dụng JavaScript. Cách chuẩn nhất để sử dụng các thành phần này trong một trang Web là sử dụng các thẻ HTML tại các vị trí thích hợp, và nếu có sử dụng các thành phần cần có hỗ trợ của JavaScript, chúng ta phải thêm mã JavaScript để gọi các hàm để khởi tạo hoặc kích hoạt chúng. Để sử dụng thành phần popover, chúng ta cần hỗ trợ của JavaScript.
Các thẻ HTML để tạo popover rất đơn giản, bạn chỉ cần chỉ định phần tử HTML nào sẽ kích hoạt popover. Trong trường hợp của chúng ta, đó là các liên kết với tên của user xuất hiện trong các bài viết. Các liên kết này đã được định nghĩa trong template con app/templates/_post.html:
1 2 3 |
<a href="{{ url_for('main.user', username=post.author.username) }}"> {{ post.author.username }} </a> |
Theo hướng dẫn trong tài liệu trực tuyến của popover, chúng ta cần gọi hàm JavaScript popover()
cho mỗi liên kết tương tự như trên trong toàn bộ trang Web được hiển thị để khởi tạo popup. Hàm khởi tạo sẽ chấp nhận một số tùy chọn để cấu hình cho popup, bao gồm các tùy chọn về nội dung hiển thị, phương thức để kích hoạt khi cần hiển thị hay xóa popup (bằng cách bấm hay di chuyển con trỏ chuột, …), định dạng của nội dung hiển thị là văn bản thuần hay HTML và một vài lựa chọn khác. Tuy vậy, chúng ta cần giải quyết một số vấn đề sau để thành phần này có thể hoạt động hoàn hảo với Myblog:
- Mỗi trang Web của ứng dụng sẽ có nhiều liên kết đến tên của các user, chúng ta phải tìm cách để tìm ra tất cả các liên kết này bằng JavaScript sau khi các trang Web được hiển thị để khởi tạo các popover.
- Ví dụ về popover trong trang tài liệu của Boostrap chỉ dùng các dữ liệu tĩnh – các thông tin cần được hiển thị sẽ được đưa vào thuộc tính
data-content
của phần tử HTML cần có popover. Nhờ đó, mỗi khi người sử dụng di chuyển con trỏ chuột trên phần tử này và kích hoạt sự kiện hover, Bootstrap chỉ cần hiển thị popover với các nội dung có sẵn. Tuy nhiên, ứng dụng của chúng ta không làm việc theo cách này. Điều chúng ta cần là gởi một yêu cầu theo kỹ thuật Ajax đến server để nhận được các dữ liệu cần thiết, và sau khi nhận được các dữ liệu này từ server, chúng ta có thể hiển thị chúng qua popover. - Khi sử dụng popover với chế độ kích hoạt bằng cách di chuyển con trỏ chuột trên một phần tử trong trang, nó sẽ được hiển thị chừng nào chúng ta còn giữ con trỏ chuột trong phạm vi của phần tử đó. Nhưng nếu chúng ta di chuyển con trỏ chuột ra khỏi phần tử, popup sẽ biến mất. Điều này dẫn đến một kết quả phụ ngoài ý muốn là nếu người sử dụng di chuyển con trỏ chuột vào phạm vi của chính popover đang được hiển thị, nó cũng sẽ biến mất. Chúng ta cần tìm ra cách để thay đổi hành vi này để người sử dụng có thể di chuyển con trỏ chuột vào bên trong popover và nếu cần thiết, có thể click vào các liên kết đang được nó hiển thị.
Rắc rối quá phải không? Thật ra thì điều này rất bình thường khi làm việc với các ứng dụng có sử dụng trình duyệt. Vì vậy, chúng ta phải hiểu rõ cách hoạt động của HTML, DOM và JavaScript để đưa ra giải pháp tốt nhất và tạo ra giao diện thân thiện với người sử dụng.
Thi hành hàm JavaScript khi nạp trang Web
Rõ ràng là chúng ta cần thực hiện một số lệnh JavaScript ngay khi mỗi trang Web được nạp (load) vào bộ nhớ. Ở đây, chúng ta sẽ thực hiện gọi một hàm JavaScript để tìm tất cả các liên kết đến các username trong trang và thực hiện các bước cần thiết để khởi tạo popover.
Để thuận tiện, chúng ta sẽ sử dụng thư viện JavaScript đi kèm với Bootstrap là jQuery. Khi sử dụng jQuery, bạn có thể đăng ký một hàm sẽ được thực thi khi trang Web được nạp bằng cách đưa nó vào lệnh $(…)
. Chúng ta có thể đưa mã này vào template app/templates/base.html, nhờ đó nó sẽ được thực hiện trên mỗi trang trong ứng dụng:
app/templates/base.html: Thi hành hàm sau khi trang được nạp.
1 2 3 4 5 6 7 8 |
... <script> // ... $(function() { // Các mã để khởi tạo và thiết lập popover }); </script> |
Chúng ta sẽ đưa đoạn mã JavaScript ở trên vào trong khối script đã được khai báo khi xây dựng chức năng phiên dịch tức thời trong Phần 14.
Tìm kiếm các phần tử trong DOM với các bộ chọn (Selector)
Vấn đề đầu tiên chúng ta cần giải quyết là tạo một hàm JavaScript để tìm tất cả các liên kết với user trong mỗi trang của ứng dụng. Hàm này sẽ được thi hành khi trang Web tương ứng được nạp xong và có nhiệm vụ thực hiện các bước chuẩn bị cần thiết để hiển thị thành phần popover.
Nếu bạn còn nhớ trong Phần 14, các phần tử HTML có liên quan đến quá trình dịch thuật tức thời có một ID duy nhất. Ví dụ như một bài viết với ID=123 sẽ có thêm một thuộc tính là id="post123"
đi kèm theo. Và nhờ jQuery, chúng ta có thể sử dụng biểu thức $('#post123')
để xác định vị trí của phần từ này trong DOM (Nếu bạn không nhớ DOM là gì, tham khảo Phần 14). Hàm $() trong jQuery rất mạnh và hỗ trợ cho một loại ngôn ngữ truy vấn tương đối phức tạp dựa trên các bộ chọn (selector) CSS để tìm kiếm các phần tử HTML. Nếu chưa nắm được khái niệm CSS selector, bạn có thể tham khảo các tài liệu tiếng Việt hoặc tiếng Anh.
Selector mà chúng ta đã sử dụng trong chức năng phiên dịch được thiết kế để tìm một phần tử dựa vào thuộc tính (attribute) Id của phần tử đó. Các selector kiểu này chỉ cho phép chúng ta tìm một phần tử tại một thời điểm vì mỗi phần tử chỉ có một ID duy nhất. Một cách khác để tìm các phần tử HTML trong trang Web là sử dụng các selector với thuộc tính class, và cách này cho phép chúng ta có thể tìm nhiều hơn một phần tử HTML trong trang Web vì nhiều phần tử có thể được gán cùng thuộc tính class. Ví dụ như chúng ta có thể đánh dấu tất cả các liên kết đến user với thuộc tính class="user_popup"
, và sau đó chúng ta có thể nhận được một danh sách các liên kết từ jQuery với selector $('.user-popup')
(trong các selector CSS, dấu #
sẽ tìm phần tử nhờ ID, còn dấu .
sẽ tìm các phần tử nhờ class). Giá trị trả về trong trường hợp này sẽ là một tập hợp các phần tử có thuộc tính class tương ứng.
Popover và DOM
Từ các ví dụ về popover trong tài liệu của Bootstrap và tìm hiểu về phần tử này trong DOM (Bạn có thể sử dụng công cụ “Developer Tools”của trình duyệt Chrome – phím tắt là Ctrl-Shift-I
– để kiểm tra các phần tử của DOM trong bất kỳ trang Web nào), chúng ta có thể thấy là Bootstrap tạo ra các phần tử popover ngang hàng với các phần tử mà chúng hỗ trợ. Điều này dẫn đến kết quả là khi chúng ta di chuyển con trỏ chuột ra khỏi phần tử cần được hiển thị popover thì popover sẽ tự động được xóa đi. Trong phần lớn trường hợp, điều này là hợp lý. Tuy nhiên, nó cũng đưa đến một kết quả phụ là nếu chúng ta di chuyển con trỏ chuột vào bên trong popover, popover cũng sẽ bị xóa đi, và đây không phải là điều chúng ta muốn.
Để khắc phục điều này, chúng ta sẽ dùng một mẹo là làm cho phần tử popover được tạo ra ở vị trí “con” của phần tử cần tạo popover thay vì ngang hàng như mặc định. Và nhờ đó, phần tử popover cũng sẽ kết thừa sự kiện di chuyển chuột của phần tử “cha”. Theo tài liệu của Bootstrap vể các tùy chọn của popover, chúng ta có thể làm điều này bằng cách truyền phần tử “cha” như là một tham số trong tùy chọn container
.
Tuy nhiên, đến đây chúng ta lại phải đối diện với một rắc rối khác. Đối với các phần tử HTML như là nút bấm, hoặc các phần tử dựa trên <div>
hoặc <span>
, chúng ta có thể dễ dàng tạo ra một popover như là một phần tử con của chúng mà không gặp vấn đề gì. Nhưng trong trường hợp này, đối tượng cần tạo popover là các liên kết và sử dụng các phần tử <a>
, do đó, nếu chúng ta đặt popover tại vị trí “con” của các phần tử này, các popover sẽ kế thừa luôn cả liên kết của phần tử “cha” là <a>
. Mã HTML sinh ra trong trường hợp này sẽ tương tự như sau:
1 2 3 4 |
<a href="..." class="user_popup"> username <div> ... Phần tử popover ... </div> </a> |
Với mã này, các popover cũng sẽ có tác dụng như các liên kết và nếu người sử dụng bấm vào chúng, họ sẽ được đưa đến trang hồ sơ cá nhân của user tương ứng. Đây không phải là điều chúng ta muốn. Vì vậy, chúng ta cần đưa mã HTML của popover ra khỏi phần tử <a>
. Để làm được việc này, chúng ta cần sử dụng một mẹo khác. Chúng ta sẽ đóng gói phần tử <a>
vào trong một phần tử <span>
và liên kết sự kiện di chuyển con trỏ chuột (hover) và popover tương ứng với phần tử <span>
thay vì <a>
. Kết quả là chúng ta sẽ có cấu trúc HTML như sau:
1 2 3 4 5 6 |
<span class="user_popup"> <a href="..."> username </a> <div> ... Phần tử popover ... </div> </span> |
Các phẩn tử <div>
và <span>
rất có ích trong việc tổ chức cấu trúc của DOM vì chúng không được hiển thị trên trang Web theo mặc định. Các phần tử <div>
là các phần tử khối (block), tương tự như các đoạn văn bản (paragraph) trong HTML, còn các phần tử <span>
là các phần tử nội tuyến (inline) có vai trò tương tự như một từ trong đoạn văn bản. Chúng ta sử dụng phần tử <span>
vì phần tử <a>
mà chúng ta cần hiển thị popover cũng là một phần tử nội tuyến.
Đến đây, chúng ta có thể cập nhật template con app/templates/_post.html với phần tử <span>
như sau:
1 2 3 4 5 6 7 8 9 |
... {% set user_link %} <span class="user_popup"> <a href="{{ url_for('main.user', username=post.author.username) }}"> {{ post.author.username }} </a> </span> {% endset %} ... |
Chúng ta có thể kết thúc phần cập nhật mã HTML ở đây. Khi chúng ta gọi hàm popover()
trên các phần tử <span>
để khởi tạo popover trong phần sau, Bootstrap sẽ tự động hiển thị chúng tại ác vị trí thích hợp.
Sự kiện di chuyển con trỏ chuột (hover)
Như đã nói ở phần trên, cách hoạt động theo mặc định của popover trong thư viện Bootstrap không đủ linh hoạt để đáp ứng cho nhu cầu của chúng ta. Nhưng nếu đọc kỹ tài liệu, chúng ta sẽ thấy popover có thể được kích hoạt bằng một số các sự kiện khác nhau tùy theo giá trị được truyền cho tham số trigger
khi khởi tạo. Đặc biệt là trong chế độ “manual” (thủ công), chúng ta có thể hiển thị hoặc xóa các popover bằng cách gọi các hàm JavaScript tương ứng. Chế độ này cho phép chúng ta tạo ra các logic để điều khiển cơ chế kích hoạt và hoạt động của popover theo ý muốn. Vì vậy, chúng ta sẽ sử dụng tùy chọn này và tự xây dựng cơ chế xử lý sự kiện hover theo thiết kế của chúng ta.
Bước tiếp theo là kết hợp sự kiện “hover” vào tất cả các liên kết trong trang với sự hỗ trợ của jQuery. Trong jQuery, chúng ta có thể gắn sự kiện hover vào bất kỳ phần tử HTML nào bằng cách gọi hàm element.hover(handlerIn, handlerOut)
. Nếu chúng ta gọi hàm này trên một tập hợp các phần tử, jQuery sẽ liên kết sự kiện này với tất cả các phần tử trong tập hợp. Bản thân hai tham số trong hàm này là các hàm sẽ được gọi khi người sử dụng di chuyển con trỏ chuột vào hoặc ra khỏi phạm vi của các phần tử cần được hiển thị popover:
app/templates/base.html: Sự kiện hover
1 2 3 4 5 6 7 8 9 10 11 12 |
$(function() { $('.user_popup').hover( function(event) { // Hàm xử lý khi con trỏ chuột bên trong phần tử (mouse in) var elem = $(event.currentTarget); }, function(event) { // Hàm xử lý khi con trỏ chuột rời khỏi phần tử (mouse out) var elem = $(event.currentTarget); } ) }); |
Tham số event
là đối tượng về sự kiện và có chứa các thông tin rất hữu dụng. Trong trường hợp này, chúng ta sẽ tìm được phần tử HTML tạo ra sự kiện này từ event.currentTarget.
Trình duyệt sẽ tạo ra sự kiện hover ngay lập tức khi con trỏ chuột tiến vào phạm vi của phần tử tương ứng. Trong trường hợp của popover, chúng ta không muốn kích hoạt nó ngay lập tức mà đợi một khoảng thời gian ngắn sau khi con trỏ chuột ở trong phạm vi này rồi mới kích hoạt để tránh trường hợp popover sẽ xuất hiện khi người sử dụng vô tình lướt con trỏ chuột qua các phần tử tương ứng. Tuy vậy, bởi vì sự kiện hover không hỗ trợ cho việc đợi theo mặc định, chúng ta phải giải quyết cả vấn đề này. Và để làm điều đó, chúng ta sẽ thêm một bộ đếm giờ (timer) với thời gian là một giây vào mã xử lý sự kiện mỗi khi con trỏ chuột di chuyển vào phạm vi của phần tử HTML có popover (mouse in):
app/templates/base.html: Thời gian chờ cho sự kiện hover.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$(function() { var timer = null; $('.user_popup').hover( function(event) { // Hàm xử lý khi con trỏ chuột bên trong phần tử (mouse in) var elem = $(event.currentTarget); timer = setTimeout(function() { timer = null; // logic để hiển thị popup }, 1000); }, function(event) { // Hàm xử lý khi con trỏ chuột rời khỏi phần tử (mouse out) var elem = $(event.currentTarget); if (timer) { clearTimeout(timer); timer = null; } } ) }); |
Hàm setTimeout()
được hỗ trợ sẵn trong các trình duyệt và cần hai tham số là một hàm khác và một thời khoảng với đơn vị là mili giây. Khi được gọi, hàm này sẽ thực hiện hàm được cung cấp trong tham số sau thời khoảng đã định. Vì vậy, theo mã hiện thời của chúng ta, hàm mà chúng ta đã truyền vào (tạm thời còn trống) sẽ được thực hiện một giây sau khi sự kiện hover được tạo ra. Và nhờ vào khả năng closure của JavaScript, hàm này có thể truy cập các biến ở tầm vực lớn hơn như là elem
.
Chúng ta sẽ chứa đối tượng bộ đếm giờ (timer) trong biến timer
được định nghĩa bên ngoài lời gọi hàm hover()
để cả hai hàm được truyền vào hover()
(một để xử lý sự kiện mouse in và một xử lý sự kiện mouse out) đều có thể sử dụng đối tượng này. Lý do chính để sử dụng bộ đếm giờ ở đây là để cung cấp trải nghiệm tốt cho người sử dụng như đã nói ở trên. Bằng cách này, chúng ta có thể đảm bảo rằng nếu người sử dụng di chuyển con trỏ chuột vào một trong số các liên kết này và dừng con trỏ chuột tại đó khoảng nửa giây và sau đó di chuyển đi nơi khác, bộ đếm giờ sẽ không tiếp tục hoạt động và hiển thị popover. Vì vậy, hàm xử lý sự kiện khi con trỏ chuột di chuyển ra khỏi phạm vi của phần tử HTML có liên quan sẽ tìm xem có bộ đếm giờ đang hoạt động hay không, và nếu có thì hủy nó.
Yêu cầu Ajax
Ajax không phải là một chủ đề mới và chúng ta đã sử dụng kỹ thuật này trong Phần 14 để xây dựng chức năng dịch thuật tức thời. Để sử dụng Ajax trong thư viện jQuery, chúng ta sẽ dùng hàm $.ajax()
. Hàm này sẽ gởi một yêu cầu bất đồng bộ (asynchronous request) đến server.
Yêu cầu Ajax được gởi đến server sẽ có địa chỉ /user/<username>/popup mà chúng ta đã thêm vào ứng dụng ở phần đầu. Hồi đáp (hoặc phản hồi – response) từ server sẽ có các mã HTML cần cho popover hiển thị.
Có một khó khăn nhỏ cho chúng ta ở đây là làm sao để tìm được giá trị của username
để đưa vào địa chỉ của yêu cầu. Hàm JavaScript xử lý sự kiện hover mà chúng ta tạo ra là một hàm tổng quát, nó sẽ được thực thi với tất cả các liên kết đến user trong trang và không biết trước sẽ có những username nào trong trang. Vì vậy, chúng ta cần phải tìm cách để hàm này có thể tìm được username cần thiết từ các liên kết.
Như đã nói ở trên, biến elem
chứa các thông tin về phần tử đã tạo ra sự kiện hover, cụ thể là phần tử <span>
có chứa phần tử <a>
theo như thiết kế của chúng ta. Nhưng ở đây chúng ta sử dụng hàm jQuery $()
để trả về một danh sách các phần tử. Vì vậy, đây sẽ là một danh sách chỉ có một phần tử. Để lấy được tên người sử dụng (username) từ danh sách này, chúng ta sẽ tra cứu DOM, bắt đầu từ phần tử đầu tiên và duy nhất là elem
, tìm đến phần tử con thứ nhất của nó là <a>
và lấy ra văn bản có trong phần tử này, cũng chính là username mà chúng ta cần tìm. Quá trình này rất đơn giản với hàm hỗ trợ từ thư viện jQuery để duyệt qua cấu trúc của DOM:
1 |
elem.first().text().trim() |
Trong đoạn mã trên, hàm first()
trong thư viện jQuery sẽ trả về phần tử đầu tiên trong danh sách các phần tử con của elem. Hàm text()
sẽ trả về nội dung văn bản tổng hợp của một phần tử và các phần tử con của nó và được loại bỏ các thẻ HTML. Hàm này không có chức năng cắt khoảng trắng từ văn bản, vì vậy, nếu như bạn có thẻ <a>
trên một dòng, văn bản trong thẻ trong dòng tiếp theo, và thẻ </a>
trên dòng thứ ba, hàm text()
sẽ loại bỏ các thẻ <a>
và </a>
nhưng trả về toàn bộ phần văn bản nằm giữa hai thẻ này, bao gồm cả các ký tự trắng như ký tự khoảng cách giữa các hàng, ký tự xuống dòng … Để loại bỏ các khoảng trắng, chúng ta sẽ sử dụng hàm JavaScript trim()
.
Và đó là tất cả những gì chúng ta cần để tạo ra một yêu cầu và gởi đến server:
app/templates/base.html: Yêu cầu JavaScript bất đồng bộ hoặc XHR.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
$(function() { var timer = null; var xhr = null; $('.user_popup').hover( function(event) { // Hàm xử lý khi con trỏ chuột bên trong phần tử (mouse in) var elem = $(event.currentTarget); timer = setTimeout(function() { timer = null; xhr = $.ajax( '/user/' + elem.first().text().trim() + '/popup').done( function(data) { xhr = null // Khởi tạo và hiển thị popover } ); }, 1000); }, function(event) { // Hàm xử lý khi con trỏ chuột rời khỏi phần tử (mouse out) var elem = $(event.currentTarget); if (timer) { clearTimeout(timer); timer = null; } else if (xhr) { xhr.abort(); xhr = null; } else { // Kết thúc và xóa popover } } ) }); |
Ở đây chúng ta định nghĩa một biến tên là xhr
để chứa đối tượng tương ứng với yêu cầu bất đồng bộ mà chúng ta khởi tạo với hàm $.ajax()
. Đáng tiếc là khi tạo ra các URL bằng JavaScript, chúng ta không thể xử dụng hàm url_for()
của Flask, vì vậy, chúng ta phải ghép nối các phần của URL theo cách thủ công.
Khi được gọi, hàm $.ajax()
sẽ trả về một đối tượng JavaScript đặc biệt đại diện cho tác vụ bất đồng bộ gọi là promise. Chúng ta có thể thêm một hàm sẽ được gọi lại (callback) khi hàm $.ajax()
nhận được dữ liệu từ server vào quá trình này bằng cách sử dụng hàm .done(function)
. Hàm callback này sẽ nhận được hồi đáp từ server trong tham số data
như trong đoạn mã ở trên. Đây cũng là phần mã HTML mà chúng ta sẽ truyền cho popover để hiển thị.
Nhưng trước khi chúng ta chuyển qua mã cho popover, chúng ta cần thực hiện một vài thao tác nữa để bảo đảm rằng người sử dụng có được trải nghiệm tốt với ứng dụng. Trong phần trên, chúng ta đã sử dụng một logic trong hàm xử lý sự kiện “mouse out” để ngưng bộ đếm giờ khi người sử dụng di chuyển con trỏ chuột ra khỏi thẻ <span>
. Và chúng ta cũng sẽ áp dụng cách tương tự như vậy cho yêu cầu bất đồng bộ này và thêm một mệnh đề nữa để hủy bỏ đối tượng xhr
nếu nó có tồn tại.
Khởi tạo và hủy bỏ popover
Đến đây, chúng ta có thể khởi tạo popover với tham số data
chứa dữ liệu nhận được từ server và được truyền vào hàm callback vừa được thêm vào:
app/templates/base.html: Hiển thị popover.
1 2 3 4 5 6 7 8 9 10 11 |
function(data) { xhr = null; elem.popover({ trigger: 'manual', html: true, animation: false, container: elem, content: data }).popover('show'); flask_moment_render_all(); } |
Quá trình khởi tạo thật sự của popover rất đơn giản vì hàm popover()
của thư viện Bootstrap đã đảm nhận hết các công việc cần thiết. Các tùy chọn cho popover được thiết lập nhờ các tham số truyền cho hàm này. Chúng ta thiết lập cấu hình kích hoạt cho popover này theo chế độ “manual” (thủ công), sử dụng nội dung hiển thị dạng HTML, không sử dụng hiệu ứng làm mờ (để popover có thể được hiển thị và xóa đi nhanh hơn). Chúng ta cũng thiết lập cho phần tử elem
cũng là phần tử cha của popover để nó kế thừa cách xử lý với hành vi hover như đã thảo luận chi tiết ở phần trên. Và cuối cùng, chúng ta sẽ truyền đối tượng data
có chứa nội dung cần hiển thị cho tham số content
.
Khi được gọi, hàm popover()
sẽ trả về một thành phần popover mới được tạo ra. Và đáng ngạc nhiên là thành phần này cũng sẽ có một phương thức gọi là popover()
để hiển thị. Đó là lý do chúng ta phải thực hiện thêm một lời gọi hàm popover('show')
thứ hai để hiển thị popover trên trang Web.
Nội dung được hiển thị trong popover sẽ có thời điểm cuối cùng user đã sử dụng ứng dụng (last seen), dữ liệu này được tạo ra nhờ thư viện Flask-Moment mà chúng ta đã sử dụng trong Phần 12. Theo tài liệu của thư viện này, khi các phần tử Flask-Moment được thêm vào trang bằng Ajax, chúng ta cần gọi hàm flask_moment_render_all()
để hiển thị chúng một cách thích hợp.
Và cuối cùng, chúng ta cần xóa bỏ popover trong hàm xử lý sự kiện “mouse out”. Hàm này hiện đang có các logic để hủy bỏ các tác vụ của popover khi người sử dụng di chuyển chuột ra khỏi phạm vi của phần tử có popover. Nếu toàn bộ các logic này không đúng, có nghĩa là popover đang được hiển thị và người sử dụng đang di chuyển chuột ra khỏi phần tử có popover. Trong trường hợp đó, chúng ta cần gọi hàm popover('destroy')
để hủy bỏ popover hiện tại.
app/templates/base.html: Hủy bỏ popover.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function(event) { // Hàm xử lý khi con trỏ chuột rời khỏi phần tử (mouse out) var elem = $(event.currentTarget); if (timer) { clearTimeout(timer); timer = null; } else if (xhr) { xhr.abort(); xhr = null; } else { elem.popover('destroy'); } } |
Chúng ta sẽ tạm ngừng ở đây. Hẹn gặp bạn trong phần tiếp theo.