實作 PHP API & 留言板 SPA(下)


Posted by Nicolakacha on 2020-09-12

上篇實作 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 要在我們的留言板做哪些事:

  1. 在留言板準備好畫面的時候,就去後端拿所有的留言並且放在畫面上。
  2. 當新增按鈕被點擊的時候,我們就把留言送到後端存進資料庫。
// 先把 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);
  });
});

再來看第一個函式裡面要做什麼事:

  1. 串 API 拿到資料,我們用 getCommentsAPI 這個 function 來做。
  2. 拿到資料以後,把裡面的 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>&nbsp;&nbsp;${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, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
}

到這裡我們讀取留言的功能就完成啦,接著來做新增留言 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>&nbsp;&nbsp;${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 一開始的兩個任務完成惹:

  1. 在留言板準備好畫面的時候,就去後端拿所有的留言並且放在畫面上。
  2. 當新增按鈕被點擊的時候,我們就把留言送到後端存進資料庫。

完整程式碼

const APIUrl = 'http://mentor-program.co/mtr04group1/Nicolakacha/week12/board';

function encodeHTML(s) {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
}

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>&nbsp;&nbsp;${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);
  });
});

留言板 DEMO

下一篇:實作 PHP API & 留言板 SPA (番外篇:實作載入更多功能)


#API #PHP #javascript #jquery #Bootstrap







Related Posts

Redux basic

Redux basic

[Day06] Applicative

[Day06] Applicative

#Tailwind #Webpack resolve-url-loader cannot operate: CSS error

#Tailwind #Webpack resolve-url-loader cannot operate: CSS error


Comments