Hướng Dẫn Xây Dựng Books Store Với NodeJS, Express và MongoDB - Phần 4

Hướng Dẫn Xây Dựng Books Store Với NodeJS, Express và MongoDB - Phần 4

Chức Năng Hiển Thị Chi Tiết, Cập Nhật Và Xóa Books

Định Nghĩa Và Controller Router Hiển Thị Chi Tiết Trong Books

Mình sẽ nói sơ qua một chút về nguyên lí hoạt động của thèn này tí nha ^^.
Để hiển thị chi tiết(show detail) books thì chỉ có cách là lấy id của books đó rồi render ra các fields như: title, desc, author,...
Bắt đầu code thôi nào
Như các bạn đã biết thì trong phần 2 mình đã làm chức năng tạo books trong đó mình đã có tạo một thẻ liên kết a trước rồi. Mục đích là khi click vào đấy thì sẽ show detail books.
Khi các bạn vào trong views có file books/index.pug thì sẽ thấy một thẻ a được tạo sẵn rồi.

Cũng trong file books.routes.js ta sẽ tạo một router router.get('/:id,...), trong router này sẽ là nơi mà chúng ta dùng để show detail bằng cách sử dụng id của chúng.
Trước tiên ta sẽ dựa vào id của books để lấy data của books rồi render data đó ra thôi. Populate này giúp chúng ta tìm được tên tác giả thì cần phải truy vấn vào document của chúng, các bạn tìm hiểu thêm tại đây nhađây nữa nha

// Show Details Books
router.get('/:id', async (req, res, next) => {
    try {
       // find data một books dựa vào id
        const book = await Book.findById(req.params.id).populate('author').exec();
        res.render('books/show', {
            book: book
        })
    } catch {
        res.redirect('/')
    }
})

Mình sẽ log ra data books khi click vào books để show detail để cho các bạn dễ hiểu nha

Giao Diện Hiển Thị Chi Tiết Trong Books

Trong views bạn tạo cho mình thư mục là books sẽ bao gồm 4 file index.pug, new.pug, show.pug, edit.pug.
Trước tiên các bạn nhớ extends layout vào nha sau đó chúng ta cần hiển thị những thông tin cần thiết của một books như: Cover image, title, author,... Khi chúng ta findById của books thì nó chỉ find một books có id mà nó đang tìm nên không cần phải lặp qua rồi hiển thị data. Các bạn để ý ở dưới thì sẽ thấy một thẻ a với tên là view author để khi click vào đấy nó sẽ đưa chúng ta đến trang show detail của author mà đã tạo books đấy.

Ở đây các bạn tạo hẳn luôn cho mình một thẻ div để bao bọc những chức năng như: view author, delete, edit books. Các bạn cũng biết mục đích của nó là làm gì rồi đấy, tạo luôn cho rồi tí nữa làm mấy phần dưới đỡ tốn thời gian =)))

books/show.pug

extends ../layouts/layout
block content
   .container

       ul
          li
            a(href="") 
              img(src=book.ImageUrl, width="250" height="280")
          li Title: 
            = book.title
          li Author: 
            = book.author.name  
          li Description: 
            = book.description
          li PageCount: 
            = book.pageCount
          li publishDate: 
            = book.publishDate.toDateString()
       div    
          a(href='/books/' + book.id +'/edit') Edit
          form(method="POST" action="/books/" + book.id + "?_method=DELETE")
               button(type='submit') Delete            
          a(href='/authors/' + book.author.id) View Author      

Và đây là kết quả khi chúng ta show detail books nha:

Định Nghĩa Router Cập Nhật Trong Books

Thì thèn cập nhật này cách làm nó cũng tựa tựa như tạo books thôi chỉ khác là thay vì sẽ tạo một cái mới thì ta sẽ lấy id của books đấy để mà edit lại thông tin mà mình đã tạo.
Để mà show detail được chúng ta phải dựa vào id của author, thì update books cũng tương tự vậy thôi nha.
Bắt đầu code thôi nào
Như các bạn đã thấy ở trên khi mình show detail books thì trong phần hiển thị chi tiết mình đã tạo một thẻ liên kết a với tên là edit để khi click vào đấy sẽ chuyển đến một trang edit books.

