上篇實作 PHP API & 留言板 SPA(上)做完了需要用到的兩個 API 之後,這次要來把前端頁面完成並串接 API 啦~
我們這次會使用 Bootstrap 和 JQuery 這兩套工具,也順便練習看看跟自己寫 CSS 和 Vanilla JavaScript 有什麼不同,前端的留言板頁面會長這樣,這次的重點不是切版,所以就不細述啦:
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Board API DEMO</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="./script.js"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.css">
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="navbar navbar-expand-lg navbar-dark bg-primary">
<nav class="container">
<span class="navbar-brand mb-0 h1">Board API DEMO</span>
</nav>
</div>
<div class="container">
<form class="add-comment-form mt-4 mb-4">
<div class="form-group">
<label for="nickname">Nickname</label>
<input class="form-control" name="nickname" aria-describedby="emailHelp">
</div>
<div class="form-group">
<label for="content-textarea">Please leave your message here</label>
<textarea name="content" class="form-control" aria-label="With textarea"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div class="comments"></div>
</div>
</body>
</html>
CSS
@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400&display=swap');
@import url("https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css");
* {
font-family: 'Ubuntu';
}
.card .content {
margin-bottom: 0;
}
.card .time {
color: rgb(112, 112, 112);
}
.card-top {
justify-content: space-between;
}
接下來就是 JavaScript 的事啦,首先我們可以先來回顧一下, JavaScript 要在我們的留言板做哪些事:
- 在留言板準備好畫面的時候,就去後端拿所有的留言並且放在畫面上。
- 當新增按鈕被點擊的時候,我們就把留言送到後端存進資料庫。
// 先把 API 網址宣告成一個變數,供之後使用
const APIUrl = 'http://example_api.php';
// 當留言板準備好的時候
$(document).ready(() => {
// 從後端拿到留言,放在畫面上的 .comments 區塊
const commentsDOM = $('.comments');
getComments(commentsDOM);
// 當新增按鈕被點擊的時候,把留言送到後端存進資料庫
$('.add-comment-form').submit((e) => {
// 記得要阻擋 submit 的預設事件
e.preventDefault();
addComment(commentsDOM);
});
});
再來看第一個函式裡面要做什麼事:
- 串 API 拿到資料,我們用 getCommentsAPI 這個 function 來做。
- 拿到資料以後,把裡面的 discussion 一筆一筆都 append 到 .comments 的區塊。
function getComments(commentsDOM) {
getCommentsAPI();
}
getCommentAPI() 很簡單,只是要串 API 而已,拿到資料之後,在把資料交給 callback function:
function getCommentsAPI(cb) {
let url = `${APIUrl}/api_comments.php?site_key=nicolas`;
$.ajax({ url })
.done(data => cb(data))
.fail(err => console.log(err));
}
這個 callback function 要做的就是我們剛才的第 2 點說的:拿到資料以後,把裡面的 discussion 一筆一筆都 append 到 .comments 的區塊。
function getComments(commentsDOM) {
// 如果資料不 ok 就 return 結束
getCommentsAPI((data) => {
if (!data.ok) {
return;
}
// 不然就把 data.discussion 存成 comments,並 append 到 .comments 區塊中
const comments = data.discussion;
for (let i = 0; i < comments.length; i += 1) {
appendCommentToDOM(commentsDOM, comments[i]);
}
});
}
接著就是要把 appendCommentToDOM() 這個 function 實際做出來啦。
我們會需要一個模板,並在每次 for 迴圈執行的時候,把裡面的暱稱、留言內容和時間用變數替換掉,最後我們拿到的資料本來就是 DESC 倒著排的,最新的在最上面,所以把留言都 append 到容器中就可以了,這個容器就是我們剛才傳進來的參數。 .comments。
function appendCommentToDOM(container, comment) {
const html = `
<div class="card m-2">
<div class="card-body">
<div class="card-top d-flex">
<h5 class="card-title"><i class="fa fa-user" aria-hidden="true"></i> ${encodeHTML(comment.nickname)}</h5>
<p class="card-text time">${comment.created_at}</p>
</div>
<p class="card-text content">${encodeHTML(comment.content)}</p>
<input hidden value="${comment.id}"/>
</div>
</div>`;
container.append(html);
}
這裡有一個小細節是,顯示留言的部份都要做跳脫字元處理,避免被 HTML 解析:
function encodeHTML(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
}
到這裡我們讀取留言的功能就完成啦,接著來做新增留言 addComment(commentsDOM) 的部份:
這裡的做法是,把新留言顯示到畫面上和存到資料庫內的動作分開。
我們不需要等到留言存好資料庫,再來刷新頁面顯示最新的留言,而是存資料後,直接先把最新的留言用 JavaScript 先放在畫面上,下次頁面刷新的時候,雖然我們剛才動態放在 DOM 的新留言已經不見了,但因為這筆新留言也已經存在資料庫,所以讀取留言的 function 會幫我們把全部的留言顯示出來,頁面看起來和重新載入前還是一樣的。
function addComment(commentsDOM) {
const nickname = $('input[name=nickname]').val();
const content = $('textarea[name=content]').val();
const remindMsg = '<div class="alert alert-danger mt-5" role="alert">Please complete both nickname and content!</div>';
// 如果沒填資料,就顯示提醒訊息,並 return 停止這個 function
if (nickname === '' || content === '') {
$('.alert').remove();
$('.main').prepend(remindMsg);
return;
}
// 資料都有填好才會走到這一步,我們把資料宣告成一個物件
const newComment = {
site_key: 'nicolas',
nickname,
content,
};
// 把資料發 POST request 給後端 API,存到資料庫
$.ajax({
type: 'POST',
url: `${APIUrl}/api_add_comments.php`,
data: newComment,
})
// POST 完資料我們要做的是放在 .done 裡面的 callback function
.done()
.fail(err => console.log(err));
}
存完資料以後,我們把剛才的 newComment 用 appendCommentDOM 加到畫面上,只是這樣會被 append 到最下面,所以我把剛才的 appendCommentToDOM 稍微修改一下,增加一個參數,讓我們可以決定要 prepend 還是 append。
.done((data) => {
// 我們的 newComment 沒有留言時間,因為這是後端自動產生的,所以多存一個時間
newComment.created_at = data.created_at;
appendCommentToDOM(commentsDOM, newComment, true);
//留言完之後,清空留言框框
$('.form-control').val('');
// 走到這裡代表已經新增留言成功了,如果曾經有提醒請填寫完整的訊息,也拿掉
$('.alert').remove();
})
改過的 appendCommentToDOM 就像這樣
function appendCommentToDOM(container, comment, isPrepend) {
const html = `
<div class="card m-2">
<div class="card-body">
<div class="card-top d-flex">
<h5 class="card-title"><i class="fa fa-user" aria-hidden="true"></i> ${encodeHTML(comment.nickname)}</h5>
<p class="card-text time">${comment.created_at}</p>
</div>
<p class="card-text content">${encodeHTML(comment.content)}</p>
<input hidden value="${comment.id}"/>
</div>
</div>`;
if (isPrepend) {
container.prepend(html);
} else {
container.append(html);
}
}
總結
至此我們透過函式填空法,一步步拆解要做的事,就能很清楚地把這支 script.js 一開始的兩個任務完成惹:
- 在留言板準備好畫面的時候,就去後端拿所有的留言並且放在畫面上。
- 當新增按鈕被點擊的時候,我們就把留言送到後端存進資料庫。
完整程式碼
const APIUrl = 'http://mentor-program.co/mtr04group1/Nicolakacha/week12/board';
function encodeHTML(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
}
function addComment(commentsDOM) {
const nickname = $('input[name=nickname]').val();
const content = $('textarea[name=content]').val();
const remindMsg = '<div class="alert alert-danger mt-5" role="alert">Please complete both nickname and content!</div>';
if (nickname === '' || content === '') {
$('.alert').remove();
$('.main').prepend(remindMsg);
return;
}
const newComment = {
site_key: 'nicolas',
nickname,
content,
};
$.ajax({
type: 'POST',
url: `${APIUrl}/api_add_comments.php`,
data: newComment,
})
.done((data) => {
newComment.created_at = data.created_at;
appendCommentToDOM(commentsDOM, newComment, true);
$('.form-control').val('');
$('.alert').remove();
})
.fail(err => console.log(err));
}
function appendCommentToDOM(container, comment, isPrepend) {
const html = `
<div class="card m-2">
<div class="card-body">
<div class="card-top d-flex">
<h5 class="card-title"><i class="fa fa-user" aria-hidden="true"></i> ${encodeHTML(comment.nickname)}</h5>
<p class="card-text time">${comment.created_at}</p>
</div>
<p class="card-text content">${encodeHTML(comment.content)}</p>
<input hidden value="${comment.id}"/>
</div>
</div>`;
if (isPrepend) {
container.prepend(html);
} else {
container.append(html);
}
}
function getCommentsAPI(cb) {
let url = `${APIUrl}/api_comments.php?site_key=nicolas`;
$.ajax({ url })
.done(data => cb(data))
.fail(err => console.log(err));
}
function getComments(commentsDOM) {
getCommentsAPI((data) => {
if (!data.ok) {
return;
}
const comments = data.discussion;
for (let i = 0; i < comments.length; i += 1) {
appendCommentToDOM(commentsDOM, comments[i]);
}
});
}
$(document).ready(() => {
const commentsDOM = $('.comments');
getComments(commentsDOM);
$('.add-comment-form').submit((e) => {
e.preventDefault();
addComment(commentsDOM);
});
});