Pet Project: Làm một ToDo App không thể đơn giản và xấu xí hơn với VueJS

Một hai hôm vừa rồi mình bỗng dưng nổi hứng muốn học thêm bí kíp võ công để tiếp tục hành tẩu giang hồ, sau một hồi vò đầu bứt tóc tự vấn thì cuối cùng mình cũng đã quyết định đi tìm đọc một bí kíp chân kinh nổi tiếng - VueJS, xem review và các bài đánh giá trên mạng thì mọi người đa số nói VueJS là một loại kiếm phổ tương đối dễ tiếp cận và làm quen - không giống quyển kiếm phổ nổi tiếng với khả năng gây tẩu hỏa nhập ma cho tân môn đồ như AngularJS, cộng với việc cũng muốn học về cái framework này từ lâu nên mình đã mò vào Google và tìm ngay những bài viết liên quan đến việc học Vue, lên trang chủ của Vue để xem những video cơ bản miễn phí (Vue có phần hướng dẫn cơ bản và document cực kỳ tốt, dễ hiểu).

Sau cả ngày mò mẫm, tích lũy được chút ít kiến thức cơ bản về nó thì mình quyết định sẽ làm một cái gì đó nó thực tế chút, và các bạn biết một cái app đơn giản mà có đủ các tác vụ CRUD (Create-Read-Update-Delete) rồi đó - Todo. Cá nhân mình nghĩ ai mới tiếp cận với cái gì đó mới thì cũng đều phải làm thử cái app này, sau đó hiểu rõ cấu trúc và cách nó hoạt động rồi mới bắt đầu thực hiện những thứ nâng cao hơn.

Ban đầu mình cũng màu mè, định xây dựng cả một cái layout hoành tráng và những chức năng đỉnh cao này kia để luyện tập, nhưng dựng layout xong rồi lại thấy việc này thực sự không cần thiết, cái mình cần là hiểu bản chất của sự việc trước đã xong rồi muốn làm những cái đỉnh cao hơn thì có thể để sau cũng được không cần gấp. Nên là mình bỏ hết những cái dây mơ rễ má đi, cuối cùng cho ra một cái giao diện trắng đen không thể nào xấu xí và củ chuối hơn.


Quá xấu xí.

Vậy nên mình viết bài này để chia sẻ về quá trình mình tạo ra cái app này trong lần đầu học Vue của mình, vừa để ghi lại kỷ niệm "lần đầu" vọc vạch, vừa để giúp cho một bạn nào mới tiếp cận với VueJS được tham khảo một cái project thuộc tầng siêu cơ bản của nó, khi bạn hiểu được bản chất và cấu trúc của Vue rồi thì sau này mọi việc sẽ được thực hiện dễ dàng hơn. Sau khi tạm lĩnh hội được những thứ cốt lõi của VueJS để rời sơn trang đi hành tẩu giang hồ thì mình sẽ thực hiện tài xây dựng lại cả cái dự án SimpleQuiz, nâng cấp nó lên và hướng tới một ứng dụng web được sử dụng rộng rãi bởi cộng đồng.

Thôi mình sẽ không dài dòng nữa mà mình sẽ vào thẳng việc chính luôn, nếu trong quá trình đọc bài viết có chỗ khó hiểu thì bạn luôn có thể sốc lại kiến thức với trang tài liệu chính chủ của Vue tại đây, và như mình đã nói thì phần tài liệu của Vue không thể chê vào đâu được.

Giới thiệu

Nói sơ về một số thứ sẽ được mình sử dụng trong ứng dụng tí hon này:


  • Não
  • VueJS (duh..)
  • LocalStorage để lưu và tải dữ liệu
  • Extension "Vue Devtools" trên Chrome để debug và theo dõi thay đổi dữ liệu

Để bắt đầu thì bạn hãy tạo một thư mục mới với cấu trúc tùy sở thích và thói quen, sau đó chèn thư viện VueJS vào trong thẻ head của file index. Bạn có thể sử dụng bất cứ đường dẫn nào tới VueJS mà bạn muốn, tuy nhiên thì mình thường tải luôn file js về và để ở thư mục local cho tiện trong việc truy xuất.


Những thành phần ở trong mình sẽ giải thích thêm ở bên dưới.

Trước khi bắt đầu bắt tay vào xây dựng các component thì mình sẽ phải tạo một Vue Instance, chỉ định vùng hoạt động của nó là phần tử có class là "wrapper" trong trang web, chính là thẻ div bao trọn toàn bộ các thành phần của ứng dụng todo này. Đồng thời mình cũng sẽ khai báo một số biến dữ liệu tổng thể mà chúng ta sẽ sử dụng để xử lý với những component sau này.


Khởi tạo một Vue Instance.