Trong thư mục routes có file books.routes.js các bạn tạo cho mình hai router update đó là router.get('/:id/edit',...) để render ra views và router.put('/:id',...) để cập nhật books nha. Trước mắt các bạn chỉ cần tạo và render ra views trước cho mình là được.

// Get Update Books
router.get('/:id/edit', async (req, res, next) => {
    try {
        const book = await Book.findById(req.params.id) // find một book trong DB dựa vào id của book đó
        const authors = await Author.find() // find data author trong DB
        
       // Truyền data authors, book vào views để có thể định nghĩa và render được nó ra trong view
        res.render('books/edit', {
            authors: authors, 
            book: book
        })
    } catch {
        res.redirect('/books')
    }
})

// Update Books
router.put('/:id', async (req, res, next) => {
  
})

Giao Diện Cập Nhật Trong Books

Trong views bạn tạo cho mình thư mục là books sẽ bao gồm 4 file index.pug, new.pug, show.pug, edit.pug.
Ở đây thì nói thật thì nó chả khác gì tạo books cả chỉ khác là action trong form nó sẽ thay đổi để phù hợp với chức năng mình đang sử dụng thôi nha.
Lưu ý: Các bạn để ý thì sẽ thấy các fields tạo và cập nhật books nó giống nhau đúng không nào, bây giờ các bạn tạo một file sẽ chứa các fields đó để khi cần bạn có thể tái sử dụng lại được nha.
books/edit.pug

extends ../layouts/layout
block content
   .container  

      h2 Edit Books
      form( action="/books/" + book.id + "?_method=PUT" method="POST"  enctype="multipart/form-data")
        div
          div
            label(for="name" ) Title
            input(placeholder="Title", type="text" name="title" value=book.title )
          div   
            label(for="author") Author
            select(name="author")
              each author in authors 
                if author.id === book.author
                  option(selected label=author.name, value=author.id)
                else
                  option( label=author.name, value=author.id)
        div
          div
            label(for="publish date") Publish Date
            input(type="date" name="publishDate" 
                        value= book.publishDate == null ? '' :
                        book.publishDate.toISOString().split('T')[0])

          div
            label(for="page count") Page Count 
            input(placeholder="Page Count",type="number" name="pageCount" min="1" value= book.pageCount )                
        div
          div
            label(for="coverImg") Image Cover
            input(type="file" name="ImageUrl" required)                
      
          div
          label(for="description") Description
          textarea(name="description", book.description )    
        div
            a(href="/books") Cancel
            button(type="submit") Update

Và đây là giao diện để cập nhật books:

Controller Cập Nhật Trong Books

Để có thể cập nhật được books và sử dụng được phương thức PUT ta cần sử dụng một middleware đó là method override.
Trong views có file books/edit.pug các bạn thêm cho mình đoạn mã này "?_method=PUT" vào trong action của form cái này mình đã thêm vào phần action phía trên rồi nha

Trong file books.routes.js ta có router.put('/:id',...) dùng để update books. Ở đây mình sẽ hông giải thích dài dòng nữa nha bởi vì tạo books như nào thì cập books như vậy thôi :))

// Update Books
router.put('/:id', upload.single('ImageUrl'), async (req, res, next) => {
    const result = await cloudinary.v2.uploader.upload(req.file.path)
    const authors = await Author.find()
    let book
    try {
        book = await Book.findById(req.params.id) // find một book dựa vào id và edit lại book
            book.title = req.body.title,
            book.author = req.body.author,
            book.publishDate = new Date(req.body.publishDate),
            book.pageCount = req.body.pageCount,
            book.description = req.body.description,
            book.ImageUrl = result.secure_url
        await book.save()

        res.redirect('/books')

    } catch {
        res.render('books/edit', {
            authors: authors,
            book: book,
            errorMessage: 'Error Updating Book'
        })
    }
})

Nếu mà có lỗi khi cập nhật books thì trong books/edit.pug các bạn thêm dòng code ở dưới vào phía sau thẻ h2 Edit Books để hiển thị thông báo lỗi nha.

    if locals.errorMessage
        = errorMessage