Todo App của mình sẽ có 3 component riêng biệt, cụ thể là:
  1. <todo-input> _ Hiển thị form nhập và lưu Todo
  2. <todo-error> _ Hiển thị lỗi nhập liệu
  3. <todo-list> _ Hiển thị danh sách Todo đã lưu

Thêm Todo

Component todo-input sẽ là component đầu tiên mà chúng ta thực hiện khởi tạo:


Tạo component todo-input.

Hãy lưu ý khi khai báo template cho component thì ta phải đóng mở code block bằng dấu huyền chứ không phải là một dấu nháy đơn.
Ở trong thẻ input mình có bind dữ liệu của placeholder vào một biến dữ liệu được khai báo bên dưới để cho các bạn quen một chút về việc sử dụng các directive trong Vue.
Directive v-model có tác dụng tương tự như thuộc tính value trong thẻ input, nhưng ở Vue thì v-model được sử dụng để xử lý dữ liệu đầu vào.

Bây giờ ta muốn khi nhấn nút "Add" thì dữ liệu trong input sẽ được đưa vào trong mảng todoList[] thì mình nút "Add" sẽ có vai trò submit để đẩy dữ liệu của form đi.
Khi muốn submit một form bất kỳ thì bạn sẽ sử dụng @submit cho form đó, bổ sung @submit.prevent để ngăn trình duyệt load lại trang, tham số là một phương thức addTodo được định nghĩ ở trong component đó luôn.
Theo lẽ thường tình thì dữ liệu và phương thức được định nghĩa trong một component sẽ không thể được sử dụng bởi các yếu tố bên ngoài, trường hợp này chúng ta phải "phát sóng" nó ra ngoài để chỗ khác có thể thấy được dữ liệu của component đó bằng cách sử dụng $emit.


Truyền dữ liệu ra ngoài với this.$emit

Như ảnh trên thì mình tạo một hàm addTodo, gán một biến todoInput để lấy giá trị của ô input, nếu kiểm tra có dữ liệu thì chạy $emit và reset lại giá trị của ô input. Nếu bạn chưa nhập gì thì sẽ hiện thông báo lỗi, phần này mình sẽ giải thích rõ hơn khi chúng ta thực hiện xây dựng component hiển thị lỗi cho Todo App này.

Song song với việc trên mình cũng muốn có một cái nút để xóa hết tất cả Todo hiện tại nên mình đã thê một hàm resetTodos trong component hiện tại, cách nó hoạt động cũng tương tự như hai phương thức ở trên. Tuy nhiên với hàm resetTodos thì mình không truyền giá trị bổ sung nào vào $emit cả vì tác vụ của nó chỉ đơn giản là xóa đi tất cả dữ liệu trong mảng todoList, thay vào đó mình chỉ truyền tên của directive mà mình sẽ sử dụng với phương thức tương ứng trong Vue Instance.

Để sử dụng những hàm và giá trị được "phát sóng" thì ta sẽ thêm vào một directive trong thẻ component đã được định nghĩa trong $emit như thế này:


Sử dụng $emit.

Trong đó @add-todo là tham số đầu của hàm $emit và tham số của @add-todo chính là tên hàm được định nghĩa trong Vue Instance. Hiện tại trong Vue Instance của chúng ta chưa có cái hàm nào tên là addTodo và resetTodos cả nên ta sẽ bắt đầu xây dựng nó ngay.


Hai hàm add và reset Todo.

Theo như hàm addTodo mà chúng ta xây dựng thì khi người dùng nhấn vào nút add thì nó sẽ tạo ra một object mới với ba thuộc tính là id, content, isDiabled sau đó đẩy object hiện tại vào mảng todoList được định nghĩa trong Vue Instance.
Hàm resetTodos đơn giản là xóa toàn bộ dữ liệu trong mảng todoList và reset mọi giá trị liên quan đến nó.

Hiển thị Todo

Tạm thời xong phần thêm Todo cơ bản, tuy nhiên chúng ta chưa làm gì để danh sách Todo được hiển thị ra bên ngoài cả nên chúng ta sẽ bắt tay xây dựng component cho nó ngay.


Props dùng để định nghĩa những giá trị có thể được truyền vào component thông qua việc nhập thuộc tính tương ứng trong thẻ HTML.
Lần này component của chúng ta có sự xuất hiện của một thứ gì đó lạ lạ - props. Như mình đã định nghĩa ở caption dưới ảnh, mình sẽ sử dụng props để lấy dữ liệu từ mảng todoList trong Vue Instance, truyền vào component này để nó xử lý bên trong và in ra danh sách todo như mình mong muốn.

Ở trong phần template mình sẽ kiểm tra xem mảng todoList có dữ liệu hay không, nếu chưa có thì sẽ in ra thẻ <p> với nội dung là "There is no todo", ngược lại thì nó sẽ dùng vòng lặp để in ra số lượng thẻ li tương ứng. Như ở đây thì mình có in ra cả một số nút chức năng như Delete, Edit.

Trong component này có một số thứ ta cần phải xử lý như là nút chỉnh sửa và xóa todo. Các phương thức này đều có cách xử lý tương tự như component todo-input trước đó, tuy nhiên lần này có truyền thêm tham số.


Các phương thức trong component todo-list
Hai phương thức xóa và sửa thì khá dễ hiểu khi ta sẽ phải truyền giá trị todoId vào để xử lý, tuy nhiên phần lưu lại nội dung đã sửa thì phải truyền cả id lẫn cả nội dung mới của cái todo đó nữa, khổ nỗi phương thức $emit chỉ cho truyền vào có 2 tham số thì làm thế nào?
Cách giải quyết ở đây là truyền luôn một đối tượng vào tham số thứ 2 như trong hình.

Vì mình muốn todo mới nhất phải nằm ở đầu danh sách nhưng vòng lặp hiện tại thì lại hiển thị todo mới nhất ở cuối nên mình phải thực hiện đảo chiều lại mảng todoList với hàm reverseArray, sử dụng ngay trong directive v-for để in ra được thứ tự dữ liệu mà mình mong muốn.


Todo mới nhất sẽ luôn nằm ở trên đầu danh sách.
Xóa Todo

Về phần nút Delete thì mình sẽ nói sơ qua và tập trung chủ yếu vào nút Edit vì mình không thực hiện xử lý và viết phương thức nào cao siêu cả. Hàm delete chỉ đơn giản set null cho todo có ID nhất định, sau đó quét lại mảng todoList, lần lượt đưa dữ liệu ở mảng cũ vào mảng mới và bỏ qua những index có giá trị null. Thay thế todoList với mảng mới đó và woa-la! Ta có chức năng Delete Todo! Quá dễ và củ chuối phải không?

Nếu thắc mắc về những vấn đề liên quan đến phương thức này thì hãy truy cập repo github mình chia sẻ ở cuối bài viết.

Sửa Todo

Phần cuối cùng mà mình sẽ nói đến là chức năng Edit, ý tưởng ở đây là khi bạn thêm vào một todo thì nó sẽ hiển thị dưới dạng giá trị trong một ô input và có thuộc tính readonly. Sau khi nhấn "Edit" thì ô input này sẽ có thể được chỉnh sửa, đồng thời nút "Edit" cũng được sửa thành nút "Save" để lưu lại thay đổi. Sau khi lưu thì nội dung todo trong danh sách thay đổi, input trở lại thành readonly và "Save" được chuyển lại thành "Edit".

Việc đầu tiên mình phải làm là xử lý nút "Edit" sao cho khi ấn vào thì nó sẽ chuyển thành nút "Save" và ô input không còn thuộc tính readonly nữa. Chuyện này thì khá dễ thực hiện, chỉ việc lấy id của todo đó, duyệt mảng todoList, gặp đúng id đó thì setDisable=false cho ô input thôi.

Bỏ thuộc tính readonly
Tiếp theo là phương thức để lưu lại thay đổi của todo đó, ở đây chúng ta cần quan tâm đến hai tham số là id và nội dung của todo. Khi có id thì ta duyệt mảng, thay nội dung cho todo có id chỉ định và thêm thuộc tính readonly vào todo đó.

Vì trước đó chúng ta có sử dụng $emit để truyền một object bao gồm id và content cho hàm saveEdit nên khi viết phương thức trong Vue Instance ta có thể sử dụng object đó một cách dễ dàng.

Duyệt mảng, bắt id, đổi content, thay thuộc tính.
Đến đây khi nhấn "Save" sau khi sửa thì todo đó sẽ được cập nhật ngay.
Phương thức saveTodos mình dùng ở đây để thao tác lưu những dữ liệu đã được cập nhật vào localStorage để đồng bộ ở cả hai bên.

Lời kết

Vậy là mình đã có thể xây một todo app đơn giản, xấu xí và củ chuối và mình mong bạn cũng làm được thông qua bài viết này.

Qua việc thực hiện làm Todo App này thì mình đã biết sử dụng những kiến thức cơ bản nhất của VueJS, cách tạo Instance, Component, Emit,...

Xin thứ lỗi nếu có chỗ hoang mang hoặc khó hiểu, nếu bạn có thắc mắc hoặc góp ý thì hãy cứ để lại một comment và mình sẽ cải thiện mỗi ngày một tốt hơn.

Source code: https://github.com/ythien123456/vue

- Thien Nguyen