Và đây là kết quả khi chúng ta cập nhật books:

Định Nghĩa Và Controller Router Xóa Trong Books

Như các bạn đã thấy ở trên khi mình show detail thì trong phần hiển thị chi tiết mình đã tạo một form có chứa một button để xóa books

Cũng trong file books.routes.js ta có router.delete('/:id',...) dùng để xóa books. Xóa book với xóa author theo nguyên lí nó cũng không khác gì nhau nên mình sẽ không giải thích thêm nữa. Nó chỉ khác ở chỗ là khi xóa author thì books mà được author sẽ bị xóa, còn khi xóa books thì author nó vẫn hiển thị bình thường.

// Delete Books
router.delete('/:id', async (req, res, next) => {
    let book
    try {
        book = await Book.findById(req.params.id) // find một book dựa vào id
        await book.remove() // Xóa book trong DB
        res.redirect('/books')// chuyển hướng sang trang /books nếu xóa thành công
    } catch {      
          res.redirect(`/books/${book.id}`) 
    }
})

Trong views có file books/show.pug các bạn thêm cho mình đoạn mã này "?_method=DELETE" vào trong action của form cái này mình đã thêm vào phần action trong phần show detail ở phía trên rồi nha.
Và đây là kết quả khi chúng ta thực hiện xóa books nha:

Hiển Thị Trang HomePage

Định Nghĩa Và Controller Router Trang HomePage

Trong router có file index.routes.jsở đây các bạ tạo cho mình một router.get('/',...) để có thể get data và hiển thị ra trang homepage. Tại trang homepage này mục đích của nó chỉ là hiển thị books mà thôi.
Đầu tiên các bạn cần require model books vào trước nha, sau đó chúng ta sẽ find toàn bộ data books dựa vào thời gian tạo mà nó sẽ hiển thị ra trước hay là sau nha.

const Book = require("../models/books.model")

// Home page
router.get('/', async (req, res, next) => {
  let books
  try {
    // find books được sắp xếp theo thứ tự books nào tạo trước books nào tạo sau và chỉ giới hạn hiển thị 20 books thôi nha
    books = await Book.find().sort({createdAt: 'desc'}).limit(20).exec() 
  } catch {
    books = []
  }
  res.render('index', {
    books: books  // Truyền data books vào views để render ra dữ liệu
  });
})

Giao Diện Hiển Thị Trang HomePage

Trong views có file index.pug sẽ là nơi mà chúng sẽ code phần view cho trang homepage. Để hiển thị ra books thì cũng đơn giản thôi ta chỉ cần lặp qua chúng rồi render ra ảnh bìa kèm theo tiêu đề, sau khi lặp qua các books rồi thì ta cần tạo một thẻ liên kết a để khi click vào đấy sẽ show detail books là được nha.

extends layouts/layout
block content
   .container
      h2 Books Store 
      each book in books
        a(href='/books/' + book.id)
         img(src=book.ImageUrl, width="250" height="280")
         h3= book.title

Và đây là kết quả khi chúng ta hiển thị trang HomePage nha:

CSS Lại Giao Diện Cho Project

Trong public có file /stylesheets/style.css các bạn nhớ import link css vào trong project nha.
Riêng cái phần CSS này các bạn có thể sáng tạo theo sở thích của bản thân mình không nhất thiết phải làm giống mình nha.
Phần css này mình code khá ngắn mình tận dụng những class mà bootstrap hỗ trợ để style cho nó nha

Ở đây mình lưu ý một chút đó là các bạn sẽ thay thế những width và height trong các thẻ img dùng để hiển thị cover image bằng một class img-item nha. Để khi mình chia layout bằng bootstrap thì ảnh nó không bị vỡ nha.
Để biết thêm chi tiết mình chia layout như nào thì xem tại đây nha, mình cũng đã nói rồi riêng phần này các bạn có thể tự style theo sức sáng tạo của bản thân mình nha.
CSS Project

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: sans-serif;
}

li {
    list-style: none;
}

a,
a:hover,
a:active,
a:visited,
a:focus {
    text-decoration: none;
}

label{
    font-weight: 700;
}

.h-30{
    height: 2rem;
}

/* Header */
.navbar {
    width: 100%;
}

.navbar-light .navbar-toggler {
    outline: none;
    border: none;
}

/* Content */
.title-h2 {
    text-align: center;
    font-weight: 700;
    margin-bottom: 2rem;
}

.item-wrapper:hover {
    box-shadow: 0px 0px 10px #444444;
}

.item-text {
    padding: .8rem 0;
    color: #333333;
    font-size: 1.3rem;
    overflow: hidden;
    text-overflow: ellipsis;
    text-transform: capitalize;
    text-align: center;
}

.item-text:hover {
    color: #337ab7;
}

.img-item {
    width: 100%;
    height: 280px;
}

.form-control,
.btn {
    border-radius: inherit;
}

Và đây là kết quả khi chúng style lại toàn bộ giao diện cho project nha:

Deploy Project Lên Heroku

Để mà deploy được một project lên heroku thì các bạn đọc một topic mà mình đã từng viết để hướng dẫn tại đây nha

Gợi Ý Hướng Phát Triển Project

  • Tạo một folder sẽ lấy nới chứa các file có source code lặp đi lặp lại nhiều lần
    VD: Như form tạo books/author và thông báo lỗi thì sẽ tạo cho nó một file riêng, nếu muốn dùng thì chỉ cần include vào là được tìm hiểu thêm tại đây nha
  • Phát triển thành một trang ebook chẳng hạn khi click show detail book thì sẽ xuất hiển tác giả giới thiệu và sẽ có một cái nút nào đó khi click vào đấy sẽ mở một file PDF chẳng hạn,... Các bạn cần tạo cho mình một trang Admin và xác thực, phân quyền cho nó chỉ có bạn mới được vào để đăng sách, ở đây mình không cần yêu cầu user login hay làm gì cả. Hoặc các bạn có thể phát một nền tảng chia sẻ Ebook có thể cho người dùng đăng ebook lên và mình sẽ kiểm diệt ebook đăng lên,...
  • Phát triển thành một trang web bán sách, phát triển thêm các chức năng đăng nhập đăng ký để khi user muốn mua hàng thì cần phải login vào mới mua được mình có viết một bằng hướng dẫn sử dụng jwt để làm hai chức năng trên bạn có thể tham khảo tại đây nha
  • Phát triển thành một trang TMĐT mình sẽ cho phép người bán đăng sách lên bán. Mình sẽ là nơi trung gian để kiểm soát kiểm diệt sách đươc đăng lên bán và người mua. Chúng ta cần phân quyền ra ví dụ mình là nơi trung gian thì sẽ Admin có những chức năng như: diệt sách cần bán, xóa sách vi phạm bản quyền...Còn người bán sẽ có chức năng của seller như đăng bài, xóa bài của shop mình,.. Còn người mua thì chỉ có việc mua sách và đánh giá sách,.v.v.

Vậy Là Xong Rồi Nha Vui Quá =)))
Kết thúc một series về project này nha vã thật sự, các bạn có thể tham khảo code mà mình đã push lên github tại đây nha.

Lời Kết

Vậy là xong bài Hướng Dẫn Xây Dựng Books Store Với NodeJS, Express và MongoDB - Phần 4 cũng là phần cuối rồi nhé. Mình mong muốn sau khi các bạn hoàn thành xong project này sẽ hiểu và nắm rõ hơn về nodejs, express và mongodb nói riêng và lập trình nói chúng. Các bạn có thể dựa vào project này để có thể phát triển và scale project này lớn hơn nữa nha.
Các bạn nhớ like và theo dõi fanpage Thanh Long Dev để nhận những thông báo về bài viết mới nhất nha.
Nếu các bạn cảm thấy bài viết của mình hay thì các bạn có thể ủng hộ mình để mình có thêm động lực để ra những bài topic hay và chất lượng hơn ủng hộ mình tại đây nha.
Chúc Các Bạn Thành Công!!